use libloading::{Library, Symbol};
use std::ffi::CStr;
use std::os::raw::c_char;
use std::path::Path;
use wavecraft_protocol::{
DEV_PROCESSOR_VTABLE_VERSION, DevProcessorVTable, ParameterInfo, ProcessorInfo,
};
#[derive(Debug)]
pub enum PluginLoaderError {
LibraryLoad(libloading::Error),
SymbolNotFound(String),
NullPointer(&'static str),
JsonParse(serde_json::Error),
InvalidUtf8(std::str::Utf8Error),
FileRead(std::io::Error),
VtableVersionMismatch { found: u32, expected: u32 },
}
impl std::fmt::Display for PluginLoaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LibraryLoad(e) => write!(f, "Failed to load plugin library: {}", e),
Self::SymbolNotFound(name) => write!(f, "Symbol not found: {}", name),
Self::NullPointer(func) => write!(f, "FFI function {} returned null", func),
Self::JsonParse(e) => write!(f, "Failed to parse parameter JSON: {}", e),
Self::InvalidUtf8(e) => write!(f, "Invalid UTF-8 in FFI response: {}", e),
Self::FileRead(e) => write!(f, "Failed to read file: {}", e),
Self::VtableVersionMismatch { found, expected } => write!(
f,
"Dev processor vtable version mismatch: found {}, expected {}",
found, expected
),
}
}
}
impl std::error::Error for PluginLoaderError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::LibraryLoad(e) => Some(e),
Self::JsonParse(e) => Some(e),
Self::InvalidUtf8(e) => Some(e),
Self::FileRead(e) => Some(e),
_ => None,
}
}
}
type GetParamsJsonFn = unsafe extern "C" fn() -> *mut c_char;
type GetProcessorsJsonFn = unsafe extern "C" fn() -> *mut c_char;
type FreeStringFn = unsafe extern "C" fn(*mut c_char);
type DevProcessorVTableFn = unsafe extern "C" fn() -> DevProcessorVTable;
fn parse_json_from_ffi<T>(
json_ptr: *mut c_char,
function_name: &'static str,
) -> Result<T, PluginLoaderError>
where
T: serde::de::DeserializeOwned,
{
if json_ptr.is_null() {
return Err(PluginLoaderError::NullPointer(function_name));
}
let c_str = unsafe { CStr::from_ptr(json_ptr) };
let json_str = c_str.to_str().map_err(PluginLoaderError::InvalidUtf8)?;
serde_json::from_str(json_str).map_err(PluginLoaderError::JsonParse)
}
pub struct PluginParamLoader {
parameters: Vec<ParameterInfo>,
processors: Vec<ProcessorInfo>,
dev_processor_vtable: DevProcessorVTable,
_library: Library,
}
impl PluginParamLoader {
pub fn load_params_from_file<P: AsRef<Path>>(
json_path: P,
) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
let contents =
std::fs::read_to_string(json_path.as_ref()).map_err(PluginLoaderError::FileRead)?;
let params: Vec<ParameterInfo> =
serde_json::from_str(&contents).map_err(PluginLoaderError::JsonParse)?;
Ok(params)
}
pub fn load<P: AsRef<Path>>(dylib_path: P) -> Result<Self, PluginLoaderError> {
let library =
unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
library.get(b"wavecraft_get_params_json\0").map_err(|e| {
PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
})?
};
let free_string: Symbol<FreeStringFn> = unsafe {
library.get(b"wavecraft_free_string\0").map_err(|e| {
PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
})?
};
let get_processors_json: Symbol<GetProcessorsJsonFn> = unsafe {
library
.get(b"wavecraft_get_processors_json\0")
.map_err(|e| {
PluginLoaderError::SymbolNotFound(format!(
"wavecraft_get_processors_json: {}",
e
))
})?
};
let params = unsafe {
let json_ptr = get_params_json();
let parsed =
parse_json_from_ffi::<Vec<ParameterInfo>>(json_ptr, "wavecraft_get_params_json")?;
free_string(json_ptr);
parsed
};
let processors = unsafe {
let json_ptr = get_processors_json();
let parsed = parse_json_from_ffi::<Vec<ProcessorInfo>>(
json_ptr,
"wavecraft_get_processors_json",
)?;
free_string(json_ptr);
parsed
};
let dev_processor_vtable = Self::load_processor_vtable(&library)?;
Ok(Self {
parameters: params,
processors,
dev_processor_vtable,
_library: library,
})
}
pub fn load_params_only<P: AsRef<Path>>(
dylib_path: P,
) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
let library =
unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
library.get(b"wavecraft_get_params_json\0").map_err(|e| {
PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
})?
};
let free_string: Symbol<FreeStringFn> = unsafe {
library.get(b"wavecraft_free_string\0").map_err(|e| {
PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
})?
};
let params = unsafe {
let json_ptr = get_params_json();
let parsed =
parse_json_from_ffi::<Vec<ParameterInfo>>(json_ptr, "wavecraft_get_params_json")?;
free_string(json_ptr);
parsed
};
Ok(params)
}
pub fn load_processors_only<P: AsRef<Path>>(
dylib_path: P,
) -> Result<Vec<ProcessorInfo>, PluginLoaderError> {
let library =
unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
let get_processors_json: Symbol<GetProcessorsJsonFn> = unsafe {
library
.get(b"wavecraft_get_processors_json\0")
.map_err(|e| {
PluginLoaderError::SymbolNotFound(format!(
"wavecraft_get_processors_json: {}",
e
))
})?
};
let free_string: Symbol<FreeStringFn> = unsafe {
library.get(b"wavecraft_free_string\0").map_err(|e| {
PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
})?
};
let processors = unsafe {
let json_ptr = get_processors_json();
let parsed = parse_json_from_ffi::<Vec<ProcessorInfo>>(
json_ptr,
"wavecraft_get_processors_json",
)?;
free_string(json_ptr);
parsed
};
Ok(processors)
}
pub fn parameters(&self) -> &[ParameterInfo] {
&self.parameters
}
pub fn processors(&self) -> &[ProcessorInfo] {
&self.processors
}
#[allow(dead_code)]
pub fn get_parameter(&self, id: &str) -> Option<&ParameterInfo> {
self.parameters.iter().find(|p| p.id == id)
}
pub fn dev_processor_vtable(&self) -> &DevProcessorVTable {
&self.dev_processor_vtable
}
fn load_processor_vtable(library: &Library) -> Result<DevProcessorVTable, PluginLoaderError> {
let symbol: Symbol<DevProcessorVTableFn> = unsafe {
library
.get(b"wavecraft_dev_create_processor\0")
.map_err(|e| {
PluginLoaderError::SymbolNotFound(format!(
"wavecraft_dev_create_processor: {}",
e
))
})?
};
let vtable = unsafe { symbol() };
if vtable.version != DEV_PROCESSOR_VTABLE_VERSION {
return Err(PluginLoaderError::VtableVersionMismatch {
found: vtable.version,
expected: DEV_PROCESSOR_VTABLE_VERSION,
});
}
Ok(vtable)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = PluginLoaderError::SymbolNotFound("test_symbol".to_string());
assert!(err.to_string().contains("test_symbol"));
}
#[test]
fn test_null_pointer_error() {
let err = PluginLoaderError::NullPointer("wavecraft_get_params_json");
assert!(err.to_string().contains("null"));
}
#[test]
fn test_file_read_error() {
let err = PluginLoaderError::FileRead(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
assert!(err.to_string().contains("Failed to read file"));
}
#[test]
fn test_vtable_version_mismatch_error_display() {
let err = PluginLoaderError::VtableVersionMismatch {
found: 1,
expected: 2,
};
assert!(err.to_string().contains("version mismatch"));
assert!(err.to_string().contains("found 1"));
assert!(err.to_string().contains("expected 2"));
}
#[test]
fn test_load_params_from_file() {
use wavecraft_protocol::ParameterType;
let dir = std::env::temp_dir().join("wavecraft_test_sidecar");
let _ = std::fs::create_dir_all(&dir);
let json_path = dir.join("wavecraft-params.json");
let params = vec![ParameterInfo {
id: "gain".to_string(),
name: "Gain".to_string(),
param_type: ParameterType::Float,
value: 0.5,
default: 0.5,
min: 0.0,
max: 1.0,
unit: Some("dB".to_string()),
group: Some("Main".to_string()),
variants: None,
}];
let json = serde_json::to_string_pretty(¶ms).unwrap();
std::fs::write(&json_path, &json).unwrap();
let loaded = PluginParamLoader::load_params_from_file(&json_path).unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].id, "gain");
assert_eq!(loaded[0].name, "Gain");
assert!((loaded[0].default - 0.5).abs() < f32::EPSILON);
let _ = std::fs::remove_file(&json_path);
let _ = std::fs::remove_dir(&dir);
}
#[test]
fn test_load_params_from_file_not_found() {
let result = PluginParamLoader::load_params_from_file("/nonexistent/path.json");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, PluginLoaderError::FileRead(_)));
}
#[test]
fn test_load_params_from_file_invalid_json() {
let dir = std::env::temp_dir().join("wavecraft_test_bad_json");
let _ = std::fs::create_dir_all(&dir);
let json_path = dir.join("bad-params.json");
std::fs::write(&json_path, "not valid json").unwrap();
let result = PluginParamLoader::load_params_from_file(&json_path);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
PluginLoaderError::JsonParse(_)
));
let _ = std::fs::remove_file(&json_path);
let _ = std::fs::remove_dir(&dir);
}
#[test]
fn test_parse_processors_json() {
let json = r#"[{"id":"oscillator"},{"id":"output_gain"}]"#;
let processors: Vec<ProcessorInfo> =
serde_json::from_str(json).expect("json should deserialize");
assert_eq!(processors.len(), 2);
assert_eq!(processors[0].id, "oscillator");
assert_eq!(processors[1].id, "output_gain");
}
#[test]
fn test_parse_processors_json_invalid() {
let json = "not valid json";
let parsed: Result<Vec<ProcessorInfo>, _> = serde_json::from_str(json);
assert!(parsed.is_err());
}
}