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
29thread_local! {
32 static LOG_CALLBACK: std::cell::RefCell<Option<LogCallbackFn>> = const { std::cell::RefCell::new(None) };
33}
34
35fn set_log_callback(callback: Option<LogCallbackFn>) {
37 LOG_CALLBACK.with(|cb| {
38 *cb.borrow_mut() = callback;
39 });
40}
41
42unsafe extern "C" fn ffi_log_callback(
48 level: u8,
49 target: *const c_char,
50 message: *const u8,
51 message_len: usize,
52) {
53 LOG_CALLBACK.with(|cb| {
54 if let Some(callback) = cb.borrow().as_ref() {
55 let log_level = LogLevel::from_u8(level);
56
57 let target_str = if target.is_null() {
59 ""
60 } else {
61 unsafe { std::ffi::CStr::from_ptr(target) }
62 .to_str()
63 .unwrap_or("")
64 };
65
66 let message_str = if message.is_null() || message_len == 0 {
68 ""
69 } else {
70 let bytes = unsafe { std::slice::from_raw_parts(message, message_len) };
71 std::str::from_utf8(bytes).unwrap_or("")
72 };
73
74 callback(log_level, target_str, message_str);
75 }
76 });
77}
78
79pub struct NativePluginLoader;
83
84impl NativePluginLoader {
85 pub fn load<P: AsRef<Path>>(path: P) -> ConsumerResult<NativePlugin> {
99 Self::load_with_config(path, &PluginConfig::default(), None)
100 }
101
102 pub fn load_with_config<P: AsRef<Path>>(
125 path: P,
126 config: &PluginConfig,
127 log_callback: Option<LogCallbackFn>,
128 ) -> ConsumerResult<NativePlugin> {
129 let path = path.as_ref();
130 debug!("Loading plugin from: {}", path.display());
131
132 let library = unsafe { Library::new(path) }?;
135
136 let plugin_create: PluginCreateFn = unsafe { *library.get(b"plugin_create\0")? };
138 let plugin_init: PluginInitFn = unsafe { *library.get(b"plugin_init\0")? };
139 let plugin_call: PluginCallFn = unsafe { *library.get(b"plugin_call\0")? };
140 let plugin_shutdown: PluginShutdownFn = unsafe { *library.get(b"plugin_shutdown\0")? };
141 let plugin_get_state: PluginGetStateFn = unsafe { *library.get(b"plugin_get_state\0")? };
142 let plugin_get_rejected_count: PluginGetRejectedCountFn =
143 unsafe { *library.get(b"plugin_get_rejected_count\0")? };
144 let plugin_set_log_level: PluginSetLogLevelFn =
145 unsafe { *library.get(b"plugin_set_log_level\0")? };
146 let plugin_free_buffer: PluginFreeBufferFn =
147 unsafe { *library.get(b"plugin_free_buffer\0")? };
148
149 let plugin_call_raw: Option<PluginCallRawFn> =
151 unsafe { library.get(b"plugin_call_raw\0").ok().map(|s| *s) };
152 let rb_response_free: Option<RbResponseFreeFn> =
153 unsafe { library.get(b"rb_response_free\0").ok().map(|s| *s) };
154
155 set_log_callback(log_callback);
157 let ffi_callback: LogCallback = Some(ffi_log_callback);
158
159 let plugin_ptr = unsafe { plugin_create() };
162 if plugin_ptr.is_null() {
163 return Err(ConsumerError::NullHandle);
164 }
165
166 let config_json = serde_json::to_vec(config)?;
168
169 let handle: FfiPluginHandle = unsafe {
172 plugin_init(
173 plugin_ptr,
174 config_json.as_ptr(),
175 config_json.len(),
176 ffi_callback,
177 )
178 };
179
180 if handle.is_null() {
181 return Err(ConsumerError::NullHandle);
182 }
183
184 debug!("Plugin initialized with handle: {:?}", handle);
185
186 Ok(unsafe {
188 NativePlugin::new(
189 library,
190 handle,
191 plugin_call,
192 plugin_call_raw,
193 plugin_shutdown,
194 plugin_get_state,
195 plugin_get_rejected_count,
196 plugin_set_log_level,
197 plugin_free_buffer,
198 rb_response_free,
199 )
200 })
201 }
202
203 pub fn load_bundle<P: AsRef<Path>>(bundle_path: P) -> ConsumerResult<NativePlugin> {
217 Self::load_bundle_with_config(bundle_path, &PluginConfig::default(), None)
218 }
219
220 pub fn load_bundle_with_config<P: AsRef<Path>>(
239 bundle_path: P,
240 config: &PluginConfig,
241 log_callback: Option<LogCallbackFn>,
242 ) -> ConsumerResult<NativePlugin> {
243 let bundle_path = bundle_path.as_ref();
244 debug!("Loading bundle from: {}", bundle_path.display());
245
246 let mut loader = BundleLoader::open(bundle_path)?;
248
249 if !loader.supports_current_platform() {
251 return Err(ConsumerError::Bundle(
252 rustbridge_bundle::BundleError::UnsupportedPlatform(
253 "Current platform not supported by bundle".to_string(),
254 ),
255 ));
256 }
257
258 let instance_id = EXTRACT_INSTANCE.fetch_add(1, Ordering::Relaxed);
261 let extract_dir = bundle_path
262 .parent()
263 .unwrap_or(Path::new("."))
264 .join(".rustbridge-cache")
265 .join(loader.manifest().plugin.name.as_str())
266 .join(loader.manifest().plugin.version.as_str())
267 .join(instance_id.to_string());
268
269 let lib_path = loader.extract_library_for_current_platform(&extract_dir)?;
271
272 debug!("Extracted library to: {}", lib_path.display());
273
274 Self::load_with_config(lib_path, config, log_callback)
276 }
277
278 pub fn load_bundle_variant_with_config<P: AsRef<Path>>(
302 bundle_path: P,
303 variant: &str,
304 config: &PluginConfig,
305 log_callback: Option<LogCallbackFn>,
306 ) -> ConsumerResult<NativePlugin> {
307 let bundle_path = bundle_path.as_ref();
308 debug!(
309 "Loading bundle variant '{}' from: {}",
310 variant,
311 bundle_path.display()
312 );
313
314 let mut loader = BundleLoader::open(bundle_path)?;
316
317 let platform = rustbridge_bundle::Platform::current().ok_or_else(|| {
319 ConsumerError::Bundle(rustbridge_bundle::BundleError::UnsupportedPlatform(
320 "Current platform is not supported".to_string(),
321 ))
322 })?;
323
324 if !loader.supports_current_platform() {
325 return Err(ConsumerError::Bundle(
326 rustbridge_bundle::BundleError::UnsupportedPlatform(
327 "Current platform not supported by bundle".to_string(),
328 ),
329 ));
330 }
331
332 let instance_id = EXTRACT_INSTANCE.fetch_add(1, Ordering::Relaxed);
335 let extract_dir = bundle_path
336 .parent()
337 .unwrap_or(Path::new("."))
338 .join(".rustbridge-cache")
339 .join(loader.manifest().plugin.name.as_str())
340 .join(loader.manifest().plugin.version.as_str())
341 .join(format!("{variant}-{instance_id}"));
342
343 let lib_path = loader.extract_library_variant(platform, variant, &extract_dir)?;
345
346 debug!("Extracted variant library to: {}", lib_path.display());
347
348 Self::load_with_config(lib_path, config, log_callback)
350 }
351
352 pub fn load_bundle_to_dir<P: AsRef<Path>, Q: AsRef<Path>>(
363 bundle_path: P,
364 extract_dir: Q,
365 config: &PluginConfig,
366 log_callback: Option<LogCallbackFn>,
367 ) -> ConsumerResult<NativePlugin> {
368 Self::load_bundle_verified(
369 bundle_path,
370 Some(extract_dir),
371 config,
372 log_callback,
373 false,
374 None,
375 )
376 }
377
378 pub fn load_bundle_with_verification<P: AsRef<Path>>(
401 bundle_path: P,
402 config: &PluginConfig,
403 log_callback: Option<LogCallbackFn>,
404 verify_signatures: bool,
405 public_key_override: Option<&str>,
406 ) -> ConsumerResult<NativePlugin> {
407 Self::load_bundle_verified(
408 bundle_path,
409 None::<&Path>,
410 config,
411 log_callback,
412 verify_signatures,
413 public_key_override,
414 )
415 }
416
417 fn load_bundle_verified<P: AsRef<Path>, Q: AsRef<Path>>(
419 bundle_path: P,
420 extract_dir: Option<Q>,
421 config: &PluginConfig,
422 log_callback: Option<LogCallbackFn>,
423 verify_signatures: bool,
424 public_key_override: Option<&str>,
425 ) -> ConsumerResult<NativePlugin> {
426 let bundle_path = bundle_path.as_ref();
427 debug!("Loading bundle from: {}", bundle_path.display());
428
429 let mut loader = BundleLoader::open(bundle_path)?;
431
432 let platform = rustbridge_bundle::Platform::current().ok_or_else(|| {
434 ConsumerError::Bundle(rustbridge_bundle::BundleError::UnsupportedPlatform(
435 "Current platform is not supported".to_string(),
436 ))
437 })?;
438
439 if !loader.supports_current_platform() {
440 return Err(ConsumerError::Bundle(
441 rustbridge_bundle::BundleError::UnsupportedPlatform(
442 "Current platform not supported by bundle".to_string(),
443 ),
444 ));
445 }
446
447 let extract_dir_path: std::path::PathBuf = match extract_dir {
451 Some(dir) => dir.as_ref().to_path_buf(),
452 None => {
453 let instance_id = EXTRACT_INSTANCE.fetch_add(1, Ordering::Relaxed);
454 bundle_path
455 .parent()
456 .unwrap_or(Path::new("."))
457 .join(".rustbridge-cache")
458 .join(loader.manifest().plugin.name.as_str())
459 .join(loader.manifest().plugin.version.as_str())
460 .join(instance_id.to_string())
461 }
462 };
463
464 let lib_path = if verify_signatures {
466 loader.extract_library_verified(
467 platform,
468 &extract_dir_path,
469 true,
470 public_key_override,
471 )?
472 } else {
473 loader.extract_library_for_current_platform(&extract_dir_path)?
474 };
475
476 debug!("Extracted library to: {}", lib_path.display());
477
478 Self::load_with_config(lib_path, config, log_callback)
480 }
481
482 pub fn load_by_name(name: &str) -> ConsumerResult<NativePlugin> {
501 Self::load_by_name_with_config(name, &PluginConfig::default(), None)
502 }
503
504 pub fn load_by_name_with_config(
506 name: &str,
507 config: &PluginConfig,
508 log_callback: Option<LogCallbackFn>,
509 ) -> ConsumerResult<NativePlugin> {
510 let lib_name = library_filename(name);
511
512 let search_paths = [
514 std::path::PathBuf::from("."),
515 std::path::PathBuf::from("./target/release"),
516 std::path::PathBuf::from("./target/debug"),
517 ];
518
519 for search_path in &search_paths {
520 let full_path = search_path.join(&lib_name);
521 if full_path.exists() {
522 debug!("Found library at: {}", full_path.display());
523 return Self::load_with_config(full_path, config, log_callback);
524 }
525 }
526
527 debug!("Attempting to load '{}' from system paths", lib_name);
529 Self::load_with_config(&lib_name, config, log_callback)
530 }
531}
532
533fn library_filename(name: &str) -> String {
539 #[cfg(target_os = "linux")]
540 {
541 format!("lib{name}.so")
542 }
543 #[cfg(target_os = "macos")]
544 {
545 format!("lib{name}.dylib")
546 }
547 #[cfg(target_os = "windows")]
548 {
549 format!("{name}.dll")
550 }
551 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
552 {
553 format!("lib{name}.so")
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 #![allow(non_snake_case)]
560 #![allow(clippy::unwrap_used)]
561
562 use super::*;
563 use std::ffi::CString;
564
565 #[test]
566 fn NativePluginLoader___load___nonexistent_library___returns_error() {
567 let result = NativePluginLoader::load("/nonexistent/library.so");
568
569 assert!(result.is_err());
570 let err = result.err().unwrap();
571 assert!(matches!(err, ConsumerError::LibraryLoad(_)));
572 }
573
574 #[test]
575 fn NativePluginLoader___load_bundle___nonexistent_bundle___returns_error() {
576 let result = NativePluginLoader::load_bundle("/nonexistent/bundle.rbp");
577
578 assert!(result.is_err());
579 let err = result.err().unwrap();
580 assert!(matches!(err, ConsumerError::Bundle(_)));
581 }
582
583 #[test]
584 fn NativePluginLoader___load_bundle_variant___nonexistent_bundle___returns_error() {
585 let result = NativePluginLoader::load_bundle_variant_with_config(
586 "/nonexistent/bundle.rbp",
587 "debug",
588 &PluginConfig::default(),
589 None,
590 );
591
592 assert!(result.is_err());
593 let err = result.err().unwrap();
594 assert!(matches!(err, ConsumerError::Bundle(_)));
595 }
596
597 #[test]
598 fn ffi_log_callback___no_callback_set___does_not_panic() {
599 set_log_callback(None);
601
602 let target = CString::new("test").unwrap();
604 let message = b"test message";
605
606 unsafe {
608 ffi_log_callback(2, target.as_ptr(), message.as_ptr(), message.len());
609 }
610 }
611
612 #[test]
613 fn ffi_log_callback___with_callback___invokes_callback() {
614 use std::sync::Arc;
615 use std::sync::atomic::{AtomicBool, Ordering};
616
617 let called = Arc::new(AtomicBool::new(false));
618 let called_clone = called.clone();
619
620 let callback: LogCallbackFn = Arc::new(move |level, target, message| {
621 assert_eq!(level, LogLevel::Info);
622 assert_eq!(target, "test");
623 assert_eq!(message, "test message");
624 called_clone.store(true, Ordering::SeqCst);
625 });
626
627 set_log_callback(Some(callback));
628
629 let target = CString::new("test").unwrap();
630 let message = b"test message";
631
632 unsafe {
633 ffi_log_callback(2, target.as_ptr(), message.as_ptr(), message.len());
634 }
635
636 assert!(called.load(Ordering::SeqCst));
637
638 set_log_callback(None);
640 }
641
642 #[test]
643 fn ffi_log_callback___null_pointers___uses_empty_strings() {
644 use std::sync::Arc;
645 use std::sync::atomic::{AtomicBool, Ordering};
646
647 let called = Arc::new(AtomicBool::new(false));
648 let called_clone = called.clone();
649
650 let callback: LogCallbackFn = Arc::new(move |_level, target, message| {
651 assert_eq!(target, "");
652 assert_eq!(message, "");
653 called_clone.store(true, Ordering::SeqCst);
654 });
655
656 set_log_callback(Some(callback));
657
658 unsafe {
659 ffi_log_callback(2, std::ptr::null(), std::ptr::null(), 0);
660 }
661
662 assert!(called.load(Ordering::SeqCst));
663
664 set_log_callback(None);
666 }
667}