Skip to main content

rustant_plugins/
loader.rs

1//! Native plugin loader — loads .so/.dll/.dylib plugins via libloading.
2//!
3//! Native plugins expose a `rustant_plugin_create` symbol that returns a boxed Plugin trait object.
4
5use crate::{Plugin, PluginError};
6use std::path::{Path, PathBuf};
7
8/// Loader for native dynamic library plugins.
9pub struct NativePluginLoader {
10    search_dirs: Vec<PathBuf>,
11}
12
13impl NativePluginLoader {
14    /// Create a new native plugin loader.
15    pub fn new() -> Self {
16        Self {
17            search_dirs: Vec::new(),
18        }
19    }
20
21    /// Add a directory to search for plugin libraries.
22    pub fn add_search_dir(&mut self, dir: impl Into<PathBuf>) {
23        self.search_dirs.push(dir.into());
24    }
25
26    /// List available plugin libraries in search directories.
27    pub fn discover(&self) -> Vec<PathBuf> {
28        let mut plugins = Vec::new();
29        for dir in &self.search_dirs {
30            if let Ok(entries) = std::fs::read_dir(dir) {
31                for entry in entries.flatten() {
32                    let path = entry.path();
33                    if is_plugin_library(&path) {
34                        plugins.push(path);
35                    }
36                }
37            }
38        }
39        plugins
40    }
41
42    /// Load a plugin from a dynamic library path.
43    ///
44    /// # Safety
45    ///
46    /// Loading native plugins executes arbitrary code. Only load trusted plugins.
47    pub unsafe fn load(&self, path: &Path) -> Result<Box<dyn Plugin>, PluginError> {
48        let lib = libloading::Library::new(path)
49            .map_err(|e| PluginError::LoadFailed(format!("{}: {}", path.display(), e)))?;
50
51        // Look for the plugin creation function
52        let create_fn: libloading::Symbol<unsafe extern "C" fn() -> *mut dyn Plugin> =
53            lib.get(b"rustant_plugin_create").map_err(|e| {
54                PluginError::LoadFailed(format!(
55                    "Symbol 'rustant_plugin_create' not found in {}: {}",
56                    path.display(),
57                    e
58                ))
59            })?;
60
61        let raw = create_fn();
62        if raw.is_null() {
63            return Err(PluginError::LoadFailed(
64                "Plugin creation function returned null".into(),
65            ));
66        }
67
68        let plugin = Box::from_raw(raw);
69
70        // Keep the library alive by leaking it (plugin owns the code)
71        std::mem::forget(lib);
72
73        Ok(plugin)
74    }
75}
76
77impl Default for NativePluginLoader {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83/// Check if a file path looks like a plugin shared library.
84fn is_plugin_library(path: &Path) -> bool {
85    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
86    matches!(ext, "so" | "dll" | "dylib")
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_is_plugin_library() {
95        assert!(is_plugin_library(Path::new("libfoo.so")));
96        assert!(is_plugin_library(Path::new("foo.dll")));
97        assert!(is_plugin_library(Path::new("libfoo.dylib")));
98        assert!(!is_plugin_library(Path::new("foo.rs")));
99        assert!(!is_plugin_library(Path::new("foo.toml")));
100        assert!(!is_plugin_library(Path::new("foo")));
101    }
102
103    #[test]
104    fn test_native_loader_discover_empty() {
105        let dir = tempfile::TempDir::new().unwrap();
106        let mut loader = NativePluginLoader::new();
107        loader.add_search_dir(dir.path());
108        let plugins = loader.discover();
109        assert!(plugins.is_empty());
110    }
111
112    #[test]
113    fn test_native_loader_discover_finds_libs() {
114        let dir = tempfile::TempDir::new().unwrap();
115
116        // Create fake library files
117        std::fs::write(dir.path().join("libplugin.so"), b"fake").unwrap();
118        std::fs::write(dir.path().join("plugin.dll"), b"fake").unwrap();
119        std::fs::write(dir.path().join("README.md"), b"docs").unwrap();
120
121        let mut loader = NativePluginLoader::new();
122        loader.add_search_dir(dir.path());
123        let plugins = loader.discover();
124        assert_eq!(plugins.len(), 2);
125    }
126
127    #[test]
128    fn test_native_loader_discover_nonexistent_dir() {
129        let mut loader = NativePluginLoader::new();
130        loader.add_search_dir("/nonexistent/path");
131        let plugins = loader.discover();
132        assert!(plugins.is_empty());
133    }
134}