- queue_index: mmap-based priority queue with safe storage wrapper - dataset_hash: BLAKE3 parallel hashing with rayon - common: FFI utilities with panic recovery - Minimal deps: ~20 total (rayon, blake3, memmap2, walkdir, chrono) - Drop crossbeam, prometheus - use stdlib + manual metrics - Makefile: cargo build targets, help text updated - Forgejo CI: clippy, tests, miri, cargo-deny - C FFI compatible with existing Go bindings
143 lines
3.7 KiB
Rust
143 lines
3.7 KiB
Rust
//! 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<T>(f: impl FnOnce() -> T + std::panic::UnwindSafe) -> Option<T> {
|
|
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<String> {
|
|
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<Option<String>> = RefCell::new(None);
|
|
}
|
|
|
|
/// Store an error message
|
|
pub fn set_error(msg: impl Into<String>) {
|
|
LAST_ERROR.with(|e| {
|
|
*e.borrow_mut() = Some(msg.into());
|
|
});
|
|
}
|
|
|
|
/// Get and clear the last error
|
|
pub fn take_error() -> Option<String> {
|
|
LAST_ERROR.with(|e| e.borrow_mut().take())
|
|
}
|
|
|
|
/// Peek at the last error without clearing
|
|
pub fn peek_error() -> Option<String> {
|
|
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);
|
|
}
|
|
}
|
|
}
|