1use libloading::{Library, Symbol};
4use std::ffi::CStr;
5use std::os::raw::c_char;
6use std::path::Path;
7use wavecraft_protocol::{
8 DEV_PROCESSOR_VTABLE_VERSION, DevProcessorVTable, ParameterInfo, ProcessorInfo,
9};
10
11#[derive(Debug)]
13pub enum PluginLoaderError {
14 LibraryLoad(libloading::Error),
16 SymbolNotFound(String),
18 NullPointer(&'static str),
20 JsonParse(serde_json::Error),
22 InvalidUtf8(std::str::Utf8Error),
24 FileRead(std::io::Error),
26 VtableVersionMismatch { found: u32, expected: u32 },
28}
29
30impl std::fmt::Display for PluginLoaderError {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Self::LibraryLoad(e) => write!(f, "Failed to load plugin library: {}", e),
34 Self::SymbolNotFound(name) => write!(f, "Symbol not found: {}", name),
35 Self::NullPointer(func) => write!(f, "FFI function {} returned null", func),
36 Self::JsonParse(e) => write!(f, "Failed to parse JSON payload: {}", e),
37 Self::InvalidUtf8(e) => write!(f, "Invalid UTF-8 in FFI response: {}", e),
38 Self::FileRead(e) => write!(f, "Failed to read file: {}", e),
39 Self::VtableVersionMismatch { found, expected } => write!(
40 f,
41 "Dev processor vtable version mismatch: found {}, expected {}. Rebuild the plugin with the current SDK.",
42 found, expected
43 ),
44 }
45 }
46}
47
48impl std::error::Error for PluginLoaderError {
49 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
50 match self {
51 Self::LibraryLoad(e) => Some(e),
52 Self::JsonParse(e) => Some(e),
53 Self::InvalidUtf8(e) => Some(e),
54 Self::FileRead(e) => Some(e),
55 _ => None,
56 }
57 }
58}
59
60type GetParamsJsonFn = unsafe extern "C" fn() -> *mut c_char;
61type GetProcessorsJsonFn = unsafe extern "C" fn() -> *mut c_char;
62type FreeStringFn = unsafe extern "C" fn(*mut c_char);
63type DevProcessorVTableFn = unsafe extern "C" fn() -> DevProcessorVTable;
64
65struct FfiStringGuard {
66 ptr: *mut c_char,
67 free_string: FreeStringFn,
68}
69
70impl FfiStringGuard {
71 fn new(ptr: *mut c_char, free_string: FreeStringFn) -> Self {
72 Self { ptr, free_string }
73 }
74}
75
76impl Drop for FfiStringGuard {
77 fn drop(&mut self) {
78 if !self.ptr.is_null() {
79 unsafe { (self.free_string)(self.ptr) };
82 }
83 }
84}
85
86fn parse_json_from_ffi<T>(
87 json_ptr: *mut c_char,
88 function_name: &'static str,
89) -> Result<T, PluginLoaderError>
90where
91 T: serde::de::DeserializeOwned,
92{
93 if json_ptr.is_null() {
94 return Err(PluginLoaderError::NullPointer(function_name));
95 }
96
97 let c_str = unsafe { CStr::from_ptr(json_ptr) };
99 let json_str = c_str.to_str().map_err(PluginLoaderError::InvalidUtf8)?;
100 serde_json::from_str(json_str).map_err(PluginLoaderError::JsonParse)
101}
102
103fn load_json_via_ffi<T>(
104 get_json: unsafe extern "C" fn() -> *mut c_char,
105 free_string: unsafe extern "C" fn(*mut c_char),
106 function_name: &'static str,
107) -> Result<T, PluginLoaderError>
108where
109 T: serde::de::DeserializeOwned,
110{
111 unsafe {
115 let json_ptr = get_json();
116 let guard = FfiStringGuard::new(json_ptr, free_string);
117 let parsed = parse_json_from_ffi::<T>(guard.ptr, function_name)?;
118 Ok(parsed)
119 }
120}
121
122pub struct PluginParamLoader {
143 parameters: Vec<ParameterInfo>,
144 processors: Vec<ProcessorInfo>,
145 dev_processor_vtable: DevProcessorVTable,
147 _library: Library,
150}
151
152impl PluginParamLoader {
153 pub fn load_params_from_file<P: AsRef<Path>>(
158 json_path: P,
159 ) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
160 let contents =
161 std::fs::read_to_string(json_path.as_ref()).map_err(PluginLoaderError::FileRead)?;
162 let params: Vec<ParameterInfo> =
163 serde_json::from_str(&contents).map_err(PluginLoaderError::JsonParse)?;
164 Ok(params)
165 }
166
167 pub fn load<P: AsRef<Path>>(dylib_path: P) -> Result<Self, PluginLoaderError> {
169 let library =
173 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
174
175 let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
179 library.get(b"wavecraft_get_params_json\0").map_err(|e| {
180 PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
181 })?
182 };
183
184 let free_string: Symbol<FreeStringFn> = unsafe {
188 library.get(b"wavecraft_free_string\0").map_err(|e| {
189 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
190 })?
191 };
192
193 let get_processors_json: Symbol<GetProcessorsJsonFn> = unsafe {
195 library
196 .get(b"wavecraft_get_processors_json\0")
197 .map_err(|e| {
198 PluginLoaderError::SymbolNotFound(format!(
199 "wavecraft_get_processors_json: {}",
200 e
201 ))
202 })?
203 };
204
205 let params = load_json_via_ffi::<Vec<ParameterInfo>>(
214 *get_params_json,
215 *free_string,
216 "wavecraft_get_params_json",
217 )?;
218
219 let processors = load_json_via_ffi::<Vec<ProcessorInfo>>(
220 *get_processors_json,
221 *free_string,
222 "wavecraft_get_processors_json",
223 )?;
224
225 let dev_processor_vtable = Self::load_processor_vtable(&library)?;
227
228 Ok(Self {
229 parameters: params,
230 processors,
231 dev_processor_vtable,
232 _library: library,
233 })
234 }
235
236 pub fn load_params_only<P: AsRef<Path>>(
241 dylib_path: P,
242 ) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
243 let library =
246 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
247
248 let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
252 library.get(b"wavecraft_get_params_json\0").map_err(|e| {
253 PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
254 })?
255 };
256
257 let free_string: Symbol<FreeStringFn> = unsafe {
261 library.get(b"wavecraft_free_string\0").map_err(|e| {
262 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
263 })?
264 };
265
266 let params = load_json_via_ffi::<Vec<ParameterInfo>>(
268 *get_params_json,
269 *free_string,
270 "wavecraft_get_params_json",
271 )?;
272
273 Ok(params)
274 }
275
276 pub fn load_processors_only<P: AsRef<Path>>(
278 dylib_path: P,
279 ) -> Result<Vec<ProcessorInfo>, PluginLoaderError> {
280 let library =
282 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
283
284 let get_processors_json: Symbol<GetProcessorsJsonFn> = unsafe {
286 library
287 .get(b"wavecraft_get_processors_json\0")
288 .map_err(|e| {
289 PluginLoaderError::SymbolNotFound(format!(
290 "wavecraft_get_processors_json: {}",
291 e
292 ))
293 })?
294 };
295
296 let free_string: Symbol<FreeStringFn> = unsafe {
298 library.get(b"wavecraft_free_string\0").map_err(|e| {
299 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
300 })?
301 };
302
303 let processors = load_json_via_ffi::<Vec<ProcessorInfo>>(
305 *get_processors_json,
306 *free_string,
307 "wavecraft_get_processors_json",
308 )?;
309
310 Ok(processors)
311 }
312
313 pub fn parameters(&self) -> &[ParameterInfo] {
315 &self.parameters
316 }
317
318 pub fn processors(&self) -> &[ProcessorInfo] {
320 &self.processors
321 }
322
323 #[allow(dead_code)]
325 pub fn get_parameter(&self, id: &str) -> Option<&ParameterInfo> {
326 self.parameters.iter().find(|p| p.id == id)
327 }
328
329 pub fn dev_processor_vtable(&self) -> &DevProcessorVTable {
331 &self.dev_processor_vtable
332 }
333
334 fn load_processor_vtable(library: &Library) -> Result<DevProcessorVTable, PluginLoaderError> {
336 let symbol: Symbol<DevProcessorVTableFn> = unsafe {
340 library
341 .get(b"wavecraft_dev_create_processor\0")
342 .map_err(|e| {
343 PluginLoaderError::SymbolNotFound(format!(
344 "wavecraft_dev_create_processor: {}",
345 e
346 ))
347 })?
348 };
349
350 let vtable = unsafe { symbol() };
356
357 if vtable.version != DEV_PROCESSOR_VTABLE_VERSION {
359 return Err(PluginLoaderError::VtableVersionMismatch {
360 found: vtable.version,
361 expected: DEV_PROCESSOR_VTABLE_VERSION,
362 });
363 }
364
365 Ok(vtable)
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_error_display() {
375 let err = PluginLoaderError::SymbolNotFound("test_symbol".to_string());
376 assert!(err.to_string().contains("test_symbol"));
377 }
378
379 #[test]
380 fn test_null_pointer_error() {
381 let err = PluginLoaderError::NullPointer("wavecraft_get_params_json");
382 assert!(err.to_string().contains("null"));
383 }
384
385 #[test]
386 fn test_file_read_error() {
387 let err = PluginLoaderError::FileRead(std::io::Error::new(
388 std::io::ErrorKind::NotFound,
389 "file not found",
390 ));
391 assert!(err.to_string().contains("Failed to read file"));
392 }
393
394 #[test]
395 fn test_vtable_version_mismatch_error_display() {
396 let err = PluginLoaderError::VtableVersionMismatch {
397 found: 1,
398 expected: 2,
399 };
400 assert!(err.to_string().contains("version mismatch"));
401 assert!(err.to_string().contains("found 1"));
402 assert!(err.to_string().contains("expected 2"));
403 }
404
405 #[test]
406 fn test_load_params_from_file() {
407 use wavecraft_protocol::ParameterType;
408
409 let dir = std::env::temp_dir().join("wavecraft_test_sidecar");
410 let _ = std::fs::create_dir_all(&dir);
411 let json_path = dir.join("wavecraft-params.json");
412
413 let params = vec![ParameterInfo {
414 id: "gain".to_string(),
415 name: "Gain".to_string(),
416 param_type: ParameterType::Float,
417 value: 0.5,
418 default: 0.5,
419 min: 0.0,
420 max: 1.0,
421 unit: Some("dB".to_string()),
422 group: Some("Main".to_string()),
423 variants: None,
424 }];
425
426 let json = serde_json::to_string_pretty(¶ms).unwrap();
427 std::fs::write(&json_path, &json).unwrap();
428
429 let loaded = PluginParamLoader::load_params_from_file(&json_path).unwrap();
430 assert_eq!(loaded.len(), 1);
431 assert_eq!(loaded[0].id, "gain");
432 assert_eq!(loaded[0].name, "Gain");
433 assert!((loaded[0].default - 0.5).abs() < f32::EPSILON);
434
435 let _ = std::fs::remove_file(&json_path);
437 let _ = std::fs::remove_dir(&dir);
438 }
439
440 #[test]
441 fn test_load_params_from_file_not_found() {
442 let result = PluginParamLoader::load_params_from_file("/nonexistent/path.json");
443 assert!(result.is_err());
444 let err = result.unwrap_err();
445 assert!(matches!(err, PluginLoaderError::FileRead(_)));
446 }
447
448 #[test]
449 fn test_load_params_from_file_invalid_json() {
450 let dir = std::env::temp_dir().join("wavecraft_test_bad_json");
451 let _ = std::fs::create_dir_all(&dir);
452 let json_path = dir.join("bad-params.json");
453
454 std::fs::write(&json_path, "not valid json").unwrap();
455
456 let result = PluginParamLoader::load_params_from_file(&json_path);
457 assert!(result.is_err());
458 assert!(matches!(
459 result.unwrap_err(),
460 PluginLoaderError::JsonParse(_)
461 ));
462
463 let _ = std::fs::remove_file(&json_path);
465 let _ = std::fs::remove_dir(&dir);
466 }
467
468 #[test]
469 fn test_parse_processors_json() {
470 let json = r#"[{"id":"test_tone"},{"id":"output_gain"}]"#;
471 let processors: Vec<ProcessorInfo> =
472 serde_json::from_str(json).expect("json should deserialize");
473
474 assert_eq!(processors.len(), 2);
475 assert_eq!(processors[0].id, "test_tone");
476 assert_eq!(processors[1].id, "output_gain");
477 }
478
479 #[test]
480 fn test_parse_processors_json_invalid() {
481 let json = "not valid json";
482 let parsed: Result<Vec<ProcessorInfo>, _> = serde_json::from_str(json);
483 assert!(parsed.is_err());
484 }
485}