Skip to main content

minion_engine/plugins/
loader.rs

1use anyhow::{Context as _, Result};
2use libloading::Library;
3
4use super::PluginStep;
5
6/// Loads a plugin from a shared library (.so / .dylib) file.
7///
8/// The library must export a C-ABI function with the signature:
9/// ```c
10/// extern "C" fn create_plugin() -> *mut dyn PluginStep;
11/// ```
12pub struct PluginLoader {
13    /// Keeps the loaded library alive for as long as the loader exists
14    _libraries: Vec<Library>,
15}
16
17impl PluginLoader {
18    pub fn new() -> Self {
19        Self {
20            _libraries: Vec::new(),
21        }
22    }
23
24    /// Load a plugin from the given path.
25    ///
26    /// # Safety
27    /// Loading and calling foreign functions from shared libraries is inherently
28    /// unsafe. The caller must ensure the library is a valid minion plugin.
29    pub fn load_plugin(path: &str) -> Result<Box<dyn PluginStep>> {
30        // SAFETY: We are loading a shared library that is expected to expose
31        // a `create_plugin` symbol following the documented ABI.
32        unsafe {
33            let lib =
34                Library::new(path).with_context(|| format!("Failed to load library at '{path}'"))?;
35
36            let constructor: libloading::Symbol<unsafe extern "C" fn() -> *mut dyn PluginStep> =
37                lib.get(b"create_plugin\0").with_context(|| {
38                    format!("Library '{path}' does not export 'create_plugin' symbol")
39                })?;
40
41            let raw = constructor();
42            if raw.is_null() {
43                anyhow::bail!("Plugin constructor in '{path}' returned null pointer");
44            }
45
46            // Transfer ownership. The Library must outlive the plugin — for
47            // production use you would want to keep the Library somewhere. Here
48            // we intentionally leak it (acceptable for long-lived plugins) so
49            // that the vtable references remain valid.
50            std::mem::forget(lib);
51
52            Ok(Box::from_raw(raw))
53        }
54    }
55}
56
57impl Default for PluginLoader {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62