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}
25
26impl std::fmt::Display for PluginLoaderError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 Self::LibraryLoad(e) => write!(f, "Failed to load plugin library: {}", e),
30 Self::SymbolNotFound(name) => write!(f, "Symbol not found: {}", name),
31 Self::NullPointer(func) => write!(f, "FFI function {} returned null", func),
32 Self::JsonParse(e) => write!(f, "Failed to parse parameter JSON: {}", e),
33 Self::InvalidUtf8(e) => write!(f, "Invalid UTF-8 in FFI response: {}", e),
34 Self::FileRead(e) => write!(f, "Failed to read file: {}", e),
35 }
36 }
37}
38
39impl std::error::Error for PluginLoaderError {
40 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
41 match self {
42 Self::LibraryLoad(e) => Some(e),
43 Self::JsonParse(e) => Some(e),
44 Self::InvalidUtf8(e) => Some(e),
45 Self::FileRead(e) => Some(e),
46 _ => None,
47 }
48 }
49}
50
51type GetParamsJsonFn = unsafe extern "C" fn() -> *mut c_char;
52type FreeStringFn = unsafe extern "C" fn(*mut c_char);
53type DevProcessorVTableFn = unsafe extern "C" fn() -> DevProcessorVTable;
54
55pub struct PluginParamLoader {
76 parameters: Vec<ParameterInfo>,
77 dev_processor_vtable: Option<DevProcessorVTable>,
79 _library: Library,
82}
83
84impl PluginParamLoader {
85 pub fn load_params_from_file<P: AsRef<Path>>(
90 json_path: P,
91 ) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
92 let contents =
93 std::fs::read_to_string(json_path.as_ref()).map_err(PluginLoaderError::FileRead)?;
94 let params: Vec<ParameterInfo> =
95 serde_json::from_str(&contents).map_err(PluginLoaderError::JsonParse)?;
96 Ok(params)
97 }
98
99 pub fn load<P: AsRef<Path>>(dylib_path: P) -> Result<Self, PluginLoaderError> {
101 let library =
105 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
106
107 let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
111 library.get(b"wavecraft_get_params_json\0").map_err(|e| {
112 PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
113 })?
114 };
115
116 let free_string: Symbol<FreeStringFn> = unsafe {
120 library.get(b"wavecraft_free_string\0").map_err(|e| {
121 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
122 })?
123 };
124
125 let params = unsafe {
134 let json_ptr = get_params_json();
135 if json_ptr.is_null() {
136 return Err(PluginLoaderError::NullPointer("wavecraft_get_params_json"));
137 }
138
139 let c_str = CStr::from_ptr(json_ptr);
140 let json_str = c_str.to_str().map_err(PluginLoaderError::InvalidUtf8)?;
141
142 let params: Vec<ParameterInfo> =
143 serde_json::from_str(json_str).map_err(PluginLoaderError::JsonParse)?;
144
145 free_string(json_ptr);
146
147 params
148 };
149
150 let dev_processor_vtable = Self::try_load_processor_vtable(&library);
152
153 Ok(Self {
154 parameters: params,
155 dev_processor_vtable,
156 _library: library,
157 })
158 }
159
160 pub fn load_params_only<P: AsRef<Path>>(
165 dylib_path: P,
166 ) -> Result<Vec<ParameterInfo>, PluginLoaderError> {
167 let library =
170 unsafe { Library::new(dylib_path.as_ref()) }.map_err(PluginLoaderError::LibraryLoad)?;
171
172 let get_params_json: Symbol<GetParamsJsonFn> = unsafe {
176 library.get(b"wavecraft_get_params_json\0").map_err(|e| {
177 PluginLoaderError::SymbolNotFound(format!("wavecraft_get_params_json: {}", e))
178 })?
179 };
180
181 let free_string: Symbol<FreeStringFn> = unsafe {
185 library.get(b"wavecraft_free_string\0").map_err(|e| {
186 PluginLoaderError::SymbolNotFound(format!("wavecraft_free_string: {}", e))
187 })?
188 };
189
190 let params = unsafe {
192 let json_ptr = get_params_json();
193 if json_ptr.is_null() {
194 return Err(PluginLoaderError::NullPointer("wavecraft_get_params_json"));
195 }
196
197 let c_str = CStr::from_ptr(json_ptr);
198 let json_str = c_str.to_str().map_err(PluginLoaderError::InvalidUtf8)?;
199
200 let params: Vec<ParameterInfo> =
201 serde_json::from_str(json_str).map_err(PluginLoaderError::JsonParse)?;
202
203 free_string(json_ptr);
204
205 params
206 };
207
208 Ok(params)
209 }
210
211 pub fn parameters(&self) -> &[ParameterInfo] {
213 &self.parameters
214 }
215
216 #[allow(dead_code)]
218 pub fn get_parameter(&self, id: &str) -> Option<&ParameterInfo> {
219 self.parameters.iter().find(|p| p.id == id)
220 }
221
222 pub fn dev_processor_vtable(&self) -> Option<&DevProcessorVTable> {
228 self.dev_processor_vtable.as_ref()
229 }
230
231 fn try_load_processor_vtable(library: &Library) -> Option<DevProcessorVTable> {
236 let symbol: Symbol<DevProcessorVTableFn> =
242 unsafe { library.get(b"wavecraft_dev_create_processor\0").ok()? };
243
244 let vtable = unsafe { symbol() };
250
251 if vtable.version != DEV_PROCESSOR_VTABLE_VERSION {
253 tracing::warn!(
254 "Plugin dev-audio vtable version {} != expected {}. \
255 Audio processing disabled. Update your SDK dependency.",
256 vtable.version,
257 DEV_PROCESSOR_VTABLE_VERSION
258 );
259 return None;
260 }
261
262 Some(vtable)
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_error_display() {
272 let err = PluginLoaderError::SymbolNotFound("test_symbol".to_string());
273 assert!(err.to_string().contains("test_symbol"));
274 }
275
276 #[test]
277 fn test_null_pointer_error() {
278 let err = PluginLoaderError::NullPointer("wavecraft_get_params_json");
279 assert!(err.to_string().contains("null"));
280 }
281
282 #[test]
283 fn test_file_read_error() {
284 let err = PluginLoaderError::FileRead(std::io::Error::new(
285 std::io::ErrorKind::NotFound,
286 "file not found",
287 ));
288 assert!(err.to_string().contains("Failed to read file"));
289 }
290
291 #[test]
292 fn test_load_params_from_file() {
293 use wavecraft_protocol::ParameterType;
294
295 let dir = std::env::temp_dir().join("wavecraft_test_sidecar");
296 let _ = std::fs::create_dir_all(&dir);
297 let json_path = dir.join("wavecraft-params.json");
298
299 let params = vec![ParameterInfo {
300 id: "gain".to_string(),
301 name: "Gain".to_string(),
302 param_type: ParameterType::Float,
303 value: 0.5,
304 default: 0.5,
305 unit: Some("dB".to_string()),
306 group: Some("Main".to_string()),
307 }];
308
309 let json = serde_json::to_string_pretty(¶ms).unwrap();
310 std::fs::write(&json_path, &json).unwrap();
311
312 let loaded = PluginParamLoader::load_params_from_file(&json_path).unwrap();
313 assert_eq!(loaded.len(), 1);
314 assert_eq!(loaded[0].id, "gain");
315 assert_eq!(loaded[0].name, "Gain");
316 assert!((loaded[0].default - 0.5).abs() < f32::EPSILON);
317
318 let _ = std::fs::remove_file(&json_path);
320 let _ = std::fs::remove_dir(&dir);
321 }
322
323 #[test]
324 fn test_load_params_from_file_not_found() {
325 let result = PluginParamLoader::load_params_from_file("/nonexistent/path.json");
326 assert!(result.is_err());
327 let err = result.unwrap_err();
328 assert!(matches!(err, PluginLoaderError::FileRead(_)));
329 }
330
331 #[test]
332 fn test_load_params_from_file_invalid_json() {
333 let dir = std::env::temp_dir().join("wavecraft_test_bad_json");
334 let _ = std::fs::create_dir_all(&dir);
335 let json_path = dir.join("bad-params.json");
336
337 std::fs::write(&json_path, "not valid json").unwrap();
338
339 let result = PluginParamLoader::load_params_from_file(&json_path);
340 assert!(result.is_err());
341 assert!(matches!(
342 result.unwrap_err(),
343 PluginLoaderError::JsonParse(_)
344 ));
345
346 let _ = std::fs::remove_file(&json_path);
348 let _ = std::fs::remove_dir(&dir);
349 }
350}