sklears_core/plugin/
loader.rs

1//! Plugin Loader
2//!
3//! This module provides dynamic library loading capabilities for the plugin system.
4//! It enables loading plugins from shared libraries at runtime with proper
5//! lifecycle management and error handling.
6
7use super::registry::PluginRegistry;
8#[cfg(feature = "dynamic_loading")]
9use super::Plugin;
10use crate::error::{Result, SklearsError};
11#[cfg(feature = "dynamic_loading")]
12use std::collections::HashMap;
13use std::sync::Arc;
14
15/// Plugin loader for dynamic library loading
16///
17/// The PluginLoader provides functionality to load plugins from dynamic libraries
18/// at runtime. It manages the lifecycle of loaded libraries and integrates with
19/// the plugin registry for automatic plugin registration.
20///
21/// # Features
22///
23/// - Dynamic library loading from files or directories
24/// - Automatic plugin discovery and registration
25/// - Proper library lifecycle management
26/// - Cross-platform support (Windows DLL, Linux SO, macOS dylib)
27/// - Error handling and validation
28///
29/// # Examples
30///
31/// ```rust,ignore
32/// use sklears_core::plugin::{PluginLoader, PluginRegistry};
33/// use std::sync::Arc;
34///
35/// let registry = Arc::new(PluginRegistry::new());
36/// let mut loader = PluginLoader::new(registry.clone());
37///
38/// // Load a single plugin
39/// # #[cfg(feature = "dynamic_loading")]
40/// loader.load_from_library("./plugins/my_plugin.so", "my_plugin")?;
41///
42/// // Load all plugins from a directory
43/// # #[cfg(feature = "dynamic_loading")]
44/// let loaded = loader.load_from_directory("./plugins/")?;
45/// println!("Loaded {} plugins", loaded.len());
46/// # Ok::<(), Box<dyn std::error::Error>>(())
47/// ```
48///
49/// # Safety
50///
51/// Dynamic library loading involves unsafe operations. The plugin loader
52/// takes precautions to ensure safety, but loaded plugins must be trusted
53/// and properly implemented.
54#[allow(dead_code)]
55#[derive(Debug)]
56pub struct PluginLoader {
57    /// Loaded libraries (plugin_id -> library)
58    /// This field is only available when the dynamic_loading feature is enabled
59    #[cfg(feature = "dynamic_loading")]
60    libraries: HashMap<String, libloading::Library>,
61
62    /// Plugin registry for automatic registration
63    registry: Arc<PluginRegistry>,
64}
65
66impl PluginLoader {
67    /// Create a new plugin loader
68    ///
69    /// Creates a new plugin loader that will register loaded plugins
70    /// with the provided registry.
71    ///
72    /// # Arguments
73    ///
74    /// * `registry` - The plugin registry to use for registration
75    ///
76    /// # Examples
77    ///
78    /// ```rust
79    /// use sklears_core::plugin::{PluginLoader, PluginRegistry};
80    /// use std::sync::Arc;
81    ///
82    /// let registry = Arc::new(PluginRegistry::new());
83    /// let loader = PluginLoader::new(registry);
84    /// ```
85    pub fn new(registry: Arc<PluginRegistry>) -> Self {
86        Self {
87            #[cfg(feature = "dynamic_loading")]
88            libraries: HashMap::new(),
89            registry,
90        }
91    }
92
93    /// Load a plugin from a dynamic library
94    ///
95    /// This method loads a plugin from the specified library file and
96    /// registers it with the given plugin ID. The library must export
97    /// a `create_plugin` function that returns a boxed plugin instance.
98    ///
99    /// # Arguments
100    ///
101    /// * `library_path` - Path to the dynamic library file
102    /// * `plugin_id` - Unique identifier for the plugin
103    ///
104    /// # Returns
105    ///
106    /// Ok(()) on successful loading and registration, or an error if
107    /// the library cannot be loaded or the plugin cannot be created.
108    ///
109    /// # Safety
110    ///
111    /// This function uses unsafe code to load and call functions from
112    /// dynamic libraries. The loaded library must be trusted and properly
113    /// implement the expected plugin interface.
114    ///
115    /// # Examples
116    ///
117    /// ```rust,ignore
118    /// use sklears_core::plugin::{PluginLoader, PluginRegistry};
119    /// use std::sync::Arc;
120    ///
121    /// let registry = Arc::new(PluginRegistry::new());
122    /// let mut loader = PluginLoader::new(registry);
123    ///
124    /// # #[cfg(feature = "dynamic_loading")]
125    /// loader.load_from_library("./libmy_plugin.so", "my_plugin")?;
126    /// # Ok::<(), Box<dyn std::error::Error>>(())
127    /// ```
128    #[cfg(feature = "dynamic_loading")]
129    pub fn load_from_library(&mut self, library_path: &str, plugin_id: &str) -> Result<()> {
130        unsafe {
131            // Load the dynamic library
132            let lib = libloading::Library::new(library_path).map_err(|e| {
133                SklearsError::InvalidOperation(format!(
134                    "Failed to load library '{}': {}",
135                    library_path, e
136                ))
137            })?;
138
139            // Get the plugin creation function
140            // The library must export a function with this exact signature
141            let create_plugin: libloading::Symbol<fn() -> Box<dyn Plugin>> =
142                lib.get(b"create_plugin").map_err(|e| {
143                    SklearsError::InvalidOperation(format!(
144                        "Failed to get create_plugin symbol from '{}': {}",
145                        library_path, e
146                    ))
147                })?;
148
149            // Create the plugin instance
150            let plugin = create_plugin();
151
152            // Validate the plugin ID matches what's expected
153            let _plugin_metadata = plugin.metadata();
154            if plugin.id() != plugin_id {
155                return Err(SklearsError::InvalidOperation(format!(
156                    "Plugin ID mismatch: expected '{}', got '{}'",
157                    plugin_id,
158                    plugin.id()
159                )));
160            }
161
162            // Register the plugin with the registry
163            self.registry.register(plugin_id, plugin).map_err(|e| {
164                SklearsError::InvalidOperation(format!(
165                    "Failed to register plugin '{}': {}",
166                    plugin_id, e
167                ))
168            })?;
169
170            // Store the library to keep it loaded
171            // This prevents the library from being unloaded while the plugin is in use
172            self.libraries.insert(plugin_id.to_string(), lib);
173
174            Ok(())
175        }
176    }
177
178    /// Unload a plugin library
179    ///
180    /// This method unregisters the plugin and unloads its associated library.
181    /// The plugin's cleanup method will be called before unloading.
182    ///
183    /// # Arguments
184    ///
185    /// * `plugin_id` - The ID of the plugin to unload
186    ///
187    /// # Returns
188    ///
189    /// Ok(()) on successful unloading, or an error if the operation fails.
190    ///
191    /// # Examples
192    ///
193    /// ```rust,ignore
194    /// use sklears_core::plugin::{PluginLoader, PluginRegistry};
195    /// use std::sync::Arc;
196    ///
197    /// let registry = Arc::new(PluginRegistry::new());
198    /// let mut loader = PluginLoader::new(registry);
199    ///
200    /// // Load a plugin first
201    /// # #[cfg(feature = "dynamic_loading")]
202    /// loader.load_from_library("./libmy_plugin.so", "my_plugin")?;
203    ///
204    /// // Then unload it
205    /// # #[cfg(feature = "dynamic_loading")]
206    /// loader.unload_library("my_plugin")?;
207    /// # Ok::<(), Box<dyn std::error::Error>>(())
208    /// ```
209    #[cfg(feature = "dynamic_loading")]
210    pub fn unload_library(&mut self, plugin_id: &str) -> Result<()> {
211        // Unregister the plugin first (this will call cleanup)
212        self.registry.unregister(plugin_id).map_err(|e| {
213            SklearsError::InvalidOperation(format!(
214                "Failed to unregister plugin '{}': {}",
215                plugin_id, e
216            ))
217        })?;
218
219        // Remove the library (this will unload it when dropped)
220        if self.libraries.remove(plugin_id).is_none() {
221            return Err(SklearsError::InvalidOperation(format!(
222                "Library for plugin '{}' was not found",
223                plugin_id
224            )));
225        }
226
227        Ok(())
228    }
229
230    /// Load plugins from a directory
231    ///
232    /// This method scans a directory for dynamic libraries and attempts
233    /// to load plugins from each one. It recognizes common library extensions
234    /// (.so, .dll, .dylib) and uses the filename (without extension) as the plugin ID.
235    ///
236    /// # Arguments
237    ///
238    /// * `directory` - Path to the directory containing plugin libraries
239    ///
240    /// # Returns
241    ///
242    /// A vector of successfully loaded plugin IDs, or an error if the
243    /// directory cannot be read.
244    ///
245    /// # Examples
246    ///
247    /// ```rust,ignore
248    /// use sklears_core::plugin::{PluginLoader, PluginRegistry};
249    /// use std::sync::Arc;
250    ///
251    /// let registry = Arc::new(PluginRegistry::new());
252    /// let mut loader = PluginLoader::new(registry);
253    ///
254    /// # #[cfg(feature = "dynamic_loading")]
255    /// let loaded_plugins = loader.load_from_directory("./plugins/")?;
256    /// println!("Successfully loaded {} plugins", loaded_plugins.len());
257    /// for plugin_id in &loaded_plugins {
258    ///     println!("  - {}", plugin_id);
259    /// }
260    /// # Ok::<(), Box<dyn std::error::Error>>(())
261    /// ```
262    #[cfg(feature = "dynamic_loading")]
263    pub fn load_from_directory(&mut self, directory: &str) -> Result<Vec<String>> {
264        use std::fs;
265
266        // Read the directory contents
267        let entries = fs::read_dir(directory).map_err(|e| {
268            SklearsError::InvalidOperation(format!(
269                "Failed to read directory '{}': {}",
270                directory, e
271            ))
272        })?;
273
274        let mut loaded_plugins = Vec::new();
275
276        for entry in entries {
277            let entry = entry.map_err(|e| {
278                SklearsError::InvalidOperation(format!("Failed to read directory entry: {}", e))
279            })?;
280
281            let path = entry.path();
282
283            // Only process files (not subdirectories)
284            if path.is_file() {
285                // Check for known library extensions
286                if let Some(extension) = path.extension() {
287                    let ext_str = extension.to_string_lossy().to_lowercase();
288                    if ext_str == "so" || ext_str == "dll" || ext_str == "dylib" {
289                        // Use the filename (without extension) as the plugin ID
290                        if let Some(plugin_id) = path.file_stem().and_then(|s| s.to_str()) {
291                            match self.load_from_library(path.to_str().unwrap(), plugin_id) {
292                                Ok(()) => {
293                                    loaded_plugins.push(plugin_id.to_string());
294                                    println!("Successfully loaded plugin: {}", plugin_id);
295                                }
296                                Err(e) => {
297                                    eprintln!(
298                                        "Failed to load plugin '{}' from '{}': {}",
299                                        plugin_id,
300                                        path.display(),
301                                        e
302                                    );
303                                    // Continue loading other plugins despite failures
304                                }
305                            }
306                        } else {
307                            eprintln!(
308                                "Could not determine plugin ID from filename: {}",
309                                path.display()
310                            );
311                        }
312                    }
313                }
314            }
315        }
316
317        Ok(loaded_plugins)
318    }
319
320    /// Get the list of loaded library plugin IDs
321    ///
322    /// Returns a vector of plugin IDs for all currently loaded libraries.
323    ///
324    /// # Returns
325    ///
326    /// A vector of plugin IDs for loaded libraries.
327    #[cfg(feature = "dynamic_loading")]
328    pub fn get_loaded_libraries(&self) -> Vec<String> {
329        self.libraries.keys().cloned().collect()
330    }
331
332    /// Check if a library is loaded
333    ///
334    /// # Arguments
335    ///
336    /// * `plugin_id` - The plugin ID to check
337    ///
338    /// # Returns
339    ///
340    /// true if the library is loaded, false otherwise.
341    #[cfg(feature = "dynamic_loading")]
342    pub fn is_library_loaded(&self, plugin_id: &str) -> bool {
343        self.libraries.contains_key(plugin_id)
344    }
345
346    /// Get the plugin registry
347    ///
348    /// Returns a reference to the plugin registry used by this loader.
349    ///
350    /// # Returns
351    ///
352    /// A reference to the plugin registry.
353    pub fn registry(&self) -> &Arc<PluginRegistry> {
354        &self.registry
355    }
356
357    /// Unload all libraries
358    ///
359    /// This method unloads all currently loaded libraries and unregisters
360    /// all their associated plugins.
361    ///
362    /// # Returns
363    ///
364    /// Ok(()) on success, or an error if any unload operation fails.
365    #[cfg(feature = "dynamic_loading")]
366    pub fn unload_all(&mut self) -> Result<()> {
367        let plugin_ids: Vec<String> = self.libraries.keys().cloned().collect();
368
369        for plugin_id in plugin_ids.into_iter() {
370            if let Err(e) = self.unload_library(&plugin_id) {
371                eprintln!("Failed to unload plugin '{}': {}", plugin_id, e);
372                // Continue unloading other plugins despite failures
373            }
374        }
375
376        Ok(())
377    }
378
379    /// Get statistics about loaded libraries
380    ///
381    /// Returns information about the current state of the loader.
382    ///
383    /// # Returns
384    ///
385    /// A tuple of (number of loaded libraries, list of plugin IDs).
386    #[cfg(feature = "dynamic_loading")]
387    pub fn get_statistics(&self) -> (usize, Vec<String>) {
388        let plugin_ids = self.get_loaded_libraries();
389        (plugin_ids.len(), plugin_ids)
390    }
391}
392
393/// Stub implementations for when dynamic loading is not available
394#[cfg(not(feature = "dynamic_loading"))]
395impl PluginLoader {
396    /// Load a plugin from a dynamic library (stub implementation)
397    ///
398    /// This method is not available when the `dynamic_loading` feature is disabled.
399    /// It will always return an error indicating that dynamic loading is not supported.
400    pub fn load_from_library(&mut self, _library_path: &str, _plugin_id: &str) -> Result<()> {
401        Err(SklearsError::InvalidOperation(
402            "Dynamic loading is not enabled. Rebuild with the 'dynamic_loading' feature to use this functionality.".to_string()
403        ))
404    }
405
406    /// Unload a plugin library (stub implementation)
407    ///
408    /// This method is not available when the `dynamic_loading` feature is disabled.
409    pub fn unload_library(&mut self, _plugin_id: &str) -> Result<()> {
410        Err(SklearsError::InvalidOperation(
411            "Dynamic loading is not enabled. Rebuild with the 'dynamic_loading' feature to use this functionality.".to_string()
412        ))
413    }
414
415    /// Load plugins from a directory (stub implementation)
416    ///
417    /// This method is not available when the `dynamic_loading` feature is disabled.
418    pub fn load_from_directory(&mut self, _directory: &str) -> Result<Vec<String>> {
419        Err(SklearsError::InvalidOperation(
420            "Dynamic loading is not enabled. Rebuild with the 'dynamic_loading' feature to use this functionality.".to_string()
421        ))
422    }
423}