use crate::errors::{Result, TrustformersError};
use crate::plugins::{Plugin, PluginInfo};
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex};
#[derive(Debug)]
pub struct PluginLoader {
library_cache: Arc<Mutex<HashMap<String, LibraryHandle>>>,
static_plugins: Arc<Mutex<HashMap<String, StaticPluginFactory>>>,
cache_hits: Arc<Mutex<u64>>,
cache_misses: Arc<Mutex<u64>>,
#[allow(dead_code)]
config: LoaderConfig,
}
impl PluginLoader {
pub fn new() -> Self {
Self {
library_cache: Arc::new(Mutex::new(HashMap::new())),
static_plugins: Arc::new(Mutex::new(HashMap::new())),
cache_hits: Arc::new(Mutex::new(0)),
cache_misses: Arc::new(Mutex::new(0)),
config: LoaderConfig::default(),
}
}
pub fn with_config(config: LoaderConfig) -> Self {
Self {
library_cache: Arc::new(Mutex::new(HashMap::new())),
static_plugins: Arc::new(Mutex::new(HashMap::new())),
cache_hits: Arc::new(Mutex::new(0)),
cache_misses: Arc::new(Mutex::new(0)),
config,
}
}
pub fn load_plugin_info<P: AsRef<Path>>(&self, path: P) -> Result<PluginInfo> {
let path = path.as_ref();
let metadata_path = path.with_extension("json");
if metadata_path.exists() {
return self.load_metadata_file(&metadata_path);
}
self.load_embedded_metadata(path)
}
pub fn load_plugin(&self, info: &PluginInfo) -> Result<Box<dyn Plugin>> {
if let Ok(static_plugins) = self.static_plugins.lock() {
if let Some(factory) = static_plugins.get(info.name()) {
return factory();
}
}
self.load_dynamic_plugin(info)
}
pub fn register_static_plugin(&self, name: &str, factory: StaticPluginFactory) -> Result<()> {
let mut static_plugins = self
.static_plugins
.lock()
.map_err(|_| TrustformersError::lock_error("Failed to acquire lock".to_string()))?;
static_plugins.insert(name.to_string(), factory);
Ok(())
}
pub fn unload_library(&self, name: &str) -> Result<()> {
let mut cache = self
.library_cache
.lock()
.map_err(|_| TrustformersError::lock_error("Failed to acquire lock".to_string()))?;
cache.remove(name);
Ok(())
}
pub fn clear_cache(&self) -> Result<()> {
let mut cache = self
.library_cache
.lock()
.map_err(|_| TrustformersError::lock_error("Failed to acquire lock".to_string()))?;
cache.clear();
Ok(())
}
pub fn stats(&self) -> Result<LoaderStats> {
let cache = self
.library_cache
.lock()
.map_err(|_| TrustformersError::lock_error("Failed to acquire lock".to_string()))?;
let static_plugins = self
.static_plugins
.lock()
.map_err(|_| TrustformersError::lock_error("Failed to acquire lock".to_string()))?;
let cache_hits = self
.cache_hits
.lock()
.map_err(|_| TrustformersError::lock_error("Failed to acquire lock".to_string()))?;
let cache_misses = self
.cache_misses
.lock()
.map_err(|_| TrustformersError::lock_error("Failed to acquire lock".to_string()))?;
Ok(LoaderStats {
cached_libraries: cache.len(),
static_plugins: static_plugins.len(),
cache_hits: *cache_hits,
cache_misses: *cache_misses,
})
}
fn load_metadata_file<P: AsRef<Path>>(&self, path: P) -> Result<PluginInfo> {
let content = std::fs::read_to_string(path)
.map_err(|e| TrustformersError::io_error(format!("Failed to read metadata: {}", e)))?;
serde_json::from_str(&content)
.map_err(|e| TrustformersError::serialization_error(format!("Invalid metadata: {}", e)))
}
fn load_embedded_metadata<P: AsRef<Path>>(&self, path: P) -> Result<PluginInfo> {
let path = path.as_ref();
Err(TrustformersError::plugin_error(format!(
"{} has no companion metadata file and this loader cannot read embedded metadata; \
provide {}.json describing the plugin (name, version, description, dependencies)",
path.display(),
path.with_extension("").display()
)))
}
fn load_dynamic_plugin(&self, info: &PluginInfo) -> Result<Box<dyn Plugin>> {
{
let cache = self
.library_cache
.lock()
.map_err(|_| TrustformersError::lock_error("Failed to acquire lock".to_string()))?;
if let Some(handle) = cache.get(info.name()) {
if let Ok(mut hits) = self.cache_hits.lock() {
*hits += 1;
}
return handle.create_plugin();
}
}
if let Ok(mut misses) = self.cache_misses.lock() {
*misses += 1;
}
let handle = LibraryHandle::load(info)?;
let plugin = handle.create_plugin()?;
{
let mut cache = self
.library_cache
.lock()
.map_err(|_| TrustformersError::lock_error("Failed to acquire lock".to_string()))?;
cache.insert(info.name().to_string(), handle);
}
Ok(plugin)
}
}
impl Default for PluginLoader {
fn default() -> Self {
Self::new()
}
}
pub type StaticPluginFactory = fn() -> Result<Box<dyn Plugin>>;
#[derive(Debug)]
struct LibraryHandle {
#[allow(dead_code)]
name: String,
_entry_point: String,
}
impl LibraryHandle {
fn load(info: &PluginInfo) -> Result<Self> {
Err(TrustformersError::plugin_error(format!(
"cannot load plugin '{}' from {}: dynamic library loading is not implemented in \
trustformers-core. Register the plugin with \
PluginLoader::register_static_plugin instead.",
info.name(),
info.entry_point()
)))
}
fn create_plugin(&self) -> Result<Box<dyn Plugin>> {
Err(TrustformersError::plugin_error(format!(
"cannot instantiate plugin '{}': dynamic symbol resolution is not implemented",
self.name
)))
}
}
#[derive(Debug, Clone)]
pub struct LoaderConfig {
pub cache_enabled: bool,
pub max_cached_libraries: usize,
pub load_timeout_secs: u64,
pub lazy_loading: bool,
pub symbol_prefix: String,
}
impl Default for LoaderConfig {
fn default() -> Self {
Self {
cache_enabled: true,
max_cached_libraries: 50,
load_timeout_secs: 30,
lazy_loading: true,
symbol_prefix: "create_plugin".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct LoaderStats {
pub cached_libraries: usize,
pub static_plugins: usize,
pub cache_hits: u64,
pub cache_misses: u64,
}
#[derive(Debug, Clone)]
pub enum LoadError {
LibraryNotFound(String),
SymbolNotFound(String),
InitializationFailed(String),
InvalidFormat(String),
VersionMismatch(String),
DependencyNotSatisfied(String),
}
impl std::fmt::Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LoadError::LibraryNotFound(path) => write!(f, "Library not found: {}", path),
LoadError::SymbolNotFound(symbol) => write!(f, "Symbol not found: {}", symbol),
LoadError::InitializationFailed(msg) => write!(f, "Initialization failed: {}", msg),
LoadError::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
LoadError::VersionMismatch(msg) => write!(f, "Version mismatch: {}", msg),
LoadError::DependencyNotSatisfied(dep) => {
write!(f, "Dependency not satisfied: {}", dep)
},
}
}
}
impl std::error::Error for LoadError {}
#[macro_export]
macro_rules! register_static_plugin {
($plugin_type:ty, $name:expr) => {
pub fn register_plugin() -> $crate::errors::Result<Box<dyn $crate::plugins::Plugin>> {
Ok(Box::new(<$plugin_type>::default()))
}
#[cfg(feature = "static-plugins")]
#[ctor::ctor]
fn register() {
use $crate::plugins::PluginLoader;
let loader = PluginLoader::new();
let _ = loader.register_static_plugin($name, register_plugin);
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Default, Clone)]
struct StaticTestPlugin {
config: HashMap<String, serde_json::Value>,
}
impl Plugin for StaticTestPlugin {
fn name(&self) -> &str {
"static-one"
}
fn version(&self) -> &str {
"1.0.0"
}
fn description(&self) -> &str {
"statically registered test plugin"
}
fn configure(&mut self, config: HashMap<String, serde_json::Value>) -> Result<()> {
self.config = config;
Ok(())
}
fn get_config(&self) -> &HashMap<String, serde_json::Value> {
&self.config
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn forward(&self, input: crate::tensor::Tensor) -> Result<crate::tensor::Tensor> {
Ok(input)
}
}
fn make_static_plugin() -> Result<Box<dyn Plugin>> {
Ok(Box::new(StaticTestPlugin::default()))
}
#[test]
fn test_dynamic_loading_is_refused_and_nothing_is_cached() {
let loader = PluginLoader::new();
let info = PluginInfo::new("ghost", "0.1.0", "never loaded", &[]);
let error = loader.load_plugin(&info).expect_err("no dynamic loader is linked");
let message = error.to_string();
assert!(
message.contains("register_static_plugin"),
"the error must point at the supported path: {message}"
);
assert!(
!message.contains("in this example"),
"user-facing errors must not mention an example: {message}"
);
let cache = loader.library_cache.lock().expect("lock");
assert!(
cache.is_empty(),
"a failed load must not populate the cache"
);
}
#[test]
fn test_embedded_metadata_is_refused() {
let loader = PluginLoader::new();
let path =
std::env::temp_dir().join(format!("trustformers_plugin_{}.so", std::process::id()));
std::fs::write(&path, b"not a real plugin").expect("write failed");
let error = loader
.load_plugin_info(&path)
.expect_err("no metadata file exists and none can be read from the binary");
assert!(
error.to_string().contains(".json"),
"the error must say what is missing: {error}"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_companion_metadata_is_read() {
let loader = PluginLoader::new();
let base =
std::env::temp_dir().join(format!("trustformers_plugin_meta_{}", std::process::id()));
let library = base.with_extension("so");
let metadata = base.with_extension("json");
std::fs::write(&library, b"binary").expect("write failed");
std::fs::write(
&metadata,
serde_json::to_string(&PluginInfo::new("real", "2.1.0", "declared", &[]))
.expect("serialize failed"),
)
.expect("write failed");
let info = loader.load_plugin_info(&library).expect("metadata file must be read");
assert_eq!(info.name(), "real");
assert_eq!(
info.version().to_string(),
"2.1.0",
"the version must come from the file, not a hardcoded 1.0.0"
);
std::fs::remove_file(&library).ok();
std::fs::remove_file(&metadata).ok();
}
#[test]
fn test_static_plugins_still_load() {
let loader = PluginLoader::new();
loader
.register_static_plugin("static-one", make_static_plugin)
.expect("registration failed");
let info = PluginInfo::new("static-one", "1.0.0", "static", &[]);
assert!(
loader.load_plugin(&info).is_ok(),
"static registration is the supported path and must work"
);
}
}