Foreign Function Interface (FFI)

Raw Pointers at FFI Boundaries

At an FFI boundary, C pointers (const T* and T*) map to Rust raw pointers (*const T and *mut T) or to non-null pointer types (NonNull<T>, Option<&T>, Option<&mut T>). Unlike Rust references (&T and &mut T), raw pointers may be null (std::ptr::null()), unaligned, uninitialized, or aliased, so validating raw pointers before converting them into Rust references or slices is critical for FFI soundness.

C Caller Passes:  (const uint8_t *ptr, size_t len)
                              |
                              v
Rust FFI Boundary Checks:
  1. Is `len == 0`?          ---> Return `&[]` immediately!
                                  (Never pass NULL to `slice::from_raw_parts`, even with len 0!)
  2. Is `ptr.is_null()`?     ---> Return error code (e.g., `-1`)
  3. Is `ptr.is_aligned()`?  ---> Required for `T` with `align_of::<T>() > 1`
  4. Construct slice safely  ---> `unsafe { std::slice::from_raw_parts(ptr, len) }`

Why Fat Pointers (&[T] and &str) Cannot Cross extern "C" Directly

In Rust, a slice reference &[T] or string slice &str is a 16-byte fat pointer (on 64-bit platforms) containing (data_ptr: *const T, len: usize) laid out with the unstable #[repr(Rust)] representation. C has no built-in equivalent to a Rust fat pointer.

Always decompose a slice into two separate parameters across extern "C": a thin raw pointer (*const T or *mut T) and a length (usize):

Rust Internal Type FFI Parameter Signature (extern "C") Conversion Method
&[T] ptr: *const T, len: usize slice.as_ptr(), slice.len() -> slice::from_raw_parts(ptr, len)
&mut [T] ptr: *mut T, len: usize slice.as_mut_ptr(), slice.len() -> slice::from_raw_parts_mut(ptr, len)
Option<&T> Option<&T> (or *const T) Niche optimization: Option<&T> is guaranteed to have the exact same ABI as a nullable C pointer const T* (None == NULL)!
Option<NonNull<T>> Option<NonNull<T>> (or *mut T) Guaranteed to have the exact same layout and ABI as *mut T (None == NULL).

Critical Rule: slice::from_raw_parts(NULL, 0) Is Undefined Behavior!

In C, it is common to pass (NULL, 0) to represent an empty buffer. However, in Rust, std::slice::from_raw_parts(ptr, len) requires ptr to be non-null and aligned even when len == 0! Passing (std::ptr::null(), 0) to slice::from_raw_parts is immediate Undefined Behavior (and caught by Miri).

Always handle len == 0 before calling slice::from_raw_parts:

/// Safely converts a C `(ptr, len)` buffer pair into `Option<&'a [T]>`.
///
/// - Returns `Some(&[])` if `len == 0` (even if `ptr` is null!).
/// - Returns `None` if `len > 0` and `ptr` is null or misaligned.
///
/// # Safety
///
/// If `len > 0` and `ptr` is non-null and aligned, `ptr` must point to `len`
/// contiguous initialized elements of `T` that remain valid and unmutated for `'a`.
pub unsafe fn slice_from_c_parts<'a, T>(ptr: *const T, len: usize) -> Option<&'a [T]> {
    if len == 0 {
        return Some(&[]);
    }
    if ptr.is_null() || !ptr.is_aligned() {
        return None;
    }
    // SAFETY: `len > 0`, `ptr` is non-null and properly aligned for `T`,
    // and caller guarantees `len` initialized elements.
    Some(unsafe { std::slice::from_raw_parts(ptr, len) })
}

/// Outbound C function accepting an out-parameter via nullable `Option<&mut u64>`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sum_u32_buffer(
    ptr: *const u32,
    len: usize,
    out_sum: Option<&mut u64>,
) -> i32 {
    let Some(out) = out_sum else {
        return -1; // `out_sum` was NULL
    };
    // SAFETY: Caller guarantees `(ptr, len)` is a valid buffer if non-null.
    let Some(slice) = (unsafe { slice_from_c_parts(ptr, len) }) else {
        return -2; // `ptr` was NULL or misaligned with `len > 0`
    };

    *out = slice.iter().copied().map(u64::from).sum();
    0
}

fn main() {
    let mut total = 0u64;

    // 1. Passing `(NULL, 0)` succeeds safely and produces 0:
    let rc_empty = unsafe { sum_u32_buffer(std::ptr::null(), 0, Some(&mut total)) };
    assert_eq!((rc_empty, total), (0, 0));

    // 2. Passing a real slice:
    let nums = [10u32, 20, 30, 40];
    let rc_ok = unsafe { sum_u32_buffer(nums.as_ptr(), nums.len(), Some(&mut total)) };
    assert_eq!((rc_ok, total), (0, 100));

    println!("Safe C buffer sum: rc={rc_ok}, total={total}");
}
Safe C buffer sum: rc=0, total=100