Skip to main content

palladium_plugin/
native.rs

1//! Native shared-library plugin loading.
2//!
3//! [`PluginRegistry::load_native`] opens a shared library with `libloading`,
4//! reads the `pd_plugin_init` export, verifies the ABI version, and registers
5//! a factory for each actor type the plugin declares.
6//!
7//! # Symbol contract
8//!
9//! Every native plugin must export the following C-linkage symbols:
10//!
11//! ```c
12//! PdPluginInfo* pd_plugin_init(void);
13//! void          pd_plugin_shutdown(void);
14//! void*         pd_actor_create (const char* type_name, uint32_t type_name_len,
15//!                                const uint8_t* config,  uint32_t config_len);
16//! void          pd_actor_destroy(void* actor);
17//! int32_t       pd_actor_on_start  (void* actor, PdActorContext* ctx);
18//! int32_t       pd_actor_on_message(void* actor, PdActorContext* ctx,
19//!                                   const uint8_t* envelope_bytes,
20//!                                   const uint8_t* payload, uint32_t payload_len);
21//! void          pd_actor_on_stop   (void* actor, PdActorContext* ctx, int32_t reason);
22//! ```
23//!
24//! If any symbol is missing, `load_native` returns
25//! `Err(PluginError::LoadFailed(...))`.
26
27use std::ffi::c_void;
28use std::path::Path;
29
30use palladium_plugin_api::{PdActorTypeInfo, PdPluginInfo, PD_ABI_VERSION};
31
32use crate::error::PluginError;
33use crate::ffi_actor::{FfiActor, FfiVtable};
34use crate::manifest::{PluginInfo, PluginKind};
35use crate::registry::{LoadedPlugin, PluginRegistry};
36
37// ── Symbol names ──────────────────────────────────────────────────────────────
38
39const SYM_INIT: &[u8] = b"pd_plugin_init\0";
40const SYM_CREATE: &[u8] = b"pd_actor_create\0";
41const SYM_DESTROY: &[u8] = b"pd_actor_destroy\0";
42const SYM_ON_START: &[u8] = b"pd_actor_on_start\0";
43const SYM_ON_MESSAGE: &[u8] = b"pd_actor_on_message\0";
44const SYM_ON_STOP: &[u8] = b"pd_actor_on_stop\0";
45
46// ── Helpers ───────────────────────────────────────────────────────────────────
47
48/// Verify that the ABI version declared by the plugin matches the engine's.
49///
50/// # Safety
51///
52/// `info` must point to a valid [`PdPluginInfo`] that remains live for the
53/// duration of this call.
54unsafe fn check_abi_version(info: *const PdPluginInfo) -> Result<(), PluginError> {
55    let found = unsafe { (*info).abi_version };
56    if found != PD_ABI_VERSION {
57        return Err(PluginError::AbiMismatch {
58            expected: PD_ABI_VERSION,
59            found,
60        });
61    }
62    Ok(())
63}
64
65/// Read a plugin name or type-name from a `(*const u8, u32)` pair.
66///
67/// Returns `Err(LoadFailed)` if the bytes are not valid UTF-8.
68///
69/// # Safety
70///
71/// `ptr` must point to at least `len` valid bytes that remain live for the
72/// duration of this call.
73unsafe fn read_str(ptr: *const u8, len: u32) -> Result<String, PluginError> {
74    let slice = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
75    std::str::from_utf8(slice)
76        .map(|s| s.to_string())
77        .map_err(|e| PluginError::LoadFailed(format!("invalid UTF-8 in plugin string: {e}")))
78}
79
80/// Read the array of `PdActorTypeInfo` structs from a `PdPluginInfo`.
81///
82/// Returns a `Vec` of `(type_name, vtable)` pairs; each pair registers one
83/// actor type factory.
84///
85/// # Safety
86///
87/// `info` and the `actor_types` array it points to must remain valid for the
88/// duration of this function.
89unsafe fn read_actor_types(
90    info: *const PdPluginInfo,
91    vtable: FfiVtable,
92) -> Result<Vec<(String, FfiVtable)>, PluginError> {
93    let count = unsafe { (*info).actor_type_count } as usize;
94    let types_ptr: *const PdActorTypeInfo = unsafe { (*info).actor_types };
95
96    let mut result = Vec::with_capacity(count);
97    for i in 0..count {
98        // SAFETY: caller guarantees the array has `count` valid elements.
99        let type_info: &PdActorTypeInfo = unsafe { &*types_ptr.add(i) };
100        let type_name = unsafe { read_str(type_info.type_name, type_info.type_name_len) }?;
101        result.push((type_name, vtable));
102    }
103    Ok(result)
104}
105
106// ── PluginRegistry::load_native ───────────────────────────────────────────────
107
108impl<R: palladium_runtime::Reactor> PluginRegistry<R> {
109    /// Load a native shared library plugin from `path`.
110    ///
111    /// Steps:
112    /// 1. Open the library with [`libloading`].
113    /// 2. Resolve all required C symbols; fail fast on missing exports.
114    /// 3. Call `pd_plugin_init` and verify the returned ABI version.
115    /// 4. For each declared actor type, register a factory that constructs an
116    ///    [`FfiActor`] wrapping the plugin's lifecycle functions.
117    /// 5. Record the loaded library in the registry so it stays mapped in
118    ///    memory for the lifetime of the actors it created.
119    ///
120    /// Returns [`PluginInfo`] describing the newly loaded plugin.
121    ///
122    /// # Errors
123    ///
124    /// - [`PluginError::LoadFailed`] — library not found, symbol missing, or
125    ///   UTF-8 decoding of plugin name/type names failed.
126    /// - [`PluginError::InitFailed`] — `pd_plugin_init` returned null.
127    /// - [`PluginError::AbiMismatch`] — `abi_version` in the returned struct
128    ///   does not equal [`PD_ABI_VERSION`].
129    pub fn load_native(&mut self, path: &Path) -> Result<PluginInfo, PluginError> {
130        // ── 1. Open the library ───────────────────────────────────────────────
131        // SAFETY: libloading calls dlopen; the library stays alive in `lib`.
132        let lib = unsafe { libloading::Library::new(path) }
133            .map_err(|e| PluginError::LoadFailed(e.to_string()))?;
134
135        // ── 2. Resolve symbols ────────────────────────────────────────────────
136        // Each `*sym` dereferences the `Symbol<fn>` to get the raw function
137        // pointer value. This is safe because `lib` will outlive the pointers
138        // (it moves into `LoadedPlugin::Native { _lib: lib }` below).
139
140        type InitFn = unsafe extern "C" fn() -> *const PdPluginInfo;
141        type CreateFn = unsafe extern "C" fn(*const u8, u32, *const u8, u32) -> *mut c_void;
142        type DestroyFn = unsafe extern "C" fn(*mut c_void);
143        type OnStartFn =
144            unsafe extern "C" fn(*mut c_void, *mut palladium_plugin_api::PdActorContext) -> i32;
145        type OnMessageFn = unsafe extern "C" fn(
146            *mut c_void,
147            *mut palladium_plugin_api::PdActorContext,
148            *const u8,
149            *const u8,
150            u32,
151        ) -> i32;
152        type OnStopFn =
153            unsafe extern "C" fn(*mut c_void, *mut palladium_plugin_api::PdActorContext, i32);
154
155        macro_rules! load_sym {
156            ($name:expr, $ty:ty) => {{
157                let sym: libloading::Symbol<$ty> = unsafe { lib.get($name) }.map_err(|e| {
158                    PluginError::LoadFailed(format!(
159                        "missing symbol `{}`: {e}",
160                        std::str::from_utf8($name)
161                            .unwrap_or("<invalid>")
162                            .trim_end_matches('\0')
163                    ))
164                })?;
165                // Copy the function pointer out before `sym` (and its lifetime
166                // tied to `lib`) is dropped. Safe because `lib` outlives the copy.
167                *sym
168            }};
169        }
170
171        let init_fn: InitFn = load_sym!(SYM_INIT, InitFn);
172        let vtable = FfiVtable {
173            create: load_sym!(SYM_CREATE, CreateFn),
174            destroy: load_sym!(SYM_DESTROY, DestroyFn),
175            on_start: load_sym!(SYM_ON_START, OnStartFn),
176            on_message: load_sym!(SYM_ON_MESSAGE, OnMessageFn),
177            on_stop: load_sym!(SYM_ON_STOP, OnStopFn),
178        };
179
180        // ── 3. Call pd_plugin_init and verify ABI ─────────────────────────────
181        // SAFETY: symbol was resolved from the library we just opened.
182        let info_ptr: *const PdPluginInfo = unsafe { init_fn() };
183        if info_ptr.is_null() {
184            return Err(PluginError::InitFailed);
185        }
186        // SAFETY: non-null, returned by the plugin itself, valid for as long
187        // as the library is loaded (which it will be — it's in `lib`).
188        unsafe { check_abi_version(info_ptr) }?;
189
190        // ── 4. Read plugin name and version ───────────────────────────────────
191        let plugin_name = unsafe { read_str((*info_ptr).name, (*info_ptr).name_len) }?;
192        let version = unsafe {
193            format!(
194                "{}.{}.{}",
195                (*info_ptr).version_major,
196                (*info_ptr).version_minor,
197                (*info_ptr).version_patch,
198            )
199        };
200
201        // ── 5. Register factories for each actor type ─────────────────────────
202        let actor_types = unsafe { read_actor_types(info_ptr, vtable) }?;
203        let actor_type_count = actor_types.len();
204
205        for (type_name, vtable) in actor_types {
206            let type_name_bytes: Vec<u8> = type_name.as_bytes().to_vec();
207            self.register_type(plugin_name.clone(), type_name, move |config: &[u8]| {
208                let config_bytes = config.to_vec();
209                // SAFETY: vtable is a copy of valid function pointers from a
210                // library that is kept alive in LoadedPlugin::Native { _lib }.
211                let state = unsafe {
212                    (vtable.create)(
213                        type_name_bytes.as_ptr(),
214                        type_name_bytes.len() as u32,
215                        config_bytes.as_ptr(),
216                        config_bytes.len() as u32,
217                    )
218                };
219                if state.is_null() {
220                    return Err(PluginError::InitFailed);
221                }
222                Ok(Box::new(FfiActor::<R>::new(state, vtable)))
223            });
224        }
225
226        // ── 6. Record the loaded library ──────────────────────────────────────
227        let info = PluginInfo {
228            name: plugin_name.clone(),
229            version,
230            kind: PluginKind::Native,
231            actor_type_count,
232        };
233        self.insert_loaded(LoadedPlugin::Native {
234            _lib: lib,
235            info: info.clone(),
236        });
237
238        Ok(info)
239    }
240}