loadable_node_abi/lib.rs
1//! ABI-stable plugin interface.
2//!
3//! Both the host and the plugin depend on this crate. Everything that
4//! crosses the `dlopen` boundary is defined here and uses `abi_stable`
5//! types (`RVec`, `RString`, `RResult`, `RBox`, sabi-trait objects).
6//!
7//! `RuntimeData` itself is **not** in this surface — instead it is
8//! serialized to msgpack bytes (`RVec<u8>` via `rmp-serde::to_vec_named`)
9//! at the boundary. That keeps `remotemedia-core` out of the FFI
10//! contract entirely, so a plugin built against a different rustc /
11//! feature set can still load.
12//!
13//! For the full contract — wire format, versioning policy, plugin
14//! author rules, change history — see
15//! [`docs/LOADABLE_NODE_ABI.md`](../../../docs/LOADABLE_NODE_ABI.md).
16
17use abi_stable::{
18 declare_root_module_statics,
19 library::RootModule,
20 package_version_strings, sabi_trait,
21 sabi_types::VersionStrings,
22 std_types::{RBox, RErr, ROk, RResult, RString, RVec},
23 StableAbi,
24};
25use async_ffi::{FfiFuture, FutureExt};
26
27/// FFI-safe node.
28///
29/// `process` returns an `FfiFuture` — an ABI-stable future the host
30/// can `.await` directly. Plugin-side async runtimes (or none) work as
31/// long as the future polls to completion without referencing
32/// runtime-specific globals; in practice, plugins that call back into
33/// host services do so by polling synchronous state from the async
34/// block.
35///
36/// # Forward compatibility (multi-output extension)
37///
38/// `process` is marked `#[sabi(last_prefix_field)]` — that's the cut
39/// between the original FFI surface (1.x) and any methods added in
40/// minor versions. Methods added below it must carry a default impl
41/// so older plugins (whose vtables only expose `process`) continue to
42/// load. Hosts compiled against the newer ABI then transparently fall
43/// back to the default whenever a plugin omits the new method.
44///
45/// `process_multi` is the multi-output sibling of `process`: a node's
46/// `process_streaming` callback can fire N times per input (think
47/// SileroVAD emitting `Json(event)` plus the audio passthrough), and
48/// the single-output `process` would silently drop everything but the
49/// first emission. `process_multi` returns the full `RVec` so the
50/// host can dispatch each blob into the streaming callback chain.
51///
52/// The default impl wraps `process` as a 1-element `RVec` so plugins
53/// that only implement single-output stay correct (just lossy when
54/// the underlying node was actually multi-output — same behaviour as
55/// before this method existed).
56#[sabi_trait]
57pub trait FfiNode: Send + Sync + 'static {
58 fn node_type(&self) -> RString;
59
60 #[sabi(last_prefix_field)]
61 fn process(&self, input: RVec<u8>) -> FfiFuture<RResult<RVec<u8>, RString>>;
62
63 /// Multi-output process. Returns ALL emissions from one input as
64 /// a flat `RVec<RVec<u8>>` (each inner vec is one rmp-serde
65 /// `RuntimeData` blob, in emission order).
66 ///
67 /// Default impl: delegate to `process` and wrap its single output
68 /// as a 1-element vec. Plugins that haven't been rebuilt against
69 /// the multi-output ABI keep working — just without multi-output
70 /// semantics. Plugins that override this method get full N-output
71 /// fidelity through the `LoadableNodeAdapter` host wiring.
72 fn process_multi(&self, input: RVec<u8>) -> FfiFuture<RResult<RVec<RVec<u8>>, RString>> {
73 let fut = self.process(input);
74 async move {
75 match fut.await {
76 ROk(out) => ROk(RVec::from(vec![out])),
77 RErr(e) => RErr(e),
78 }
79 }
80 .into_ffi()
81 }
82
83 /// One-time, per-session initialization hook for lazy-load plugins.
84 ///
85 /// Forwarded from the host's `AsyncStreamingNode::initialize()`
86 /// once per session, before the first `process` call. Plugins that
87 /// do all their work eagerly inside `FfiNodeFactory::create()`
88 /// (e.g. audio2face's `Audio2FaceLipSyncNode::load`, live2d-render's
89 /// `WgpuBackend::new`) can leave this defaulted. Plugins with a
90 /// non-trivial init (e.g. llama-cpp spawning a worker thread that
91 /// loads a multi-GB GGUF) must override it — without forwarding,
92 /// the worker is never spawned and `process` returns "worker not
93 /// running".
94 ///
95 /// `session_id` and `node_id` are forwarded as RStrings so the
96 /// plugin can log / tag work with them. `emit_progress` is NOT
97 /// forwarded today — progress events emitted from inside a
98 /// loadable plugin's `initialize()` are silently dropped. Plugin
99 /// authors who need progress visibility should wrap heavy init in
100 /// the host (e.g. via `WarmSessionPool::prewarm` which fires its
101 /// own progress before delegating).
102 ///
103 /// Default impl: no-op. Older plugins not rebuilt against this
104 /// method keep compiling, just without lazy-init semantics.
105 fn initialize(
106 &self,
107 _session_id: RString,
108 _node_id: RString,
109 ) -> FfiFuture<RResult<(), RString>> {
110 async { ROk(()) }.into_ffi()
111 }
112}
113
114/// Owned trait object for an FFI node.
115///
116/// `sabi_trait` drops the lifetime parameter when the trait has a
117/// `'static` bound, so the alias does not name a lifetime.
118pub type FfiNodeBox = FfiNode_TO<RBox<()>>;
119
120/// FFI-safe factory — produces FfiNode instances from a JSON params blob.
121#[sabi_trait]
122pub trait FfiNodeFactory: Send + Sync + 'static {
123 fn node_type(&self) -> RString;
124 fn create(&self, params: RString) -> RResult<FfiNodeBox, RString>;
125}
126
127/// Owned trait object for an FFI factory.
128pub type FfiNodeFactoryBox = FfiNodeFactory_TO<RBox<()>>;
129
130/// Root module exported by every plugin.
131///
132/// abi_stable validates layout, abi_stable version, and prefix-type
133/// compatibility when the host calls `NodePluginRef::load_from_file`.
134#[repr(C)]
135#[derive(StableAbi)]
136#[sabi(kind(Prefix(prefix_ref = NodePluginRef)))]
137#[sabi(missing_field(panic))]
138pub struct NodePlugin {
139 /// Returns every factory this plugin provides.
140 #[sabi(last_prefix_field)]
141 pub list_factories: extern "C" fn() -> RVec<FfiNodeFactoryBox>,
142}
143
144impl RootModule for NodePluginRef {
145 declare_root_module_statics! {NodePluginRef}
146 const BASE_NAME: &'static str = "node_plugin";
147 const NAME: &'static str = "node_plugin";
148 const VERSION_STRINGS: VersionStrings = package_version_strings!();
149}