FFI Vtable Reference
Cloacina plugins (.cloacina packages) export a fixed FFI vtable that
the host calls by positional index. The vtable is declared by the
CloacinaPlugin trait in crates/cloacina-workflow-plugin/src/lib.rs
and is dispatched at runtime by fidius
— the plugin framework Cloacina uses to load shared libraries and
call into them by index. The host-side fidius API is provided by the
fidius-host crate; the plugin-side helpers come from fidius-core.
The canonical method indices are exported as constants from
cloacina-workflow-plugin:
pub const METHOD_GET_TASK_METADATA: usize = 0;
pub const METHOD_EXECUTE_TASK: usize = 1;
pub const METHOD_GET_GRAPH_METADATA: usize = 2;
pub const METHOD_EXECUTE_GRAPH: usize = 3;
pub const METHOD_GET_REACTOR_METADATA: usize = 4;
pub const METHOD_GET_TRIGGER_METADATA: usize = 5;
pub const METHOD_INVOKE_TRIGGER_POLL: usize = 6;
pub const METHOD_GET_TRIGGERLESS_GRAPH_METADATA: usize = 7;
pub const METHOD_INVOKE_TRIGGERLESS_GRAPH: usize = 8;
Both the trait declaration and the constants live in the same file, so
any reorder or addition forces a re-numbering in the same diff. The
host re-exports these constants from
cloacina::computation_graph::packaging_bridge so existing call sites
don’t have to change their import path.
| Wire input | () |
| Wire output | Result<PackageTasksMetadata, PluginError> |
| Optional since | — (always present) |
Returns the package’s full task catalog — for each task, its namespace,
dependency list, description, and source location. The reconciler calls
this at load time (step 6 of the reconciler pipeline) to register a DynamicLibraryTask constructor in the host
Runtime per declared task.
| Wire input | TaskExecutionRequest { task_name: String, context_json: String } |
| Wire output | Result<TaskExecutionResult, PluginError> (with success: bool, context_json: Option<String>, error: Option<String>) |
| Optional since | — |
Executes a named task with a JSON-serialized context. The host calls
this on the executor’s blocking thread; the cdylib runs the task on
its own tokio runtime. The result’s context_json carries the updated
context back across the boundary.
| Wire input | () |
| Wire output | Result<GraphPackageMetadata, PluginError> |
| Optional since | — |
Returns the package’s primary reactor-bound computation graph metadata
— a holdover slot from the pre-CLOACI-I-0101 1:1 reactor-per-graph model.
As of I-0101, reactors are declared standalone via #[reactor(...)] and
graphs bind to them via trigger = reactor("name") (see Reactor
Lifecycle),
so the “synthesized-reactor” form this method historically described no
longer exists. The metadata still carries name, reaction mode (when_any
/ when_all), input strategy (latest / sequential), and accumulator
declarations for whichever reactor-bound graph the package nominates here.
Packages without a CG return PluginError; the reconciler treats that as
“no primary CG” and skips step 5.
| Wire input | GraphExecutionRequest { cache: HashMap<String, String> } |
| Wire output | Result<GraphExecutionResult, PluginError> (with terminal_outputs_json: Option<Vec<String>>) |
| Optional since | — |
Fires the bundled CG with a snapshot of accumulator boundary values.
The reactor calls this on every fire; the result’s
terminal_outputs_json is the per-terminal-node serialized output.
| Wire input | () |
| Wire output | Result<Vec<ReactorPackageMetadata>, PluginError> |
| Optional since | v2 — pre-v2 plugins return CallError::NotImplemented |
Returns the package’s reactor declarations (split-form CG support).
The reconciler treats NotImplemented and Ok(Vec::new())
identically — both mean “package declares no reactors” — and skips
the reactor load step for that package.
| Wire input | () |
| Wire output | Result<Vec<TriggerPackageMetadata>, PluginError> |
| Optional since | v2 |
Returns the package’s trigger declarations. The reconciler routes
cron-shaped entries (where cron_expression: Some(...)) to the cron
scheduler via CronWorkflowRegistrar; non-cron entries get a host-side
FfiTriggerImpl adapter that proxies Trigger::poll() back into the
plugin via method 6.
| Wire input | TriggerInvokeRequest { trigger_name: String } |
| Wire output | Result<TriggerInvokeResult, PluginError> (with fire: bool, optional context_json) |
| Optional since | v2 |
Polls a named trigger across the FFI boundary. Why this exists:
inventory entries do not span shared-library linker boundaries, so
the host cannot build a host-side Arc<dyn Trigger> directly from the
plugin’s inventory section. The FfiTriggerImpl adapter caches the
trigger’s metadata (poll interval, cron expression, allow-concurrent
flag) at registration time, so only the actual poll() call crosses
the boundary on each tick.
The host calls this on a tokio::task::spawn_blocking so the cdylib’s
synchronous fidius dispatch doesn’t block the host’s async runtime
while user poll() code runs.
| Wire input | () |
| Wire output | Result<Vec<TriggerlessGraphMetadataEntry>, PluginError> |
| Optional since | v2 |
Returns trigger-less computation graphs declared by the package.
Trigger-less CGs are not bound to a reactor and don’t consume
accumulator boundaries; they’re invoked directly by workflow tasks
via #[task(invokes = "graph_name")]. The metadata entry carries the
graph name and its terminal-node-output names; the reconciler builds
host-side TriggerlessGraphRegistration adapters that dispatch
invocation through method 8.
| Wire input | TriggerlessGraphInvokeRequest { graph_name: String, context_json: String } |
| Wire output | Result<TriggerlessGraphInvokeResult, PluginError> (with terminal_outputs_json: Option<String>) |
| Optional since | v2 |
Invokes a named trigger-less CG with a workflow context. Same blocking
- cross-runtime pattern as method 6: the cdylib’s tokio runtime drives the graph execution, the host receives the terminal outputs.
Python .cloacina packages are loaded via PyO3 rather than the FFI
vtable — they do not implement CloacinaPlugin directly. However,
the host must be compiled with Python support to load them. A
host built without the Python feature will reject Python packages at
load time with:
RegistryError::RegistrationFailed(
"Python package <name> received but no PythonRuntime is attached"
)
If you operate a multi-language deployment, ensure your host build includes Python support (or run separate hosts per language). Rust- only packages have no such requirement; the FFI vtable is language-neutral.
- The trait is annotated
#[fidius::plugin_interface(version = 2, buffer = PluginAllocated)]. fidius-host computes anINTERFACE_HASHfrom the trait shape; mismatched hashes are rejected at load time, preventing silent ABI drift. - Adding a method requires bumping the version, marking the new
method
#[optional(since = N)], and adding the canonical method-index constant in the same edit. The unifiedcloacina::package!()shell macro emits the new method automatically. - Deleting or reordering a method is a hard breaking change. Don’t.
package!()macro reference — what emits this vtable.- Reconciler Pipeline — how the host consumes the vtable across the six load steps.
- Inventory and Runtime Seeding — why these methods exist.