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
)?
- 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.
- Signature Mismatch: An error in the Rust
extern
block declaration (e.g., wrong argument types like usingi32
when C’sint
isi16
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 fromlibc
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.