graft

Native plugin ABI and runtime loading for trusted shared-library extensions. Manifest-based, fail-fast, C-ABI friendly.

C++20Shared LibrariesC ABIPlugin Runtime

C++ plugin systems usually die at the boundary. The demo works, the shared library loads, the class looks clean, and then one compiler update or one different standard library turns the whole thing into a crash generator. std::string crosses the DLL line, exception semantics drift, vtables stop matching, and you’re debugging memory corruption in code that should have been boring.

The honest answer is to stop pretending the boundary is friendly. A host and a plugin need a contract that can survive real builds on real machines. That means a manifest, an ABI version, a runtime declaration, and an explicit entrypoint. The host opens the library, validates the contract, and only then binds the symbol it actually needs.

The boundary

The boundary stays narrow on purpose:

  • manifest first
  • typed entrypoint second
  • no shared C++ class ABI
  • fail fast before plugin code runs

Graft is not a marketplace, a package manager, or a distributed extension system. It gives you a trustworthy native runtime boundary for code you intend to load in-process.

The contract

Every graft-native shared library exports one well-known manifest symbol:

inline constexpr const char* MANIFEST_SYMBOL = "graft_manifest";

That manifest carries the fields the host needs before executing anything:

  • plugin id
  • display name
  • version
  • runtime kind (native or worker)
  • entrypoint symbol name
  • ABI version

Bad metadata, wrong ABI, missing entrypoint, unknown runtime kind: all of that explodes at load time, not after the host has already crossed into foreign code.

Exporting a plugin

#include <icy/graft/graft.h>

namespace detector {
inline constexpr const char* ENTRYPOINT = "motion_detector_api";
}

ICY_GRAFT_PLUGIN("motion-detector",
                 "Motion Detector",
                 "1.0.0",
                 graft::RUNTIME_NATIVE,
                 detector::ENTRYPOINT)

That macro emits the standard manifest with the current ABI version. The actual entrypoint can be whatever typed function-table or constructor contract your host expects. Graft cares that the contract is explicit, not that every plugin in the world pretends to share one magical superclass.

Loading a plugin

#include <icy/graft/graft.h>

using GetApiFunc = const MotionDetectorApi* (*)();

icy::graft::Library lib;
lib.open("./libmotion_detector.so");

const auto& manifest = lib.manifest();
auto getApi = lib.entrypoint<GetApiFunc>();
const MotionDetectorApi* api = getApi();

Library::open() validates the manifest before the host touches the plugin API. If the manifest is missing, the ABI is wrong, or the runtime is nonsense, the load fails immediately.

What this buys you

For trusted native extensions, this is the difference between a runtime boundary and a wish:

  • the host can reject incompatible libraries before any plugin code runs
  • the plugin contract is inspectable without reverse-engineering symbols by convention
  • runtime kind stays explicit, so native and worker payloads share vocabulary without sharing mechanics
  • the loader logic lives in one place instead of being reimplemented differently in every host

Good fit for media processors, detector backends, codec adapters, and other high-trust in-process extensions where performance matters and the host needs predictable loading.

With Pacm

Pacm handles delivery. Graft handles binding.

Pacm can install a package that declares:

{
  "extension": {
    "loader": "graft",
    "runtime": "native",
    "entrypoint": "lib/libmotion_detector.so",
    "abi-version": 1,
    "capabilities": ["processor.video", "detector"]
  }
}

After install, the host resolves extension.entrypoint relative to the package directory and hands that path to graft::Library.

  • Pacm fetches, verifies, extracts, and versions the package
  • Graft validates, opens, and binds the runtime payload

Delivery and loading are different problems. Most systems blur them together and get both wrong.