sh_layer4/plugin_loader/
dylib.rs1use super::PluginMeta;
6use crate::types::Layer4Result;
7use anyhow::{anyhow, Context};
8use libloading::{Library, Symbol};
9use parking_lot::RwLock;
10use std::collections::HashMap;
11use std::path::Path;
12use std::sync::Arc;
13
14pub type PluginCreateFn = extern "C" fn() -> *mut ();
17
18pub type PluginNameFn = extern "C" fn(*const ()) -> *const std::ffi::c_char;
20
21pub type PluginVersionFn = extern "C" fn(*const ()) -> *const std::ffi::c_char;
23
24#[allow(improper_ctypes_definitions)]
28pub type PluginMetaFn = extern "C" fn() -> PluginMeta;
29
30pub type PluginInitializeFn = extern "C" fn(*mut (), *const std::ffi::c_char) -> i32;
33
34pub type PluginDestroyFn = extern "C" fn(*mut ());
36
37pub struct DylibLoader {
39 libraries: RwLock<HashMap<String, Arc<Library>>>,
41 handles: RwLock<HashMap<String, *mut ()>>,
43 metas: RwLock<HashMap<String, PluginMeta>>,
45}
46
47unsafe impl Send for DylibLoader {}
49unsafe impl Sync for DylibLoader {}
50
51impl DylibLoader {
52 pub fn new() -> Self {
54 Self {
55 libraries: RwLock::new(HashMap::new()),
56 handles: RwLock::new(HashMap::new()),
57 metas: RwLock::new(HashMap::new()),
58 }
59 }
60
61 pub fn is_valid_library(path: &Path) -> bool {
63 if !path.exists() || !path.is_file() {
64 return false;
65 }
66
67 path.extension()
68 .and_then(|ext| ext.to_str())
69 .map(|ext| matches!(ext, "so" | "dylib" | "dll"))
70 .unwrap_or(false)
71 }
72
73 pub unsafe fn load(&self, path: &Path) -> Layer4Result<(String, PluginMeta)> {
82 let path_str = path.to_string_lossy().to_string();
83
84 let library =
86 Library::new(path).with_context(|| format!("Failed to load library: {}", path_str))?;
87
88 let meta: Symbol<PluginMetaFn> = library
90 .get(b"plugin_meta")
91 .with_context(|| "Symbol 'plugin_meta' not found")?;
92 let meta = meta();
93
94 let create: Symbol<PluginCreateFn> = library
96 .get(b"plugin_create")
97 .with_context(|| "Symbol 'plugin_create' not found")?;
98
99 let handle = create();
101
102 let name = meta.name.clone();
103
104 self.libraries
106 .write()
107 .insert(name.clone(), Arc::new(library));
108 self.handles.write().insert(name.clone(), handle);
109 self.metas.write().insert(name.clone(), meta.clone());
110
111 tracing::info!(
112 "Loaded dylib plugin: {} v{} from {}",
113 meta.name,
114 meta.version,
115 path_str
116 );
117
118 Ok((name, meta))
119 }
120
121 pub fn load_safe(&self, path: &Path) -> Layer4Result<(String, PluginMeta)> {
123 if !Self::is_valid_library(path) {
124 return Err(anyhow!("Invalid library file: {:?}", path));
125 }
126
127 unsafe { self.load(path) }
129 }
130
131 pub fn get_name(&self, name: &str) -> Option<String> {
133 let meta = self.metas.read().get(name).cloned()?;
134 Some(meta.name)
135 }
136
137 pub fn get_version(&self, name: &str) -> Option<String> {
139 let meta = self.metas.read().get(name).cloned()?;
140 Some(meta.version)
141 }
142
143 pub fn get_meta(&self, name: &str) -> Option<PluginMeta> {
145 self.metas.read().get(name).cloned()
146 }
147
148 pub fn call_initialize(&self, name: &str, config_json: &str) -> Option<bool> {
153 let library = self.libraries.read().get(name).cloned()?;
154 let handle = *self.handles.read().get(name)?;
155
156 unsafe {
158 let init_fn: Symbol<PluginInitializeFn> = library.get(b"plugin_initialize").ok()?;
159 let config_cstr = std::ffi::CString::new(config_json).ok()?;
160 let result = init_fn(handle, config_cstr.as_ptr());
161 Some(result == 0)
162 }
163 }
164
165 pub fn unload(&self, name: &str) -> Layer4Result<()> {
167 let library = self.libraries.write().remove(name);
169 let handle = self.handles.write().remove(name);
170 self.metas.write().remove(name);
171
172 if let (Some(library), Some(handle)) = (library, handle) {
173 if let Ok(lib) = Arc::try_unwrap(library) {
175 unsafe {
177 if let Ok(destroy) = lib.get::<Symbol<PluginDestroyFn>>(b"plugin_destroy") {
178 destroy(handle);
179 }
180 }
181 }
182 }
183
184 tracing::info!("Unloaded dylib plugin: {}", name);
185 Ok(())
186 }
187
188 pub fn list(&self) -> Vec<String> {
190 self.metas.read().keys().cloned().collect()
191 }
192
193 pub fn is_loaded(&self, name: &str) -> bool {
195 self.metas.read().contains_key(name)
196 }
197
198 pub fn count(&self) -> usize {
200 self.metas.read().len()
201 }
202}
203
204impl Default for DylibLoader {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210#[macro_export]
233macro_rules! declare_plugin {
234 ($plugin_type:ty, $name:expr, $version:expr) => {
235 static mut PLUGIN_INSTANCE: Option<Box<$plugin_type>> = None;
236
237 #[no_mangle]
239 pub extern "C" fn plugin_create() -> *mut () {
240 unsafe {
241 PLUGIN_INSTANCE = Some(Box::new(<$plugin_type>::default()));
242 PLUGIN_INSTANCE.as_mut().unwrap().as_mut() as *mut () as *mut ()
243 }
244 }
245
246 #[no_mangle]
248 pub extern "C" fn plugin_destroy(_ptr: *mut ()) {
249 unsafe {
250 PLUGIN_INSTANCE = None;
251 }
252 }
253
254 #[no_mangle]
256 pub extern "C" fn plugin_meta() -> $crate::plugin_loader::PluginMeta {
257 $crate::plugin_loader::PluginMeta {
258 name: $name.to_string(),
259 version: $version.to_string(),
260 ..Default::default()
261 }
262 }
263
264 #[no_mangle]
267 pub extern "C" fn plugin_initialize(
268 _ptr: *mut (),
269 config_json: *const std::ffi::c_char,
270 ) -> i32 {
271 unsafe {
273 if let Some(instance) = PLUGIN_INSTANCE.as_ref() {
274 let config_str = if config_json.is_null() {
276 "{}"
277 } else {
278 std::ffi::CStr::from_ptr(config_json)
279 .to_str()
280 .unwrap_or("{}")
281 };
282
283 tracing::info!("Plugin {} initialized with config: {}", $name, config_str);
286 0 } else {
288 1 }
290 }
291 }
292 };
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_dylib_loader_creation() {
301 let loader = DylibLoader::new();
302 assert!(loader.list().is_empty());
303 assert_eq!(loader.count(), 0);
304 }
305
306 #[test]
307 fn test_is_valid_library() {
308 let tmp_so = tempfile::NamedTempFile::with_suffix(".so").unwrap();
310 assert!(DylibLoader::is_valid_library(tmp_so.path()));
311
312 let tmp_dll = tempfile::NamedTempFile::with_suffix(".dll").unwrap();
313 assert!(DylibLoader::is_valid_library(tmp_dll.path()));
314
315 let tmp_dylib = tempfile::NamedTempFile::with_suffix(".dylib").unwrap();
316 assert!(DylibLoader::is_valid_library(tmp_dylib.path()));
317
318 let tmp_txt = tempfile::NamedTempFile::with_suffix(".txt").unwrap();
319 assert!(!DylibLoader::is_valid_library(tmp_txt.path()));
320
321 assert!(!DylibLoader::is_valid_library(Path::new("/nonexistent.so")));
323 }
324
325 #[test]
326 fn test_plugin_meta_default() {
327 let meta = PluginMeta::default();
328 assert_eq!(meta.name, "unknown");
329 assert_eq!(meta.version, "0.1.0");
330 }
331
332 #[test]
333 fn test_loader_default() {
334 let loader = DylibLoader::default();
335 assert!(loader.list().is_empty());
336 }
337}