Memory Layout and Representation

ABI Compatibility

An Application Binary Interface (ABI) defines the low-level machine rules for binary code interoperability: how function arguments and return values are passed in CPU registers or on the stack, which registers the caller or callee must preserve, how symbols are named, and how stack unwinding occurs.

Why Rust Does Not Have a Stable Default ABI

Functions declared with fn (implicitly extern "Rust") and types without #[repr(C)] or #[repr(transparent)] use Rust's unstable internal calling convention. The compiler reserves the right to change register assignments, argument ordering, and struct decomposition across compiler releases or optimization flags.

Consequently, two dynamic libraries (.so, .dylib, .dll) compiled with different versions of rustc cannot safely call extern "Rust" functions or pass #[repr(Rust)] structs directly to each other.

ABI String Specifiers

Explicit ABI specifiers on function definitions (extern "ABI" fn) and function pointer types (extern "ABI" fn(...)) select a standardized platform calling convention:

ABI Specifier Unwinding Allowed Primary Use Case
extern "Rust" (default fn) Yes Standard Rust-to-Rust calls within the same build
extern "C" No (aborts on unwind across boundary) Standard C FFI on the target platform (System V AMD64 ABI on Unix, Microsoft x64 on Windows)
extern "C-unwind" Yes C or C++ FFI that permits Rust panics or C++ throw exceptions to unwind across the frame
extern "system" No OS APIs (stdcall on 32-bit Windows, equivalent to "C" on 64-bit targets)
extern "system-unwind" Yes Unwind-capable OS callbacks

FFI-Safe Type Mapping Across extern "C"

To pass data safely across an extern "C" boundary, both the calling convention and the argument types must be ABI-compatible with C. The improper_ctypes_definitions lint warns when a non-FFI-safe type is used in an extern "C" signature.

Rust Type C Equivalent (<stdint.h> / <stddef.h>) ABI Notes
u8, u16, u32, u64, usize uint8_t, uint16_t, uint32_t, uint64_t, size_t Exact match
i8, i16, i32, i64, isize int8_t, int16_t, int32_t, int64_t, ssize_t / ptrdiff_t Exact match
f32, f64, bool float, double, _Bool (bool in <stdbool.h>) Exact match
*const T, *mut T (where T: Sized) const T*, T* Nullable thin pointer
&T, &mut T, NonNull<T> (T: Sized) const T*, T* Non-null thin pointer
Option<&T>, Option<extern "C" fn()> const T*, function pointer Nullable via guaranteed niche optimization
#[repr(C)] struct S { ... } struct S { ... } Passed in registers or stack per C struct ABI
#[repr(transparent)] struct W(T) T Passed in the exact same registers as T
&[T], &str, Vec<T>, String Not ABI-compatible Must pass (ptr: *const T, len: usize) separately

Defining and Calling an extern "C" Function Interface

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct BoundingBox {
    pub width: f64,
    pub height: f64,
}

#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
pub struct ScaleFactor(pub f64);

pub extern "C" fn scale_box(
    bbox: BoundingBox,
    factor: ScaleFactor,
    out_area: Option<&mut f64>,
) -> BoundingBox {
    let scaled = BoundingBox {
        width: bbox.width * factor.0,
        height: bbox.height * factor.0,
    };
    if let Some(area) = out_area {
        *area = scaled.width * scaled.height;
    }
    scaled
}

fn main() {
    let c_fn: extern "C" fn(BoundingBox, ScaleFactor, Option<&mut f64>) -> BoundingBox = scale_box;

    let mut area = 0.0;
    let result = c_fn(
        BoundingBox {
            width: 4.0,
            height: 5.0,
        },
        ScaleFactor(2.0),
        Some(&mut area),
    );

    println!("scaled: {:?}, area: {}", result, area);
}
scaled: BoundingBox { width: 8.0, height: 10.0 }, area: 80