25.4 Interfacing with C Code (FFI)

A primary motivation for unsafe is the Foreign Function Interface (FFI), enabling Rust code to call functions written in C (or other languages exposing a C-compatible Application Binary Interface, ABI) and allowing C code to call Rust functions.

When interfacing with C, it’s crucial to use C-compatible types in Rust declarations. The sizes of C types like int or long can vary across different platforms and architectures. Rust’s fixed-size types like i32 or i64 might not always match. To handle this correctly, the libc crate provides type aliases that correspond to C types for the specific target platform. For example, libc::c_int represents C’s int, libc::c_double represents C’s double, and so on. Using these types in your extern "C" declarations is best practice for portable FFI.

To call a C function from Rust, you first declare its signature within an unsafe extern "C" block. The "C" ABI specification ensures that Rust uses the correct calling conventions (argument passing, return value handling) expected by C code. The unsafe keyword on the extern block itself indicates that the declarations within may not be fully checked by the Rust compiler for adherence to Rust’s safety rules, or is required by stricter compiler/lint configurations.

// First, add `libc` to your Cargo.toml dependencies:
// [dependencies]
// libc = "0.2" # Or the latest version

// Import the C-compatible types from the libc crate.
use libc::{c_int, c_double};

// Assume linkage with the standard C math library (libm) or C standard library.
// This might happen automatically via libc or require explicit linking
// depending on the platform and build configuration (e.g., using #[link(name = "m")]).

unsafe extern "C" {
    // Declare the C function signature using types from the `libc` crate
    // to ensure they match the C types on the target platform.
    fn abs(input: c_int) -> c_int;     // Corresponds to C's int abs(int)
    fn sqrt(input: c_double) -> c_double; // Corresponds to C's double sqrt(double)
}

fn main() {
    // Rust-side types
    let number_rs: i32 = -10;
    let float_num_rs: f64 = 16.0;

    // Calling external functions declared in an `extern` block is unsafe.
    unsafe {
        // Cast Rust types to C-compatible types before calling.
        // And cast results back if needed.
        let abs_result_c = abs(number_rs as c_int);
        let abs_result_rs = abs_result_c as i32;
        println!("C abs({}) = {}", number_rs, abs_result_rs);

        let sqrt_result_c = sqrt(float_num_rs as c_double);
        let sqrt_result_rs = sqrt_result_c as f64; // c_double is often f64
        println!("C sqrt({}) = {}", float_num_rs, sqrt_result_rs);
    }
}

Why is calling foreign functions unsafe?

  1. External Code Verification: Rust’s compiler cannot analyze the source code of the C function to verify its memory safety, thread safety, or adherence to any implicit contracts. The C function might contain bugs, access invalid memory, or cause data races.
  2. Signature Mismatch: An error in the Rust extern block declaration (e.g., wrong argument types like using i32 when C’s int is i16 on a given platform, incorrect return type, different number of arguments compared to the actual C function) can lead to stack corruption, misinterpretation of data, and other forms of undefined behavior. Using types from libc helps mitigate mismatches related to type sizes.

Best Practice: Wrap unsafe FFI calls within safe Rust functions. These wrappers can handle type conversions (like casting i32 to libc::c_int), enforce preconditions, check return values for errors (if applicable according to the C API’s conventions), and provide an idiomatic Rust interface.

// Ensure libc is a dependency and import c_int
use libc::c_int;

// Declare the external C function within an unsafe extern "C" block.
unsafe extern "C" { fn abs(input: c_int) -> c_int; }

// Safe wrapper function encapsulating the unsafe call.
// This wrapper uses i32 for its public Rust API for convenience.
fn safe_abs(input: i32) -> i32 {
    // The unsafe block is localized here for the call.
    unsafe {
        // Cast the i32 input to libc::c_int for the C call.
        let c_input = input as c_int;
        let c_result = abs(c_input);
        // Cast the libc::c_int result back to i32 for the Rust API.
        c_result as i32
    }
    // This simplified wrapper assumes that the range of values for `input` (i32)
    // is appropriate for C's `abs(int)` and that the result also fits in an `i32`.
    // On platforms where `libc::c_int` is `i32` (common), this is a direct mapping.
    // If `libc::c_int` were narrower than `i32` (e.g., 16-bit), the `as c_int` cast
    // would truncate, and the `as i32` cast for the result would sign-extend.
    // For `abs`, this is often acceptable, but for other functions, more careful
    // conversion (e.g., using `try_into()` or range checks) might be necessary.
}

fn main() {
    println!("Absolute value via safe wrapper: {}", safe_abs(-5)); // Outputs 5
}

This encapsulation contains the unsafety, making the rest of the Rust code interact with a safe API. The use of libc types in the extern "C" block significantly improves the portability and correctness of the FFI declarations.