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