6.8 Unsafe Rust and C Interoperability

While Rust prioritizes safety, sometimes you need capabilities that the compiler cannot statically guarantee are safe. This is often required for low-level systems programming tasks (like interacting directly with hardware), optimizing performance-critical code, or interfacing with other languages like C that don’t share Rust’s guarantees. For these situations, Rust provides the unsafe keyword (detailed in Chapter 25).

6.8.1 unsafe Blocks and Functions

Inside an unsafe block or function, you gain access to five additional capabilities (“superpowers”) that are normally disallowed in safe Rust:

  1. Dereferencing raw pointers (*const T, *mut T).
  2. Calling unsafe functions or methods (including C functions via FFI and low-level intrinsics).
  3. Accessing or modifying mutable static variables.
  4. Implementing unsafe traits.
  5. Accessing fields of unions (unions require unsafe because Rust can’t guarantee which variant is active).
fn main() {
    let mut num = 5;

    // Creating raw pointers is safe (doesn't dereference)
    let r1 = &num as *const i32; // Immutable raw pointer
    let r2 = &mut num as *mut i32; // Mutable raw pointer

    // Dereferencing raw pointers requires an unsafe block
    unsafe {
        println!("r1 points to: {}", *r1); // Read via raw pointer
        *r2 = 10; // Write via raw mutable pointer
    }
    // Outside the unsafe block, normal rules apply again.

    println!("num is now: {}", num); // Prints: num is now: 10
}

Using unsafe signifies that you, the programmer, are taking responsibility for upholding memory safety for the operations within that block. The compiler trusts you to ensure that raw pointers are valid, functions uphold their contracts, etc. It’s crucial to minimize the scope of unsafe blocks and carefully document why they are necessary and correct. unsafe does not turn off the borrow checker entirely; it only enables these specific extra capabilities.

6.8.2 Interfacing with C (FFI)

Rust’s Foreign Function Interface (FFI) allows seamless calling of C code from Rust and exposing Rust code to be called by C. This involves using raw pointers and often unsafe blocks.

Calling C from Rust:

// Declare the C function signature using `extern "C"`
// This tells Rust to use the C Application Binary Interface (ABI).
// In Rust 2021+, extern blocks require `unsafe` if they contain functions.
unsafe extern "C" {
    fn abs(input: i32) -> i32; // Example: C standard library abs function
}

fn main() {
    let number = -5;
    // Calling external functions declared in `extern` blocks is unsafe
    let absolute_value = unsafe { abs(number) };
    println!("The absolute value of {} is {}", number, absolute_value);
}

Calling Rust from C:

Rust code compiled as a library (crate-type = ["cdylib"] or similar):

// Disable Rust's name mangling and use the C ABI
#[no_mangle]
pub extern "C" fn rust_adder(a: i32, b: i32) -> i32 {
    println!("Rust function called from C!");
    a + b
}

C code linking against the compiled Rust library:

#include <stdio.h>
#include <stdint.h> // For int32_t

// Declare the Rust function signature as it appears to C
extern int32_t rust_adder(int32_t a, int32_t b);

int main() {
    int32_t result = rust_adder(10, 12);
    printf("Result from Rust: %d\n", result); // Output: Result from Rust: 22
    return 0;
}

Tools like cbindgen (generates C/C++ headers from Rust code) and bindgen (generates Rust bindings from C/C++ headers) automate much of the boilerplate involved in FFI.