Skip to main content

palladium_plugin/
registry.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::sync::Arc;
4
5use crate::error::PluginError;
6use crate::manifest::{PluginInfo, PluginKind};
7use crate::wasm::{WasmHost, WasmImports, WasmVal};
8use crate::wasm_actor::WasmActor;
9
10// ── ActorTypeEntry ────────────────────────────────────────────────────────────
11
12/// A registered actor type factory belonging to a loaded plugin.
13pub(crate) struct ActorTypeEntry<R: palladium_runtime::Reactor> {
14    /// Name of the plugin that registered this type.
15    pub(crate) plugin_name: String,
16    /// Factory that constructs a boxed actor from a config byte slice.
17    pub(crate) factory: ActorFactory<R>,
18}
19
20pub(crate) type ActorFactory<R> =
21    Box<dyn Fn(&[u8]) -> Result<Box<dyn palladium_actor::Actor<R>>, PluginError> + Send + Sync>;
22
23// ── LoadedPlugin ──────────────────────────────────────────────────────────────
24
25/// Internal representation of a loaded plugin.
26///
27/// For native plugins, the [`libloading::Library`] handle is kept alive here
28/// to prevent the shared library from being unloaded while actors that were
29/// created from it are still running. Drop order matters: actor instances must
30/// be stopped before the registry entry is removed.
31pub(crate) enum LoadedPlugin {
32    Native {
33        /// Keeps the shared library mapped in process memory.
34        _lib: libloading::Library,
35        info: PluginInfo,
36    },
37    #[allow(dead_code)] // constructed in Phase 5.6 (load_wasm)
38    Wasm { info: PluginInfo },
39}
40
41impl LoadedPlugin {
42    pub(crate) fn info(&self) -> &PluginInfo {
43        match self {
44            Self::Native { info, .. } => info,
45            Self::Wasm { info } => info,
46        }
47    }
48}
49
50// ── PluginRegistry ────────────────────────────────────────────────────────────
51
52/// Manages loaded plugins and their actor type factories.
53///
54/// `PluginRegistry` is the single source of truth for which plugins are
55/// loaded and which actor types they export. It does **not** interact with
56/// the engine directly — callers are responsible for stopping any running
57/// actors before calling [`unload`](Self::unload).
58pub struct PluginRegistry<R: palladium_runtime::Reactor> {
59    /// Loaded plugin metadata, keyed by plugin name.
60    plugins: HashMap<String, LoadedPlugin>,
61    /// Actor type factories, keyed by type name.
62    types: HashMap<String, ActorTypeEntry<R>>,
63}
64
65impl<R: palladium_runtime::Reactor> PluginRegistry<R> {
66    /// Create an empty registry with no loaded plugins.
67    pub fn new() -> Self {
68        Self {
69            plugins: HashMap::new(),
70            types: HashMap::new(),
71        }
72    }
73
74    /// List metadata for all currently loaded plugins, sorted by name.
75    pub fn list(&self) -> Vec<PluginInfo> {
76        let mut infos: Vec<PluginInfo> = self.plugins.values().map(|p| p.info().clone()).collect();
77        infos.sort_by(|a, b| a.name.cmp(&b.name));
78        infos
79    }
80
81    /// Create an actor instance from a registered type.
82    ///
83    /// `config` is an opaque byte slice forwarded to the factory; pass `&[]`
84    /// for types that require no configuration.
85    ///
86    /// Returns `Err(PluginError::UnknownType(...))` if no factory is registered
87    /// for `type_name`.
88    pub fn create_actor(
89        &self,
90        type_name: &str,
91        config: &[u8],
92    ) -> Result<Box<dyn palladium_actor::Actor<R>>, PluginError> {
93        let entry = self
94            .types
95            .get(type_name)
96            .ok_or_else(|| PluginError::UnknownType(type_name.to_string()))?;
97        (entry.factory)(config)
98    }
99
100    /// Return actor type names registered by a specific plugin, sorted.
101    pub fn actor_types_for_plugin(&self, plugin_name: &str) -> Vec<String> {
102        let mut names: Vec<String> = self
103            .types
104            .iter()
105            .filter(|(_, entry)| entry.plugin_name == plugin_name)
106            .map(|(type_name, _)| type_name.clone())
107            .collect();
108        names.sort();
109        names
110    }
111
112    /// Unload a plugin by name.
113    ///
114    /// Removes all actor type factories that belong to `name` from the
115    /// registry, then drops the plugin's library handle (for native plugins,
116    /// this calls `dlclose`).
117    ///
118    /// **Caller's responsibility**: stop all running actors created by this
119    /// plugin *before* calling `unload`. Dropping the native library while
120    /// actors are still executing code from it is undefined behaviour.
121    ///
122    /// Returns `Err(PluginError::UnknownType(...))` if no plugin named `name`
123    /// is loaded.
124    pub fn unload(&mut self, name: &str) -> Result<(), PluginError> {
125        if !self.plugins.contains_key(name) {
126            return Err(PluginError::UnknownType(name.to_string()));
127        }
128        self.types.retain(|_, entry| entry.plugin_name != name);
129        self.plugins.remove(name);
130        Ok(())
131    }
132
133    /// Register an actor type factory.
134    ///
135    /// Called internally by [`load_native`](Self::load_native) and
136    /// [`load_wasm`](Self::load_wasm) after reading plugin metadata.
137    ///
138    /// Can also be used for **programmatic (in-process) plugin registration**
139    /// — for example, registering test actors without loading a shared library.
140    /// Note that `register_type` alone does not add an entry to [`list`](Self::list);
141    /// the plugin must also be recorded via `insert_loaded` to appear there.
142    pub fn register_type(
143        &mut self,
144        plugin_name: impl Into<String>,
145        type_name: impl Into<String>,
146        factory: impl Fn(&[u8]) -> Result<Box<dyn palladium_actor::Actor<R>>, PluginError>
147            + Send
148            + Sync
149            + 'static,
150    ) {
151        let plugin_name = plugin_name.into();
152        let type_name = type_name.into();
153        self.types.insert(
154            type_name,
155            ActorTypeEntry {
156                plugin_name,
157                factory: Box::new(factory),
158            },
159        );
160    }
161
162    /// Insert a loaded plugin entry without going through file loading.
163    ///
164    /// Used by `load_native` / `load_wasm` after reading the library.
165    /// Also useful in tests for verifying `list()` and `unload()` together.
166    pub(crate) fn insert_loaded(&mut self, plugin: LoadedPlugin) {
167        let name = plugin.info().name.clone();
168        self.plugins.insert(name, plugin);
169    }
170
171    /// Load a WASM plugin from `path`, using `host` to compile and inspect it.
172    ///
173    /// Protocol:
174    /// 1. Read `.wasm` bytes from `path`.
175    /// 2. Compile via `host.compile`.
176    /// 3. Instantiate with no-op imports (metadata pass).
177    /// 4. Call the module's `pd_plugin_init() -> i32` export, which returns a
178    ///    pointer into WASM linear memory where a WASM-layout `PdPluginInfo`
179    ///    struct is stored (32-bit pointer fields).
180    /// 5. Parse the struct to extract name, version, ABI version, and actor
181    ///    type names.  Reject mismatched ABI.
182    /// 6. Register a [`WasmActor`] factory for each declared actor type.
183    pub fn load_wasm(
184        &mut self,
185        path: &Path,
186        host: Arc<dyn WasmHost>,
187    ) -> Result<PluginInfo, PluginError> {
188        let bytes = std::fs::read(path).map_err(|e| PluginError::LoadFailed(e.to_string()))?;
189
190        let module = host
191            .compile(&bytes)
192            .map_err(|e| PluginError::WasmError(e.to_string()))?;
193
194        // ── Metadata pass ─────────────────────────────────────────────────────
195        let mut meta = host
196            .instantiate(module.as_ref(), WasmImports::default())
197            .map_err(|e| PluginError::WasmError(e.to_string()))?;
198
199        let init_rets = meta
200            .call("pd_plugin_init", &[])
201            .map_err(|e| PluginError::WasmError(e.to_string()))?;
202
203        let info_offset = match init_rets.first() {
204            Some(WasmVal::I32(off)) => *off as usize,
205            _ => return Err(PluginError::InitFailed),
206        };
207
208        // Read first page of WASM memory for struct parsing.
209        let memory = meta
210            .memory_read(0, 65536)
211            .map_err(|e| PluginError::WasmError(e.to_string()))?;
212
213        let (info, type_names) = parse_wasm_plugin_info(&memory, info_offset)?;
214
215        // ── Register factories ────────────────────────────────────────────────
216        let module = Arc::new(module);
217        for type_name in &type_names {
218            let mc = Arc::clone(&module);
219            let hc = Arc::clone(&host);
220            let tn = type_name.clone();
221            let pn = info.name.clone();
222            self.register_type(&pn, &tn, move |_config: &[u8]| {
223                Ok(Box::new(WasmActor::<R>::new(
224                    Arc::clone(&mc),
225                    Arc::clone(&hc),
226                    1_000_000,
227                )) as Box<dyn palladium_actor::Actor<R>>)
228            });
229        }
230
231        self.insert_loaded(LoadedPlugin::Wasm { info: info.clone() });
232        Ok(info)
233    }
234}
235
236// ── WASM metadata parsing ─────────────────────────────────────────────────────
237
238/// Parse a WASM-layout `PdPluginInfo` struct from a memory snapshot.
239///
240/// WASM modules use 32-bit pointer fields, so pointer fields are read as
241/// `u32` regardless of the host pointer width.
242///
243/// # Layout (little-endian u32 fields)
244///
245/// | Offset | Field |
246/// |--------|-------|
247/// | 0      | `name_ptr` |
248/// | 4      | `name_len` |
249/// | 8      | `version_major` |
250/// | 12     | `version_minor` |
251/// | 16     | `version_patch` |
252/// | 20     | `abi_version` |
253/// | 24     | `actor_type_count` |
254/// | 28     | `actor_types_ptr` (→ array of 16-byte type-info records) |
255fn parse_wasm_plugin_info(
256    memory: &[u8],
257    offset: usize,
258) -> Result<(PluginInfo, Vec<String>), PluginError> {
259    const INFO_SIZE: usize = 32;
260    const TYPE_INFO_SIZE: usize = 16;
261
262    if offset + INFO_SIZE > memory.len() {
263        return Err(PluginError::InitFailed);
264    }
265
266    let name_ptr = read_u32_le(memory, offset)? as usize;
267    let name_len = read_u32_le(memory, offset + 4)? as usize;
268    let ver_major = read_u32_le(memory, offset + 8)?;
269    let ver_minor = read_u32_le(memory, offset + 12)?;
270    let ver_patch = read_u32_le(memory, offset + 16)?;
271    let abi_version = read_u32_le(memory, offset + 20)?;
272    let type_count = read_u32_le(memory, offset + 24)? as usize;
273    let types_ptr = read_u32_le(memory, offset + 28)? as usize;
274
275    if abi_version != palladium_plugin_api::PD_ABI_VERSION {
276        return Err(PluginError::AbiMismatch {
277            expected: palladium_plugin_api::PD_ABI_VERSION,
278            found: abi_version,
279        });
280    }
281
282    let name = read_str(memory, name_ptr, name_len)?;
283
284    let mut type_names = Vec::with_capacity(type_count);
285    for i in 0..type_count {
286        let base = types_ptr + i * TYPE_INFO_SIZE;
287        if base + TYPE_INFO_SIZE > memory.len() {
288            return Err(PluginError::InitFailed);
289        }
290        let tn_ptr = read_u32_le(memory, base)? as usize;
291        let tn_len = read_u32_le(memory, base + 4)? as usize;
292        type_names.push(read_str(memory, tn_ptr, tn_len)?);
293    }
294
295    let info = PluginInfo {
296        name,
297        version: format!("{ver_major}.{ver_minor}.{ver_patch}"),
298        kind: PluginKind::Wasm,
299        actor_type_count: type_count,
300    };
301
302    Ok((info, type_names))
303}
304
305fn read_u32_le(memory: &[u8], offset: usize) -> Result<u32, PluginError> {
306    let b = memory
307        .get(offset..offset + 4)
308        .ok_or(PluginError::InitFailed)?;
309    Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
310}
311
312fn read_str(memory: &[u8], ptr: usize, len: usize) -> Result<String, PluginError> {
313    let bytes = memory.get(ptr..ptr + len).ok_or(PluginError::InitFailed)?;
314    std::str::from_utf8(bytes)
315        .map(|s| s.to_string())
316        .map_err(|_| PluginError::InitFailed)
317}
318
319impl<R: palladium_runtime::Reactor> Default for PluginRegistry<R> {
320    fn default() -> Self {
321        Self::new()
322    }
323}