Optional Methods¶
This tutorial extends the Calculator plugin from
Your First Plugin with an optional multiply method.
You will learn how to evolve an interface without breaking existing plugins, how
the capability bitfield works, and how the host checks whether a loaded plugin
supports an optional method before calling it.
Prerequisites¶
- Completed Your First Plugin
- A working
calculator-workspacewith interface, plugin, and host crates
What you will learn¶
- Declare optional methods with
#[optional(since = N)] - Implement them in a plugin
- Check capabilities from the host before calling
- Understand what happens when an older plugin lacks the optional method
Step 1: Add the optional method to the interface¶
Open calculator-interface/src/lib.rs and add a multiply method annotated
with #[optional(since = 2)]. Bump the interface version to 2 to signal
the addition. Also add the necessary input/output types:
pub use fidius::{plugin_impl, PluginError};
#[fidius::plugin_interface(version = 2, buffer = PluginAllocated)]
pub trait Calculator: Send + Sync {
fn add(&self, input: AddInput) -> AddOutput;
#[optional(since = 2)]
fn multiply(&self, input: MulInput) -> MulOutput;
}
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,
}
#[derive(Serialize, Deserialize)]
pub struct MulInput {
pub a: i64,
pub b: i64,
}
#[derive(Serialize, Deserialize)]
pub struct MulOutput {
pub result: i64,
}
Key points about #[optional(since = 2)]:
- The
sincevalue is informational -- it documents which interfaceversion(from#[plugin_interface(version = N, ...)]) introduced the method. Here the interface started atversion = 1with onlyadd; bumping toversion = 2and annotatingmultiplywithsince = 2documents the evolution. Thesincevalue does not change the interface hash (only required methods contribute to the hash). - Optional methods do not break backward compatibility. A plugin compiled
against the old interface (without
multiply) will still load and work foraddcalls. - You can have up to 64 optional methods per interface (they are tracked in a
u64capability bitfield).
The macro generates capability-bit constants and wraps optional vtable entries in Option -- see the ABI specification for details.
Step 2: Implement the optional method in the plugin¶
Open calculator-plugin/src/lib.rs and add the multiply implementation:
use calculator_interface::{
plugin_impl, Calculator,
AddInput, AddOutput,
MulInput, MulOutput,
};
pub struct BasicCalculator;
#[plugin_impl(Calculator)]
impl Calculator for BasicCalculator {
fn add(&self, input: AddInput) -> AddOutput {
AddOutput {
result: input.a + input.b,
}
}
fn multiply(&self, input: MulInput) -> MulOutput {
MulOutput {
result: input.a * input.b,
}
}
}
fidius_core::fidius_plugin_registry!();
When #[plugin_impl(Calculator)] sees that the impl block includes multiply,
and multiply appears in Calculator_OPTIONAL_METHODS, it sets the
corresponding capability bit in the plugin descriptor. In this case, capability
bit 0 is set, so the plugin's capabilities field becomes 0x1.
Step 3: Call the optional method from the host¶
Open calculator-host/src/main.rs. With the typed CalculatorClient, the
host just calls client.multiply(...); the capability check happens inside
the Client and returns CallError::NotImplemented if the plugin doesn't
implement it:
use calculator_interface::{AddInput, CalculatorClient, MulInput};
use fidius::CallError;
use fidius_host::{PluginHandle, PluginHost};
fn main() {
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");
let loaded = host
.load("BasicCalculator")
.expect("failed to load BasicCalculator");
let handle = PluginHandle::from_loaded(loaded);
let client = CalculatorClient::from_handle(handle);
// Required method -- always present.
let sum = client
.add(&AddInput { a: 3, b: 7 })
.expect("add() failed");
println!("add(3, 7) = {}", sum.result);
// Optional method -- Client checks the capability bit internally.
match client.multiply(&MulInput { a: 4, b: 5 }) {
Ok(product) => println!("multiply(4, 5) = {}", product.result),
Err(CallError::NotImplemented { .. }) => {
println!("multiply is not supported by this plugin");
}
Err(e) => panic!("multiply failed: {e}"),
}
}
The generated CalculatorClient::multiply inspects the plugin's capability
bitfield before dispatching. If the bit isn't set, it short-circuits with
CallError::NotImplemented { bit: ... } — no FFI call is made.
If you need lower-level access, PluginHandle::has_capability(bit) and
PluginHandle::call_method(index, ...) remain available. The capability bit
is a zero-based index among optional methods (so multiply is bit 0). The
method index is zero-based across all methods in declaration order
(add = 0, multiply = 1).
Step 4: Build and run¶
Expected output:
Step 5: Simulate an old plugin without multiply¶
To see what happens when a plugin does not implement the optional method,
create a second plugin crate that only implements add:
calculator-plugin-v1/Cargo.toml¶
[package]
name = "calculator-plugin-v1"
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-v1/src/lib.rs¶
Notice that multiply is not implemented. The Rust compiler does not
require it because the #[plugin_interface] macro treats optional methods as
having default implementations in the vtable:
use calculator_interface::{plugin_impl, Calculator, AddInput, AddOutput};
pub struct LegacyCalculator;
#[plugin_impl(Calculator)]
impl Calculator for LegacyCalculator {
fn add(&self, input: AddInput) -> AddOutput {
AddOutput {
result: input.a + input.b,
}
}
}
fidius_core::fidius_plugin_registry!();
Add "calculator-plugin-v1" to the workspace members in the root
Cargo.toml:
# Cargo.toml
[workspace]
resolver = "2"
members = [
"calculator-interface",
"calculator-plugin",
"calculator-plugin-v1",
"calculator-host",
]
Rebuild, then run:
If the host loads LegacyCalculator (change the name in the host.load(...)
call), the output will be:
The plugin loads without error. The capability bit 0 is 0 because
LegacyCalculator does not implement multiply. The host's
has_capability(0) check returns false, and the host skips the call.
You can also inspect capabilities via the CLI¶
Output includes a Capabilities hex value. For a plugin that implements
multiply, you will see 0x0000000000000001 (bit 0 set). For one that does
not, you will see 0x0000000000000000.
Interface evolution rules¶
For the full compatibility matrix (which changes are backward-compatible and which require recompilation), see Interface Evolution.
Next steps¶
- Signing Plugins -- protect plugin integrity with Ed25519 signatures
- Your First Plugin -- review the basics