Skip to main content

fidius_host/
handle.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//! `PluginHandle` — the unified, caller-facing proxy over a loaded plugin.
16//!
17//! A `PluginHandle` is backend-agnostic: callers use the same
18//! `call_method` / `call_method_raw` API whether the plugin is a cdylib, a
19//! Python package, or (Phase 2) a WASM component. The backend lives in the
20//! private [`Backend`] enum.
21//!
22//! ## Why an enum backend (FIDIUS-I-0021)
23//!
24//! The backends don't share a typed wire: cdylib decodes concrete-type
25//! **bincode** (not reconstructable from an erased value) while Python/WASM
26//! consume a self-describing [`fidius_core::Value`]. An enum (rather than
27//! `Box<dyn PluginExecutor>`) lets the generic `call_method<I, O>` branch with
28//! the concrete `I`/`O` in scope and serialise with each backend's native
29//! currency — so the **cdylib path stays byte-identical** to before this
30//! refactor (`bincode(input)` straight to the FFI; `Value` is never involved).
31
32use serde::de::DeserializeOwned;
33use serde::Serialize;
34
35use fidius_core::descriptor::PluginDescriptor;
36
37use crate::error::{CallError, LoadError};
38use crate::executor::cdylib::CdylibExecutor;
39#[cfg(feature = "python")]
40use crate::executor::python::Pyo3Executor;
41#[cfg(feature = "wasm")]
42use crate::executor::wasm::WasmComponentExecutor;
43#[cfg(any(feature = "python", feature = "wasm"))]
44use crate::executor::{PluginExecutor, ValueExecutor};
45use crate::types::PluginInfo;
46
47/// The execution backend behind a [`PluginHandle`].
48///
49/// One variant per runtime. The WASM variant lands in Phase 2.
50enum Backend {
51    Cdylib(CdylibExecutor),
52    /// `.py` package via `fidius-python`'s embedded interpreter. Only present
53    /// when the `python` feature is enabled.
54    #[cfg(feature = "python")]
55    Python(Pyo3Executor),
56    /// `.wasm` component via wasmtime. Only present when the `wasm` feature is
57    /// enabled.
58    #[cfg(feature = "wasm")]
59    Wasm(WasmComponentExecutor),
60}
61
62/// A handle to a loaded plugin, ready for calling methods.
63///
64/// Holds the active execution backend. `call_method()` handles serialization,
65/// dispatch, and cleanup; concurrent calls from multiple threads are safe as
66/// long as the underlying plugin is thread-safe (the cdylib macro enforces
67/// `&self`-only methods; the Python backend serialises through the GIL).
68pub struct PluginHandle {
69    backend: Backend,
70}
71
72impl PluginHandle {
73    /// Create a `PluginHandle` from a freshly loaded cdylib plugin.
74    pub fn from_loaded(plugin: crate::loader::LoadedPlugin) -> Self {
75        Self {
76            backend: Backend::Cdylib(CdylibExecutor::from_loaded(plugin)),
77        }
78    }
79
80    /// Create a `PluginHandle` from a descriptor already registered in the
81    /// current process's inventory (a `#[plugin_impl]` linked as a normal
82    /// rlib). No dylib is loaded. Used by `Client::in_process(plugin_name)`.
83    pub fn from_descriptor(desc: &'static PluginDescriptor) -> Result<Self, LoadError> {
84        Ok(Self {
85            backend: Backend::Cdylib(CdylibExecutor::from_descriptor(desc)?),
86        })
87    }
88
89    /// Look up a descriptor in the current process's inventory registry by
90    /// `plugin_name` (the Rust struct name passed to `#[plugin_impl]`).
91    pub fn find_in_process_descriptor(
92        plugin_name: &str,
93    ) -> Result<&'static PluginDescriptor, LoadError> {
94        CdylibExecutor::find_in_process_descriptor(plugin_name)
95    }
96
97    /// Create a `PluginHandle` backed by a loaded Python plugin. `info` is
98    /// built by the loader from the package manifest + interface descriptor.
99    /// Only available with the `python` feature.
100    #[cfg(feature = "python")]
101    pub fn from_python(py: fidius_python::PythonPluginHandle, info: PluginInfo) -> Self {
102        Self {
103            backend: Backend::Python(Pyo3Executor::new(py, info)),
104        }
105    }
106
107    /// Create a `PluginHandle` backed by a loaded WASM component. Only
108    /// available with the `wasm` feature.
109    #[cfg(feature = "wasm")]
110    pub fn from_wasm(executor: WasmComponentExecutor) -> Self {
111        Self {
112            backend: Backend::Wasm(executor),
113        }
114    }
115
116    /// Call a plugin method by vtable index.
117    ///
118    /// Serializes the input with the backend's native wire (cdylib → bincode;
119    /// Python/WASM → [`fidius_core::Value`]), dispatches, and decodes the
120    /// result into `O`. No built-in timeout — see the `fidius` crate docs.
121    pub fn call_method<I: Serialize, O: DeserializeOwned>(
122        &self,
123        index: usize,
124        input: &I,
125    ) -> Result<O, CallError> {
126        match &self.backend {
127            // cdylib: serialise the concrete type with bincode directly — byte
128            // for byte what the plugin's shim decodes (no `Value` hop).
129            Backend::Cdylib(e) => e.call_method(index, input),
130            // python: cross via the self-describing `Value` currency.
131            #[cfg(feature = "python")]
132            Backend::Python(e) => {
133                let args = fidius_core::to_value(input)
134                    .map_err(|err| CallError::Serialization(err.to_string()))?;
135                let out = ValueExecutor::call(e, index, args)?;
136                fidius_core::from_value(out)
137                    .map_err(|err| CallError::Deserialization(err.to_string()))
138            }
139            // wasm: same self-describing `Value` currency as python.
140            #[cfg(feature = "wasm")]
141            Backend::Wasm(e) => {
142                let args = fidius_core::to_value(input)
143                    .map_err(|err| CallError::Serialization(err.to_string()))?;
144                let out = ValueExecutor::call(e, index, args)?;
145                fidius_core::from_value(out)
146                    .map_err(|err| CallError::Deserialization(err.to_string()))
147            }
148        }
149    }
150
151    /// Start a server-streaming method call by vtable index (FIDIUS-I-0026).
152    ///
153    /// Returns a [`crate::stream::ChunkStream`] — a `futures::Stream` of
154    /// `Result<Value, _>` the caller pulls with `.next().await`. Backpressure and
155    /// cancellation are structural: a slow consumer parks the producer, and
156    /// dropping the stream tears the producer down. All three backends stream:
157    /// Python and WASM cross via the self-describing [`Value`] currency; cdylib
158    /// crosses items as concrete bincode of the item type `O` and decodes them
159    /// here (FIDIUS-T-0137).
160    ///
161    /// `O` is the stream's item type. Python/WASM ignore it (they're already
162    /// `Value`-native); cdylib uses it to `bincode::<O>`-decode each item.
163    #[cfg(feature = "streaming")]
164    pub async fn call_streaming<I: Serialize, O: DeserializeOwned + Serialize>(
165        &self,
166        index: usize,
167        input: &I,
168    ) -> Result<crate::stream::ChunkStream, CallError> {
169        match &self.backend {
170            // cdylib: concrete bincode of the args (no `Value` hop), then the
171            // iterator-handle streaming path (FIDIUS-I-0026 CS.1). Items also cross
172            // as concrete bincode, decoded by `cdylib_stream_decode::<O>`.
173            Backend::Cdylib(e) => {
174                let input_bytes = fidius_core::wire::serialize(input)
175                    .map_err(|err| CallError::Serialization(err.to_string()))?;
176                e.call_streaming_raw(index, &input_bytes, cdylib_stream_decode::<O>)
177            }
178            #[cfg(feature = "python")]
179            Backend::Python(e) => {
180                let args = fidius_core::to_value(input)
181                    .map_err(|err| CallError::Serialization(err.to_string()))?;
182                crate::stream::StreamExecutor::call_streaming(e, index, args).await
183            }
184            #[cfg(feature = "wasm")]
185            Backend::Wasm(e) => {
186                let args = fidius_core::to_value(input)
187                    .map_err(|err| CallError::Serialization(err.to_string()))?;
188                crate::stream::StreamExecutor::call_streaming(e, index, args).await
189            }
190        }
191    }
192
193    /// Call a `#[wire(raw)]` method: raw bytes in, raw bytes out, no bincode.
194    pub fn call_method_raw(&self, index: usize, input: &[u8]) -> Result<Vec<u8>, CallError> {
195        match &self.backend {
196            Backend::Cdylib(e) => e.call_method_raw(index, input),
197            #[cfg(feature = "python")]
198            Backend::Python(e) => PluginExecutor::call_raw(e, index, input),
199            #[cfg(feature = "wasm")]
200            Backend::Wasm(e) => PluginExecutor::call_raw(e, index, input),
201        }
202    }
203
204    /// Check if an optional method is supported (capability bit set).
205    /// Returns `false` for `bit >= 64` and for backends without capabilities.
206    pub fn has_capability(&self, bit: u32) -> bool {
207        if bit >= 64 {
208            return false;
209        }
210        self.info().capabilities & (1u64 << bit) != 0
211    }
212
213    /// Access the plugin's owned metadata.
214    pub fn info(&self) -> &PluginInfo {
215        match &self.backend {
216            Backend::Cdylib(e) => e.info(),
217            #[cfg(feature = "python")]
218            Backend::Python(e) => PluginExecutor::info(e),
219            #[cfg(feature = "wasm")]
220            Backend::Wasm(e) => PluginExecutor::info(e),
221        }
222    }
223
224    /// Static `#[method_meta(...)]` key/value metadata for the given method,
225    /// in declaration order. Empty for out-of-range ids, for interfaces that
226    /// declared none, and for backends without descriptor metadata.
227    pub fn method_metadata(&self, method_id: u32) -> Vec<(&str, &str)> {
228        match &self.backend {
229            Backend::Cdylib(e) => e.method_metadata(method_id),
230            // Python/WASM plugins carry no descriptor-level method metadata.
231            #[cfg(feature = "python")]
232            Backend::Python(_) => Vec::new(),
233            #[cfg(feature = "wasm")]
234            Backend::Wasm(_) => Vec::new(),
235        }
236    }
237
238    /// Static `#[trait_meta(...)]` key/value metadata declared on the trait.
239    /// Empty when none was declared or for backends without descriptor metadata.
240    pub fn trait_metadata(&self) -> Vec<(&str, &str)> {
241        match &self.backend {
242            Backend::Cdylib(e) => e.trait_metadata(),
243            #[cfg(feature = "python")]
244            Backend::Python(_) => Vec::new(),
245            #[cfg(feature = "wasm")]
246            Backend::Wasm(_) => Vec::new(),
247        }
248    }
249}
250
251/// Per-item decoder for the cdylib streaming fast path (FIDIUS-T-0137): each item
252/// crosses as concrete `bincode(O)` (byte-identical to the unary cdylib wire), so
253/// we `wire::deserialize::<O>` then lift to a `Value`. This is the `decode_item`
254/// fn pointer the typed caller hands to [`CdylibExecutor::call_streaming_raw`] —
255/// `O` is monomorphised in by `call_streaming::<_, O>`.
256#[cfg(feature = "streaming")]
257fn cdylib_stream_decode<O: DeserializeOwned + Serialize>(
258    bytes: &[u8],
259) -> Result<fidius_core::Value, CallError> {
260    let item: O = fidius_core::wire::deserialize(bytes)
261        .map_err(|e| CallError::Deserialization(e.to_string()))?;
262    fidius_core::to_value(&item).map_err(|e| CallError::Serialization(e.to_string()))
263}