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}