//! Common FFI utilities for native libraries //! //! Provides safe wrappers for FFI boundary operations including: //! - Panic recovery at FFI boundaries //! - String conversion between C and Rust //! - Error handling patterns use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_int}; use std::ptr; /// Recover from panics at FFI boundaries, returning a safe default /// /// # Safety /// The closure should not leak resources on panic pub unsafe fn ffi_boundary(f: impl FnOnce() -> T + std::panic::UnwindSafe) -> Option { match std::panic::catch_unwind(f) { Ok(result) => Some(result), Err(_) => { eprintln!("FFI boundary panic caught and recovered"); None } } } /// Convert C string to Rust String /// /// # Safety /// ptr must be a valid null-terminated UTF-8 string or null pub unsafe fn c_str_to_string(ptr: *const c_char) -> Option { if ptr.is_null() { return None; } CStr::from_ptr(ptr) .to_str() .ok() .map(|s| s.to_string()) } /// Convert Rust String to C string (leaked, caller must free) /// /// Returns null on error. On success, returns a pointer that must be freed with `free_string`. pub fn string_to_c_str(s: &str) -> *mut c_char { match CString::new(s) { Ok(cstring) => cstring.into_raw(), Err(_) => ptr::null_mut(), } } /// Free a string previously created by `string_to_c_str` /// /// # Safety /// ptr must be a string previously returned by string_to_c_str, or null pub unsafe fn free_string(ptr: *mut c_char) { if !ptr.is_null() { let _ = CString::from_raw(ptr); } } /// Set an error code and message /// /// Returns -1 for error, caller should return this from FFI function pub fn set_error(error_ptr: *mut *const c_char, msg: &str) -> c_int { if !error_ptr.is_null() { unsafe { *error_ptr = string_to_c_str(msg); } } -1 } /// FFI-safe result type for boolean operations pub type FfiResult = c_int; pub const FFI_OK: FfiResult = 0; pub const FFI_ERROR: FfiResult = -1; /// Thread-local error storage for FFI boundaries pub mod error { use std::cell::RefCell; thread_local! { static LAST_ERROR: RefCell> = RefCell::new(None); } /// Store an error message pub fn set_error(msg: impl Into) { LAST_ERROR.with(|e| { *e.borrow_mut() = Some(msg.into()); }); } /// Get and clear the last error pub fn take_error() -> Option { LAST_ERROR.with(|e| e.borrow_mut().take()) } /// Peek at the last error without clearing pub fn peek_error() -> Option { LAST_ERROR.with(|e| e.borrow().clone()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_c_str_roundtrip() { let original = "hello world"; let c_ptr = string_to_c_str(original); assert!(!c_ptr.is_null()); unsafe { let recovered = c_str_to_string(c_ptr); assert_eq!(recovered, Some(original.to_string())); free_string(c_ptr); } } #[test] fn test_null_handling() { unsafe { assert_eq!(c_str_to_string(ptr::null()), None); free_string(ptr::null_mut()); // Should not panic } } #[test] fn test_ffi_boundary_recovery() { unsafe { // Normal case let result = ffi_boundary(|| 42); assert_eq!(result, Some(42)); // Panic case - should recover let result = ffi_boundary(|| { panic!("test panic"); }); assert_eq!(result, None); } } }