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:
- Dereferencing raw pointers (
*const T
,*mut T
). - Calling
unsafe
functions or methods (including C functions via FFI and low-level intrinsics). - Accessing or modifying mutable static variables.
- Implementing
unsafe
traits. - Accessing fields of
union
s (unions requireunsafe
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.