use super::PluginMeta;
use crate::types::Layer4Result;
use anyhow::{anyhow, Context};
use libloading::{Library, Symbol};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
pub type PluginCreateFn = extern "C" fn() -> *mut ();
pub type PluginNameFn = extern "C" fn(*const ()) -> *const std::ffi::c_char;
pub type PluginVersionFn = extern "C" fn(*const ()) -> *const std::ffi::c_char;
#[allow(improper_ctypes_definitions)]
pub type PluginMetaFn = extern "C" fn() -> PluginMeta;
pub type PluginInitializeFn = extern "C" fn(*mut (), *const std::ffi::c_char) -> i32;
pub type PluginDestroyFn = extern "C" fn(*mut ());
pub struct DylibLoader {
libraries: RwLock<HashMap<String, Arc<Library>>>,
handles: RwLock<HashMap<String, *mut ()>>,
metas: RwLock<HashMap<String, PluginMeta>>,
}
unsafe impl Send for DylibLoader {}
unsafe impl Sync for DylibLoader {}
impl DylibLoader {
pub fn new() -> Self {
Self {
libraries: RwLock::new(HashMap::new()),
handles: RwLock::new(HashMap::new()),
metas: RwLock::new(HashMap::new()),
}
}
pub fn is_valid_library(path: &Path) -> bool {
if !path.exists() || !path.is_file() {
return false;
}
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| matches!(ext, "so" | "dylib" | "dll"))
.unwrap_or(false)
}
pub unsafe fn load(&self, path: &Path) -> Layer4Result<(String, PluginMeta)> {
let path_str = path.to_string_lossy().to_string();
let library =
Library::new(path).with_context(|| format!("Failed to load library: {}", path_str))?;
let meta: Symbol<PluginMetaFn> = library
.get(b"plugin_meta")
.with_context(|| "Symbol 'plugin_meta' not found")?;
let meta = meta();
let create: Symbol<PluginCreateFn> = library
.get(b"plugin_create")
.with_context(|| "Symbol 'plugin_create' not found")?;
let handle = create();
let name = meta.name.clone();
self.libraries
.write()
.insert(name.clone(), Arc::new(library));
self.handles.write().insert(name.clone(), handle);
self.metas.write().insert(name.clone(), meta.clone());
tracing::info!(
"Loaded dylib plugin: {} v{} from {}",
meta.name,
meta.version,
path_str
);
Ok((name, meta))
}
pub fn load_safe(&self, path: &Path) -> Layer4Result<(String, PluginMeta)> {
if !Self::is_valid_library(path) {
return Err(anyhow!("Invalid library file: {:?}", path));
}
unsafe { self.load(path) }
}
pub fn get_name(&self, name: &str) -> Option<String> {
let meta = self.metas.read().get(name).cloned()?;
Some(meta.name)
}
pub fn get_version(&self, name: &str) -> Option<String> {
let meta = self.metas.read().get(name).cloned()?;
Some(meta.version)
}
pub fn get_meta(&self, name: &str) -> Option<PluginMeta> {
self.metas.read().get(name).cloned()
}
pub fn call_initialize(&self, name: &str, config_json: &str) -> Option<bool> {
let library = self.libraries.read().get(name).cloned()?;
let handle = *self.handles.read().get(name)?;
unsafe {
let init_fn: Symbol<PluginInitializeFn> = library.get(b"plugin_initialize").ok()?;
let config_cstr = std::ffi::CString::new(config_json).ok()?;
let result = init_fn(handle, config_cstr.as_ptr());
Some(result == 0)
}
}
pub fn unload(&self, name: &str) -> Layer4Result<()> {
let library = self.libraries.write().remove(name);
let handle = self.handles.write().remove(name);
self.metas.write().remove(name);
if let (Some(library), Some(handle)) = (library, handle) {
if let Ok(lib) = Arc::try_unwrap(library) {
unsafe {
if let Ok(destroy) = lib.get::<Symbol<PluginDestroyFn>>(b"plugin_destroy") {
destroy(handle);
}
}
}
}
tracing::info!("Unloaded dylib plugin: {}", name);
Ok(())
}
pub fn list(&self) -> Vec<String> {
self.metas.read().keys().cloned().collect()
}
pub fn is_loaded(&self, name: &str) -> bool {
self.metas.read().contains_key(name)
}
pub fn count(&self) -> usize {
self.metas.read().len()
}
}
impl Default for DylibLoader {
fn default() -> Self {
Self::new()
}
}
#[macro_export]
macro_rules! declare_plugin {
($plugin_type:ty, $name:expr, $version:expr) => {
static mut PLUGIN_INSTANCE: Option<Box<$plugin_type>> = None;
#[no_mangle]
pub extern "C" fn plugin_create() -> *mut () {
unsafe {
PLUGIN_INSTANCE = Some(Box::new(<$plugin_type>::default()));
PLUGIN_INSTANCE.as_mut().unwrap().as_mut() as *mut () as *mut ()
}
}
#[no_mangle]
pub extern "C" fn plugin_destroy(_ptr: *mut ()) {
unsafe {
PLUGIN_INSTANCE = None;
}
}
#[no_mangle]
pub extern "C" fn plugin_meta() -> $crate::plugin_loader::PluginMeta {
$crate::plugin_loader::PluginMeta {
name: $name.to_string(),
version: $version.to_string(),
..Default::default()
}
}
#[no_mangle]
pub extern "C" fn plugin_initialize(
_ptr: *mut (),
config_json: *const std::ffi::c_char,
) -> i32 {
unsafe {
if let Some(instance) = PLUGIN_INSTANCE.as_ref() {
let config_str = if config_json.is_null() {
"{}"
} else {
std::ffi::CStr::from_ptr(config_json)
.to_str()
.unwrap_or("{}")
};
tracing::info!("Plugin {} initialized with config: {}", $name, config_str);
0 } else {
1 }
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dylib_loader_creation() {
let loader = DylibLoader::new();
assert!(loader.list().is_empty());
assert_eq!(loader.count(), 0);
}
#[test]
fn test_is_valid_library() {
let tmp_so = tempfile::NamedTempFile::with_suffix(".so").unwrap();
assert!(DylibLoader::is_valid_library(tmp_so.path()));
let tmp_dll = tempfile::NamedTempFile::with_suffix(".dll").unwrap();
assert!(DylibLoader::is_valid_library(tmp_dll.path()));
let tmp_dylib = tempfile::NamedTempFile::with_suffix(".dylib").unwrap();
assert!(DylibLoader::is_valid_library(tmp_dylib.path()));
let tmp_txt = tempfile::NamedTempFile::with_suffix(".txt").unwrap();
assert!(!DylibLoader::is_valid_library(tmp_txt.path()));
assert!(!DylibLoader::is_valid_library(Path::new("/nonexistent.so")));
}
#[test]
fn test_plugin_meta_default() {
let meta = PluginMeta::default();
assert_eq!(meta.name, "unknown");
assert_eq!(meta.version, "0.1.0");
}
#[test]
fn test_loader_default() {
let loader = DylibLoader::default();
assert!(loader.list().is_empty());
}
}