Unsafe Rust
Everything so far has been "safe Rust," fully checked by the borrow checker. This chapter covers unsafe — the deliberate escape hatch, what it actually unlocks, and why it exists rather than being avoided entirely.
What unsafe Actually Unlocks
A common misconception: unsafe does not turn off the borrow checker or type checking. It unlocks exactly five specific additional abilities: dereferencing a raw pointer, calling an unsafe function, accessing/modifying a mutable static variable, implementing an unsafe trait, and accessing a union's fields. Everything else — move semantics, most borrow checking — still fully applies inside an unsafe block.
Raw Pointers
*const T and *mut T can be created in safe code — but can only be dereferenced inside unsafe. Unlike references, raw pointers can be null, can dangle, aren't guaranteed to point at valid memory, and allow multiple mutable pointers to the same location simultaneously — bypassing Course 1 Chapter 4's aliasing rule entirely:
Since the compiler can no longer verify safety here, the programmer becomes responsible for upholding it manually — exactly why dereferencing specifically needs unsafe.
Unsafe Functions
An unsafe fn is a form of documentation as much as a permission gate — it signals "I have a precondition the compiler can't check, and the caller must uphold it."
Building a Safe Abstraction Over Unsafe Code
The idiomatic pattern: wrap unsafe operations inside a safe function whose signature the compiler can fully check, with the author manually verifying the internal unsafe code is actually sound. The standard library's own split_at_mut is a real example — splitting a slice into two mutable halves is genuinely impossible to express with the safe borrow checker alone (it looks like two overlapping mutable borrows, even though the halves don't actually overlap), yet its public function signature is perfectly safe to call.
Basic FFI: Foreign Function Interface
Calling C functions from Rust requires unsafe, since the compiler has no way to verify a foreign function's own safety guarantees:
Going the other direction, #[no_mangle] pub extern "C" fn exposes a Rust function to be called from C — genuinely practical for interoperating with existing C libraries.
| Go (cgo) | Rust (extern "C") | |
|---|---|---|
| Crossing into C | Explicit cgo syntax/build tags | extern "C" blocks + required unsafe |
| Treated as | A deliberate, marked boundary | A deliberate, marked boundary |
| Real cost | Genuine performance/complexity overhead | Genuine performance/complexity overhead |
A genuine similarity, not just a contrast: both languages treat crossing into C as something to mark explicitly, not something to do casually.
Raw Pointers
*const T / *mut T — creatable safely, dereferenced only inside unsafe.
unsafe fn
Documents an uncheckable precondition; calling it also requires unsafe.
Safe Abstraction Pattern
Unsafe internals, hidden behind a fully safe, checkable public signature.
FFI (extern "C")
Calling/exposing C functions — a marked boundary the compiler can't verify.
unsafe doesn't mean undefined behavior is inevitable — it means the compiler's automatic checks are suspended for that specific operation, and a human has taken on the responsibility instead. Most unsafe code, written carefully with its invariants genuinely verified, is perfectly correct. The real danger is honest human error, not some inherent unsoundness in the keyword itself.
unsafe around code to make a persistent error disappear is the same instinct as Chapter 1's 'static quick-fix mistake and Course 2 Chapter 3's missing-trait-bound confusion — except the stakes here are genuinely higher. Reaching for unsafe without actually verifying the safety invariants it requires can introduce real undefined behavior: crashes, memory corruption, genuine security vulnerabilities — exactly the category of bug the borrow checker exists to prevent in the first place.
Coding Challenges
Write code creating a mutable raw pointer to an i32 variable, modifying the value through the pointer inside an unsafe block, and printing the original variable afterward to confirm the change took effect.
📄 View solutionWrite an extern "C" block declaring C's sqrt(x: f64) -> f64 function (from libm), and call it inside an unsafe block to compute the square root of 16.0.
📄 View solutionExplain, using this chapter's split_at_mut example, why a function's SIGNATURE can be completely safe to call even though its IMPLEMENTATION contains unsafe code internally — and what obligation this places on whoever writes that implementation.
📄 View solutionChapter 2 Quick Reference
- unsafe unlocks exactly 5 things: raw pointer deref, unsafe fn calls, mutable statics, unsafe traits, union fields — nothing else changes
- *const T / *mut T — creatable safely; can be null/dangling; dereferencing requires unsafe
- unsafe fn — documents an uncheckable precondition; calling it also requires an unsafe block
- Safe abstraction pattern — hide unsafe internals behind a fully safe, checkable public function (how the standard library itself is built)
- extern "C" { fn ... } — declares a foreign function; calling it requires unsafe
- #[no_mangle] pub extern "C" fn — exposes a Rust function to be called from C
- Go's cgo and Rust's extern "C" both treat crossing into C as a deliberate, explicitly marked boundary
- unsafe is not a fix for a stubborn error — misuse risks real undefined behavior, not just a style complaint
- Next chapter: Async Rust — async/.await, Futures, the tokio runtime, contrast with Node's event loop and Go's goroutines