In this lesson, you will learn how to bridge the gap between Rust’s expressive Result type and the rigid integer-based error codes expected by C-compatible FFI interfaces.
The FFI Challenge
When calling code across an FFI (Foreign Function Interface) boundary, Rust’s Result<T, E> cannot be passed directly because it is a complex struct layout specific to the Rust compiler. C environments typically expect a simple return code (like 0 for success and negative values for various errors) and use pointer arguments to "return" actual data.
To handle this, we must map our internal Rust errors into a stable, integer-based representation before the stack frame exits to the caller.
The Translation Pattern
The most robust approach is to define a "Translation Layer." You define an enum of status codes that matches the C ABI, then use a helper function to convert your internal logic errors into these codes.
#[repr(C)]
pub enum FfiResult {
Success = 0,
InvalidInput = 1,
InternalError = 2,
}
// Internal Rust logic
fn perform_work() -> Result<i32, MyError> { /* ... */ }
// The FFI boundary function
#[no_mangle]
pub extern "C" fn run_work(out_ptr: *mut i32) -> FfiResult {
match perform_work() {
Ok(val) => {
unsafe { *out_ptr = val; }
FfiResult::Success
}
Err(MyError::Invalid) => FfiResult::InvalidInput,
Err(_) => FfiResult::InternalError,
}
}
Why this approach works
- Safety: By using
repr(C), we guarantee the memory layout of the status enum remains stable across compiler versions, preventing undefined behavior. - Encapsulation: Your internal code can continue to use powerful
Resultchaining and custom error types, while the boundary layer acts as a strict "adapter." - Control: You define the exact mapping. If an error contains extra diagnostic data (like a string message), you can handle this by writing that string into a pre-allocated buffer provided by the caller, or by logging it to a global trace buffer before returning a generic error code.
Key Takeaway: Always map internal Rust Result types to a #[repr(C)] status enum at the boundary to maintain ABI compatibility and prevent leaks of Rust-specific type layouts.