Your First Python Fidius Plugin¶
Fidius supports Python as a plugin language. The plugin author writes Python
— no Rust, no Cargo, no per-architecture builds — and the host loads it
through the same PluginHost API used for Rust cdylib plugins.
This tutorial walks through building a Python plugin that implements an existing Rust-defined interface, packaging it, and loading it from a host.
Prerequisites¶
- A Rust interface crate that uses
#[plugin_interface](see Your First Plugin for how to write one). python3on the host machine (system Python is fine; bundling the interpreter is not required for v1).- The
fidiusCLI installed:cargo install fidius-cli. - The host binary built with the
pythonfeature onfidius-host:[dependencies] fidius-host = { version = "0.2", features = ["python"] }.
What you will build¶
my-greeter-py/
├── package.toml # declares runtime = "python"
├── greeter.py # the actual plugin code
├── requirements.txt # optional pip dependencies
└── vendor/ # populated by `fidius pack`
└── ...
The deployable artifact is a .fid archive containing the directory above.
Hosts drop it into a search path; PluginHost::load_python(...) returns a
PythonPluginHandle ready to dispatch.
Step 1: Generate the Python stub¶
If your interface crate's source is at crates/greeter/src/lib.rs and
declares a Greeter trait with #[plugin_interface], generate the stub:
fidius python-stub \
--interface crates/greeter/src/lib.rs \
--trait-name Greeter \
--out my-greeter-py/greeter.py
The stub gives you:
__interface_hash__ = 0x...— the constant the host uses at load time to verify your plugin matches the interface it claims to implement.- One
@method-decorated stub function per trait method, with type hints from a Rust → Python primitive mapping table.
Open the file and replace each raise NotImplementedError with your
implementation. Don't change the __interface_hash__ constant or the
function signatures — drift from the trait surfaces as a load-time error.
A finished greeter.py might look like:
from fidius import method, PluginError
__interface_hash__ = 0xdeadbeefcafef00d # from `fidius python-stub`
@method
def greet(name: str) -> str:
if not name:
raise PluginError("EMPTY_INPUT", "name must be non-empty")
return f"Hello, {name}!"
Step 2: Write the manifest¶
Create my-greeter-py/package.toml:
[package]
name = "my-greeter"
version = "0.1.0"
interface = "Greeter"
interface_version = 1
runtime = "python"
[metadata]
description = "A friendly greeting service."
[python]
entry_module = "greeter"
# requirements = "requirements.txt" # uncomment if you have dependencies
The runtime = "python" field tells the host's discover() and
load_python(...) to route this package through the Python loader.
Step 3: (Optional) Declare Python dependencies¶
If your plugin uses third-party libraries (numpy, pyarrow, anything from
PyPI), list them in my-greeter-py/requirements.txt:
fidius pack will run pip install -r requirements.txt --target vendor/
during packaging, baking the deps into the archive. Pre-existing vendor/
directories are respected (use this if you want hand-controlled or
reproducible-build vendoring).
Step 4: Pack the plugin¶
This produces my-greeter-0.1.0.fid — a single self-contained archive
including your .py file, the manifest, and vendor/ if you declared
dependencies.
Step 5: (Optional) Sign the plugin¶
fidius keygen --out mykey
fidius package sign --key mykey.secret ./my-greeter-py
fidius package pack ./my-greeter-py # repack to include the .sig
Signed plugins can be verified by the host before load via verify_package.
Step 6: Load and call from the host¶
In the host's Rust code:
use fidius_host::PluginHost;
// The descriptor is generated by your interface crate's #[plugin_interface]
// macro — it lives in the companion module alongside the cdylib metadata.
use greeter_interface::__fidius_Greeter::Greeter_PYTHON_DESCRIPTOR;
let host = PluginHost::builder()
.search_path("./plugins")
.build()?;
let handle = host.load_python("my-greeter", &Greeter_PYTHON_DESCRIPTOR)?;
// Call: methods are looked up by index in the descriptor's order. The host
// will gain a typed Client wrapper for Python plugins in a future release;
// today you call through the handle directly.
let input = serde_json::to_vec(&("World".to_string(),))?;
let out = handle.call_typed_json(0, &input)?; // index 0 = greet
let result: String = serde_json::from_slice(&out)?;
println!("{result}"); // → "Hello, World!"
For methods declared #[wire(raw)] on the Rust trait, use call_raw:
Constraint: shared sys.modules¶
All Python plugins loaded into one host process share the embedded Python
interpreter's sys.modules. If two plugins vendor conflicting versions of
the same library (e.g. numpy==1.24 vs numpy==1.26), the first one to
load wins for both. Subsequent plugins silently see the wrong version.
Treat this as a deployment constraint: coordinate dependency versions
across the Python plugins a host loads, the same way you would for any
Python application embedding multiple subpackages. A future fidius
release may use CPython sub-interpreters (PEP 684) to isolate per-plugin
sys.modules, but for v1 it's the operator's responsibility.
Building host binaries that link fidius-host --features python¶
The python feature on fidius-host transitively links libpython via
PyO3. On macOS framework builds, your binary needs an rpath pointing at
the framework directory or it will abort at launch with:
The fix is a small build.rs in your binary crate that uses
pyo3-build-config to emit the right linker arg. See
crates/fidius-host/build.rs in the fidius repo for a working example
that's already feature-gated on CARGO_FEATURE_PYTHON.
What's not yet supported¶
- Process isolation. Python plugins run in the same address space as the host. A segfault in plugin code (or in a misbehaving native dep) takes the host down with it. A future Process tier with subprocess workers and kill-on-deadline timeouts is the natural next step for the untrusted-plugin use case.
- Built-in timeouts.
PluginHost::load_python(...).call_*runs to completion. If you need a watchdog, wrap the host process itself. - Hot reload. Reloading a plugin requires a host restart; Python's
sys.modulescache makes in-process re-import unreliable. - Typed Client wrapper. Today you dispatch through the
PythonPluginHandledirectly. A future enhancement will let the same generatedClienttype backed by either a cdylib or a Python plugin transparently.