Course contentsShow
← Rust
Lesson 869 of 1,9107. Error Handling PatternsFree lesson

Error Handling for FFI Boundaries

Map between Rust's Result and C-style return codes effectively.

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

  1. Safety: By using repr(C), we guarantee the memory layout of the status enum remains stable across compiler versions, preventing undefined behavior.
  2. Encapsulation: Your internal code can continue to use powerful Result chaining and custom error types, while the boundary layer acts as a strict "adapter."
  3. 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.