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

Navigating the FFI Boundary

You will be able to define the memory layout requirements and ABI constraints when crossing the Rust/C border.

In this lesson, you will learn how to align Rust’s internal memory representations with the rigid requirements of the C Application Binary Interface (ABI) to ensure safe cross-language communication.

The ABI Challenge

When you pass a struct across the Foreign Function Interface (FFI) boundary, you are moving data between two environments with different philosophies. Rust’s compiler, rustc, is designed to optimize memory layout for performance, often reordering fields or adding padding in ways that are not stable across compiler versions. Conversely, C relies on a predictable, linear layout defined by the C ABI.

If you pass a native Rust struct to C, your program will likely suffer from "memory misalignment," leading to garbage values or segmentation faults because C expects a specific offset for every member.

Ensuring Compatibility with #[repr(C)]

To bridge this gap, you must force the compiler to adhere to the C layout rules using the #[repr(C)] attribute. This instructs the compiler to order fields exactly as declared and apply standard C padding rules.

Consider this cross-boundary data structure:

#[repr(C)]
pub struct SensorData {
    pub id: u32,       // 4 bytes
    pub value: f64,    // 8 bytes (padded by 4 bytes before this in C)
    pub status: u8,    // 1 byte
}

Without #[repr(C)], Rust might shuffle status to occupy the padding slot after id to save space. With the attribute, you guarantee that if a C library reads this struct, it sees exactly what you intended at the memory offsets it expects.

Beyond Layout: ABI Constraints

The boundary is not just about layout; it is about how functions are invoked. By default, Rust uses its own internal ABI, which is unstable. When declaring an extern block, you must explicitly specify the ABI to ensure the calling convention (how arguments are pushed onto the stack or placed in registers) matches:

extern "C" {
    fn process_sensor_data(data: *const SensorData);
}

Using extern "C" ensures that the generated assembly follows the system's standard C calling convention, preventing register mismatches and stack corruption during the handoff.

Key Takeaway: Use #[repr(C)] to enforce stable memory layout and extern "C" to guarantee compatible calling conventions when crossing the FFI boundary.