Understanding `unsafe` and raw pointers
unsafe is not a cheat code. It is a contract: you take responsibility for invariants the compiler can no longer prove.
This note walks through when a raw pointer is the right tool, and when it is just a way to smuggle undefined behavior into an otherwise safe crate.
What unsafe actually means
Rust still type-checks everything inside an unsafe block. What it stops doing is proving the following:
- The pointer is aligned and non-null when you dereference it.
- The referent is initialized for the type you claim.
- You do not create aliasing
&mutreferences. - You do not free memory that is still borrowed.
If those hold, the rest of the program can treat your API as safe.
A minimal raw-pointer example
pub fn write_u32_le(dst: &mut [u8], value: u32) {
assert!(dst.len() >= 4);
unsafe {
dst.as_mut_ptr().cast::<u32>().write_unaligned(value.to_le());
}
}The unsafe block is tiny. The assertion is the proof. That is the shape you want: a small escape hatch wrapped by a safe API.
When you actually need it
Reach for unsafe when the hardware or an FFI boundary cannot be expressed in safe Rust:
- SIMD loads and stores through
std::arch - Custom allocators and intrusive data structures
- C ABI callbacks that hand you a
*mut c_void
Do not use it to “make the borrow checker shut up”. That almost always means the lifetime model is wrong, not that you need raw pointers.
A practical rule
If you cannot write the safety comment in one sentence, the abstraction is too big. Split it until the invariant is local, then keep the unsafe block smaller than the comment that justifies it.