1#[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#[repr(C)]
43#[derive(Debug, Clone)]
44pub struct PluginMetadata {
45 pub name: *const c_char,
47 pub version: *const c_char,
49 pub author: *const c_char,
51 pub capabilities: PluginCapabilities,
53}
54
55#[cfg(not(target_arch = "wasm32"))]
58unsafe impl Sync for PluginMetadata {}
59
60#[cfg(not(target_arch = "wasm32"))]
63unsafe impl Send for PluginMetadata {}
64
65#[repr(C)]
67#[derive(Debug, Clone, Copy)]
68pub struct PluginCapabilities {
69 pub has_audio_worker: bool,
71 pub has_macros: bool,
73 pub has_runtime_functions: bool,
75}
76
77#[repr(C)]
82pub struct PluginInstance {
83 _private: [u8; 0],
84}
85
86pub type PluginFunctionFn = unsafe extern "C" fn(
91 instance: *mut PluginInstance,
92 runtime: *mut c_void, ) -> i64; pub 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
114type PluginMetadataFn = unsafe extern "C" fn() -> *const PluginMetadata;
120
121type PluginCreateFn = unsafe extern "C" fn() -> *mut PluginInstance;
123
124type PluginDestroyFn = unsafe extern "C" fn(instance: *mut PluginInstance);
126
127type PluginSetInternerFn = unsafe extern "C" fn(globals_ptr: *const std::ffi::c_void);
132
133type PluginGetFunctionFn = unsafe extern "C" fn(name: *const c_char) -> Option<PluginFunctionFn>;
137
138type PluginGetMacroFn = unsafe extern "C" fn(name: *const c_char) -> Option<PluginMacroFn>;
142
143#[repr(C)]
147#[derive(Debug, Clone)]
148pub struct FfiTypeInfo {
149 pub name: *const c_char,
151 pub type_data: *const u8,
153 pub type_len: usize,
155 pub stage: u8,
157}
158
159#[cfg(not(target_arch = "wasm32"))]
162unsafe impl Sync for FfiTypeInfo {}
163
164#[cfg(not(target_arch = "wasm32"))]
165unsafe impl Send for FfiTypeInfo {}
166
167type PluginGetTypeInfosFn = unsafe extern "C" fn(out_len: *mut usize) -> *const FfiTypeInfo;
172
173#[cfg(not(target_arch = "wasm32"))]
179pub struct LoadedPlugin {
180 _library: Library,
182 metadata: PluginMetadata,
184 instance: *mut PluginInstance,
186 destroy_fn: PluginDestroyFn,
188 get_function_fn: Option<PluginGetFunctionFn>,
190 get_macro_fn: Option<PluginGetMacroFn>,
192 get_type_infos_fn: Option<PluginGetTypeInfosFn>,
194}
195
196#[cfg(not(target_arch = "wasm32"))]
197impl LoadedPlugin {
198 pub fn metadata(&self) -> &PluginMetadata {
200 &self.metadata
201 }
202
203 pub fn name(&self) -> String {
205 unsafe { CStr::from_ptr(self.metadata.name) }
206 .to_string_lossy()
207 .into_owned()
208 }
209
210 pub fn version(&self) -> String {
212 unsafe { CStr::from_ptr(self.metadata.version) }
213 .to_string_lossy()
214 .into_owned()
215 }
216
217 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 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 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 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 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 let stage = match info.stage {
276 0 => EvalStage::Stage(0), 1 => EvalStage::Stage(1), 2 => EvalStage::Persistent, _ => {
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 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 unsafe {
306 (self.destroy_fn)(self.instance);
307 }
308 }
309}
310
311#[cfg(not(target_arch = "wasm32"))]
320pub struct DynPluginMacroInfo {
321 name: crate::interner::Symbol,
322 ty: crate::interner::TypeNodeId,
323 instance: *mut PluginInstance,
325 macro_fn: PluginMacroFn,
327}
328
329#[cfg(not(target_arch = "wasm32"))]
330impl DynPluginMacroInfo {
331 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 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 let mut out_ptr: *mut u8 = std::ptr::null_mut();
387 let mut out_len: usize = 0;
388
389 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 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 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#[cfg(not(target_arch = "wasm32"))]
445unsafe impl Send for DynPluginMacroInfo {}
446#[cfg(not(target_arch = "wasm32"))]
447unsafe impl Sync for DynPluginMacroInfo {}
448
449#[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 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#[cfg(not(target_arch = "wasm32"))]
526pub struct PluginLoader {
527 plugins: Vec<LoadedPlugin>,
529}
530
531#[cfg(not(target_arch = "wasm32"))]
532impl PluginLoader {
533 pub fn new() -> Self {
535 Self {
536 plugins: Vec::new(),
537 }
538 }
539
540 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 let library = unsafe { Library::new(&lib_path) }
551 .map_err(|e| PluginLoaderError::LoadFailed(lib_path.clone(), e.to_string()))?;
552
553 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 let get_function_fn: Option<Symbol<PluginGetFunctionFn>> =
574 unsafe { library.get(b"mimium_plugin_get_function\0").ok() };
575
576 let get_macro_fn: Option<Symbol<PluginGetMacroFn>> =
578 unsafe { library.get(b"mimium_plugin_get_macro\0").ok() };
579
580 let get_type_infos_fn: Option<Symbol<PluginGetTypeInfosFn>> =
582 unsafe { library.get(b"mimium_plugin_get_type_infos\0").ok() };
583
584 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 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 let instance = unsafe { create_fn() };
609 if instance.is_null() {
610 return Err(PluginLoaderError::CreateFailed);
611 }
612
613 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 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 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 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 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 let already_loaded = self.plugins.iter().any(|p| {
688 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 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 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 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 pub fn loaded_plugins(&self) -> &[LoadedPlugin] {
738 &self.plugins
739 }
740
741 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 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 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 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 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#[derive(Debug)]
887pub enum PluginLoaderError {
888 LoadFailed(PathBuf, String),
890 MissingSymbol(&'static str),
892 InvalidMetadata,
894 CreateFailed,
896 PluginDirectoryNotFound,
898 DirectoryReadFailed(PathBuf, std::io::Error),
900 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#[cfg(not(target_arch = "wasm32"))]
934fn get_plugin_directory() -> Result<PathBuf, PluginLoaderError> {
935 if let Ok(dir) = std::env::var("MIMIUM_PLUGIN_DIR") {
937 return Ok(PathBuf::from(dir));
938 }
939
940 #[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#[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#[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#[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#[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}