Your First Fidius Plugin¶
Fidius is a Rust framework for building safe, version-checked plugin systems using dynamic libraries and procedural macros.
In this tutorial you will build a complete plugin system from scratch: an
interface crate that defines a Calculator trait, a plugin crate that
implements it as a cdylib (a C-compatible dynamic library that Cargo compiles to .dylib, .so, or .dll), and a host binary that loads the plugin at runtime
and calls its add method.
By the end you will have a working example where the host calls add(3, 7) on
a dynamically loaded plugin and gets back 10.
Prerequisites¶
- Rust toolchain (1.77+ recommended)
cargoon yourPATH- The
fidiusCLI installed (cargo install fidius-cli), or you can create files manually as shown below
What you will build¶
calculator-workspace/
calculator-interface/ # defines the Calculator trait
calculator-plugin/ # implements Calculator as a cdylib
calculator-host/ # loads the plugin and calls add()
Step 1: Create a workspace¶
Create a directory and a top-level Cargo.toml:
# Cargo.toml
[workspace]
resolver = "2"
members = [
"calculator-interface",
"calculator-plugin",
"calculator-host",
]
Step 2: Create the interface crate¶
The interface crate defines the trait that plugins implement. You can scaffold it with the CLI:
This creates calculator-interface/ with a Cargo.toml and src/lib.rs. The
generated code is a starting point; replace the contents of src/lib.rs with
the definition below.
Alternatively, create the crate manually:
calculator-interface/Cargo.toml¶
[package]
name = "calculator-interface"
version = "0.1.0"
edition = "2021"
[features]
# Host applications enable this feature to receive the generated
# `CalculatorClient` typed proxy. Plugin cdylibs do NOT enable this
# feature — keeps libloading and other host-only deps out of plugins.
host = ["fidius/host"]
[dependencies]
fidius = "0.1"
serde = { version = "1", features = ["derive"] }
calculator-interface/src/lib.rs¶
pub use fidius::{plugin_impl, PluginError};
#[fidius::plugin_interface(version = 1, buffer = PluginAllocated)]
pub trait Calculator: Send + Sync {
fn add(&self, input: AddInput) -> AddOutput;
}
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct AddInput {
pub a: i64,
pub b: i64,
}
#[derive(Serialize, Deserialize)]
pub struct AddOutput {
pub result: i64,
}
Key points:
#[fidius::plugin_interface(version = 1, buffer = PluginAllocated)]annotates the trait.versionis a user-chosen integer you bump when the interface changes.buffer = PluginAllocatedmeans the plugin allocates the output buffer (the only strategy currently supported).- The trait requires
Send + Sync, methods take&self, and all argument/return types must implement Serde'sSerialize + Deserialize-- see the ABI specification for the full requirements. - The crate re-exports
fidius::plugin_implandfidius::PluginErrorso plugin crates only need to depend on the interface crate.
The #[plugin_interface] macro generates a vtable, interface hash, and descriptor builder behind the scenes -- see the ABI specification for the full list.
Step 3: Create the plugin crate¶
The plugin crate implements the interface as a cdylib shared library. You can
scaffold it with the CLI:
Or create it manually:
calculator-plugin/Cargo.toml¶
The critical line is crate-type = ["cdylib"] -- this tells Cargo to produce a
.dylib / .so / .dll instead of an .rlib.
[package]
name = "calculator-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
calculator-interface = { path = "../calculator-interface" }
fidius-core = { version = "0.1" }
serde = { version = "1", features = ["derive"] }
calculator-plugin/src/lib.rs¶
use calculator_interface::{plugin_impl, Calculator, AddInput, AddOutput};
pub struct BasicCalculator;
#[plugin_impl(Calculator)]
impl Calculator for BasicCalculator {
fn add(&self, input: AddInput) -> AddOutput {
AddOutput {
result: input.a + input.b,
}
}
}
fidius_core::fidius_plugin_registry!();
Key points:
#[plugin_impl(Calculator)]generates the FFI (Foreign Function Interface -- the mechanism for calling across language or binary boundaries) shims, a static vtable (a table of function pointers, one per method), and aPluginDescriptorforBasicCalculator. The attribute argument is the trait name -- it must match the trait annotated with#[plugin_interface].fidius_core::fidius_plugin_registry!()emits thefidius_get_registryexport symbol that the host looks up at runtime. Call it exactly once per cdylib crate. It collects all#[plugin_impl]descriptors in the crate (you can have multiple plugins in one dylib).
For details on what the macro generates, see the reference documentation.
Step 4: Create the host binary¶
The host binary uses fidius-host to discover and load the plugin, and calls
it through the generated typed CalculatorClient — no magic indices, no
duplicate input/output type definitions.
Or scaffold it with the CLI:
calculator-host/Cargo.toml¶
[package]
name = "calculator-host"
version = "0.1.0"
edition = "2021"
[dependencies]
# Enable the `host` feature on the interface crate — this turns on the
# generated CalculatorClient type and pulls in fidius-host through the
# fidius facade.
calculator-interface = { path = "../calculator-interface", features = ["host"] }
fidius-host = { version = "0.1" }
calculator-host/src/main.rs¶
use calculator_interface::{AddInput, CalculatorClient};
use fidius_host::{PluginHandle, PluginHost};
fn main() {
// Point the host at the directory containing the compiled cdylib.
// After `cargo build`, this is typically target/debug/.
let plugin_dir = std::env::args()
.nth(1)
.expect("usage: calculator-host <plugin-dir>");
let host = PluginHost::builder()
.search_path(&plugin_dir)
.build()
.expect("failed to build plugin host");
// Load the plugin by its struct name.
let loaded = host
.load("BasicCalculator")
.expect("failed to load BasicCalculator");
println!("Loaded plugin: {}", loaded.info.name);
println!(" Interface: {}", loaded.info.interface_name);
println!(" Version: {}", loaded.info.interface_version);
// Wrap in a CalculatorClient for typed, named method calls.
let handle = PluginHandle::from_loaded(loaded);
let client = CalculatorClient::from_handle(handle);
// Call add by name with strongly-typed arguments.
let output = client
.add(&AddInput { a: 3, b: 7 })
.expect("add() call failed");
println!("add(3, 7) = {}", output.result);
assert_eq!(output.result, 10);
}
Key points:
CalculatorClientis generated by#[plugin_interface]when the downstream crate enables thehostfeature. No manual wrapper required.- Method calls are by name with trait-accurate types:
client.add(&input)returnsResult<AddOutput, CallError>. No magic indices. - Input/output types are imported from the interface crate —
no duplicate
AddInput/AddOutputdefinitions in the host. A single source of truth means no drift. - Optional methods (declared with
#[optional(since = N)]) are surfaced as regular methods on the Client that check the plugin's capability bit internally and returnCallError::NotImplementedif the plugin did not implement it.
See the host API reference for the full
PluginHost builder and PluginHandle API, and for lower-level
handle.call_method(index, ...) usage when you need it.
Step 5: Build and run¶
# From the workspace root:
cargo build
# Run the host, pointing it at the directory containing the plugin dylib.
# On macOS the dylib lands in target/debug/ as libcalculator_plugin.dylib.
# On Linux it is libcalculator_plugin.so, on Windows calculator_plugin.dll.
cargo run --bin calculator-host -- target/debug/
Expected output:
Step 6: Discover plugins¶
Instead of loading a plugin by name, you can discover all plugins in a directory:
let plugins = host.discover().expect("discovery failed");
for info in &plugins {
println!("Found: {} (implements {})", info.name, info.interface_name);
}
discover() returns a Vec<PluginInfo> containing metadata (name, interface
name, interface hash, version, capabilities, buffer strategy) for every valid
plugin found.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
PluginNotFound |
Plugin name doesn't match | The name is the Rust struct name (BasicCalculator), not the crate name |
SymbolNotFound |
Missing registry export | Ensure fidius_core::fidius_plugin_registry!() is called in the plugin's lib.rs |
InvalidMagic |
Corrupt or non-fidius dylib | Check that the dylib was built from a fidius plugin crate |
| Deserialization error | Mismatched struct fields | Input/output structs in host must have identical field names and types to the interface crate |
Next steps¶
- Test Plugins -- use
fidius-testhelpers (in-process Client, dylib fixtures, signing fixtures) plus thefidius testCLI smoke command - Optional Methods -- extend the Calculator with version-gated methods
- Signing Plugins -- sign and verify plugins with Ed25519 keys