use super::core::*;
use crate::error::{OptimError, Result};
use scirs2_core::numeric::Float;
use std::any::Any;
use std::collections::HashMap;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
fn read_lock<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
lock.read().unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn write_lock<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
lock.write()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn mutex_lock<T>(lock: &Mutex<T>) -> MutexGuard<'_, T> {
lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn parse_version_triplet(version: &str) -> Option<(u64, u64, u64)> {
let core = version.split(['-', '+']).next().unwrap_or(version);
let mut parts = core.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next().unwrap_or("0").parse().ok()?;
let patch = parts.next().unwrap_or("0").parse().ok()?;
Some((major, minor, patch))
}
fn version_cmp(a: &str, b: &str) -> std::cmp::Ordering {
match (parse_version_triplet(a), parse_version_triplet(b)) {
(Some(va), Some(vb)) => va.cmp(&vb),
_ => a.cmp(b),
}
}
#[derive(Debug)]
pub struct PluginRegistry {
factories: RwLock<HashMap<String, PluginRegistration>>,
search_paths: RwLock<Vec<PathBuf>>,
config: RegistryConfig,
cache: Mutex<PluginCache>,
event_listeners: RwLock<Vec<Box<dyn RegistryEventListener>>>,
}
#[derive(Debug)]
pub struct PluginRegistration {
pub factory: Box<dyn PluginFactoryWrapper>,
pub info: PluginInfo,
pub capabilities: PluginCapabilities,
pub registered_at: std::time::SystemTime,
pub status: PluginStatus,
pub load_count: usize,
pub last_used: Option<std::time::SystemTime>,
}
pub trait PluginFactoryWrapper: Debug + Send + Sync {
fn create_f32(&self, config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f32>>>;
fn create_f64(&self, config: OptimizerConfig) -> Result<Box<dyn OptimizerPlugin<f64>>>;
fn info(&self) -> PluginInfo;
fn capabilities(&self) -> PluginCapabilities {
PluginCapabilities::default()
}
fn validate_config(&self, config: &OptimizerConfig) -> Result<()>;
fn default_config(&self) -> OptimizerConfig;
fn config_schema(&self) -> ConfigSchema;
fn supports_type(&self, datatype: &DataType) -> bool;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PluginStatus {
Active,
Disabled,
Failed(String),
Deprecated,
Maintenance,
}
#[derive(Debug, Clone)]
pub struct RegistryConfig {
pub auto_discovery: bool,
pub validate_on_registration: bool,
pub enable_caching: bool,
pub max_cache_size: usize,
pub load_timeout: std::time::Duration,
pub enable_sandboxing: bool,
pub allowed_sources: Vec<PluginSource>,
}
#[derive(Debug, Clone)]
pub enum PluginSource {
BuiltIn,
Local(PathBuf),
Remote(String),
Package(String),
}
#[derive(Debug)]
pub struct PluginCache {
instances: HashMap<String, CachedPlugin>,
stats: CacheStats,
next_sequence: u64,
}
#[derive(Debug)]
pub struct CachedPlugin {
pub plugin: Box<dyn OptimizerPlugin<f64>>,
pub config: OptimizerConfig,
pub cached_at: std::time::SystemTime,
pub access_count: usize,
pub last_accessed: std::time::SystemTime,
pub(super) sequence: u64,
}
#[derive(Debug, Default, Clone)]
pub struct CacheStats {
pub hits: usize,
pub misses: usize,
pub evictions: usize,
pub memory_used: usize,
}
pub trait RegistryEventListener: Debug + Send + Sync {
fn on_plugin_registered(&mut self, _info: &PluginInfo) {}
fn on_plugin_unregistered(&mut self, _name: &str) {}
fn on_plugin_loaded(&mut self, _name: &str) {}
fn on_plugin_load_failed(&mut self, _name: &str, _error: &str) {}
fn on_plugin_status_changed(&mut self, _name: &str, _status: &PluginStatus) {}
}
#[derive(Debug, Clone, Default)]
pub struct PluginQuery {
pub name_pattern: Option<String>,
pub category: Option<PluginCategory>,
pub required_capabilities: Vec<String>,
pub data_types: Vec<DataType>,
pub version_requirements: Option<VersionRequirement>,
pub tags: Vec<String>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct VersionRequirement {
pub min_version: Option<String>,
pub max_version: Option<String>,
pub exact_version: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PluginSearchResult {
pub plugins: Vec<PluginInfo>,
pub total_count: usize,
pub query: PluginQuery,
pub search_time: std::time::Duration,
}
impl PluginRegistry {
pub fn new(config: RegistryConfig) -> Self {
Self {
factories: RwLock::new(HashMap::new()),
search_paths: RwLock::new(Vec::new()),
config,
cache: Mutex::new(PluginCache::new()),
event_listeners: RwLock::new(Vec::new()),
}
}
pub fn global() -> &'static Self {
static INSTANCE: std::sync::OnceLock<PluginRegistry> = std::sync::OnceLock::new();
INSTANCE.get_or_init(|| {
let config = RegistryConfig::default();
let mut registry = PluginRegistry::new(config);
registry.register_builtin_plugins();
registry
})
}
pub fn register_plugin<F>(&self, factory: F) -> Result<()>
where
F: PluginFactoryWrapper + 'static,
{
let info = factory.info();
let name = info.name.clone();
if self.config.validate_on_registration {
self.validate_plugin(&factory)?;
}
let capabilities = factory.capabilities();
let registration = PluginRegistration {
factory: Box::new(factory),
info: info.clone(),
capabilities,
registered_at: std::time::SystemTime::now(),
status: PluginStatus::Active,
load_count: 0,
last_used: None,
};
{
let mut factories = write_lock(&self.factories);
factories.insert(name.clone(), registration);
}
{
let mut listeners = write_lock(&self.event_listeners);
for listener in listeners.iter_mut() {
listener.on_plugin_registered(&info);
}
}
Ok(())
}
pub fn unregister_plugin(&self, name: &str) -> Result<()> {
let mut factories = write_lock(&self.factories);
if factories.remove(name).is_some() {
drop(factories);
let mut listeners = write_lock(&self.event_listeners);
for listener in listeners.iter_mut() {
listener.on_plugin_unregistered(name);
}
Ok(())
} else {
Err(OptimError::PluginNotFound(name.to_string()))
}
}
pub fn create_optimizer<A>(
&self,
name: &str,
config: OptimizerConfig,
) -> Result<Box<dyn OptimizerPlugin<A>>>
where
A: Float + Debug + Send + Sync + 'static,
{
let mut factories = write_lock(&self.factories);
let registration = factories
.get(name)
.ok_or_else(|| OptimError::PluginNotFound(name.to_string()))?;
match registration.status {
PluginStatus::Active => {}
PluginStatus::Disabled => {
return Err(OptimError::PluginDisabled(name.to_string()));
}
PluginStatus::Failed(ref error) => {
return Err(OptimError::PluginLoadError(error.clone()));
}
PluginStatus::Deprecated => {
log::warn!("Plugin '{}' is deprecated", name);
}
PluginStatus::Maintenance => {
return Err(OptimError::PluginInMaintenance(name.to_string()));
}
}
registration.factory.validate_config(&config)?;
let optimizer = if std::any::TypeId::of::<A>() == std::any::TypeId::of::<f32>() {
let opt = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
registration.factory.create_f32(config)
}))
.map_err(|_| {
OptimError::PluginLoadError(format!(
"plugin '{name}' panicked while creating an f32 optimizer"
))
})??;
let boxed_any: Box<dyn Any> = Box::new(opt);
*boxed_any
.downcast::<Box<dyn OptimizerPlugin<A>>>()
.map_err(|_| {
OptimError::UnsupportedDataType(
"internal error: f32 downcast failed".to_string(),
)
})?
} else if std::any::TypeId::of::<A>() == std::any::TypeId::of::<f64>() {
let use_cache = self.config.enable_caching;
let cached_hit = if use_cache {
let mut cache = mutex_lock(&self.cache);
cache.get_or_record_miss(name, &config)?
} else {
None
};
let opt_f64: Box<dyn OptimizerPlugin<f64>> = if let Some(hit) = cached_hit {
hit
} else {
let config_for_cache = use_cache.then(|| config.clone());
let created = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
registration.factory.create_f64(config)
}))
.map_err(|_| {
OptimError::PluginLoadError(format!(
"plugin '{name}' panicked while creating an f64 optimizer"
))
})??;
if let Some(cache_config) = config_for_cache {
let created_ref = &created;
let cloned_for_cache =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
created_ref.clone_plugin()
}))
.map_err(|_| {
OptimError::PluginLoadError(format!(
"plugin '{name}' panicked while cloning a newly created f64 \
optimizer for the cache"
))
})?;
let mut cache = mutex_lock(&self.cache);
cache.insert(
name.to_string(),
cloned_for_cache,
cache_config,
self.config.max_cache_size,
);
}
created
};
let boxed_any: Box<dyn Any> = Box::new(opt_f64);
*boxed_any
.downcast::<Box<dyn OptimizerPlugin<A>>>()
.map_err(|_| {
OptimError::UnsupportedDataType(
"internal error: f64 downcast failed".to_string(),
)
})?
} else {
return Err(OptimError::UnsupportedDataType(format!(
"Type {} not supported",
std::any::type_name::<A>()
)));
};
if let Some(registration) = factories.get_mut(name) {
registration.load_count += 1;
registration.last_used = Some(std::time::SystemTime::now());
}
drop(factories);
let mut listeners = write_lock(&self.event_listeners);
for listener in listeners.iter_mut() {
listener.on_plugin_loaded(name);
}
Ok(optimizer)
}
pub fn list_plugins(&self) -> Vec<PluginInfo> {
let factories = read_lock(&self.factories);
factories.values().map(|reg| reg.info.clone()).collect()
}
pub fn search_plugins(&self, query: PluginQuery) -> PluginSearchResult {
let start_time = std::time::Instant::now();
let factories = read_lock(&self.factories);
let mut matching_plugins = Vec::new();
for registration in factories.values() {
if self.matches_query(®istration.info, ®istration.capabilities, &query) {
matching_plugins.push(registration.info.clone());
}
}
let total_count = matching_plugins.len();
if let Some(limit) = query.limit {
matching_plugins.truncate(limit);
}
let search_time = start_time.elapsed();
PluginSearchResult {
plugins: matching_plugins,
total_count,
query,
search_time,
}
}
pub fn get_plugin_info(&self, name: &str) -> Option<PluginInfo> {
let factories = read_lock(&self.factories);
factories.get(name).map(|reg| reg.info.clone())
}
pub fn get_plugin_status(&self, name: &str) -> Option<PluginStatus> {
let factories = read_lock(&self.factories);
factories.get(name).map(|reg| reg.status.clone())
}
pub fn set_plugin_status(&self, name: &str, status: PluginStatus) -> Result<()> {
let mut factories = write_lock(&self.factories);
let registration = factories
.get_mut(name)
.ok_or_else(|| OptimError::PluginNotFound(name.to_string()))?;
let old_status = registration.status.clone();
registration.status = status.clone();
if old_status != status {
drop(factories);
let mut listeners = write_lock(&self.event_listeners);
for listener in listeners.iter_mut() {
listener.on_plugin_status_changed(name, &status);
}
}
Ok(())
}
pub fn add_search_path<P: AsRef<Path>>(&self, path: P) {
let mut search_paths = write_lock(&self.search_paths);
search_paths.push(path.as_ref().to_path_buf());
}
pub fn discover_plugins(&self) -> Result<usize> {
if !self.config.auto_discovery {
return Ok(0);
}
let search_paths = read_lock(&self.search_paths);
let mut discovered_count = 0;
for path in search_paths.iter() {
if path.exists() && path.is_dir() {
discovered_count += self.discover_plugins_in_directory(path)?;
}
}
Ok(discovered_count)
}
pub fn add_event_listener(&self, listener: Box<dyn RegistryEventListener>) {
let mut listeners = write_lock(&self.event_listeners);
listeners.push(listener);
}
pub fn get_cache_stats(&self) -> CacheStats {
let cache = mutex_lock(&self.cache);
cache.stats.clone()
}
pub fn clear_cache(&self) {
let mut cache = mutex_lock(&self.cache);
cache.instances.clear();
cache.stats = CacheStats::default();
}
fn validate_plugin(&self, factory: &dyn PluginFactoryWrapper) -> Result<()> {
let config = factory.default_config();
let _optimizer = factory.create_f64(config)?;
Ok(())
}
fn matches_query(
&self,
info: &PluginInfo,
capabilities: &PluginCapabilities,
query: &PluginQuery,
) -> bool {
if let Some(ref pattern) = query.name_pattern {
if !info.name.contains(pattern) {
return false;
}
}
if let Some(ref category) = query.category {
if info.category != *category {
return false;
}
}
if !query.data_types.is_empty() {
let has_common_type = query
.data_types
.iter()
.any(|dt| info.supported_types.contains(dt));
if !has_common_type {
return false;
}
}
if !query.tags.is_empty() {
let has_common_tag = query.tags.iter().any(|tag| info.tags.contains(tag));
if !has_common_tag {
return false;
}
}
if let Some(ref version_req) = query.version_requirements {
if !self.version_matches(&info.version, version_req) {
return false;
}
}
if !query
.required_capabilities
.iter()
.all(|cap| capabilities.has_capability(cap))
{
return false;
}
true
}
fn version_matches(&self, version: &str, requirement: &VersionRequirement) -> bool {
if let Some(ref exact) = requirement.exact_version {
return version == exact;
}
if let Some(ref min) = requirement.min_version {
if version_cmp(version, min) == std::cmp::Ordering::Less {
return false;
}
}
if let Some(ref max) = requirement.max_version {
if version_cmp(version, max) != std::cmp::Ordering::Less {
return false;
}
}
true
}
fn discover_plugins_in_directory(&self, path: &Path) -> Result<usize> {
let mut count = 0;
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let entry_path = entry.path();
if entry_path.is_dir() {
count += self.discover_plugins_in_directory(&entry_path)?;
continue;
}
let is_candidate = match entry_path.extension().and_then(|e| e.to_str()) {
Some("so") | Some("dylib") | Some("dll") => true,
_ => entry_path.file_name().and_then(|n| n.to_str()) == Some("plugin.toml"),
};
if is_candidate {
count += 1;
}
}
Ok(count)
}
fn register_builtin_plugins(&mut self) {}
}
impl PluginCache {
fn new() -> Self {
Self {
instances: HashMap::new(),
stats: CacheStats::default(),
next_sequence: 0,
}
}
fn get_or_record_miss(
&mut self,
name: &str,
config: &OptimizerConfig,
) -> Result<Option<Box<dyn OptimizerPlugin<f64>>>> {
if let Some(entry) = self.instances.get_mut(name) {
if &entry.config == config {
self.next_sequence += 1;
entry.access_count += 1;
entry.last_accessed = std::time::SystemTime::now();
entry.sequence = self.next_sequence;
let plugin = &entry.plugin;
let clone_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
plugin.clone_plugin()
}));
return match clone_result {
Ok(cloned) => {
self.stats.hits += 1;
Ok(Some(cloned))
}
Err(_) => {
self.instances.remove(name);
self.stats.evictions += 1;
Err(OptimError::PluginLoadError(format!(
"plugin '{name}' panicked while cloning a cached f64 optimizer; \
the poisoned cache entry has been evicted"
)))
}
};
}
}
self.stats.misses += 1;
Ok(None)
}
fn insert(
&mut self,
name: String,
plugin: Box<dyn OptimizerPlugin<f64>>,
config: OptimizerConfig,
max_size: usize,
) {
if max_size == 0 {
return;
}
if !self.instances.contains_key(&name) && self.instances.len() >= max_size {
self.evict_lru();
}
self.next_sequence += 1;
let now = std::time::SystemTime::now();
self.instances.insert(
name,
CachedPlugin {
plugin,
config,
cached_at: now,
access_count: 1,
last_accessed: now,
sequence: self.next_sequence,
},
);
self.recompute_memory_used();
}
fn evict_lru(&mut self) {
let lru_name = self
.instances
.iter()
.min_by_key(|(_, cached)| cached.sequence)
.map(|(name, _)| name.clone());
if let Some(lru_name) = lru_name {
self.instances.remove(&lru_name);
self.stats.evictions += 1;
self.recompute_memory_used();
}
}
fn recompute_memory_used(&mut self) {
self.stats.memory_used = self
.instances
.values()
.map(|cached| std::mem::size_of_val(&*cached.plugin))
.sum();
}
#[cfg(test)]
fn len(&self) -> usize {
self.instances.len()
}
}
impl Default for RegistryConfig {
fn default() -> Self {
Self {
auto_discovery: true,
validate_on_registration: true,
enable_caching: true,
max_cache_size: 100,
load_timeout: std::time::Duration::from_secs(30),
enable_sandboxing: false,
allowed_sources: vec![
PluginSource::BuiltIn,
PluginSource::Local(PathBuf::from("./plugins")),
],
}
}
}
#[macro_export]
macro_rules! register_optimizer_plugin {
($factory:expr) => {
$crate::plugin::PluginRegistry::global().register_plugin($factory)?
};
}
pub struct PluginQueryBuilder {
query: PluginQuery,
}
impl Default for PluginQueryBuilder {
fn default() -> Self {
Self::new()
}
}
impl PluginQueryBuilder {
pub fn new() -> Self {
Self {
query: PluginQuery::default(),
}
}
pub fn name_pattern(mut self, pattern: &str) -> Self {
self.query.name_pattern = Some(pattern.to_string());
self
}
pub fn category(mut self, category: PluginCategory) -> Self {
self.query.category = Some(category);
self
}
pub fn data_type(mut self, datatype: DataType) -> Self {
self.query.data_types.push(datatype);
self
}
pub fn tag(mut self, tag: &str) -> Self {
self.query.tags.push(tag.to_string());
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.query.limit = Some(limit);
self
}
pub fn build(self) -> PluginQuery {
self.query
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_registry_creation() {
let config = RegistryConfig::default();
let registry = PluginRegistry::new(config);
assert_eq!(registry.list_plugins().len(), 0);
}
#[test]
fn test_plugin_query_builder() {
let query = PluginQueryBuilder::new()
.name_pattern("adam")
.category(PluginCategory::FirstOrder)
.data_type(DataType::F32)
.limit(10)
.build();
assert_eq!(query.name_pattern, Some("adam".to_string()));
assert_eq!(query.category, Some(PluginCategory::FirstOrder));
assert_eq!(query.limit, Some(10));
}
#[test]
fn discover_plugins_counts_real_files_on_disk() {
let root = std::env::temp_dir().join(format!(
"optirs_registry_discover_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
let nested = root.join("nested");
std::fs::create_dir_all(&nested).expect("create temp dir tree");
std::fs::write(root.join("plugin.toml"), "[plugin]\nname = \"x\"").expect("write");
std::fs::write(root.join("libfoo.so"), b"not a real library").expect("write");
std::fs::write(root.join("readme.txt"), b"not a plugin").expect("write");
std::fs::write(nested.join("bar.dylib"), b"not a real library").expect("write");
let config = RegistryConfig {
auto_discovery: true,
..RegistryConfig::default()
};
let registry = PluginRegistry::new(config);
registry.add_search_path(&root);
let discovered = registry
.discover_plugins()
.expect("discovery should succeed");
assert_eq!(
discovered, 3,
"expected plugin.toml + libfoo.so + nested/bar.dylib, not readme.txt"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn discover_plugins_is_a_noop_when_auto_discovery_disabled() {
let root = std::env::temp_dir().join(format!(
"optirs_registry_discover_disabled_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&root).expect("create temp dir");
std::fs::write(root.join("plugin.toml"), "[plugin]\nname = \"x\"").expect("write");
let config = RegistryConfig {
auto_discovery: false,
..RegistryConfig::default()
};
let registry = PluginRegistry::new(config);
registry.add_search_path(&root);
assert_eq!(registry.discover_plugins().expect("should succeed"), 0);
let _ = std::fs::remove_dir_all(&root);
}
}
#[cfg(test)]
mod regression_tests;