Course contentsShow
← Rust
Lesson 1625 of 1,91014. System Integration and EcosystemFree lesson

Verifying FFI Contracts

You will use static analysis and runtime checks to ensure C library invariants are maintained.

In this lesson, you will learn how to enforce rigorous FFI contracts by combining compile-time layout verification with runtime invariant checks.

When interacting with C libraries, the "contract" between your Rust code and the foreign code is often implicit. If a C header expects a struct to be 8-byte aligned but your Rust definition is 4-byte aligned, the resulting memory corruption is often invisible until it triggers a segmentation fault.

Compile-Time Verification

We start by ensuring the binary representation matches using static_assertions. Since Rust's layout can be non-deterministic, we use #[repr(C)]. To guarantee that our assumptions remain true across compiler versions or platform updates, we inject verification logic:

#[repr(C)]
struct ExternalState {
    id: u32,
    value: f64,
}

// Ensure the struct matches the C library's expectations at compile time
const _: () = assert!(std::mem::size_of::<ExternalState>() == 16);
const _: () = assert!(std::mem::align_of::<ExternalState>() == 8);

By placing these const assertions at the module level, the build will fail immediately if the layout deviates from the expected ABI, preventing runtime surprises.

Runtime Invariant Checks

Beyond memory layout, C libraries often require specific state invariants (e.g., "pointer p must never be null" or "this function must be called only after initialization"). Since the Rust compiler cannot see into the C side, we wrap these calls in an abstraction layer that performs defensive validation:

pub fn safe_ffi_call(ptr: *mut ExternalState) {
    // Runtime invariant check: Contract enforcement
    assert!(!ptr.is_null(), "FFI Contract Violation: Pointer must not be null");
    
    unsafe {
        ffi::initialize_state(ptr);
    }
}

By wrapping raw calls in a function that checks for null or state validity before passing control to the C library, you transform an undefined behavior trap into a controlled panic. This two-pronged approach—verifying memory layout at compile time and asserting state invariants at runtime—is the bedrock of robust system integration.

Key Takeaway: Use const assertions to lock your memory layout and defensive wrappers to enforce behavioral invariants, effectively "Rust-ifying" the unreliable guarantees of the FFI boundary.