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 parameter JSON: {}", 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 {}",
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
65fn parse_json_from_ffi<T>(
66 json_ptr: *mut c_char,
67 function_name: &'static str,
68) -> Result<T, PluginLoaderError>
69where
70 T: serde::de::DeserializeOwned,
71{
72 if json_ptr.is_null() {
73 return Err(PluginLoaderError::NullPointer(function_name));
74 }
75
76 let c_str = unsafe { CStr::from_ptr(json_ptr) };
78 let json_str = c_str.to_str().map_err(PluginLoaderError::InvalidUtf8)?;
79 serde_json::from_str(json_str).map_err(PluginLoaderError::JsonParse)
80}
81
82pub struct PluginParamLoader {
103 parameters: Vec<ParameterInfo>,
104 processors: Vec<ProcessorInfo>,
105 dev_processor_vtable: DevProcessorVTable,
107 _library: Library,
110}
111
112impl PluginParamLoader {
113 pub fn load_params_from_file<P: AsRef<Path>>(
118 json_path: P,
119 ) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
120 let contents =
121 std::fs::read_to_string(json_path.as_ref()).map_err(PluginLoaderError::FileRead)?;
122 let params: Vec<ParameterInfo> =
123 serde_json::from_str(&contents).map_err(PluginLoaderError::JsonParse)?;
124 Ok(params)
125 }
126
127 pub fn load<P: AsRef<Path>>(dylib_path: P) -> Result<Self, PluginLoaderError> {
129 let library =
133 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
134
135 let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
139 library.get(b"wavecraft_get_params_json\0").map_err(|e| {
140 PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
141 })?
142 };
143
144 let free_string: Symbol<FreeStringFn> = unsafe {
148 library.get(b"wavecraft_free_string\0").map_err(|e| {
149 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
150 })?
151 };
152
153 let get_processors_json: Symbol<GetProcessorsJsonFn> = unsafe {
155 library
156 .get(b"wavecraft_get_processors_json\0")
157 .map_err(|e| {
158 PluginLoaderError::SymbolNotFound(format!(
159 "wavecraft_get_processors_json: {}",
160 e
161 ))
162 })?
163 };
164
165 let params = unsafe {
174 let json_ptr = get_params_json();
175 let parsed =
176 parse_json_from_ffi::<Vec<ParameterInfo>>(json_ptr, "wavecraft_get_params_json")?;
177 free_string(json_ptr);
178 parsed
179 };
180
181 let processors = unsafe {
182 let json_ptr = get_processors_json();
183 let parsed = parse_json_from_ffi::<Vec<ProcessorInfo>>(
184 json_ptr,
185 "wavecraft_get_processors_json",
186 )?;
187 free_string(json_ptr);
188 parsed
189 };
190
191 let dev_processor_vtable = Self::load_processor_vtable(&library)?;
193
194 Ok(Self {
195 parameters: params,
196 processors,
197 dev_processor_vtable,
198 _library: library,
199 })
200 }
201
202 pub fn load_params_only<P: AsRef<Path>>(
207 dylib_path: P,
208 ) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
209 let library =
212 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
213
214 let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
218 library.get(b"wavecraft_get_params_json\0").map_err(|e| {
219 PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
220 })?
221 };
222
223 let free_string: Symbol<FreeStringFn> = unsafe {
227 library.get(b"wavecraft_free_string\0").map_err(|e| {
228 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
229 })?
230 };
231
232 let params = unsafe {
234 let json_ptr = get_params_json();
235 let parsed =
236 parse_json_from_ffi::<Vec<ParameterInfo>>(json_ptr, "wavecraft_get_params_json")?;
237 free_string(json_ptr);
238 parsed
239 };
240
241 Ok(params)
242 }
243
244 pub fn load_processors_only<P: AsRef<Path>>(
246 dylib_path: P,
247 ) -> Result<Vec<ProcessorInfo>, PluginLoaderError> {
248 let library =
250 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
251
252 let get_processors_json: Symbol<GetProcessorsJsonFn> = unsafe {
254 library
255 .get(b"wavecraft_get_processors_json\0")
256 .map_err(|e| {
257 PluginLoaderError::SymbolNotFound(format!(
258 "wavecraft_get_processors_json: {}",
259 e
260 ))
261 })?
262 };
263
264 let free_string: Symbol<FreeStringFn> = unsafe {
266 library.get(b"wavecraft_free_string\0").map_err(|e| {
267 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
268 })?
269 };
270
271 let processors = unsafe {
273 let json_ptr = get_processors_json();
274 let parsed = parse_json_from_ffi::<Vec<ProcessorInfo>>(
275 json_ptr,
276 "wavecraft_get_processors_json",
277 )?;
278 free_string(json_ptr);
279 parsed
280 };
281
282 Ok(processors)
283 }
284
285 pub fn parameters(&self) -> &[ParameterInfo] {
287 &self.parameters
288 }
289
290 pub fn processors(&self) -> &[ProcessorInfo] {
292 &self.processors
293 }
294
295 #[allow(dead_code)]
297 pub fn get_parameter(&self, id: &str) -> Option<&ParameterInfo> {
298 self.parameters.iter().find(|p| p.id == id)
299 }
300
301 pub fn dev_processor_vtable(&self) -> &DevProcessorVTable {
303 &self.dev_processor_vtable
304 }
305
306 fn load_processor_vtable(library: &Library) -> Result<DevProcessorVTable, PluginLoaderError> {
308 let symbol: Symbol<DevProcessorVTableFn> = unsafe {
312 library
313 .get(b"wavecraft_dev_create_processor\0")
314 .map_err(|e| {
315 PluginLoaderError::SymbolNotFound(format!(
316 "wavecraft_dev_create_processor: {}",
317 e
318 ))
319 })?
320 };
321
322 let vtable = unsafe { symbol() };
328
329 if vtable.version != DEV_PROCESSOR_VTABLE_VERSION {
331 return Err(PluginLoaderError::VtableVersionMismatch {
332 found: vtable.version,
333 expected: DEV_PROCESSOR_VTABLE_VERSION,
334 });
335 }
336
337 Ok(vtable)
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_error_display() {
347 let err = PluginLoaderError::SymbolNotFound("test_symbol".to_string());
348 assert!(err.to_string().contains("test_symbol"));
349 }
350
351 #[test]
352 fn test_null_pointer_error() {
353 let err = PluginLoaderError::NullPointer("wavecraft_get_params_json");
354 assert!(err.to_string().contains("null"));
355 }
356
357 #[test]
358 fn test_file_read_error() {
359 let err = PluginLoaderError::FileRead(std::io::Error::new(
360 std::io::ErrorKind::NotFound,
361 "file not found",
362 ));
363 assert!(err.to_string().contains("Failed to read file"));
364 }
365
366 #[test]
367 fn test_vtable_version_mismatch_error_display() {
368 let err = PluginLoaderError::VtableVersionMismatch {
369 found: 1,
370 expected: 2,
371 };
372 assert!(err.to_string().contains("version mismatch"));
373 assert!(err.to_string().contains("found 1"));
374 assert!(err.to_string().contains("expected 2"));
375 }
376
377 #[test]
378 fn test_load_params_from_file() {
379 use wavecraft_protocol::ParameterType;
380
381 let dir = std::env::temp_dir().join("wavecraft_test_sidecar");
382 let _ = std::fs::create_dir_all(&dir);
383 let json_path = dir.join("wavecraft-params.json");
384
385 let params = vec![ParameterInfo {
386 id: "gain".to_string(),
387 name: "Gain".to_string(),
388 param_type: ParameterType::Float,
389 value: 0.5,
390 default: 0.5,
391 min: 0.0,
392 max: 1.0,
393 unit: Some("dB".to_string()),
394 group: Some("Main".to_string()),
395 variants: None,
396 }];
397
398 let json = serde_json::to_string_pretty(¶ms).unwrap();
399 std::fs::write(&json_path, &json).unwrap();
400
401 let loaded = PluginParamLoader::load_params_from_file(&json_path).unwrap();
402 assert_eq!(loaded.len(), 1);
403 assert_eq!(loaded[0].id, "gain");
404 assert_eq!(loaded[0].name, "Gain");
405 assert!((loaded[0].default - 0.5).abs() < f32::EPSILON);
406
407 let _ = std::fs::remove_file(&json_path);
409 let _ = std::fs::remove_dir(&dir);
410 }
411
412 #[test]
413 fn test_load_params_from_file_not_found() {
414 let result = PluginParamLoader::load_params_from_file("/nonexistent/path.json");
415 assert!(result.is_err());
416 let err = result.unwrap_err();
417 assert!(matches!(err, PluginLoaderError::FileRead(_)));
418 }
419
420 #[test]
421 fn test_load_params_from_file_invalid_json() {
422 let dir = std::env::temp_dir().join("wavecraft_test_bad_json");
423 let _ = std::fs::create_dir_all(&dir);
424 let json_path = dir.join("bad-params.json");
425
426 std::fs::write(&json_path, "not valid json").unwrap();
427
428 let result = PluginParamLoader::load_params_from_file(&json_path);
429 assert!(result.is_err());
430 assert!(matches!(
431 result.unwrap_err(),
432 PluginLoaderError::JsonParse(_)
433 ));
434
435 let _ = std::fs::remove_file(&json_path);
437 let _ = std::fs::remove_dir(&dir);
438 }
439
440 #[test]
441 fn test_parse_processors_json() {
442 let json = r#"[{"id":"oscillator"},{"id":"output_gain"}]"#;
443 let processors: Vec<ProcessorInfo> =
444 serde_json::from_str(json).expect("json should deserialize");
445
446 assert_eq!(processors.len(), 2);
447 assert_eq!(processors[0].id, "oscillator");
448 assert_eq!(processors[1].id, "output_gain");
449 }
450
451 #[test]
452 fn test_parse_processors_json_invalid() {
453 let json = "not valid json";
454 let parsed: Result<Vec<ProcessorInfo>, _> = serde_json::from_str(json);
455 assert!(parsed.is_err());
456 }
457}