wavecraft_bridge/
plugin_loader.rs1use libloading::{Library, Symbol};
4use std::ffi::CStr;
5use std::os::raw::c_char;
6use std::path::Path;
7use wavecraft_protocol::{DEV_PROCESSOR_VTABLE_VERSION, DevProcessorVTable, ParameterInfo};
8
9#[derive(Debug)]
11pub enum PluginLoaderError {
12 LibraryLoad(libloading::Error),
14 SymbolNotFound(String),
16 NullPointer(&'static str),
18 JsonParse(serde_json::Error),
20 InvalidUtf8(std::str::Utf8Error),
22 FileRead(std::io::Error),
24 VtableVersionMismatch { found: u32, expected: u32 },
26}
27
28impl std::fmt::Display for PluginLoaderError {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Self::LibraryLoad(e) => write!(f, "Failed to load plugin library: {}", e),
32 Self::SymbolNotFound(name) => write!(f, "Symbol not found: {}", name),
33 Self::NullPointer(func) => write!(f, "FFI function {} returned null", func),
34 Self::JsonParse(e) => write!(f, "Failed to parse parameter JSON: {}", e),
35 Self::InvalidUtf8(e) => write!(f, "Invalid UTF-8 in FFI response: {}", e),
36 Self::FileRead(e) => write!(f, "Failed to read file: {}", e),
37 Self::VtableVersionMismatch { found, expected } => write!(
38 f,
39 "Dev processor vtable version mismatch: found {}, expected {}",
40 found, expected
41 ),
42 }
43 }
44}
45
46impl std::error::Error for PluginLoaderError {
47 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
48 match self {
49 Self::LibraryLoad(e) => Some(e),
50 Self::JsonParse(e) => Some(e),
51 Self::InvalidUtf8(e) => Some(e),
52 Self::FileRead(e) => Some(e),
53 _ => None,
54 }
55 }
56}
57
58type GetParamsJsonFn = unsafe extern "C" fn() -> *mut c_char;
59type FreeStringFn = unsafe extern "C" fn(*mut c_char);
60type DevProcessorVTableFn = unsafe extern "C" fn() -> DevProcessorVTable;
61
62pub struct PluginParamLoader {
83 parameters: Vec<ParameterInfo>,
84 dev_processor_vtable: DevProcessorVTable,
86 _library: Library,
89}
90
91impl PluginParamLoader {
92 pub fn load_params_from_file<P: AsRef<Path>>(
97 json_path: P,
98 ) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
99 let contents =
100 std::fs::read_to_string(json_path.as_ref()).map_err(PluginLoaderError::FileRead)?;
101 let params: Vec<ParameterInfo> =
102 serde_json::from_str(&contents).map_err(PluginLoaderError::JsonParse)?;
103 Ok(params)
104 }
105
106 pub fn load<P: AsRef<Path>>(dylib_path: P) -> Result<Self, PluginLoaderError> {
108 let library =
112 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
113
114 let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
118 library.get(b"wavecraft_get_params_json\0").map_err(|e| {
119 PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
120 })?
121 };
122
123 let free_string: Symbol<FreeStringFn> = unsafe {
127 library.get(b"wavecraft_free_string\0").map_err(|e| {
128 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
129 })?
130 };
131
132 let params = unsafe {
141 let json_ptr = get_params_json();
142 if json_ptr.is_null() {
143 return Err(PluginLoaderError::NullPointer("wavecraft_get_params_json"));
144 }
145
146 let c_str = CStr::from_ptr(json_ptr);
147 let json_str = c_str.to_str().map_err(PluginLoaderError::InvalidUtf8)?;
148
149 let params: Vec<ParameterInfo> =
150 serde_json::from_str(json_str).map_err(PluginLoaderError::JsonParse)?;
151
152 free_string(json_ptr);
153
154 params
155 };
156
157 let dev_processor_vtable = Self::load_processor_vtable(&library)?;
159
160 Ok(Self {
161 parameters: params,
162 dev_processor_vtable,
163 _library: library,
164 })
165 }
166
167 pub fn load_params_only<P: AsRef<Path>>(
172 dylib_path: P,
173 ) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
174 let library =
177 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
178
179 let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
183 library.get(b"wavecraft_get_params_json\0").map_err(|e| {
184 PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
185 })?
186 };
187
188 let free_string: Symbol<FreeStringFn> = unsafe {
192 library.get(b"wavecraft_free_string\0").map_err(|e| {
193 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
194 })?
195 };
196
197 let params = unsafe {
199 let json_ptr = get_params_json();
200 if json_ptr.is_null() {
201 return Err(PluginLoaderError::NullPointer("wavecraft_get_params_json"));
202 }
203
204 let c_str = CStr::from_ptr(json_ptr);
205 let json_str = c_str.to_str().map_err(PluginLoaderError::InvalidUtf8)?;
206
207 let params: Vec<ParameterInfo> =
208 serde_json::from_str(json_str).map_err(PluginLoaderError::JsonParse)?;
209
210 free_string(json_ptr);
211
212 params
213 };
214
215 Ok(params)
216 }
217
218 pub fn parameters(&self) -> &[ParameterInfo] {
220 &self.parameters
221 }
222
223 #[allow(dead_code)]
225 pub fn get_parameter(&self, id: &str) -> Option<&ParameterInfo> {
226 self.parameters.iter().find(|p| p.id == id)
227 }
228
229 pub fn dev_processor_vtable(&self) -> &DevProcessorVTable {
231 &self.dev_processor_vtable
232 }
233
234 fn load_processor_vtable(library: &Library) -> Result<DevProcessorVTable, PluginLoaderError> {
236 let symbol: Symbol<DevProcessorVTableFn> = unsafe {
240 library
241 .get(b"wavecraft_dev_create_processor\0")
242 .map_err(|e| {
243 PluginLoaderError::SymbolNotFound(format!(
244 "wavecraft_dev_create_processor: {}",
245 e
246 ))
247 })?
248 };
249
250 let vtable = unsafe { symbol() };
256
257 if vtable.version != DEV_PROCESSOR_VTABLE_VERSION {
259 return Err(PluginLoaderError::VtableVersionMismatch {
260 found: vtable.version,
261 expected: DEV_PROCESSOR_VTABLE_VERSION,
262 });
263 }
264
265 Ok(vtable)
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_error_display() {
275 let err = PluginLoaderError::SymbolNotFound("test_symbol".to_string());
276 assert!(err.to_string().contains("test_symbol"));
277 }
278
279 #[test]
280 fn test_null_pointer_error() {
281 let err = PluginLoaderError::NullPointer("wavecraft_get_params_json");
282 assert!(err.to_string().contains("null"));
283 }
284
285 #[test]
286 fn test_file_read_error() {
287 let err = PluginLoaderError::FileRead(std::io::Error::new(
288 std::io::ErrorKind::NotFound,
289 "file not found",
290 ));
291 assert!(err.to_string().contains("Failed to read file"));
292 }
293
294 #[test]
295 fn test_vtable_version_mismatch_error_display() {
296 let err = PluginLoaderError::VtableVersionMismatch {
297 found: 1,
298 expected: 2,
299 };
300 assert!(err.to_string().contains("version mismatch"));
301 assert!(err.to_string().contains("found 1"));
302 assert!(err.to_string().contains("expected 2"));
303 }
304
305 #[test]
306 fn test_load_params_from_file() {
307 use wavecraft_protocol::ParameterType;
308
309 let dir = std::env::temp_dir().join("wavecraft_test_sidecar");
310 let _ = std::fs::create_dir_all(&dir);
311 let json_path = dir.join("wavecraft-params.json");
312
313 let params = vec![ParameterInfo {
314 id: "gain".to_string(),
315 name: "Gain".to_string(),
316 param_type: ParameterType::Float,
317 value: 0.5,
318 default: 0.5,
319 min: 0.0,
320 max: 1.0,
321 unit: Some("dB".to_string()),
322 group: Some("Main".to_string()),
323 }];
324
325 let json = serde_json::to_string_pretty(¶ms).unwrap();
326 std::fs::write(&json_path, &json).unwrap();
327
328 let loaded = PluginParamLoader::load_params_from_file(&json_path).unwrap();
329 assert_eq!(loaded.len(), 1);
330 assert_eq!(loaded[0].id, "gain");
331 assert_eq!(loaded[0].name, "Gain");
332 assert!((loaded[0].default - 0.5).abs() < f32::EPSILON);
333
334 let _ = std::fs::remove_file(&json_path);
336 let _ = std::fs::remove_dir(&dir);
337 }
338
339 #[test]
340 fn test_load_params_from_file_not_found() {
341 let result = PluginParamLoader::load_params_from_file("/nonexistent/path.json");
342 assert!(result.is_err());
343 let err = result.unwrap_err();
344 assert!(matches!(err, PluginLoaderError::FileRead(_)));
345 }
346
347 #[test]
348 fn test_load_params_from_file_invalid_json() {
349 let dir = std::env::temp_dir().join("wavecraft_test_bad_json");
350 let _ = std::fs::create_dir_all(&dir);
351 let json_path = dir.join("bad-params.json");
352
353 std::fs::write(&json_path, "not valid json").unwrap();
354
355 let result = PluginParamLoader::load_params_from_file(&json_path);
356 assert!(result.is_err());
357 assert!(matches!(
358 result.unwrap_err(),
359 PluginLoaderError::JsonParse(_)
360 ));
361
362 let _ = std::fs::remove_file(&json_path);
364 let _ = std::fs::remove_dir(&dir);
365 }
366}