fetch_ml/native/rust/common/src/lib.rs
Jeremie Fraeys 7efefa1933
feat(native): implement Rust native layer as a test
- 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
2026-03-14 17:45:58 -04:00

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);
}
}
}