Plugin author guide

This is a step-by-step guide to writing, building, and shipping a Confium plugin. A Confium plugin is a dynamic library (.so / .dylib / .dll) that implements the plugin contract: a small set of bootstrap symbols plus per-interface implementation functions.

For the host-side API that loads and drives plugins, see the generated header target/confium.h. For the engine architecture, see the architecture page.

How a plugin is structured

Every plugin is a Rust cdylib that depends on confium-api and uses the procedural macros from confium-macros to generate the contract symbols. At minimum, a plugin exports:

  • cfmp_interface_version — returns the plugin-contract ABI version.
  • cfmp_initialize(cfm, opts) — called once after load; receive a host handle and options.
  • cfmp_finalize(cfm) — called before unload; release resources.
  • cfmp_query_interfaces — returns a packed stream naming the interface types and versions the plugin implements.
  • cfmp_metadata (optional) — returns static metadata (name, version, vendor, license) for the registry.

For each interface it advertises (e.g. hash), the plugin exports the corresponding cfmp_\{interface\}_* symbols at the negotiated version.

Step 1: create the crate

cargo new --lib my-plugin
cd my-plugin

Edit Cargo.toml:

[package]
name = "my-plugin"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
confium-api = { path = "../confium/crates/confium-api" }
confium-macros = { path = "../confium/crates/confium-macros" }

Step 2: declare the plugin

Use the #[confium::plugin] attribute macro to emit the bootstrap symbols. The macro takes metadata and the list of interfaces you implement.

use confium_api::PluginMetadata;
use confium_macros::confium_plugin;

#[confium_plugin(
    name = "my-hash",
    version = "0.1.0",
    vendor = "Example Corp",
    license = "BSD-2-Clause",
    interfaces = ["hash"]
)]
pub struct MyPlugin;

impl MyPlugin {
    /// Called once on load. `cfm` is a host handle you can use to look up
    /// other plugins (e.g. a dependency). `opts` carries string/uint
    /// options from the loader.
    pub fn init(cfm: *mut std::ffi::c_void, opts: &confium_api::Options) -> Result<(), confium_api::Error> {
        // Read configuration, open files, allocate state, ...
        Ok(())
    }

    /// Called once before unload. Release everything `init` acquired.
    pub fn finalize(cfm: *mut std::ffi::c_void) {
        // ...
    }
}

Step 3: implement an interface

For the hash interface, implement the per-instance lifecycle: create, update, finalize, reset, clone, and size queries. The #[confium::interface] macro generates the cfmp_hash_* symbols that wire your Rust impl into the C ABI.

use confium_macros::confium_interface;

pub struct Sha256 {
    // your hasher state
}

#[confium_interface(name = "hash", version = 0)]
impl Sha256 {
    /// Construct a new hasher. `opts` carries provider-specific options.
    pub fn create(opts: &confium_api::Options) -> Result<Self, confium_api::Error> {
        // ...
        # ![allow(unimplemented)]
        unimplemented!()
    }

    /// Feed bytes into the hasher.
    pub fn update(&mut self, data: &[u8]) -> Result<(), confium_api::Error> {
        // ...
        unimplemented!()
    }

    /// Produce the final digest. `out` is caller-allocated and at least
    /// `output_size` bytes long.
    pub fn finalize(&mut self, out: &mut [u8]) -> Result<(), confium_api::Error> {
        // ...
        unimplemented!()
    }

    /// Reset to the initial state (reusable context).
    pub fn reset(&mut self) -> Result<(), confium_api::Error> { unimplemented!() }

    /// Digest length in bytes (e.g. 32 for SHA-256).
    pub fn output_size(&self) -> u32 { 32 }

    /// Internal block size in bytes (e.g. 64 for SHA-256).
    pub fn block_size(&self) -> u32 { 64 }
}

Step 4: declare dependencies (optional)

If your plugin depends on another provider (e.g. you wrap a botan symmetric primitive), declare it so Confium refuses to load you unless the dependency is satisfied:

#[confium_plugin(
    name = "my-aead",
    // ...
    dependencies = [
        confium_api::Dependency::provider("botan", ">=3.0,<4.0"),
    ]
)]
pub struct MyPlugin;

Confium resolves dependencies before invoking cfmp_initialize. An unmet dependency fails the load with Error::PluginDependencyUnmet.

Step 5: build

cargo build --release

The output is target/release/libmy_plugin.so (Linux), libmy_plugin.dylib (macOS), or my_plugin.dll (Windows).

Step 6: load and test

For local testing, load the plugin via libloading in a test binary, or link it into the daemon. See crates/confium-mock-plugin/ for a reference implementation and crates/confium-test-harness/ for the NIST-style test vectors harness.

Once the plugin is published to the registry (Step 7 below), the CLI can install and exercise it:

confium install my-hash           # from the registry
confium list
confium info my-hash

Step 7: publish to the registry

Once your plugin is ready, publish it to the Confium plugin registry so others can discover and verify it:

  1. Generate a manifest describing the plugin (name, version, interfaces, checksum, signature).
  2. Submit it as a PR against sites/registry/plugins/.
  3. The registry’s publisher-key and trust-root model is documented under sites/registry/.

Reference: the bootstrap symbols

Symbol Role
cfmp_interface_version Returns the plugin-contract ABI version this plugin targets.
cfmp_initialize(cfm, opts) One-time setup. cfm is an opaque host handle; opts is the loader-supplied option bag.
cfmp_finalize(cfm) One-time teardown.
cfmp_query_interfaces Returns name\0version\0 pairs terminated by an empty name. Drives per-interface version negotiation.
cfmp_metadata (optional) Returns a *const CFMPluginMetadata for the registry. Omit if you do not want to be listed.
cfmp_query_dependencies (optional) Returns a *const CFMDependency list. Confium resolves these before cfmp_initialize.

Reference: interface wire formats

Interface versioning is independent per interface. A single plugin can implement hash v0 and cipher v1. The cfmp_query_interfaces payload encodes this as:

b"hash\0\x00\x00cipher\0\x01\x00\0"   // hash v0, cipher v1

Each entry is name + \0 + one version byte + \0. The list is terminated by an empty name (a leading \0).

Troubleshooting

Plugin fails to load (Error::PluginDependencyUnmet): A declared dependency is missing or out of range. Run confium list to see loaded providers and check the version range in your Dependency::provider(...) call.

Plugin loads but interface calls return Error::InterfaceNotSupported: The interface name in cfmp_query_interfaces does not match what the host requested, or the version byte is higher than the host supports. Lower the version or fix a typo in the interface name.

cfmp_* symbols missing from the built library: Ensure crate-type = ["cdylib"] is set in [lib] and that each implementation is covered by #[confium::interface]. Run nm -D target/release/libmy_plugin.so | grep cfmp to verify exports.