Level 4 · Deep Cuts

Unsafe & FFI 🧨

The keyword with the scariest reputation in Rust, and the most misunderstood. unsafe is not a cheat code that turns Rust into C. It is a contract, and it is how Rust talks to the entire outside world.

What unsafe actually means 📜

Some true things cannot be proven by the borrow checker. The classic example: taking two mutable references into different halves of one array is perfectly fine, but the checker cannot always see that they never overlap. unsafe is you telling the compiler: "I have checked the part you cannot. Hold me to everything else."

Inside an unsafe block you gain exactly five extra abilities, and not one more:

Everything else still applies. Ownership: still checked. Borrows: still checked. Types: still checked. unsafe does not turn off Rust; it turns off one very specific safety net, inside a clearly fenced area you can grep for.

Raw pointers 📌

References (&T, &mut T) are pointers with seatbelts: always valid, always aligned, aliasing rules enforced. Raw pointers (*const T, *mut T) are the pointers C has: just an address, no promises. Making one is safe; reading through it is where the contract begins:

fn main() {
    let x = 42;
    let p = &raw const x;      // creating a raw pointer is safe
    unsafe {
        // reading through it is not: that needs the keyword
        println!("x through a raw pointer: {}", *p);
    }
}

Why the split? Because pointer arithmetic and address juggling are fine to compute. Only the dereference can explode, so only the dereference needs the fence around it.

The safety comment: unsafe culture 📝

An unsafe fn says "calling me has conditions." The conditions go in a # Safety doc section, and every call site writes a // SAFETY: comment proving it meets them. This convention is near-universal in serious Rust code, and clippy can enforce it:

/// # Safety
/// `i` must be less than `slice.len()`.
unsafe fn peek(slice: &[i32], i: usize) -> i32 {
    unsafe { *slice.get_unchecked(i) }
}

fn main() {
    let nums = [10, 20, 30];
    // SAFETY: 1 < nums.len(), which is 3
    let n = unsafe { peek(&nums, 1) };
    println!("{n}");
}

Safe abstractions: the real pattern 🧯

Here is the move that makes Rust work: wrap the unsafe part in a function whose interface makes misuse impossible, and nobody upstream ever touches the keyword. You have been using such wrappers all along:

fn main() {
    let mut v = [1, 2, 3, 4, 5, 6];
    let (left, right) = v.split_at_mut(3);  // two &mut into one array!
    left[0] = 100;
    right[0] = 400;
    println!("{v:?}");
}

split_at_mut contains unsafe internally (the borrow checker cannot prove the halves are disjoint, but the bounds check can), yet the function itself is safe to call: there is no input that makes it misbehave. Vec, String, Mutex, every collection you have used is unsafe machinery behind a safe interface. The standard library is proof that the pattern scales.

FFI: calling C 🌉

FFI, the foreign function interface, is how Rust calls code written in other languages. Declare the foreign function's signature in an extern "C" block, and Rust can call straight into any C library on the system. All foreign calls are unsafe by definition: the C compiler made no Rust promises:

unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    // SAFETY: C's abs reads one integer and returns one integer
    let n = unsafe { abs(-3) };
    println!("|-3| = {n}");
}

That is a real call into the C standard library, no crate needed. This door swings both ways (extern "C" fn plus #[unsafe(no_mangle)] exports Rust to C), and it is exactly how Rust is creeping into the Linux kernel, Firefox, and decades-old codebases: one wrapped C library at a time, safe interfaces spreading outward.

🔬 Trust, but verify The miri tool (rustup component add miri, then cargo miri run) interprets your program and catches undefined behavior in unsafe code at runtime: use-after-free, out-of-bounds reads, the works. If you write unsafe code, miri is your conscience.
Exercise

Wrap C's sharp edge

C's abs has a genuine trap: calling it on INT_MIN (Rust's i32::MIN) is undefined behavior, because -i32::MIN does not fit in an i32. Write a safe wrapper fn safe_abs(n: i32) -> Option<i32> that refuses the poison value and proves its call with a // SAFETY: comment. Test both cases.

Reveal solution
unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

/// C's abs is undefined behavior on INT_MIN, so the safe wrapper refuses it.
fn safe_abs(n: i32) -> Option<i32> {
    if n == i32::MIN {
        return None;
    }
    // SAFETY: abs is well-defined for every value except i32::MIN,
    // which we just excluded
    Some(unsafe { abs(n) })
}

fn main() {
    println!("{:?}", safe_abs(-7));
    println!("{:?}", safe_abs(i32::MIN));
}

This tiny function is the whole philosophy in miniature: a dangerous thing, a guard that makes the danger unreachable, and a safe signature so callers cannot get it wrong. Congratulations, you just built your first safe abstraction over C. 🧱 (In real code you would just call Rust's built-in n.checked_abs(), which returns exactly this Option.)

🧠 The takeaway unsafe is a fence, not a free-for-all: five extra powers, grep-able boundaries, safety comments as receipts, and safe wrappers so the danger never leaks upstream. Most Rustaceans ship for years without writing any. Now, when you finally need it, you know the rules of the contract.