Skip to main content

mimium_lang/plugin/
loader.rs

1//! Dynamic plugin loader for native targets.
2//!
3//! This module provides functionality to load mimium plugins from shared
4//! libraries (DLL/SO/DYLIB) at runtime. Plugins must export a standard set of
5//! C-compatible functions that allow the runtime to discover and register them.
6//!
7//! # Plugin ABI
8//!
9//! A plugin library must export the following functions:
10//!
11//! - `mimium_plugin_metadata() -> *const PluginMetadata`
12//!   Returns plugin name, version, and capabilities.
13//!
14//! - `mimium_plugin_create() -> *mut PluginInstance`
15//!   Creates a new instance of the plugin.
16//!
17//! - `mimium_plugin_destroy(instance: *mut PluginInstance)`
18//!   Destroys a plugin instance.
19//!
20//! # Safety
21//!
22//! Loading plugins is inherently unsafe as it involves executing arbitrary code
23//! from dynamic libraries. The loader performs basic validation but cannot
24//! guarantee plugin correctness or safety.
25
26#[cfg(not(target_arch = "wasm32"))]
27use std::ffi::{CStr, c_char, c_void};
28#[cfg(not(target_arch = "wasm32"))]
29use std::path::{Path, PathBuf};
30
31#[cfg(not(target_arch = "wasm32"))]
32use libloading::{Library, Symbol};
33
34// -------------------------------------------------------------------------
35// Plugin interface types
36// -------------------------------------------------------------------------
37
38/// Metadata describing a plugin.
39///
40/// This struct is returned by the plugin's `mimium_plugin_metadata()` function
41/// and provides basic information about the plugin.
42#[repr(C)]
43#[derive(Debug, Clone)]
44pub struct PluginMetadata {
45    /// Plugin name (null-terminated UTF-8).
46    pub name: *const c_char,
47    /// Plugin version string (null-terminated UTF-8).
48    pub version: *const c_char,
49    /// Author name (null-terminated UTF-8).
50    pub author: *const c_char,
51    /// Plugin capabilities flags.
52    pub capabilities: PluginCapabilities,
53}
54
55// SAFETY: PluginMetadata contains only pointers to static strings that are
56// guaranteed to outlive the program, so it is safe to share across threads.
57#[cfg(not(target_arch = "wasm32"))]
58unsafe impl Sync for PluginMetadata {}
59
60// SAFETY: PluginMetadata contains only pointers to static strings that are
61// guaranteed to outlive the program, so it is safe to send across threads.
62#[cfg(not(target_arch = "wasm32"))]
63unsafe impl Send for PluginMetadata {}
64
65/// Flags describing plugin capabilities.
66#[repr(C)]
67#[derive(Debug, Clone, Copy)]
68pub struct PluginCapabilities {
69    /// Plugin provides audio processing (has `on_sample` callback).
70    pub has_audio_worker: bool,
71    /// Plugin provides compile-time macros.
72    pub has_macros: bool,
73    /// Plugin provides runtime functions.
74    pub has_runtime_functions: bool,
75}
76
77/// Opaque handle to a plugin instance.
78///
79/// The actual structure is defined by the plugin library; the runtime only
80/// manipulates it through the plugin's exported functions.
81#[repr(C)]
82pub struct PluginInstance {
83    _private: [u8; 0],
84}
85
86/// Type signature for plugin functions that use RuntimeHandle.
87///
88/// Plugin functions receive a mutable reference to the plugin instance and
89/// a RuntimeHandle for accessing arguments and setting return values.
90pub type PluginFunctionFn = unsafe extern "C" fn(
91    instance: *mut PluginInstance,
92    runtime: *mut c_void, // RuntimeHandle as opaque pointer
93) -> i64; // ReturnCode
94
95/// Type signature for plugin macro functions.
96///
97/// Macro functions are compile-time transformations that take serialized
98/// arguments and return a serialized result. The signature is:
99///
100/// - `instance`: Mutable pointer to the plugin instance
101/// - `args_ptr`: Pointer to serialized arguments (bincode-encoded `Vec<(Value, TypeNodeId)>`)
102/// - `args_len`: Length of the serialized arguments buffer
103/// - `out_ptr`: Output pointer for the serialized result buffer
104/// - `out_len`: Output length of the serialized result buffer
105/// - Returns: Status code (0 = success, negative = error)
106pub type PluginMacroFn = unsafe extern "C" fn(
107    instance: *mut c_void,
108    args_ptr: *const u8,
109    args_len: usize,
110    out_ptr: *mut *mut u8,
111    out_len: *mut usize,
112) -> i32;
113
114// -------------------------------------------------------------------------
115// Plugin function signatures
116// -------------------------------------------------------------------------
117
118/// Type of the `mimium_plugin_metadata` export.
119type PluginMetadataFn = unsafe extern "C" fn() -> *const PluginMetadata;
120
121/// Type of the `mimium_plugin_create` export.
122type PluginCreateFn = unsafe extern "C" fn() -> *mut PluginInstance;
123
124/// Type of the `mimium_plugin_destroy` export.
125type PluginDestroyFn = unsafe extern "C" fn(instance: *mut PluginInstance);
126
127/// Type of the `mimium_plugin_set_interner` export.
128///
129/// Shares the host's session globals with the plugin so that interned IDs
130/// (TypeNodeId, ExprNodeId, Symbol) are valid across the DLL boundary.
131type PluginSetInternerFn = unsafe extern "C" fn(globals_ptr: *const std::ffi::c_void);
132
133/// Type of the `mimium_plugin_get_function` export.
134///
135/// Returns a function pointer for the named plugin function, or null if not found.
136type PluginGetFunctionFn = unsafe extern "C" fn(name: *const c_char) -> Option<PluginFunctionFn>;
137
138/// Type of the `mimium_plugin_get_macro` export.
139///
140/// Returns a function pointer for the named macro function, or null if not found.
141type PluginGetMacroFn = unsafe extern "C" fn(name: *const c_char) -> Option<PluginMacroFn>;
142
143/// FFI-safe representation of type information.
144///
145/// Used to pass type information from plugins to the host.
146#[repr(C)]
147#[derive(Debug, Clone)]
148pub struct FfiTypeInfo {
149    /// Function/macro name (null-terminated UTF-8).
150    pub name: *const c_char,
151    /// Serialized type (bincode-encoded TypeNodeId).
152    pub type_data: *const u8,
153    /// Length of serialized type data.
154    pub type_len: usize,
155    /// Stage where this function is available (0=Macro, 1=Machine, 2=Persistent).
156    pub stage: u8,
157}
158
159// SAFETY: FfiTypeInfo contains only pointers to data that outlives the plugin,
160// so it is safe to share across threads.
161#[cfg(not(target_arch = "wasm32"))]
162unsafe impl Sync for FfiTypeInfo {}
163
164#[cfg(not(target_arch = "wasm32"))]
165unsafe impl Send for FfiTypeInfo {}
166
167/// Type of the `mimium_plugin_get_type_infos` export.
168///
169/// Returns an array of type information structures.
170/// `out_len` is set to the number of elements in the returned array.
171type PluginGetTypeInfosFn = unsafe extern "C" fn(out_len: *mut usize) -> *const FfiTypeInfo;
172
173// -------------------------------------------------------------------------
174// Loaded plugin handle
175// -------------------------------------------------------------------------
176
177/// A loaded plugin with its library and exported functions.
178#[cfg(not(target_arch = "wasm32"))]
179pub struct LoadedPlugin {
180    /// The dynamic library handle (kept alive for the lifetime of the plugin).
181    _library: Library,
182    /// Plugin metadata.
183    metadata: PluginMetadata,
184    /// Plugin instance pointer.
185    instance: *mut PluginInstance,
186    /// Destroy function pointer (called on drop).
187    destroy_fn: PluginDestroyFn,
188    /// Function lookup function pointer (optional).
189    get_function_fn: Option<PluginGetFunctionFn>,
190    /// Macro function lookup function pointer (optional).
191    get_macro_fn: Option<PluginGetMacroFn>,
192    /// Type information lookup function pointer (optional).
193    get_type_infos_fn: Option<PluginGetTypeInfosFn>,
194}
195
196#[cfg(not(target_arch = "wasm32"))]
197impl LoadedPlugin {
198    /// Get plugin metadata.
199    pub fn metadata(&self) -> &PluginMetadata {
200        &self.metadata
201    }
202
203    /// Get the plugin name as a Rust string.
204    pub fn name(&self) -> String {
205        unsafe { CStr::from_ptr(self.metadata.name) }
206            .to_string_lossy()
207            .into_owned()
208    }
209
210    /// Get the plugin version as a Rust string.
211    pub fn version(&self) -> String {
212        unsafe { CStr::from_ptr(self.metadata.version) }
213            .to_string_lossy()
214            .into_owned()
215    }
216
217    /// Get a plugin function by name.
218    ///
219    /// Returns `None` if the function is not found or if the plugin doesn't
220    /// support function lookup.
221    pub fn get_function(&self, name: &str) -> Option<PluginFunctionFn> {
222        let get_fn = self.get_function_fn?;
223        let name_cstr = std::ffi::CString::new(name).ok()?;
224        unsafe { get_fn(name_cstr.as_ptr()) }
225    }
226
227    /// Get a plugin macro function by name.
228    ///
229    /// Returns `None` if the macro is not found or if the plugin doesn't
230    /// support macro function lookup.
231    pub fn get_macro(&self, name: &str) -> Option<PluginMacroFn> {
232        let get_fn = self.get_macro_fn?;
233        let name_cstr = std::ffi::CString::new(name).ok()?;
234        unsafe { get_fn(name_cstr.as_ptr()) }
235    }
236
237    /// Get type information from the plugin.
238    ///
239    /// Returns a vector of type information if the plugin supports it.
240    pub fn get_type_infos(&self) -> Option<Vec<crate::plugin::ExtFunTypeInfo>> {
241        use crate::interner::{ToSymbol, TypeNodeId};
242        use crate::plugin::{EvalStage, ExtFunTypeInfo};
243
244        let get_fn = self.get_type_infos_fn?;
245        let mut len: usize = 0;
246        let array_ptr = unsafe { get_fn(&mut len as *mut usize) };
247
248        if array_ptr.is_null() || len == 0 {
249            crate::log::debug!("Plugin {} has no type info or returned null", self.name());
250            return None;
251        }
252
253        crate::log::debug!("Plugin {} provided {} type info entries", self.name(), len);
254        let mut result = Vec::with_capacity(len);
255        for i in 0..len {
256            let info = unsafe { &*array_ptr.add(i) };
257
258            // Convert C string to Rust string
259            let name_str = unsafe { CStr::from_ptr(info.name) }
260                .to_string_lossy()
261                .into_owned();
262            let name = name_str.to_symbol();
263
264            // Deserialize type data
265            let type_slice = unsafe { std::slice::from_raw_parts(info.type_data, info.type_len) };
266            let ty: TypeNodeId = match bincode::deserialize(type_slice) {
267                Ok(t) => t,
268                Err(e) => {
269                    crate::log::warn!("Failed to deserialize type for {name_str}: {e:?}");
270                    continue;
271                }
272            };
273
274            // Convert stage number to EvalStage
275            let stage = match info.stage {
276                0 => EvalStage::Stage(0),   // Macro stage (compile-time)
277                1 => EvalStage::Stage(1),   // Machine stage (runtime)
278                2 => EvalStage::Persistent, // Persistent stage
279                _ => {
280                    crate::log::warn!("Unknown stage {} for {}", info.stage, name_str);
281                    continue;
282                }
283            };
284
285            result.push(ExtFunTypeInfo::new(name, ty, stage));
286        }
287
288        Some(result)
289    }
290
291    /// Get the plugin instance pointer (for advanced use).
292    ///
293    /// # Safety
294    ///
295    /// The returned pointer is valid for the lifetime of this LoadedPlugin.
296    pub unsafe fn instance_ptr(&self) -> *mut PluginInstance {
297        self.instance
298    }
299}
300
301#[cfg(not(target_arch = "wasm32"))]
302impl Drop for LoadedPlugin {
303    fn drop(&mut self) {
304        // SAFETY: instance is valid and was created by this plugin's create function.
305        unsafe {
306            (self.destroy_fn)(self.instance);
307        }
308    }
309}
310
311// -------------------------------------------------------------------------
312// Dynamic plugin macro wrapper
313// -------------------------------------------------------------------------
314
315/// Wrapper for dynamically loaded plugin macro functions.
316///
317/// This implements the `MacroFunction` trait by calling the FFI bridge
318/// and serializing/deserializing arguments and results.
319#[cfg(not(target_arch = "wasm32"))]
320pub struct DynPluginMacroInfo {
321    name: crate::interner::Symbol,
322    ty: crate::interner::TypeNodeId,
323    /// Plugin instance pointer
324    instance: *mut PluginInstance,
325    /// Macro function pointer
326    macro_fn: PluginMacroFn,
327}
328
329#[cfg(not(target_arch = "wasm32"))]
330impl DynPluginMacroInfo {
331    /// Create a new dynamic plugin macro wrapper.
332    ///
333    /// # Safety
334    ///
335    /// - `instance` must be a valid pointer to the plugin instance
336    /// - `macro_fn` must be a valid function pointer for the macro
337    /// - Both must remain valid for the lifetime of this struct
338    pub unsafe fn new(
339        name: crate::interner::Symbol,
340        ty: crate::interner::TypeNodeId,
341        instance: *mut PluginInstance,
342        macro_fn: PluginMacroFn,
343    ) -> Self {
344        Self {
345            name,
346            ty,
347            instance,
348            macro_fn,
349        }
350    }
351}
352
353#[cfg(not(target_arch = "wasm32"))]
354impl crate::plugin::MacroFunction for DynPluginMacroInfo {
355    fn get_name(&self) -> crate::interner::Symbol {
356        self.name
357    }
358
359    fn get_type(&self) -> crate::interner::TypeNodeId {
360        self.ty
361    }
362
363    fn get_fn(&self) -> crate::plugin::MacroFunType {
364        use std::cell::RefCell;
365        use std::rc::Rc;
366
367        let instance = self.instance;
368        let macro_fn = self.macro_fn;
369
370        Rc::new(RefCell::new(
371            move |args: &[(crate::interpreter::Value, crate::interner::TypeNodeId)]| {
372                use crate::runtime::ffi_serde::{deserialize_value, serialize_macro_args};
373
374                // Serialize arguments
375                let args_bytes = match serialize_macro_args(args) {
376                    Ok(b) => b,
377                    Err(e) => {
378                        crate::log::error!("Failed to serialize macro arguments: {e}");
379                        let err_expr = crate::ast::Expr::Error
380                            .into_id(crate::utils::metadata::Location::internal());
381                        return crate::interpreter::Value::ErrorV(err_expr);
382                    }
383                };
384
385                // Prepare output buffers
386                let mut out_ptr: *mut u8 = std::ptr::null_mut();
387                let mut out_len: usize = 0;
388
389                // Call FFI function
390                let result_code = unsafe {
391                    macro_fn(
392                        instance as *mut c_void,
393                        args_bytes.as_ptr(),
394                        args_bytes.len(),
395                        &mut out_ptr,
396                        &mut out_len,
397                    )
398                };
399
400                if result_code != 0 {
401                    crate::log::error!(
402                        "Dynamic plugin macro function returned error code: {result_code}"
403                    );
404                    let err_expr = crate::ast::Expr::Error
405                        .into_id(crate::utils::metadata::Location::internal());
406                    return crate::interpreter::Value::ErrorV(err_expr);
407                }
408
409                if out_ptr.is_null() || out_len == 0 {
410                    crate::log::error!("Dynamic plugin macro function returned null/empty result");
411                    let err_expr = crate::ast::Expr::Error
412                        .into_id(crate::utils::metadata::Location::internal());
413                    return crate::interpreter::Value::ErrorV(err_expr);
414                }
415
416                // Deserialize result
417                let out_bytes = unsafe { std::slice::from_raw_parts(out_ptr, out_len) };
418                let result = match deserialize_value(out_bytes) {
419                    Ok(v) => v,
420                    Err(e) => {
421                        crate::log::error!("Failed to deserialize macro result: {e}");
422                        let err_expr = crate::ast::Expr::Error
423                            .into_id(crate::utils::metadata::Location::internal());
424                        crate::interpreter::Value::ErrorV(err_expr)
425                    }
426                };
427
428                // Clean up allocated output buffer
429                unsafe {
430                    let _ = Box::from_raw(std::slice::from_raw_parts_mut(out_ptr, out_len));
431                }
432
433                result
434            },
435        ))
436    }
437}
438
439// SAFETY: DynPluginMacroInfo doesn't implement Send/Sync by default due to raw pointers,
440// but in our use case:
441// - The plugin instance is guaranteed to be valid for the macro's lifetime
442// - Macro functions are only called from the compiler thread, never concurrently
443// - The LoadedPlugin that owns the instance is kept alive by PluginLoader
444#[cfg(not(target_arch = "wasm32"))]
445unsafe impl Send for DynPluginMacroInfo {}
446#[cfg(not(target_arch = "wasm32"))]
447unsafe impl Sync for DynPluginMacroInfo {}
448
449// -------------------------------------------------------------------------
450// Dynamic plugin runtime function wrapper
451// -------------------------------------------------------------------------
452
453/// Wrapper for dynamically loaded plugin runtime functions.
454///
455/// This implements the `MachineFunction` trait by calling the FFI bridge.
456#[cfg(not(target_arch = "wasm32"))]
457pub struct DynPluginFunctionInfo {
458    name: crate::interner::Symbol,
459    instance: *mut PluginInstance,
460    function_fn: PluginFunctionFn,
461}
462
463#[cfg(not(target_arch = "wasm32"))]
464impl DynPluginFunctionInfo {
465    /// Create a new dynamic plugin function wrapper.
466    ///
467    /// # Safety
468    ///
469    /// - `instance` must be a valid pointer to the plugin instance
470    /// - `function_fn` must be a valid function pointer
471    /// - Both must remain valid for the lifetime of this struct
472    pub unsafe fn new(
473        name: crate::interner::Symbol,
474        instance: *mut PluginInstance,
475        function_fn: PluginFunctionFn,
476    ) -> Self {
477        Self {
478            name,
479            instance,
480            function_fn,
481        }
482    }
483}
484
485#[cfg(not(target_arch = "wasm32"))]
486impl crate::plugin::MachineFunction for DynPluginFunctionInfo {
487    fn get_name(&self) -> crate::interner::Symbol {
488        self.name
489    }
490
491    fn get_fn(&self) -> crate::plugin::ExtClsType {
492        use std::cell::RefCell;
493        use std::rc::Rc;
494
495        let instance = self.instance;
496        let function_fn = self.function_fn;
497
498        Rc::new(RefCell::new(
499            move |machine: &mut crate::runtime::vm::Machine| {
500                let ret = unsafe {
501                    function_fn(
502                        instance,
503                        machine as *mut crate::runtime::vm::Machine as *mut c_void,
504                    )
505                };
506                ret as crate::runtime::vm::ReturnCode
507            },
508        ))
509    }
510}
511
512#[cfg(not(target_arch = "wasm32"))]
513unsafe impl Send for DynPluginFunctionInfo {}
514#[cfg(not(target_arch = "wasm32"))]
515unsafe impl Sync for DynPluginFunctionInfo {}
516
517// -------------------------------------------------------------------------
518// Plugin loader
519// -------------------------------------------------------------------------
520
521/// Dynamic plugin loader.
522///
523/// Manages loading and unloading of plugin libraries. The loader searches
524/// for plugins in standard directories and validates them before loading.
525#[cfg(not(target_arch = "wasm32"))]
526pub struct PluginLoader {
527    /// All loaded plugins.
528    plugins: Vec<LoadedPlugin>,
529}
530
531#[cfg(not(target_arch = "wasm32"))]
532impl PluginLoader {
533    /// Create a new plugin loader.
534    pub fn new() -> Self {
535        Self {
536            plugins: Vec::new(),
537        }
538    }
539
540    /// Load a plugin from the specified path.
541    ///
542    /// The path should point to a shared library without extension
543    /// (e.g., "path/to/plugin" will load "path/to/plugin.dll" on Windows).
544    pub fn load_plugin<P: AsRef<Path>>(&mut self, path: P) -> Result<(), PluginLoaderError> {
545        let base_path = path.as_ref();
546        let lib_path = get_library_path(base_path)?;
547
548        // Load library
549        // SAFETY: Loading arbitrary code is inherently unsafe.
550        let library = unsafe { Library::new(&lib_path) }
551            .map_err(|e| PluginLoaderError::LoadFailed(lib_path.clone(), e.to_string()))?;
552
553        // Load required symbols
554        let metadata_fn: Symbol<PluginMetadataFn> = unsafe {
555            library
556                .get(b"mimium_plugin_metadata\0")
557                .map_err(|_| PluginLoaderError::MissingSymbol("mimium_plugin_metadata"))?
558        };
559
560        let create_fn: Symbol<PluginCreateFn> = unsafe {
561            library
562                .get(b"mimium_plugin_create\0")
563                .map_err(|_| PluginLoaderError::MissingSymbol("mimium_plugin_create"))?
564        };
565
566        let destroy_fn: Symbol<PluginDestroyFn> = unsafe {
567            library
568                .get(b"mimium_plugin_destroy\0")
569                .map_err(|_| PluginLoaderError::MissingSymbol("mimium_plugin_destroy"))?
570        };
571
572        // Try to load the optional get_function symbol
573        let get_function_fn: Option<Symbol<PluginGetFunctionFn>> =
574            unsafe { library.get(b"mimium_plugin_get_function\0").ok() };
575
576        // Try to load the optional get_macro symbol
577        let get_macro_fn: Option<Symbol<PluginGetMacroFn>> =
578            unsafe { library.get(b"mimium_plugin_get_macro\0").ok() };
579
580        // Try to load the optional get_type_infos symbol
581        let get_type_infos_fn: Option<Symbol<PluginGetTypeInfosFn>> =
582            unsafe { library.get(b"mimium_plugin_get_type_infos\0").ok() };
583
584        // Get metadata
585        let metadata_ptr = unsafe { metadata_fn() };
586        if metadata_ptr.is_null() {
587            return Err(PluginLoaderError::InvalidMetadata);
588        }
589        let metadata = unsafe { (*metadata_ptr).clone() };
590
591        // Share the host's interner with the plugin.
592        // This must happen before create_fn or any other call that may touch
593        // the interner (type construction, symbol interning, etc.).
594        let set_interner_fn: Option<Symbol<PluginSetInternerFn>> =
595            unsafe { library.get(b"mimium_plugin_set_interner\0").ok() };
596        if let Some(set_interner) = &set_interner_fn {
597            let host_globals = crate::interner::get_session_globals_ptr();
598            unsafe { set_interner(host_globals) };
599            crate::log::debug!("Shared host interner with plugin");
600        } else {
601            crate::log::warn!(
602                "Plugin does not export mimium_plugin_set_interner; \
603                 interned IDs may be invalid across the DLL boundary"
604            );
605        }
606
607        // Create instance
608        let instance = unsafe { create_fn() };
609        if instance.is_null() {
610            return Err(PluginLoaderError::CreateFailed);
611        }
612
613        // Copy the destroy function pointer before moving library
614        let destroy_fn_ptr = *destroy_fn;
615        let get_function_fn_ptr = get_function_fn.as_ref().map(|f| **f);
616        let get_macro_fn_ptr = get_macro_fn.as_ref().map(|f| **f);
617        let get_type_infos_fn_ptr = get_type_infos_fn.as_ref().map(|f| **f);
618
619        // Store the loaded plugin
620        let plugin = LoadedPlugin {
621            _library: library,
622            metadata,
623            instance,
624            destroy_fn: destroy_fn_ptr,
625            get_function_fn: get_function_fn_ptr,
626            get_macro_fn: get_macro_fn_ptr,
627            get_type_infos_fn: get_type_infos_fn_ptr,
628        };
629
630        crate::log::info!("Loaded plugin: {} v{}", plugin.name(), plugin.version());
631        self.plugins.push(plugin);
632
633        Ok(())
634    }
635
636    /// Load all plugins from a specific directory.
637    pub fn load_plugins_from_dir<P: AsRef<Path>>(
638        &mut self,
639        dir: P,
640    ) -> Result<usize, PluginLoaderError> {
641        self.load_plugins_from_dir_with_skip_substrings(dir, &[])
642    }
643
644    /// Load all plugins from a specific directory, skipping files whose names
645    /// contain any of the provided substrings.
646    pub fn load_plugins_from_dir_with_skip_substrings<P: AsRef<Path>>(
647        &mut self,
648        dir: P,
649        skip_substrings: &[&str],
650    ) -> Result<usize, PluginLoaderError> {
651        let plugin_dir = dir.as_ref();
652
653        if !plugin_dir.exists() {
654            crate::log::debug!("Plugin directory not found: {}", plugin_dir.display());
655            return Ok(0);
656        }
657
658        let mut loaded_count = 0;
659        for entry in std::fs::read_dir(plugin_dir)
660            .map_err(|e| PluginLoaderError::DirectoryReadFailed(plugin_dir.to_path_buf(), e))?
661        {
662            let entry = entry
663                .map_err(|e| PluginLoaderError::DirectoryReadFailed(plugin_dir.to_path_buf(), e))?;
664            let path = entry.path();
665
666            if is_library_file(&path) && is_mimium_plugin(&path) {
667                // Skip guitools plugin - it should only be loaded as a SystemPlugin
668                // to ensure proper mainloop integration
669                let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
670                if file_name.contains("guitools") {
671                    crate::log::debug!(
672                        "Skipping guitools plugin (should be loaded as SystemPlugin only): {}",
673                        path.display()
674                    );
675                    continue;
676                }
677
678                if skip_substrings
679                    .iter()
680                    .any(|needle| file_name.contains(needle))
681                {
682                    crate::log::debug!("Skipping plugin by filter: {}", path.display());
683                    continue;
684                }
685
686                // Check if plugin is already loaded by examining the file name
687                let already_loaded = self.plugins.iter().any(|p| {
688                    // Extract plugin name from path (e.g., libmimium_midi.dylib -> mimium-midi)
689                    let plugin_name = p.name().replace('-', "_");
690                    file_name.contains(&plugin_name)
691                        || file_name.contains(&format!(
692                            "mimium_{}",
693                            plugin_name.trim_start_matches("mimium_")
694                        ))
695                });
696
697                if already_loaded {
698                    crate::log::debug!("Skipping already loaded plugin: {}", path.display());
699                    continue;
700                }
701
702                // Remove extension for load_plugin
703                let stem = path.with_extension("");
704                match self.load_plugin(&stem) {
705                    Ok(_) => {
706                        loaded_count += 1;
707                    }
708                    Err(e) => {
709                        crate::log::warn!("Failed to load plugin {}: {:?}", path.display(), e);
710                    }
711                }
712            }
713        }
714
715        Ok(loaded_count)
716    }
717
718    /// Load all plugins from the standard plugin directory.
719    pub fn load_builtin_plugins(&mut self) -> Result<(), PluginLoaderError> {
720        let plugin_dir = get_plugin_directory()?;
721        self.load_plugins_from_dir(plugin_dir)?;
722        Ok(())
723    }
724
725    /// Load all plugins from the standard plugin directory, skipping files whose
726    /// names contain any of the provided substrings.
727    pub fn load_builtin_plugins_with_skip_substrings(
728        &mut self,
729        skip_substrings: &[&str],
730    ) -> Result<(), PluginLoaderError> {
731        let plugin_dir = get_plugin_directory()?;
732        self.load_plugins_from_dir_with_skip_substrings(plugin_dir, skip_substrings)?;
733        Ok(())
734    }
735
736    /// Get a list of all loaded plugins.
737    pub fn loaded_plugins(&self) -> &[LoadedPlugin] {
738        &self.plugins
739    }
740
741    /// Get type information from all loaded plugins.
742    pub fn get_type_infos(&self) -> Vec<crate::plugin::ExtFunTypeInfo> {
743        self.plugins
744            .iter()
745            .filter_map(|plugin| plugin.get_type_infos())
746            .flatten()
747            .collect()
748    }
749
750    /// Get all macro functions from loaded plugins with their type information.
751    ///
752    /// Discovers macros automatically by iterating type info entries with
753    /// stage 0 (macro/compile-time) and looking up the corresponding FFI
754    /// function from `mimium_plugin_get_macro`.
755    pub fn get_macro_functions(
756        &self,
757    ) -> Vec<(
758        crate::interner::Symbol,
759        crate::interner::TypeNodeId,
760        Box<dyn crate::plugin::MacroFunction>,
761    )> {
762        use crate::interner::ToSymbol;
763
764        let mut result = Vec::new();
765
766        for plugin in &self.plugins {
767            if !plugin.metadata.capabilities.has_macros {
768                continue;
769            }
770
771            let get_macro_fn = match plugin.get_macro_fn {
772                Some(f) => f,
773                None => continue,
774            };
775
776            // Discover macros from type info (stage 0 = macro/compile-time)
777            let type_infos = match plugin.get_type_infos() {
778                Some(infos) => infos,
779                None => {
780                    crate::log::debug!(
781                        "Plugin {} has no type info, skipping macro discovery",
782                        plugin.name()
783                    );
784                    continue;
785                }
786            };
787
788            let macro_infos: Vec<_> = type_infos
789                .into_iter()
790                .filter(|info| matches!(info.stage, crate::plugin::EvalStage::Stage(0)))
791                .collect();
792
793            for info in macro_infos {
794                let name_str = info.name.as_str();
795                let name_cstr = match std::ffi::CString::new(name_str) {
796                    Ok(s) => s,
797                    Err(_) => continue,
798                };
799
800                let macro_fn = unsafe { get_macro_fn(name_cstr.as_ptr()) };
801                if let Some(macro_fn) = macro_fn {
802                    let ty = info.ty;
803                    let wrapper = unsafe {
804                        DynPluginMacroInfo::new(name_str.to_symbol(), ty, plugin.instance, macro_fn)
805                    };
806
807                    crate::log::info!("Registered dynamic macro: {name_str}");
808                    result.push((
809                        name_str.to_symbol(),
810                        ty,
811                        Box::new(wrapper) as Box<dyn crate::plugin::MacroFunction>,
812                    ));
813                }
814            }
815        }
816
817        result
818    }
819
820    /// Get all runtime functions from loaded plugins.
821    ///
822    /// Discovers runtime functions by iterating type info entries with
823    /// stage 1 (machine/runtime) and looking up the corresponding FFI
824    /// function from `mimium_plugin_get_function`.
825    pub fn get_runtime_functions(&self) -> Vec<Box<dyn crate::plugin::MachineFunction>> {
826        use crate::interner::ToSymbol;
827
828        let mut result = Vec::new();
829
830        for plugin in &self.plugins {
831            if !plugin.metadata.capabilities.has_runtime_functions {
832                continue;
833            }
834
835            let get_function_fn = match plugin.get_function_fn {
836                Some(f) => f,
837                None => continue,
838            };
839
840            // Discover runtime functions from type info (stage 1)
841            let type_infos = match plugin.get_type_infos() {
842                Some(infos) => infos,
843                None => continue,
844            };
845
846            let runtime_infos: Vec<_> = type_infos
847                .into_iter()
848                .filter(|info| matches!(info.stage, crate::plugin::EvalStage::Stage(1)))
849                .collect();
850
851            for info in runtime_infos {
852                let name_str = info.name.as_str();
853                let name_cstr = match std::ffi::CString::new(name_str) {
854                    Ok(s) => s,
855                    Err(_) => continue,
856                };
857
858                let func = unsafe { get_function_fn(name_cstr.as_ptr()) };
859                if let Some(func) = func {
860                    let wrapper = unsafe {
861                        DynPluginFunctionInfo::new(name_str.to_symbol(), plugin.instance, func)
862                    };
863
864                    crate::log::info!("Registered dynamic runtime function: {name_str}");
865                    result.push(Box::new(wrapper) as Box<dyn crate::plugin::MachineFunction>);
866                }
867            }
868        }
869
870        result
871    }
872}
873
874#[cfg(not(target_arch = "wasm32"))]
875impl Default for PluginLoader {
876    fn default() -> Self {
877        Self::new()
878    }
879}
880
881// -------------------------------------------------------------------------
882// Error types
883// -------------------------------------------------------------------------
884
885/// Errors that can occur during plugin loading.
886#[derive(Debug)]
887pub enum PluginLoaderError {
888    /// Failed to load the library file.
889    LoadFailed(PathBuf, String),
890    /// Required symbol not found in the library.
891    MissingSymbol(&'static str),
892    /// Plugin metadata is invalid or null.
893    InvalidMetadata,
894    /// Plugin instance creation failed (returned null).
895    CreateFailed,
896    /// Failed to determine plugin directory.
897    PluginDirectoryNotFound,
898    /// Failed to read plugin directory.
899    DirectoryReadFailed(PathBuf, std::io::Error),
900    /// Invalid library path.
901    InvalidPath,
902}
903
904impl std::fmt::Display for PluginLoaderError {
905    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
906        match self {
907            Self::LoadFailed(path, msg) => {
908                write!(f, "Failed to load plugin from {}: {}", path.display(), msg)
909            }
910            Self::MissingSymbol(name) => write!(f, "Missing required symbol: {}", name),
911            Self::InvalidMetadata => write!(f, "Invalid or null plugin metadata"),
912            Self::CreateFailed => write!(f, "Plugin instance creation failed"),
913            Self::PluginDirectoryNotFound => write!(f, "Plugin directory not found"),
914            Self::DirectoryReadFailed(path, err) => {
915                write!(f, "Failed to read directory {}: {}", path.display(), err)
916            }
917            Self::InvalidPath => write!(f, "Invalid plugin path"),
918        }
919    }
920}
921
922impl std::error::Error for PluginLoaderError {}
923
924// -------------------------------------------------------------------------
925// Helper functions
926// -------------------------------------------------------------------------
927
928/// Get the standard plugin directory.
929///
930/// Resolution order:
931/// 1. `MIMIUM_PLUGIN_DIR` environment variable (if set)
932/// 2. `$HOME/.mimium/plugins` (cross-platform default)
933#[cfg(not(target_arch = "wasm32"))]
934fn get_plugin_directory() -> Result<PathBuf, PluginLoaderError> {
935    // Try environment variable first
936    if let Ok(dir) = std::env::var("MIMIUM_PLUGIN_DIR") {
937        return Ok(PathBuf::from(dir));
938    }
939
940    // Cross-platform default: $HOME/.mimium/plugins
941    #[cfg(target_os = "windows")]
942    let home = std::env::var("USERPROFILE").ok();
943    #[cfg(not(target_os = "windows"))]
944    let home = std::env::var("HOME").ok();
945
946    home.map(|h| PathBuf::from(h).join(".mimium").join("plugins"))
947        .ok_or(PluginLoaderError::PluginDirectoryNotFound)
948}
949
950/// Determine the correct library path with platform-specific extension.
951#[cfg(not(target_arch = "wasm32"))]
952fn get_library_path(base_path: &Path) -> Result<PathBuf, PluginLoaderError> {
953    #[cfg(target_os = "windows")]
954    let ext = "dll";
955
956    #[cfg(target_os = "linux")]
957    let ext = "so";
958
959    #[cfg(target_os = "macos")]
960    let ext = "dylib";
961
962    Ok(base_path.with_extension(ext))
963}
964
965/// Check if a path is a library file.
966#[cfg(not(target_arch = "wasm32"))]
967fn is_library_file(path: &Path) -> bool {
968    if let Some(ext) = path.extension() {
969        #[cfg(target_os = "windows")]
970        return ext == "dll";
971
972        #[cfg(target_os = "linux")]
973        return ext == "so";
974
975        #[cfg(target_os = "macos")]
976        return ext == "dylib";
977    }
978    false
979}
980
981/// Check if a library file is a mimium plugin based on naming convention.
982/// Returns true if the file name starts with "mimium_" or "libmimium_".
983#[cfg(not(target_arch = "wasm32"))]
984fn is_mimium_plugin(path: &Path) -> bool {
985    if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
986        name.starts_with("mimium_") || name.starts_with("libmimium_")
987    } else {
988        false
989    }
990}
991
992// -------------------------------------------------------------------------
993// Tests
994// -------------------------------------------------------------------------
995
996#[cfg(test)]
997mod tests {
998    use super::*;
999
1000    #[test]
1001    #[cfg(not(target_arch = "wasm32"))]
1002    fn test_loader_creation() {
1003        let loader = PluginLoader::new();
1004        assert_eq!(loader.loaded_plugins().len(), 0);
1005    }
1006
1007    #[test]
1008    #[cfg(not(target_arch = "wasm32"))]
1009    fn test_library_path() {
1010        let base = Path::new("/test/plugin");
1011        let lib_path = get_library_path(base).unwrap();
1012
1013        #[cfg(target_os = "windows")]
1014        assert_eq!(lib_path, Path::new("/test/plugin.dll"));
1015
1016        #[cfg(target_os = "linux")]
1017        assert_eq!(lib_path, Path::new("/test/plugin.so"));
1018
1019        #[cfg(target_os = "macos")]
1020        assert_eq!(lib_path, Path::new("/test/plugin.dylib"));
1021    }
1022
1023    #[test]
1024    #[cfg(not(target_arch = "wasm32"))]
1025    fn test_is_library_file() {
1026        #[cfg(target_os = "windows")]
1027        {
1028            assert!(is_library_file(Path::new("plugin.dll")));
1029            assert!(!is_library_file(Path::new("plugin.so")));
1030        }
1031
1032        #[cfg(target_os = "linux")]
1033        {
1034            assert!(is_library_file(Path::new("plugin.so")));
1035            assert!(!is_library_file(Path::new("plugin.dll")));
1036        }
1037
1038        #[cfg(target_os = "macos")]
1039        {
1040            assert!(is_library_file(Path::new("plugin.dylib")));
1041            assert!(!is_library_file(Path::new("plugin.so")));
1042        }
1043    }
1044}