Foreign Function Interface (FFI)

ABI Compatibility

An Application Binary Interface (ABI) defines the low-level machine contract between compiled modules: how data types are laid out in memory, how function arguments are placed into CPU registers or onto the stack, who cleans up the stack frame, how return values are passed back, and how symbols are named in object files.

Source Level (API):   `fn compute(a: u64, b: Point) -> u64`
                                   |
                                   v  Compiled according to Target ABI
Binary Level (ABI):   - `a` passed in register `rdi` (x86_64 SysV) or `x0` (ARM64)
                      - `b` passed in `rsi` / `x1` (if <= 16B) or by pointer on stack
                      - Return value placed in `rax` / `x0`
                      - Caller-saved vs Callee-saved register preservation

Why Rust Does Not Have a Stable "Rust" ABI

Unlike C, Rust intentionally does not stabilize its default "Rust" ABI across compiler versions (rustc 1.84 vs rustc 1.85) or even across different build flags. Keeping the internal Rust ABI flexible allows rustc to perform optimizations that C compilers cannot do across public library boundaries:

  • Reordering struct and enum fields automatically to eliminate alignment padding.
  • Niche-filling optimizations (sizeof::<Option<NonZeroU64>>() == 8, sizeof::<Result<&u8, ()>>() == 8).
  • Passing small structs across registers or splitting fat pointers into separate argument registers.

Consequently, you cannot dynamically load a "Rust"-ABI shared library (crate-type = ["dylib"]) compiled with one version of rustc into a host binary compiled with another version of rustc. Whenever binary compatibility across compiler versions or languages is required, use extern "C" and #[repr(C)] (or #[repr(transparent)]) with crate-type = ["cdylib"].

The improper_ctypes and improper_ctypes_definitions Lints

rustc automatically checks every extern "C" declaration and definition and warns if you attempt to pass a non-FFI-safe Rust type (such as String, Vec<T>, &[T], Box<dyn Trait>, or a struct without #[repr(C)]) by value:

Rust Type FFI-Safe in extern "C" Signatures? Reason
i8..=i64, u8..=u64, usize, isize, f32, f64, bool Yes Standard fixed-width/pointer-width C primitives.
*const T, *mut T, NonNull<T>, &T, &mut T (where T: Sized) Yes Thin pointer matching C's T*.
Option<NonNull<T>>, Option<&T>, Option<extern "C" fn(...)> Yes Guaranteed null-pointer niche optimization (None == NULL).
#[repr(C)] struct/union/enum of FFI-safe fields Yes Deterministic platform C layout.
#[repr(transparent)] wrapper around an FFI-safe type Yes Identical ABI to the wrapped inner field (even for by-value return!).
String, Vec<T>, &str, &[T], dyn Trait, (A, B) tuples No (improper_ctypes warning) Unstable #[repr(Rust)] layout or fat pointer representation.

Guaranteed Null-Pointer Optimization Across FFI

Rust's ABI specification permanently guarantees that wrapping a non-null pointer type (&T, &mut T, NonNull<T>, Box<T>, or extern "C" fn(...)) inside Option<...> has the exact same size, alignment, and calling convention as the underlying nullable C pointer, where None is represented as 0x0 (NULL):

use core::ffi::c_int;
use std::mem::size_of;
use std::ptr::NonNull;

type CCallback = extern "C" fn(c_int) -> c_int;

// Compile-time proof that Option<...> over non-null pointers has zero size overhead:
const _: () = {
    assert!(size_of::<Option<&i32>>() == size_of::<*const i32>());
    assert!(size_of::<Option<&mut i32>>() == size_of::<*mut i32>());
    assert!(size_of::<Option<NonNull<i32>>>() == size_of::<*mut i32>());
    assert!(size_of::<Option<CCallback>>() == size_of::<*const ()>());
};

extern "C" fn triple(x: c_int) -> c_int {
    x * 3
}

/// Accepts a nullable C function pointer directly as `Option<CCallback>`!
/// If C passes `NULL`, Rust receives `None` safely without any raw pointer cast.
#[unsafe(no_mangle)]
pub extern "C" fn invoke_optional_callback(cb: Option<CCallback>, arg: c_int) -> c_int {
    match cb {
        Some(func) => func(arg),
        None => -1,
    }
}

fn main() {
    println!("With callback: {}", invoke_optional_callback(Some(triple), 14));
    println!("With NULL:     {}", invoke_optional_callback(None, 14));
}
With callback: 42
With NULL:     -1