fidius_host/executor.rs
1// Copyright 2026 Colliery, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! `PluginExecutor` — the dispatch seam across execution backends.
16//!
17//! Fidius historically carried one dispatch implementation per backend: the
18//! cdylib vtable/FFI path lived inside `PluginHandle`, and the Python (PyO3)
19//! path lived in a *separate* `PythonPluginHandle` in `fidius-python`. This
20//! module collapses that duplication: each backend is an executor, and the
21//! caller-facing [`crate::handle::PluginHandle`] wraps them in an **enum**
22//! (`Backend`) so its generic `call_method<I, O>` can serialise with each
23//! backend's native currency.
24//!
25//! ## Why an enum, not `Box<dyn>` — and why two traits
26//!
27//! The backends do **not** share a typed wire. cdylib decodes concrete-type
28//! **bincode** (not self-describing — it can't be reconstructed from an erased
29//! value), while Python (and the future WASM component backend) consume a
30//! self-describing [`fidius_core::Value`]. A single `call(method, Value)` trait
31//! method therefore cannot serve cdylib without breaking its ABI (see
32//! FIDIUS-A-0003 / FIDIUS-I-0021 amendment). So:
33//!
34//! - [`PluginExecutor`] is the **common** surface every backend shares:
35//! metadata plus the raw byte path. For cdylib, `call_raw` is *also* the
36//! carrier for typed calls (the wrapper bincode-wraps the concrete type).
37//! - [`ValueExecutor`] adds the typed [`fidius_core::Value`] call, implemented
38//! only by the self-describing backends (Python, WASM). cdylib does not
39//! implement it — `PluginHandle` routes cdylib typed calls through its own
40//! bincode `call_method`, keeping the bytes byte-identical to pre-refactor.
41
42pub mod cdylib;
43#[cfg(feature = "python")]
44pub mod python;
45#[cfg(feature = "wasm")]
46pub mod wasm;
47
48use fidius_core::Value;
49
50use crate::error::CallError;
51use crate::types::PluginInfo;
52
53pub use cdylib::CdylibExecutor;
54#[cfg(feature = "python")]
55pub use python::Pyo3Executor;
56#[cfg(feature = "wasm")]
57pub use wasm::{
58 precompile_component, validate_component, EgressDenied, EgressPolicy, WasmComponentExecutor,
59 WasmMethod,
60};
61
62/// The surface every execution backend shares.
63///
64/// Implementations must be `Send + Sync`: methods take `&self`, so a handle can
65/// be shared across threads as long as the backend is internally thread-safe.
66pub trait PluginExecutor: Send + Sync {
67 /// Owned metadata describing the loaded plugin.
68 fn info(&self) -> &PluginInfo;
69
70 /// Number of methods the plugin exposes, in interface (vtable) order.
71 fn method_count(&self) -> u32;
72
73 /// Raw bulk-bytes dispatch for `#[wire(raw)]` methods: opaque bytes in,
74 /// opaque bytes out, no per-element marshalling. Opaque bytes are
75 /// language-neutral (a WIT `list<u8>`), so this is uniform across backends.
76 fn call_raw(&self, method: usize, input: &[u8]) -> Result<Vec<u8>, CallError>;
77}
78
79/// Backends whose typed boundary is the self-describing [`Value`] model —
80/// Python today, and the Phase-2 WASM component executor.
81///
82/// cdylib deliberately does **not** implement this: its typed path is
83/// concrete-type bincode, which `Value` cannot reproduce. `PluginHandle`
84/// dispatches cdylib typed calls directly via bincode instead.
85pub trait ValueExecutor: PluginExecutor {
86 /// Typed dispatch by method index. Arguments and returns cross as a
87 /// self-describing [`Value`]; the backend maps it to its native form
88 /// (Python → `PyObject`, WASM → `component::Val`).
89 fn call(&self, method: usize, args: Value) -> Result<Value, CallError>;
90}