id: ffi-04 original_id: P.UNS.FFI.04 level: P impact: CRITICAL clippy: panic_in_result_fn
Handle Panics When Crossing FFI Boundaries
Summary
Panics must not unwind across FFI boundaries. Use catch_unwind or mark functions as extern "C-unwind".
Rationale
- Unwinding across C code is undefined behavior
- C has no concept of Rust panics
- Can corrupt C stack frames and cause crashes
- Even with
panic=abort, still UB to attempt unwinding inextern "C"
Bad Example
// DON'T: Allow panics to escape to C
#[no_mangle]
pub extern "C" fn callback(data: *const u8, len: usize) -> i32 {
let slice = unsafe { std::slice::from_raw_parts(data, len) };
// If this panics, UB occurs!
let sum: i32 = slice.iter().map(|&x| x as i32).sum();
// If this panics due to overflow in debug, UB!
process(sum)
}
// DON'T: Unwrap in extern functions
#[no_mangle]
pub extern "C" fn parse_config(path: *const c_char) -> i32 {
let path = unsafe { CStr::from_ptr(path) };
let config = std::fs::read_to_string(path.to_str().unwrap()).unwrap(); // Can panic!
0
}
Good Example
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ffi::CStr;
use std::os::raw::{c_char, c_int};
// DO: Catch panics at FFI boundary
#[no_mangle]
pub extern "C" fn safe_callback(data: *const u8, len: usize) -> c_int {
let result = catch_unwind(AssertUnwindSafe(|| {
if data.is_null() || len == 0 {
return -1;
}
let slice = unsafe { std::slice::from_raw_parts(data, len) };
let sum: i32 = slice.iter().map(|&x| x as i32).sum();
sum
}));
match result {
Ok(value) => value,
Err(_) => {
// Log error, return error code
eprintln!("Panic caught at FFI boundary");
-1
}
}
}
// DO: Use Result-based API internally
#[no_mangle]
pub extern "C" fn parse_config(path: *const c_char) -> c_int {
let result = catch_unwind(AssertUnwindSafe(|| -> Result<(), Box<dyn std::error::Error>> {
let path = unsafe { CStr::from_ptr(path) }.to_str()?;
let _config = std::fs::read_to_string(path)?;
Ok(())
}));
match result {
Ok(Ok(())) => 0,
Ok(Err(e)) => {
eprintln!("Error: {}", e);
-1
}
Err(_) => {
eprintln!("Panic in parse_config");
-2
}
}
}
// DO: For Rust-calling-Rust across C, use "C-unwind"
#[no_mangle]
pub extern "C-unwind" fn rust_callback_can_unwind() {
// This is OK to panic if called from Rust through C
// The "C-unwind" ABI allows unwinding
panic!("This is allowed");
}
FFI Error Handling Pattern
// Define error codes
const SUCCESS: c_int = 0;
const ERR_NULL_PTR: c_int = -1;
const ERR_INVALID_UTF8: c_int = -2;
const ERR_IO: c_int = -3;
const ERR_PANIC: c_int = -99;
// Thread-local for detailed error
thread_local! {
static LAST_ERROR: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
}
fn set_error(msg: String) {
LAST_ERROR.with(|e| *e.borrow_mut() = Some(msg));
}
#[no_mangle]
pub extern "C" fn get_last_error() -> *const c_char {
LAST_ERROR.with(|e| {
e.borrow().as_ref().map(|s| s.as_ptr() as *const c_char)
.unwrap_or(std::ptr::null())
})
}
Checklist
- Does my extern "C" function use catch_unwind?
- Am I avoiding unwrap/expect in FFI functions?
- Do I return error codes for error conditions?
- Have I considered using "C-unwind" for Rust-to-Rust through C?
Related Rules
ffi-08: Handle errors properly in FFIsafety-01: Panic safety
你可能感兴趣的文章
Do Not Expose Raw Pointers in Public APIs
Unsafe Code Authors Must Verify Safety Invariants
Ensure Consistent Data Layout for Custom Types
Add Safety Section in Docs for Public Unsafe Functions
Do Not Use Union Variants Across Different Lifetimes
Use MaybeUninit
Ensure C-ABI Compatibility for Strings Between Rust and C
Do Not Dereference Pointers Cast to Misaligned Types
Use Dedicated Opaque Type Pointers Instead of c_void for C Opaque Types