#[cfg(not(target_arch = "wasm32"))]
use std::ffi::{CStr, c_char, c_void};
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
#[cfg(not(target_arch = "wasm32"))]
use libloading::{Library, Symbol};
#[repr(C)]
#[derive(Debug, Clone)]
pub struct PluginMetadata {
pub name: *const c_char,
pub version: *const c_char,
pub author: *const c_char,
pub capabilities: PluginCapabilities,
}
#[cfg(not(target_arch = "wasm32"))]
unsafe impl Sync for PluginMetadata {}
#[cfg(not(target_arch = "wasm32"))]
unsafe impl Send for PluginMetadata {}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct PluginCapabilities {
pub has_audio_worker: bool,
pub has_macros: bool,
pub has_runtime_functions: bool,
}
#[repr(C)]
pub struct PluginInstance {
_private: [u8; 0],
}
pub type PluginFunctionFn = unsafe extern "C" fn(
instance: *mut PluginInstance,
runtime: *mut c_void, ) -> i64;
pub type PluginMacroFn = unsafe extern "C" fn(
instance: *mut c_void,
args_ptr: *const u8,
args_len: usize,
out_ptr: *mut *mut u8,
out_len: *mut usize,
) -> i32;
type PluginMetadataFn = unsafe extern "C" fn() -> *const PluginMetadata;
type PluginCreateFn = unsafe extern "C" fn() -> *mut PluginInstance;
type PluginDestroyFn = unsafe extern "C" fn(instance: *mut PluginInstance);
type PluginSetInternerFn = unsafe extern "C" fn(globals_ptr: *const std::ffi::c_void);
type PluginGetFunctionFn = unsafe extern "C" fn(name: *const c_char) -> Option<PluginFunctionFn>;
type PluginGetMacroFn = unsafe extern "C" fn(name: *const c_char) -> Option<PluginMacroFn>;
#[repr(C)]
#[derive(Debug, Clone)]
pub struct FfiTypeInfo {
pub name: *const c_char,
pub type_data: *const u8,
pub type_len: usize,
pub stage: u8,
}
#[cfg(not(target_arch = "wasm32"))]
unsafe impl Sync for FfiTypeInfo {}
#[cfg(not(target_arch = "wasm32"))]
unsafe impl Send for FfiTypeInfo {}
type PluginGetTypeInfosFn = unsafe extern "C" fn(out_len: *mut usize) -> *const FfiTypeInfo;
#[cfg(not(target_arch = "wasm32"))]
pub struct LoadedPlugin {
_library: Library,
metadata: PluginMetadata,
instance: *mut PluginInstance,
destroy_fn: PluginDestroyFn,
get_function_fn: Option<PluginGetFunctionFn>,
get_macro_fn: Option<PluginGetMacroFn>,
get_type_infos_fn: Option<PluginGetTypeInfosFn>,
}
#[cfg(not(target_arch = "wasm32"))]
impl LoadedPlugin {
pub fn metadata(&self) -> &PluginMetadata {
&self.metadata
}
pub fn name(&self) -> String {
unsafe { CStr::from_ptr(self.metadata.name) }
.to_string_lossy()
.into_owned()
}
pub fn version(&self) -> String {
unsafe { CStr::from_ptr(self.metadata.version) }
.to_string_lossy()
.into_owned()
}
pub fn get_function(&self, name: &str) -> Option<PluginFunctionFn> {
let get_fn = self.get_function_fn?;
let name_cstr = std::ffi::CString::new(name).ok()?;
unsafe { get_fn(name_cstr.as_ptr()) }
}
pub fn get_macro(&self, name: &str) -> Option<PluginMacroFn> {
let get_fn = self.get_macro_fn?;
let name_cstr = std::ffi::CString::new(name).ok()?;
unsafe { get_fn(name_cstr.as_ptr()) }
}
pub fn get_type_infos(&self) -> Option<Vec<crate::plugin::ExtFunTypeInfo>> {
use crate::interner::{ToSymbol, TypeNodeId};
use crate::plugin::{EvalStage, ExtFunTypeInfo};
let get_fn = self.get_type_infos_fn?;
let mut len: usize = 0;
let array_ptr = unsafe { get_fn(&mut len as *mut usize) };
if array_ptr.is_null() || len == 0 {
crate::log::debug!("Plugin {} has no type info or returned null", self.name());
return None;
}
crate::log::debug!("Plugin {} provided {} type info entries", self.name(), len);
let mut result = Vec::with_capacity(len);
for i in 0..len {
let info = unsafe { &*array_ptr.add(i) };
let name_str = unsafe { CStr::from_ptr(info.name) }
.to_string_lossy()
.into_owned();
let name = name_str.to_symbol();
let type_slice = unsafe { std::slice::from_raw_parts(info.type_data, info.type_len) };
let ty: TypeNodeId = match bincode::deserialize(type_slice) {
Ok(t) => t,
Err(e) => {
crate::log::warn!("Failed to deserialize type for {name_str}: {e:?}");
continue;
}
};
let stage = match info.stage {
0 => EvalStage::Stage(0), 1 => EvalStage::Stage(1), 2 => EvalStage::Persistent, _ => {
crate::log::warn!("Unknown stage {} for {}", info.stage, name_str);
continue;
}
};
result.push(ExtFunTypeInfo::new(name, ty, stage));
}
Some(result)
}
pub unsafe fn instance_ptr(&self) -> *mut PluginInstance {
self.instance
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Drop for LoadedPlugin {
fn drop(&mut self) {
unsafe {
(self.destroy_fn)(self.instance);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub struct DynPluginMacroInfo {
name: crate::interner::Symbol,
ty: crate::interner::TypeNodeId,
instance: *mut PluginInstance,
macro_fn: PluginMacroFn,
}
#[cfg(not(target_arch = "wasm32"))]
impl DynPluginMacroInfo {
pub unsafe fn new(
name: crate::interner::Symbol,
ty: crate::interner::TypeNodeId,
instance: *mut PluginInstance,
macro_fn: PluginMacroFn,
) -> Self {
Self {
name,
ty,
instance,
macro_fn,
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl crate::plugin::MacroFunction for DynPluginMacroInfo {
fn get_name(&self) -> crate::interner::Symbol {
self.name
}
fn get_type(&self) -> crate::interner::TypeNodeId {
self.ty
}
fn get_fn(&self) -> crate::plugin::MacroFunType {
use std::cell::RefCell;
use std::rc::Rc;
let instance = self.instance;
let macro_fn = self.macro_fn;
Rc::new(RefCell::new(
move |args: &[(crate::interpreter::Value, crate::interner::TypeNodeId)]| {
use crate::runtime::ffi_serde::{deserialize_value, serialize_macro_args};
let args_bytes = match serialize_macro_args(args) {
Ok(b) => b,
Err(e) => {
crate::log::error!("Failed to serialize macro arguments: {e}");
let err_expr = crate::ast::Expr::Error
.into_id(crate::utils::metadata::Location::internal());
return crate::interpreter::Value::ErrorV(err_expr);
}
};
let mut out_ptr: *mut u8 = std::ptr::null_mut();
let mut out_len: usize = 0;
let result_code = unsafe {
macro_fn(
instance as *mut c_void,
args_bytes.as_ptr(),
args_bytes.len(),
&mut out_ptr,
&mut out_len,
)
};
if result_code != 0 {
crate::log::error!(
"Dynamic plugin macro function returned error code: {result_code}"
);
let err_expr = crate::ast::Expr::Error
.into_id(crate::utils::metadata::Location::internal());
return crate::interpreter::Value::ErrorV(err_expr);
}
if out_ptr.is_null() || out_len == 0 {
crate::log::error!("Dynamic plugin macro function returned null/empty result");
let err_expr = crate::ast::Expr::Error
.into_id(crate::utils::metadata::Location::internal());
return crate::interpreter::Value::ErrorV(err_expr);
}
let out_bytes = unsafe { std::slice::from_raw_parts(out_ptr, out_len) };
let result = match deserialize_value(out_bytes) {
Ok(v) => v,
Err(e) => {
crate::log::error!("Failed to deserialize macro result: {e}");
let err_expr = crate::ast::Expr::Error
.into_id(crate::utils::metadata::Location::internal());
crate::interpreter::Value::ErrorV(err_expr)
}
};
unsafe {
let _ = Box::from_raw(std::slice::from_raw_parts_mut(out_ptr, out_len));
}
result
},
))
}
}
#[cfg(not(target_arch = "wasm32"))]
unsafe impl Send for DynPluginMacroInfo {}
#[cfg(not(target_arch = "wasm32"))]
unsafe impl Sync for DynPluginMacroInfo {}
#[cfg(not(target_arch = "wasm32"))]
pub struct DynPluginFunctionInfo {
name: crate::interner::Symbol,
instance: *mut PluginInstance,
function_fn: PluginFunctionFn,
}
#[cfg(not(target_arch = "wasm32"))]
impl DynPluginFunctionInfo {
pub unsafe fn new(
name: crate::interner::Symbol,
instance: *mut PluginInstance,
function_fn: PluginFunctionFn,
) -> Self {
Self {
name,
instance,
function_fn,
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl crate::plugin::MachineFunction for DynPluginFunctionInfo {
fn get_name(&self) -> crate::interner::Symbol {
self.name
}
fn get_fn(&self) -> crate::plugin::ExtClsType {
use std::cell::RefCell;
use std::rc::Rc;
let instance = self.instance;
let function_fn = self.function_fn;
Rc::new(RefCell::new(
move |machine: &mut crate::runtime::vm::Machine| {
let ret = unsafe {
function_fn(
instance,
machine as *mut crate::runtime::vm::Machine as *mut c_void,
)
};
ret as crate::runtime::vm::ReturnCode
},
))
}
}
#[cfg(not(target_arch = "wasm32"))]
unsafe impl Send for DynPluginFunctionInfo {}
#[cfg(not(target_arch = "wasm32"))]
unsafe impl Sync for DynPluginFunctionInfo {}
#[cfg(not(target_arch = "wasm32"))]
pub struct PluginLoader {
plugins: Vec<LoadedPlugin>,
}
#[cfg(not(target_arch = "wasm32"))]
impl PluginLoader {
pub fn new() -> Self {
Self {
plugins: Vec::new(),
}
}
pub fn load_plugin<P: AsRef<Path>>(&mut self, path: P) -> Result<(), PluginLoaderError> {
let base_path = path.as_ref();
let lib_path = get_library_path(base_path)?;
let library = unsafe { Library::new(&lib_path) }
.map_err(|e| PluginLoaderError::LoadFailed(lib_path.clone(), e.to_string()))?;
let metadata_fn: Symbol<PluginMetadataFn> = unsafe {
library
.get(b"mimium_plugin_metadata\0")
.map_err(|_| PluginLoaderError::MissingSymbol("mimium_plugin_metadata"))?
};
let create_fn: Symbol<PluginCreateFn> = unsafe {
library
.get(b"mimium_plugin_create\0")
.map_err(|_| PluginLoaderError::MissingSymbol("mimium_plugin_create"))?
};
let destroy_fn: Symbol<PluginDestroyFn> = unsafe {
library
.get(b"mimium_plugin_destroy\0")
.map_err(|_| PluginLoaderError::MissingSymbol("mimium_plugin_destroy"))?
};
let get_function_fn: Option<Symbol<PluginGetFunctionFn>> =
unsafe { library.get(b"mimium_plugin_get_function\0").ok() };
let get_macro_fn: Option<Symbol<PluginGetMacroFn>> =
unsafe { library.get(b"mimium_plugin_get_macro\0").ok() };
let get_type_infos_fn: Option<Symbol<PluginGetTypeInfosFn>> =
unsafe { library.get(b"mimium_plugin_get_type_infos\0").ok() };
let metadata_ptr = unsafe { metadata_fn() };
if metadata_ptr.is_null() {
return Err(PluginLoaderError::InvalidMetadata);
}
let metadata = unsafe { (*metadata_ptr).clone() };
let set_interner_fn: Option<Symbol<PluginSetInternerFn>> =
unsafe { library.get(b"mimium_plugin_set_interner\0").ok() };
if let Some(set_interner) = &set_interner_fn {
let host_globals = crate::interner::get_session_globals_ptr();
unsafe { set_interner(host_globals) };
crate::log::debug!("Shared host interner with plugin");
} else {
crate::log::warn!(
"Plugin does not export mimium_plugin_set_interner; \
interned IDs may be invalid across the DLL boundary"
);
}
let instance = unsafe { create_fn() };
if instance.is_null() {
return Err(PluginLoaderError::CreateFailed);
}
let destroy_fn_ptr = *destroy_fn;
let get_function_fn_ptr = get_function_fn.as_ref().map(|f| **f);
let get_macro_fn_ptr = get_macro_fn.as_ref().map(|f| **f);
let get_type_infos_fn_ptr = get_type_infos_fn.as_ref().map(|f| **f);
let plugin = LoadedPlugin {
_library: library,
metadata,
instance,
destroy_fn: destroy_fn_ptr,
get_function_fn: get_function_fn_ptr,
get_macro_fn: get_macro_fn_ptr,
get_type_infos_fn: get_type_infos_fn_ptr,
};
crate::log::info!("Loaded plugin: {} v{}", plugin.name(), plugin.version());
self.plugins.push(plugin);
Ok(())
}
pub fn load_plugins_from_dir<P: AsRef<Path>>(
&mut self,
dir: P,
) -> Result<usize, PluginLoaderError> {
self.load_plugins_from_dir_with_skip_substrings(dir, &[])
}
pub fn load_plugins_from_dir_with_skip_substrings<P: AsRef<Path>>(
&mut self,
dir: P,
skip_substrings: &[&str],
) -> Result<usize, PluginLoaderError> {
let plugin_dir = dir.as_ref();
if !plugin_dir.exists() {
crate::log::debug!("Plugin directory not found: {}", plugin_dir.display());
return Ok(0);
}
let mut loaded_count = 0;
for entry in std::fs::read_dir(plugin_dir)
.map_err(|e| PluginLoaderError::DirectoryReadFailed(plugin_dir.to_path_buf(), e))?
{
let entry = entry
.map_err(|e| PluginLoaderError::DirectoryReadFailed(plugin_dir.to_path_buf(), e))?;
let path = entry.path();
if is_library_file(&path) && is_mimium_plugin(&path) {
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if file_name.contains("guitools") {
crate::log::debug!(
"Skipping guitools plugin (should be loaded as SystemPlugin only): {}",
path.display()
);
continue;
}
if skip_substrings
.iter()
.any(|needle| file_name.contains(needle))
{
crate::log::debug!("Skipping plugin by filter: {}", path.display());
continue;
}
let already_loaded = self.plugins.iter().any(|p| {
let plugin_name = p.name().replace('-', "_");
file_name.contains(&plugin_name)
|| file_name.contains(&format!(
"mimium_{}",
plugin_name.trim_start_matches("mimium_")
))
});
if already_loaded {
crate::log::debug!("Skipping already loaded plugin: {}", path.display());
continue;
}
let stem = path.with_extension("");
match self.load_plugin(&stem) {
Ok(_) => {
loaded_count += 1;
}
Err(e) => {
crate::log::warn!("Failed to load plugin {}: {:?}", path.display(), e);
}
}
}
}
Ok(loaded_count)
}
pub fn load_builtin_plugins(&mut self) -> Result<(), PluginLoaderError> {
let plugin_dir = get_plugin_directory()?;
self.load_plugins_from_dir(plugin_dir)?;
Ok(())
}
pub fn load_builtin_plugins_with_skip_substrings(
&mut self,
skip_substrings: &[&str],
) -> Result<(), PluginLoaderError> {
let plugin_dir = get_plugin_directory()?;
self.load_plugins_from_dir_with_skip_substrings(plugin_dir, skip_substrings)?;
Ok(())
}
pub fn loaded_plugins(&self) -> &[LoadedPlugin] {
&self.plugins
}
pub fn get_type_infos(&self) -> Vec<crate::plugin::ExtFunTypeInfo> {
self.plugins
.iter()
.filter_map(|plugin| plugin.get_type_infos())
.flatten()
.collect()
}
pub fn get_macro_functions(
&self,
) -> Vec<(
crate::interner::Symbol,
crate::interner::TypeNodeId,
Box<dyn crate::plugin::MacroFunction>,
)> {
use crate::interner::ToSymbol;
let mut result = Vec::new();
for plugin in &self.plugins {
if !plugin.metadata.capabilities.has_macros {
continue;
}
let get_macro_fn = match plugin.get_macro_fn {
Some(f) => f,
None => continue,
};
let type_infos = match plugin.get_type_infos() {
Some(infos) => infos,
None => {
crate::log::debug!(
"Plugin {} has no type info, skipping macro discovery",
plugin.name()
);
continue;
}
};
let macro_infos: Vec<_> = type_infos
.into_iter()
.filter(|info| matches!(info.stage, crate::plugin::EvalStage::Stage(0)))
.collect();
for info in macro_infos {
let name_str = info.name.as_str();
let name_cstr = match std::ffi::CString::new(name_str) {
Ok(s) => s,
Err(_) => continue,
};
let macro_fn = unsafe { get_macro_fn(name_cstr.as_ptr()) };
if let Some(macro_fn) = macro_fn {
let ty = info.ty;
let wrapper = unsafe {
DynPluginMacroInfo::new(name_str.to_symbol(), ty, plugin.instance, macro_fn)
};
crate::log::info!("Registered dynamic macro: {name_str}");
result.push((
name_str.to_symbol(),
ty,
Box::new(wrapper) as Box<dyn crate::plugin::MacroFunction>,
));
}
}
}
result
}
pub fn get_runtime_functions(&self) -> Vec<Box<dyn crate::plugin::MachineFunction>> {
use crate::interner::ToSymbol;
let mut result = Vec::new();
for plugin in &self.plugins {
if !plugin.metadata.capabilities.has_runtime_functions {
continue;
}
let get_function_fn = match plugin.get_function_fn {
Some(f) => f,
None => continue,
};
let type_infos = match plugin.get_type_infos() {
Some(infos) => infos,
None => continue,
};
let runtime_infos: Vec<_> = type_infos
.into_iter()
.filter(|info| matches!(info.stage, crate::plugin::EvalStage::Stage(1)))
.collect();
for info in runtime_infos {
let name_str = info.name.as_str();
let name_cstr = match std::ffi::CString::new(name_str) {
Ok(s) => s,
Err(_) => continue,
};
let func = unsafe { get_function_fn(name_cstr.as_ptr()) };
if let Some(func) = func {
let wrapper = unsafe {
DynPluginFunctionInfo::new(name_str.to_symbol(), plugin.instance, func)
};
crate::log::info!("Registered dynamic runtime function: {name_str}");
result.push(Box::new(wrapper) as Box<dyn crate::plugin::MachineFunction>);
}
}
}
result
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for PluginLoader {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub enum PluginLoaderError {
LoadFailed(PathBuf, String),
MissingSymbol(&'static str),
InvalidMetadata,
CreateFailed,
PluginDirectoryNotFound,
DirectoryReadFailed(PathBuf, std::io::Error),
InvalidPath,
}
impl std::fmt::Display for PluginLoaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LoadFailed(path, msg) => {
write!(f, "Failed to load plugin from {}: {}", path.display(), msg)
}
Self::MissingSymbol(name) => write!(f, "Missing required symbol: {}", name),
Self::InvalidMetadata => write!(f, "Invalid or null plugin metadata"),
Self::CreateFailed => write!(f, "Plugin instance creation failed"),
Self::PluginDirectoryNotFound => write!(f, "Plugin directory not found"),
Self::DirectoryReadFailed(path, err) => {
write!(f, "Failed to read directory {}: {}", path.display(), err)
}
Self::InvalidPath => write!(f, "Invalid plugin path"),
}
}
}
impl std::error::Error for PluginLoaderError {}
#[cfg(not(target_arch = "wasm32"))]
fn get_plugin_directory() -> Result<PathBuf, PluginLoaderError> {
if let Ok(dir) = std::env::var("MIMIUM_PLUGIN_DIR") {
return Ok(PathBuf::from(dir));
}
#[cfg(target_os = "windows")]
let home = std::env::var("USERPROFILE").ok();
#[cfg(not(target_os = "windows"))]
let home = std::env::var("HOME").ok();
home.map(|h| PathBuf::from(h).join(".mimium").join("plugins"))
.ok_or(PluginLoaderError::PluginDirectoryNotFound)
}
#[cfg(not(target_arch = "wasm32"))]
fn get_library_path(base_path: &Path) -> Result<PathBuf, PluginLoaderError> {
#[cfg(target_os = "windows")]
let ext = "dll";
#[cfg(target_os = "linux")]
let ext = "so";
#[cfg(target_os = "macos")]
let ext = "dylib";
Ok(base_path.with_extension(ext))
}
#[cfg(not(target_arch = "wasm32"))]
fn is_library_file(path: &Path) -> bool {
if let Some(ext) = path.extension() {
#[cfg(target_os = "windows")]
return ext == "dll";
#[cfg(target_os = "linux")]
return ext == "so";
#[cfg(target_os = "macos")]
return ext == "dylib";
}
false
}
#[cfg(not(target_arch = "wasm32"))]
fn is_mimium_plugin(path: &Path) -> bool {
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
name.starts_with("mimium_") || name.starts_with("libmimium_")
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_loader_creation() {
let loader = PluginLoader::new();
assert_eq!(loader.loaded_plugins().len(), 0);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_library_path() {
let base = Path::new("/test/plugin");
let lib_path = get_library_path(base).unwrap();
#[cfg(target_os = "windows")]
assert_eq!(lib_path, Path::new("/test/plugin.dll"));
#[cfg(target_os = "linux")]
assert_eq!(lib_path, Path::new("/test/plugin.so"));
#[cfg(target_os = "macos")]
assert_eq!(lib_path, Path::new("/test/plugin.dylib"));
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_is_library_file() {
#[cfg(target_os = "windows")]
{
assert!(is_library_file(Path::new("plugin.dll")));
assert!(!is_library_file(Path::new("plugin.so")));
}
#[cfg(target_os = "linux")]
{
assert!(is_library_file(Path::new("plugin.so")));
assert!(!is_library_file(Path::new("plugin.dll")));
}
#[cfg(target_os = "macos")]
{
assert!(is_library_file(Path::new("plugin.dylib")));
assert!(!is_library_file(Path::new("plugin.so")));
}
}
}