1use crate::error::{ConsumerError, ConsumerResult};
4use crate::ffi_bindings::{
5 FfiPluginHandle, LogCallback, PluginCallFn, PluginCallRawFn, PluginCreateFn,
6 PluginFreeBufferFn, PluginGetRejectedCountFn, PluginGetStateFn, PluginInitFn,
7 PluginSetLogLevelFn, PluginShutdownFn, RbResponseFreeFn,
8};
9use crate::plugin::NativePlugin;
10use libloading::Library;
11use rustbridge_bundle::BundleLoader;
12use rustbridge_core::{LogLevel, PluginConfig};
13use std::ffi::c_char;
14use std::path::Path;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17use tracing::debug;
18
19static EXTRACT_INSTANCE: AtomicU64 = AtomicU64::new(0);
23
24pub type LogCallbackFn = Arc<dyn Fn(LogLevel, &str, &str) + Send + Sync>;
28
29static LOG_CALLBACK: std::sync::RwLock<Option<LogCallbackFn>> = std::sync::RwLock::new(None);
33
34fn set_log_callback(callback: Option<LogCallbackFn>) {
36 if let Ok(mut guard) = LOG_CALLBACK.write() {
37 *guard = callback;
38 }
39}
40
41unsafe extern "C" fn ffi_log_callback(
47 level: u8,
48 target: *const c_char,
49 message: *const u8,
50 message_len: usize,
51) {
52 let callback = LOG_CALLBACK.read().ok().and_then(|guard| guard.clone());
53 if let Some(callback) = callback {
54 let log_level = LogLevel::from_u8(level);
55
56 let target_str = if target.is_null() {
58 ""
59 } else {
60 unsafe { std::ffi::CStr::from_ptr(target) }
61 .to_str()
62 .unwrap_or("")
63 };
64
65 let message_str = if message.is_null() || message_len == 0 {
67 ""
68 } else {
69 let bytes = unsafe { std::slice::from_raw_parts(message, message_len) };
70 std::str::from_utf8(bytes).unwrap_or("")
71 };
72
73 callback(log_level, target_str, message_str);
74 }
75}
76
77pub struct NativePluginLoader;
81
82impl NativePluginLoader {
83 pub fn load<P: AsRef<Path>>(path: P) -> ConsumerResult<NativePlugin> {
97 Self::load_with_config(path, &PluginConfig::default(), None)
98 }
99
100 pub fn load_with_config<P: AsRef<Path>>(
123 path: P,
124 config: &PluginConfig,
125 log_callback: Option<LogCallbackFn>,
126 ) -> ConsumerResult<NativePlugin> {
127 let path = path.as_ref();
128 debug!("Loading plugin from: {}", path.display());
129
130 let library = unsafe { Library::new(path) }?;
133
134 let plugin_create: PluginCreateFn = unsafe { *library.get(b"plugin_create\0")? };
136 let plugin_init: PluginInitFn = unsafe { *library.get(b"plugin_init\0")? };
137 let plugin_call: PluginCallFn = unsafe { *library.get(b"plugin_call\0")? };
138 let plugin_shutdown: PluginShutdownFn = unsafe { *library.get(b"plugin_shutdown\0")? };
139 let plugin_get_state: PluginGetStateFn = unsafe { *library.get(b"plugin_get_state\0")? };
140 let plugin_get_rejected_count: PluginGetRejectedCountFn =
141 unsafe { *library.get(b"plugin_get_rejected_count\0")? };
142 let plugin_set_log_level: PluginSetLogLevelFn =
143 unsafe { *library.get(b"plugin_set_log_level\0")? };
144 let plugin_free_buffer: PluginFreeBufferFn =
145 unsafe { *library.get(b"plugin_free_buffer\0")? };
146
147 let plugin_call_raw: Option<PluginCallRawFn> =
149 unsafe { library.get(b"plugin_call_raw\0").ok().map(|s| *s) };
150 let rb_response_free: Option<RbResponseFreeFn> =
151 unsafe { library.get(b"rb_response_free\0").ok().map(|s| *s) };
152
153 if log_callback.is_some() {
158 set_log_callback(log_callback);
159 }
160 let ffi_callback: LogCallback = Some(ffi_log_callback);
161
162 let plugin_ptr = unsafe { plugin_create() };
165 if plugin_ptr.is_null() {
166 return Err(ConsumerError::NullHandle);
167 }
168
169 let config_json = serde_json::to_vec(config)?;
171
172 let handle: FfiPluginHandle = unsafe {
175 plugin_init(
176 plugin_ptr,
177 config_json.as_ptr(),
178 config_json.len(),
179 ffi_callback,
180 )
181 };
182
183 if handle.is_null() {
184 return Err(ConsumerError::NullHandle);
185 }
186
187 debug!("Plugin initialized with handle: {:?}", handle);
188
189 Ok(unsafe {
191 NativePlugin::new(
192 library,
193 handle,
194 plugin_call,
195 plugin_call_raw,
196 plugin_shutdown,
197 plugin_get_state,
198 plugin_get_rejected_count,
199 plugin_set_log_level,
200 plugin_free_buffer,
201 rb_response_free,
202 )
203 })
204 }
205
206 pub fn load_bundle<P: AsRef<Path>>(bundle_path: P) -> ConsumerResult<NativePlugin> {
220 Self::load_bundle_with_config(bundle_path, &PluginConfig::default(), None)
221 }
222
223 pub fn load_bundle_with_config<P: AsRef<Path>>(
242 bundle_path: P,
243 config: &PluginConfig,
244 log_callback: Option<LogCallbackFn>,
245 ) -> ConsumerResult<NativePlugin> {
246 let bundle_path = bundle_path.as_ref();
247 debug!("Loading bundle from: {}", bundle_path.display());
248
249 let mut loader = BundleLoader::open(bundle_path)?;
251
252 if !loader.supports_current_platform() {
254 return Err(ConsumerError::Bundle(
255 rustbridge_bundle::BundleError::UnsupportedPlatform(
256 "Current platform not supported by bundle".to_string(),
257 ),
258 ));
259 }
260
261 let instance_id = EXTRACT_INSTANCE.fetch_add(1, Ordering::Relaxed);
264 let extract_dir = bundle_path
265 .parent()
266 .unwrap_or(Path::new("."))
267 .join(".rustbridge-cache")
268 .join(loader.manifest().plugin.name.as_str())
269 .join(loader.manifest().plugin.version.as_str())
270 .join(instance_id.to_string());
271
272 let lib_path = loader.extract_library_for_current_platform(&extract_dir)?;
274
275 debug!("Extracted library to: {}", lib_path.display());
276
277 Self::load_with_config(lib_path, config, log_callback)
279 }
280
281 pub fn load_bundle_variant_with_config<P: AsRef<Path>>(
305 bundle_path: P,
306 variant: &str,
307 config: &PluginConfig,
308 log_callback: Option<LogCallbackFn>,
309 ) -> ConsumerResult<NativePlugin> {
310 let bundle_path = bundle_path.as_ref();
311 debug!(
312 "Loading bundle variant '{}' from: {}",
313 variant,
314 bundle_path.display()
315 );
316
317 let mut loader = BundleLoader::open(bundle_path)?;
319
320 let platform = rustbridge_bundle::Platform::current().ok_or_else(|| {
322 ConsumerError::Bundle(rustbridge_bundle::BundleError::UnsupportedPlatform(
323 "Current platform is not supported".to_string(),
324 ))
325 })?;
326
327 if !loader.supports_current_platform() {
328 return Err(ConsumerError::Bundle(
329 rustbridge_bundle::BundleError::UnsupportedPlatform(
330 "Current platform not supported by bundle".to_string(),
331 ),
332 ));
333 }
334
335 let instance_id = EXTRACT_INSTANCE.fetch_add(1, Ordering::Relaxed);
338 let extract_dir = bundle_path
339 .parent()
340 .unwrap_or(Path::new("."))
341 .join(".rustbridge-cache")
342 .join(loader.manifest().plugin.name.as_str())
343 .join(loader.manifest().plugin.version.as_str())
344 .join(format!("{variant}-{instance_id}"));
345
346 let lib_path = loader.extract_library_variant(platform, variant, &extract_dir)?;
348
349 debug!("Extracted variant library to: {}", lib_path.display());
350
351 Self::load_with_config(lib_path, config, log_callback)
353 }
354
355 pub fn load_bundle_to_dir<P: AsRef<Path>, Q: AsRef<Path>>(
366 bundle_path: P,
367 extract_dir: Q,
368 config: &PluginConfig,
369 log_callback: Option<LogCallbackFn>,
370 ) -> ConsumerResult<NativePlugin> {
371 Self::load_bundle_verified(
372 bundle_path,
373 Some(extract_dir),
374 config,
375 log_callback,
376 false,
377 None,
378 )
379 }
380
381 pub fn load_bundle_with_verification<P: AsRef<Path>>(
404 bundle_path: P,
405 config: &PluginConfig,
406 log_callback: Option<LogCallbackFn>,
407 verify_signatures: bool,
408 public_key_override: Option<&str>,
409 ) -> ConsumerResult<NativePlugin> {
410 Self::load_bundle_verified(
411 bundle_path,
412 None::<&Path>,
413 config,
414 log_callback,
415 verify_signatures,
416 public_key_override,
417 )
418 }
419
420 fn load_bundle_verified<P: AsRef<Path>, Q: AsRef<Path>>(
422 bundle_path: P,
423 extract_dir: Option<Q>,
424 config: &PluginConfig,
425 log_callback: Option<LogCallbackFn>,
426 verify_signatures: bool,
427 public_key_override: Option<&str>,
428 ) -> ConsumerResult<NativePlugin> {
429 let bundle_path = bundle_path.as_ref();
430 debug!("Loading bundle from: {}", bundle_path.display());
431
432 let mut loader = BundleLoader::open(bundle_path)?;
434
435 let platform = rustbridge_bundle::Platform::current().ok_or_else(|| {
437 ConsumerError::Bundle(rustbridge_bundle::BundleError::UnsupportedPlatform(
438 "Current platform is not supported".to_string(),
439 ))
440 })?;
441
442 if !loader.supports_current_platform() {
443 return Err(ConsumerError::Bundle(
444 rustbridge_bundle::BundleError::UnsupportedPlatform(
445 "Current platform not supported by bundle".to_string(),
446 ),
447 ));
448 }
449
450 let extract_dir_path: std::path::PathBuf = match extract_dir {
454 Some(dir) => dir.as_ref().to_path_buf(),
455 None => {
456 let instance_id = EXTRACT_INSTANCE.fetch_add(1, Ordering::Relaxed);
457 bundle_path
458 .parent()
459 .unwrap_or(Path::new("."))
460 .join(".rustbridge-cache")
461 .join(loader.manifest().plugin.name.as_str())
462 .join(loader.manifest().plugin.version.as_str())
463 .join(instance_id.to_string())
464 }
465 };
466
467 let lib_path = if verify_signatures {
469 loader.extract_library_verified(
470 platform,
471 &extract_dir_path,
472 true,
473 public_key_override,
474 )?
475 } else {
476 loader.extract_library_for_current_platform(&extract_dir_path)?
477 };
478
479 debug!("Extracted library to: {}", lib_path.display());
480
481 Self::load_with_config(lib_path, config, log_callback)
483 }
484
485 pub fn load_by_name(name: &str) -> ConsumerResult<NativePlugin> {
504 Self::load_by_name_with_config(name, &PluginConfig::default(), None)
505 }
506
507 pub fn load_by_name_with_config(
509 name: &str,
510 config: &PluginConfig,
511 log_callback: Option<LogCallbackFn>,
512 ) -> ConsumerResult<NativePlugin> {
513 let lib_name = library_filename(name);
514
515 let mut search_paths = vec![
517 std::path::PathBuf::from("."),
518 std::path::PathBuf::from("./target/release"),
519 std::path::PathBuf::from("./target/debug"),
520 ];
521
522 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
526 let manifest_path = std::path::PathBuf::from(manifest_dir);
527 for ancestor in manifest_path.ancestors().skip(1) {
528 let release = ancestor.join("target").join("release");
529 if release.is_dir() {
530 search_paths.push(release);
531 search_paths.push(ancestor.join("target").join("debug"));
532 break;
533 }
534 }
535 }
536
537 for search_path in &search_paths {
538 let full_path = search_path.join(&lib_name);
539 if full_path.exists() {
540 debug!("Found library at: {}", full_path.display());
541 return Self::load_with_config(full_path, config, log_callback);
542 }
543 }
544
545 debug!("Attempting to load '{}' from system paths", lib_name);
547 Self::load_with_config(&lib_name, config, log_callback)
548 }
549}
550
551fn library_filename(name: &str) -> String {
557 #[cfg(target_os = "linux")]
558 {
559 format!("lib{name}.so")
560 }
561 #[cfg(target_os = "macos")]
562 {
563 format!("lib{name}.dylib")
564 }
565 #[cfg(target_os = "windows")]
566 {
567 format!("{name}.dll")
568 }
569 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
570 {
571 format!("lib{name}.so")
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 #![allow(non_snake_case)]
578 #![allow(clippy::unwrap_used)]
579
580 use super::*;
581 use std::ffi::CString;
582
583 #[test]
584 fn NativePluginLoader___load___nonexistent_library___returns_error() {
585 let result = NativePluginLoader::load("/nonexistent/library.so");
586
587 assert!(result.is_err());
588 let err = result.err().unwrap();
589 assert!(matches!(err, ConsumerError::LibraryLoad(_)));
590 }
591
592 #[test]
593 fn NativePluginLoader___load_bundle___nonexistent_bundle___returns_error() {
594 let result = NativePluginLoader::load_bundle("/nonexistent/bundle.rbp");
595
596 assert!(result.is_err());
597 let err = result.err().unwrap();
598 assert!(matches!(err, ConsumerError::Bundle(_)));
599 }
600
601 #[test]
602 fn NativePluginLoader___load_bundle_variant___nonexistent_bundle___returns_error() {
603 let result = NativePluginLoader::load_bundle_variant_with_config(
604 "/nonexistent/bundle.rbp",
605 "debug",
606 &PluginConfig::default(),
607 None,
608 );
609
610 assert!(result.is_err());
611 let err = result.err().unwrap();
612 assert!(matches!(err, ConsumerError::Bundle(_)));
613 }
614
615 #[test]
616 fn ffi_log_callback___no_callback_set___does_not_panic() {
617 set_log_callback(None);
619
620 let target = CString::new("test").unwrap();
622 let message = b"test message";
623
624 unsafe {
626 ffi_log_callback(2, target.as_ptr(), message.as_ptr(), message.len());
627 }
628 }
629
630 #[test]
631 fn ffi_log_callback___with_callback___invokes_callback() {
632 use std::sync::Arc;
633 use std::sync::atomic::{AtomicBool, Ordering};
634
635 let called = Arc::new(AtomicBool::new(false));
636 let called_clone = called.clone();
637
638 let callback: LogCallbackFn = Arc::new(move |level, target, message| {
639 assert_eq!(level, LogLevel::Info);
640 assert_eq!(target, "test");
641 assert_eq!(message, "test message");
642 called_clone.store(true, Ordering::SeqCst);
643 });
644
645 set_log_callback(Some(callback));
646
647 let target = CString::new("test").unwrap();
648 let message = b"test message";
649
650 unsafe {
651 ffi_log_callback(2, target.as_ptr(), message.as_ptr(), message.len());
652 }
653
654 assert!(called.load(Ordering::SeqCst));
655
656 set_log_callback(None);
658 }
659
660 #[test]
661 fn ffi_log_callback___null_pointers___uses_empty_strings() {
662 use std::sync::Arc;
663 use std::sync::atomic::{AtomicBool, Ordering};
664
665 let called = Arc::new(AtomicBool::new(false));
666 let called_clone = called.clone();
667
668 let callback: LogCallbackFn = Arc::new(move |_level, target, message| {
669 assert_eq!(target, "");
670 assert_eq!(message, "");
671 called_clone.store(true, Ordering::SeqCst);
672 });
673
674 set_log_callback(Some(callback));
675
676 unsafe {
677 ffi_log_callback(2, std::ptr::null(), std::ptr::null(), 0);
678 }
679
680 assert!(called.load(Ordering::SeqCst));
681
682 set_log_callback(None);
684 }
685}