Foreign Function Interface (FFI)

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)