Foreign Function Safety
Calling or exposing a foreign function crosses outside the guarantees checked by rustc. Maintaining foreign function safety requires verifying a strict checklist of memory, lifetime, aliasing, initialization, and concurrency invariants at every FFI boundary.
+-------------------------------------------------------------------+
| The 6 Pillars of FFI Safety |
| |
| 1. Pointer Validity: Non-null, aligned, dereferenceable |
| 2. Lifetime Bounds: Pointed-to data outlives C's usage of it |
| 3. Aliasing Rules: No `&mut T` coexisting with C writes/reads|
| 4. Bit-Pattern Validity: No invalid `bool` (2) or invalid `enum` |
| 5. Initialization: Use `MaybeUninit<T>` for C out-buffers |
| 6. Thread Safety: Respect C library reentrancy / thread locks|
+-------------------------------------------------------------------+
1. Using MaybeUninit<T> for C Out-Parameters
Many C functions initialize a struct or buffer via an out-pointer (int get_stats(Stats *out);). In Rust, creating an uninitialized variable via unsafe { std::mem::uninitialized() } is immediate Undefined Behavior!
Always allocate C out-parameters using std::mem::MaybeUninit<T>, pass out.as_mut_ptr() to C, and call unsafe { out.assume_init() } only after verifying that the C function returned a success status code:
use core::ffi::c_int;
use std::mem::MaybeUninit;
#[repr(C)]
#[derive(Debug, PartialEq, Eq)]
pub struct CSystemStats {
pub active_workers: u32,
pub queued_jobs: u64,
}
// Simulated C function that populates `out_stats` and returns 0 on success:
unsafe extern "C" fn c_populate_stats(out_stats: *mut CSystemStats) -> c_int {
if out_stats.is_null() {
return -1;
}
// SAFETY: Caller passed a valid, aligned pointer to `CSystemStats`.
unsafe {
(*out_stats).active_workers = 8;
(*out_stats).queued_jobs = 120;
}
0
}
pub fn query_system_stats() -> Result<CSystemStats, i32> {
let mut uninit_stats = MaybeUninit::<CSystemStats>::uninit();
// SAFETY: `uninit_stats.as_mut_ptr()` points to properly aligned stack memory
// sized for `CSystemStats`.
let rc = unsafe { c_populate_stats(uninit_stats.as_mut_ptr()) };
if rc == 0 {
// SAFETY: `c_populate_stats` returned 0, guaranteeing all fields of
// `CSystemStats` were initialized.
Ok(unsafe { uninit_stats.assume_init() })
} else {
Err(rc)
}
}
fn main() {
let stats = query_system_stats().unwrap();
println!("Initialized stats from C out-ptr: {stats:?}");
}
Initialized stats from C out-ptr: CSystemStats { active_workers: 8, queued_jobs: 120 }
2. Defending Against Invalid Enum and bool Bit Patterns from C
In C, an enum is merely an integer, and programmers frequently pass arbitrary integers or bitwise OR combinations (FLAG_A | FLAG_B) through an enum type. In Rust, constructing an enum with a discriminant value that does not match any declared variant—or a bool whose byte value is anything other than 0 or 1—is immediate Undefined Behavior!
| Direction | Safe FFI Type | Why |
|---|---|---|
| Rust passing enum to C | #[repr(C)] enum Mode { ... } |
Rust guarantees the discriminant is one of the valid variants. |
| Rust receiving enum/flags from C | Raw integer (c_int / u32) or #[repr(transparent)] struct Flags(u32) + TryFrom<u32> |
Validates the integer before constructing a Rust enum, preventing UB if C passes an out-of-range discriminant! |
#[derive(Debug, PartialEq, Eq)]
pub enum CompressionLevel {
Fast = 1,
Balanced = 2,
Max = 3,
}
impl TryFrom<i32> for CompressionLevel {
type Error = i32;
fn try_from(raw: i32) -> Result<Self, Self::Error> {
match raw {
1 => Ok(Self::Fast),
2 => Ok(Self::Balanced),
3 => Ok(Self::Max),
invalid => Err(invalid),
}
}
}
fn main() {
println!("Valid C code 2: {:?}", CompressionLevel::try_from(2));
println!("Invalid C code 99: {:?}", CompressionLevel::try_from(99));
}
Valid C code 2: Ok(Balanced)
Invalid C code 99: Err(99)