Why this matters
Vec, String, HashMap, Rc, Mutex, threads and channels are all built on unsafe code inside the standard library. You use them from safe code every day without being able to cause undefined behavior. That is the central design idea of unsafe Rust: wrap a small amount of carefully reasoned unsafe code in a safe API, so that no possible use of the API from safe code can break its invariants. This lesson shows how that works, using the function you met in module 5, split_at_mut, and explains what makes a safe API "sound".
The problem split_at_mut solves
You cannot take two mutable borrows of one slice, even of disjoint parts, because the borrow checker does not reason about index ranges:
fn split(s: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
(&mut s[..mid], &mut s[mid..])
}
fn main() {
let mut a = [1, 2, 3, 4];
let (l, r) = split(&mut a, 2);
l[0] = 9;
r[0] = 9;
}
error[E0499]: cannot borrow `*s` as mutable more than once at a time
--> main.rs:2:26
|
1 | fn split(s: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
2 | (&mut s[..mid], &mut s[mid..])
| ---------------------^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*s` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
We know the two ranges do not overlap, so handing out both is perfectly safe. The compiler cannot prove it. This is exactly the situation unsafe is for: a fact the programmer can verify that the type system cannot express.
Implementing it with raw pointers
use std::slice;
fn split_at_mut(s: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = s.len();
let ptr = s.as_mut_ptr();
assert!(mid <= len);
// SAFETY: 0..mid and mid..len are within the slice (checked above) and do not overlap,
// so the two &mut slices never alias. Both borrow from `s` and cannot outlive it.
unsafe { (slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid)) }
}
fn main() {
let mut a = [1, 2, 3, 4, 5];
let (l, r) = split_at_mut(&mut a, 2);
l[0] = 100;
r[0] = 300;
println!("{:?} {:?}", l, r);
println!("{:?}", a);
}
[100, 2] [300, 4, 5]
[100, 2, 300, 4, 5]
s.as_mut_ptr()gives a raw pointer to the first element (safe).slice::from_raw_parts_mut(ptr, len)builds a&mut [T]from a pointer and a length (unsafe: the caller must guarantee the memory is valid, initialized, and not aliased by any other live reference for the slice's lifetime).- The
assert!runs before the unsafe block. If someone passesmid = 10, the function panics cleanly. Without it, safe code could make us build a slice that runs past the end of the array: undefined behavior from a safe function. - The function signature ties both output lifetimes to the input
&mut [i32](elision rule: one input lifetime), so the borrow checker still prevents usingawhilelorrare alive. The unsafe block only had to prove non-overlap; everything else is still checked by the compiler.
This is, in essence, how the standard library's split_at_mut is written.
Soundness
A safe function is sound if no call from safe code, with any arguments, can cause undefined behavior. An API is unsound if some safe call can. Soundness is a property of the whole API, not of the unsafe block alone:
pub fn first(v: &[u8]) -> u8 {
unsafe { *v.as_ptr() } // unsound: first(&[]) reads out of bounds
}
pub fn first(v: &[u8]) -> Option<u8> {
if v.is_empty() { return None; }
unsafe { Some(*v.as_ptr()) } // sound: the condition is checked first
}
Ways to make the unsafe condition hold:
- Check it at run time (
assert!, returnNone/Err) before the unsafe operation, as above. - Make it impossible by construction, for example with types (a non-empty type) or with private fields that only your code can change.
- Push the obligation to the caller by declaring the function
unsafe fnwith a# Safetydoc section.
Privacy protects invariants
Option 2 is how collections work. A Vec keeps len <= capacity and "the first len elements are initialized" as invariants. Its fields are private, so the only code that can change them is Vec's own methods, which maintain the invariants. Its unsafe reads can then rely on them. The SmallVec exercise builds a tiny version: get checks i < len, and len is only changed by push, which never exceeds the array's size. If the fields were pub, any safe code could set len = 1000, and get would become unsound. Module privacy is part of the safety argument.
The boundary to audit is therefore the module, not just the unsafe block: every function that can modify the fields an unsafe block relies on must preserve the invariants.
When (not) to write unsafe
- Before writing unsafe code, look for a safe way: standard library functions (
split_at_mut,swap,chunks_mut), a different data layout (indexes instead of pointers), or an existing well-reviewed crate. - If you must, keep unsafe blocks small, wrap them in a safe API, document every invariant, and test edge cases (empty input, boundaries). Run tests under Miri when available.
- Never mark a function safe if its correctness depends on a condition you do not check.
Common mistakes
- Missing the bounds check before raw-pointer access: the function is unsound even if all current callers are careful.
- Public fields on a type whose unsafe code relies on their values.
- Creating aliasing
&mutfrom raw pointers (two overlapping mutable slices): undefined behavior even if you never write through one of them. - Lifetime laundering: returning a reference built from a raw pointer with an unconstrained lifetime (
'static) when the data is local. Tie output lifetimes to inputs.
Recap
- The standard library's safe types are safe abstractions over unsafe code; the same pattern is available to you.
split_at_mutusesfrom_raw_parts_mutto produce two non-overlapping&mutslices, guarded by anassert!.- A safe API is sound if no safe usage can cause UB; check conditions, enforce them by construction, or make the function
unsafe fn. - Private fields let a module guarantee the invariants its unsafe code relies on.
- Prefer existing safe tools; keep unsafe small, documented and tested.
Going further: read the Rustonomicon chapters "Meet Safe and Unsafe" and "How Safe and Unsafe Interact", and look at the source of slice::split_at_mut (the docs have a "source" link).