Unsafe Rust and Foreign Function Interface (FFI)
Master Unsafe Rust: the 5 unsafe superpowers, raw pointer dereferencing, interfacing with C/C++ via FFI, and building sound safe abstractions.
Second post in Part 3: Advanced Systems & Production Rust. While Rust is celebrated for its ironclad compile-time safety guarantees, real-world systems programming — operating system kernels, hardware drivers, interfacing with legacy C/C++ codebases, or squeezing every drop of CPU performance — requires operations that the static compiler simply cannot verify.
Rust accommodates these realities with the unsafe keyword.
graph TD
A["Unsafe Rust"] --> B["5 Unsafe Superpowers"]
B --> B1["Dereference Raw Pointers (*const T, *mut T)"]
B --> B2["Call unsafe functions or foreign FFI functions"]
B --> B3["Implement an unsafe trait"]
B --> B4["Access or modify mutable static variables"]
B --> B5["Access fields of unions"]
A --> C["C Foreign Function Interface (FFI)"]
A --> D["Pattern: Safe Abstractions over Unsafe Core"]
Explain Like I’m 10: The Welder’s Safety Shield#
- Safe Rust: Like the light switch in your bedroom. You flip it on and off with 100% safety because all high-voltage wiring is insulated and pre-certified.
- Unsafe Rust: Like opening the breaker box directly. The compiler steps back and says: “I cannot statically check live wires here. I am giving you full manual control and total responsibility!”
unsafedoes not turn off the borrow checker or make Rust untyped; it precisely grants access to five specific low-level capabilities.
1. Raw Pointers (*const T and *mut T)#
Unlike references (&T and &mut T), raw pointers:
- Ignore borrowing rules (you can have multiple mutable pointers to the same address).
- Are not guaranteed to point to valid memory (they can be
nullor dangling). - Do not implement automatic cleanups (no automatic
Drop).
[!NOTE] Creating raw pointers is completely safe. Only dereferencing (
*pointer) to read or write memory requires anunsafeblock.
fn main() {
let mut num = 42;
// Creating raw pointers from references (Safe)
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// Dereferencing raw pointers (Unsafe)
unsafe {
println!("r1 points to value: {}", *r1);
*r2 = 100;
println!("r2 modified value to: {}", *r2);
}
}rust2. Calling Unsafe Functions and Safe Abstractions#
The golden architectural pattern in Rust is: Keep unsafe code isolated in small internal modules, and wrap it with a safe public API.
A classic example from the standard library is split_at_mut, which safely splits a single mutable slice into two non-overlapping mutable slices:
use std::slice;
fn split_at_mut<T>(values: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
println!("Left: {left:?}, Right: {right:?}");
}rust3. Foreign Function Interface (FFI): Talking to C#
Rust can invoke C functions with zero runtime overhead using the standard C ABI:
Calling C Standard Library Functions#
use std::ffi::c_int;
// Declare external C ABI function
extern "C" {
fn abs(input: c_int) -> c_int;
}
fn main() {
unsafe {
let result = abs(-42);
println!("Absolute value from C standard library: {result}");
}
}rustExporting Rust Functions to C/C++#
You can compile Rust code into static or dynamic shared libraries (.so, .dylib, .dll) callable by C, Python, Go, or Node.js:
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}rust#[no_mangle]: Instructs the Rust compiler not to mangle the symbol name, making it discoverable by the C linker.extern "C": Instructs the compiler to use the C calling convention.
Best Practices for Unsafe Code#
- Minimize Unsafe Scopes: Keep
unsafeblocks as small and localized as possible. - Document Invariants: Add comments explaining why the unsafe block is sound.
- Use Miri for Validation: Run
cargo miri testto detect Undefined Behavior (UB), memory leaks, and out-of-bounds pointer operations during test execution.
Summary#
unsafeunlocks 5 low-level capabilities necessary for hardware access, OS development, and FFI.- Raw pointers (
*const T,*mut T) provide unconstrained pointer manipulation when needed. - FFI enables seamless two-way interoperability with existing C/C++ libraries.
- Always encapsulate unsafe primitives inside sound, safe abstractions.