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        // SAFETY: The caller guarantees the plugin library at `path` is trusted.
49        let lib = unsafe {
50            libloading::Library::new(path)
51                .map_err(|e| PluginError::LoadFailed(format!("{}: {}", path.display(), e)))?
52        };
53
54        // Look for the plugin creation function
55        // SAFETY: We trust the symbol exists with the expected signature in the loaded library.
56        let create_fn: libloading::Symbol<unsafe extern "C" fn() -> *mut dyn Plugin> = unsafe {
57            lib.get(b"rustant_plugin_create").map_err(|e| {
58                PluginError::LoadFailed(format!(
59                    "Symbol 'rustant_plugin_create' not found in {}: {}",
60                    path.display(),
61                    e
62                ))
63            })?
64        };
65
66        // SAFETY: The create function is provided by the trusted plugin.
67        let raw = unsafe { create_fn() };
68        if raw.is_null() {
69            return Err(PluginError::LoadFailed(
70                "Plugin creation function returned null".into(),
71            ));
72        }
73
74        // SAFETY: `raw` is non-null and was allocated by the plugin's create function.
75        let plugin = unsafe { Box::from_raw(raw) };
76
77        // Keep the library alive by leaking it (plugin owns the code)
78        std::mem::forget(lib);
79
80        Ok(plugin)
81    }
82}
83
84impl Default for NativePluginLoader {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90/// Check if a file path looks like a plugin shared library.
91fn is_plugin_library(path: &Path) -> bool {
92    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
93    matches!(ext, "so" | "dll" | "dylib")
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_is_plugin_library() {
102        assert!(is_plugin_library(Path::new("libfoo.so")));
103        assert!(is_plugin_library(Path::new("foo.dll")));
104        assert!(is_plugin_library(Path::new("libfoo.dylib")));
105        assert!(!is_plugin_library(Path::new("foo.rs")));
106        assert!(!is_plugin_library(Path::new("foo.toml")));
107        assert!(!is_plugin_library(Path::new("foo")));
108    }
109
110    #[test]
111    fn test_native_loader_discover_empty() {
112        let dir = tempfile::TempDir::new().unwrap();
113        let mut loader = NativePluginLoader::new();
114        loader.add_search_dir(dir.path());
115        let plugins = loader.discover();
116        assert!(plugins.is_empty());
117    }
118
119    #[test]
120    fn test_native_loader_discover_finds_libs() {
121        let dir = tempfile::TempDir::new().unwrap();
122
123        // Create fake library files
124        std::fs::write(dir.path().join("libplugin.so"), b"fake").unwrap();
125        std::fs::write(dir.path().join("plugin.dll"), b"fake").unwrap();
126        std::fs::write(dir.path().join("README.md"), b"docs").unwrap();
127
128        let mut loader = NativePluginLoader::new();
129        loader.add_search_dir(dir.path());
130        let plugins = loader.discover();
131        assert_eq!(plugins.len(), 2);
132    }
133
134    #[test]
135    fn test_native_loader_discover_nonexistent_dir() {
136        let mut loader = NativePluginLoader::new();
137        loader.add_search_dir("/nonexistent/path");
138        let plugins = loader.discover();
139        assert!(plugins.is_empty());
140    }
141}