In this lesson, you will learn how to bridge the semantic gap between C-style integer return codes and Rust’s expressive Result<T, E> pattern when performing FFI calls.
The Semantic Gap
C functions typically communicate status via return codes—usually 0 for success and a non-zero integer for various failure states. In Rust, this is idiomatic noise. To integrate this into a safe ecosystem, you must encapsulate the raw integer check within a conversion layer that maps domain-specific error codes into a robust Rust enum.
Translating Return Codes to Results
When calling an external C function, the boundary is inherently unsafe. Your goal is to keep the unsafe block as thin as possible, immediately wrapping the return value in a Result.
Consider a hypothetical C function int disk_write(const char* data) where -1 represents a permission error and -2 represents a hardware failure. You shouldn’t expose these integers to the rest of your application:
#[repr(i32)]
enum DiskError {
PermissionDenied = -1,
HardwareFailure = -2,
}
fn write_data(data: &str) -> Result<(), DiskError> {
let result = unsafe { disk_write(data.as_ptr() as *const i8) };
match result {
0 => Ok(()),
-1 => Err(DiskError::PermissionDenied),
-2 => Err(DiskError::HardwareFailure),
_ => panic!("Unknown error code: {}", result),
}
}
The Pattern of Translation
By mapping these codes, you gain several benefits:
- Exhaustiveness: Rust’s compiler forces you to handle every possible error state returned by the C API.
- Contextual Typing: You transform a "magic number" into an
enumthat can implementstd::error::Error, allowing it to integrate seamlessly with the?operator in your higher-level logic. - Safety Isolation: The translation layer acts as a "firewall," ensuring that the C language's permissive error reporting cannot leak into your own application's business logic.
When the C side uses -1 to mean "everything went wrong," it is your responsibility as a systems integrator to translate that ambiguity into a strongly-typed, context-aware error that adheres to Rust's safety guarantees.
Key Takeaway: Always intercept raw FFI return codes at the immediate boundary and transform them into typed Result enums to maintain memory and logic safety.