Graft
icey::graftNative ABI contracts, host C surfaces, and runtime loading helpers
API Reference → · Source → · CMake target: icey::graft
Overview
The graft module is icey's native ABI boundary. It defines the manifest a shared library or host surface must export, the runtime kinds icey understands, and the small C++ loader that opens plugin libraries, validates their manifests, and resolves their declared entrypoint.
graft is intentionally narrow. It does not prescribe a C++ object model across the ABI boundary. Instead, the host and the plugin agree on a manifest plus one typed C entrypoint. That keeps the boundary explicit, versioned, and easier to validate at load time.
The same ABI primitives also cover host-out C surfaces. A binding such as icey-sys can bind to a host-exported function table through a per-surface manifest and entrypoint without inventing a Rust-specific shim.
Combined with pacm, the two modules cover the full lifecycle: pacm downloads and installs plugin archives, and graft loads those installed libraries into the process and hands the host the plugin entrypoint it asked for.
Dependencies: base
Architecture
The plugin-in runtime contract looks like this:
Host process Plugin shared library
────────────────────────────────── ───────────────────────────────────
graft::Library::open(path) → shared library loaded
load manifest symbol → icy_graft_manifest exported
validateManifest(...) → ABI, id, name, version, runtime checked
entrypoint<GetApiFunc>() → named exported function resolved
api = getApi() → host uses the plugin's typed APIHost-out surfaces use the same bytes with the polarity reversed:
Foreign binding / process icey shared library
────────────────────────────────── ───────────────────────────────────
resolve surface manifest symbol → icy_pipeline_api_manifest exported
validate manifest → ABI, id, name, version, runtime checked
resolve manifest.entrypoint → icy_pipeline_api exported
api = getApi() → binding uses the typed C tableFour pieces make up the shared contract:
graft::ABI_VERSION— incremented when the binary contract changes.graft::Manifest/icy_graft_manifest_t— metadata exported from a plugin or host surface.graft::RuntimeKind— currentlynative,worker, orhost.- A typed entrypoint function name stored in the manifest and resolved by
graft::Library.
Usage
Define the shared API header
The host and plugin both include the same API header. That header defines the function table and the entrypoint name the manifest will advertise.
#pragma once
#include <icy/graft/graft.h>
namespace myapp::plugin {
struct Api
{
void (*setValue)(const char* value);
const char* (*value)();
};
using GetApiFunc = const Api* (*)();
inline constexpr const char* ENTRYPOINT = "myapp_plugin_api";
} // namespace myapp::pluginExport the manifest from the plugin
The plugin implements its API and exports both the manifest and the entrypoint.
#include "mypluginapi.h"
namespace {
void setValue(const char* value) { /* ... */ }
const char* value() { /* ... */ return ""; }
const myapp::plugin::Api API = {
setValue,
value,
};
} // namespace
ICY_GRAFT_PLUGIN("my-plugin",
"My Plugin",
"1.0.0",
icy::graft::RUNTIME_NATIVE,
myapp::plugin::ENTRYPOINT)
extern "C" ICY_GRAFT_EXPORT const myapp::plugin::Api* myapp_plugin_api()
{
return &API;
}The manifest is exported as icy_graft_manifest. graft::Library reads that symbol first, validates it, then resolves the entrypoint named by manifest().entrypoint.
Load the plugin in the host
#include <icy/graft/graft.h>
#include "mypluginapi.h"
icy::graft::Library library;
library.open("./libmyplugin.so");
if (icy::graft::parseRuntimeKind(library.manifest().runtime) !=
icy::graft::RuntimeKind::Native) {
throw std::runtime_error("unexpected runtime kind");
}
auto getApi = library.entrypoint<myapp::plugin::GetApiFunc>();
const myapp::plugin::Api* api = getApi();
api->setValue("ready");If the manifest is missing required fields, the ABI version does not match, or the runtime string is unknown, library.open() throws and closes the library again before returning control to the caller.
Export a host-out C surface
Host surfaces include the C ABI primitives, choose per-surface symbols, and mark the manifest with the host runtime. They do not use the plugin manifest symbol icy_graft_manifest.
#include <icy/graft/host/pipeline.h>
namespace {
const icy_pipeline_api_t API = {
ICY_PIPELINE_API_ABI_VERSION,
sizeof(icy_pipeline_api_t),
createPipeline,
destroyPipeline,
attachRtspSource,
attachWebRtcSender,
startPipeline,
stopPipeline,
lastError,
};
} // namespace
ICY_GRAFT_HOST_SURFACE(icy_pipeline_api_manifest,
"icy.pipeline",
"icey Pipeline API",
"0.1.0",
ICY_PIPELINE_API_ENTRYPOINT)
extern "C" ICY_GRAFT_EXPORT const icy_pipeline_api_t* icy_pipeline_api()
{
return &API;
}The graft manifest ABI version only covers the manifest layout. Every host-out function table must also carry its own abi_version and struct_size fields so bindings can reject incompatible table layouts before calling through function pointers.
The first concrete host-out surface is icy_pipeline_api, exported by the
icey::pipeline_capi target. It owns a narrow RTSP-to-browser pipeline and
uses icey::webrtc_support internally:
create/destroymanage an opaqueicy_pipeline_tcreateaccepts anicy_pipeline_options_t; callers must setstruct_size, and may passname,signalling_token,room, andice_serverattach_rtsp_sourcestores the RTSP URL to open throughav::MediaCaptureattach_webrtc_senderconfigures Symple signalling; passws://host:portorwss://host:port, and usepeer_idas the local Symple user idstartopens the media source, starts a private libuv loop, joins the configured Symple room, accepts incoming calls, and begins streaming when the WebRTC session reachesActivestoptears down the stream, session, client, and private looplast_errorreturns the most recent pipeline error string, valid until the next call that mutates that pipeline
Current host-out conventions:
- one manifest symbol per public surface, such as
icy_pipeline_api_manifest - one entrypoint symbol per public surface, such as
icy_pipeline_api - opaque handles across the ABI, never C++ object pointers with public layout
- explicit create/destroy pairs for owned handles
- status-code returns for fallible calls; no C++ exceptions across the C ABI
- string lifetime documented by the owning surface
Runtime kinds
graft currently defines three runtime strings:
graft::RUNTIME_NATIVEfor code loaded directly into the current process.graft::RUNTIME_WORKERfor plugin contracts intended to execute in a worker runtime.graft::RUNTIME_HOSTfor host-exported C ABI surfaces.
Hosts can use parseRuntimeKind() and runtimeKindName() to normalize and report that value instead of string-comparing ad hoc.
graft::Library is still the plugin loader. It rejects host manifests even though the generic validateManifest() helper accepts them; use validateHostSurfaceManifest() when validating a per-surface host manifest.
Configuration
There is no runtime configuration surface for graft. The compatibility boundary is the manifest and the exported entrypoint:
- keep
graft::ABI_VERSIONin sync between host and plugin - choose a stable entrypoint name and publish it in the shared API header
- reject manifests whose runtime kind does not match the host's execution model
- version each host-out function table independently with
abi_versionandstruct_size
