Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

25.4.1 unsafe extern blocks in Rust 2024

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.

Starting with the Rust 2024 Edition, extern blocks must now be explicitly marked with the unsafe keyword: unsafe extern "C" { ... }. This change emphasizes that the declarations within the extern block carry safety obligations. The Rust compiler cannot verify the correctness of foreign function signatures or global static definitions. If these definitions are incorrect, it can lead to undefined behavior when interacting with the external C code.

Within an unsafe extern block, individual items can still be marked as safe fn or unsafe fn to indicate whether calling that specific function (or accessing that static) requires an unsafe block. If neither safe nor unsafe is specified, it defaults to unsafe fn.

// 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" {
    // sqrt (from libm) may be called with any `f64` input, and its safety is solely
    // dependent on its arguments matching the C signature. We mark it `safe fn` to
    // indicate that its *call* is not inherently unsafe, given valid arguments.
    pub safe fn sqrt(x: c_double) -> c_double;

    // strlen (from libc) requires a valid pointer, which the Rust compiler cannot
    // verify. Therefore, calling `strlen` is an unsafe operation.
    pub unsafe fn strlen(p: *const std::ffi::c_char) -> usize;

    // free (from libc) is not marked, so it defaults to unsafe fn.
    // Calling it requires an unsafe block, and you must ensure `p` is a valid pointer.
    pub fn free(p: *mut core::ffi::c_void);

    // Declaring a static variable from C. Accessing it from Rust requires
    // an unsafe block and is unsafe, similar to Rust's `static mut`.
    pub safe static IMPORTANT_BYTES: [u8; 256];
}

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

    // Calling external functions declared in an `extern` block.
    // `sqrt` is marked `safe fn`, so no unsafe block needed for its call.
    let sqrt_result_rs = sqrt(float_num_rs as c_double);
    println!("C sqrt({}) = {}", float_num_rs, sqrt_result_rs);

    // `strlen` is marked `unsafe fn`, so its call requires an unsafe block.
    let c_string = "Hello from Rust!\0"; // C strings are null-terminated
    let len = unsafe {
        strlen(c_string.as_ptr() as *const std::ffi::c_char)
    };
    println!("C strlen(\"{}\") = {}", c_string, len);

    // Accessing an external static also requires an unsafe block.
    let first_byte = unsafe { IMPORTANT_BYTES[0] };
    println!("First byte of IMPORTANT_BYTES: {}", first_byte);
}

Why is calling foreign functions unsafe (or why is the extern block 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" { pub safe 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 {
    // Since `abs` is marked `safe fn` within the `extern` block,
    // its call does *not* require an `unsafe` block here.
    let c_input = input as c_int;
    let c_result = abs(c_input);
    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.