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.

To call a C function from Rust, you first declare its signature within an extern "C" block. The "C" ABI specification ensures that Rust uses the correct calling conventions (argument passing, return value handling) expected by C code.

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

extern "C" {
    // Declare the C function signature using Rust types corresponding
    // to the C types (e.g., c_int from libc crate or Rust's i32).
    fn abs(input: i32) -> i32; // Corresponds to C's int abs(int)
    fn sqrt(input: f64) -> f64; // Corresponds to C's double sqrt(double)
}

fn main() {
    let number: i32 = -10;
    let float_num: f64 = 16.0;

    // Calling external functions declared in an `extern` block is unsafe.
    unsafe {
        let abs_result = abs(number);
        println!("C abs({}) = {}", number, abs_result);

        let sqrt_result = sqrt(float_num);
        println!("C sqrt({}) = {}", float_num, sqrt_result);
    }
}

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, 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.

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

extern "C" { fn abs(input: i32) -> i32; }

// Safe wrapper function encapsulating the unsafe call.
fn safe_abs(input: i32) -> i32 {
    // The unsafe block is localized here.
    unsafe { abs(input) }
    // Assumption: Calling C's abs with any i32 is safe if the signature matches.
    // This is generally true for standard library functions like abs.
}

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.