rustant_plugins/
loader.rs1use crate::{Plugin, PluginError};
6use std::path::{Path, PathBuf};
7
8pub struct NativePluginLoader {
10 search_dirs: Vec<PathBuf>,
11}
12
13impl NativePluginLoader {
14 pub fn new() -> Self {
16 Self {
17 search_dirs: Vec::new(),
18 }
19 }
20
21 pub fn add_search_dir(&mut self, dir: impl Into<PathBuf>) {
23 self.search_dirs.push(dir.into());
24 }
25
26 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 pub unsafe fn load(&self, path: &Path) -> Result<Box<dyn Plugin>, PluginError> {
48 let lib = unsafe {
50 libloading::Library::new(path)
51 .map_err(|e| PluginError::LoadFailed(format!("{}: {}", path.display(), e)))?
52 };
53
54 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 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 let plugin = unsafe { Box::from_raw(raw) };
76
77 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
90fn 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 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}