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}