Recipe
Wrap an unsafe operation in a safe API
Use when: safe Rust refuses something you can argue is correct, such as two &mut into one slice, and no standard-library function already does it
Goal: a safe function whose checks make its unsafe core valid for every input, so callers never write unsafe
Taught in Safe abstractions over unsafe code
Quick recipe
-
1
Search the docs for the operation (
split_at_mut,get_disjoint_mut,swap). If one exists, use it and stop.Example
Moving chips between two seats:
seats.get_disjoint_mut([from, to])already returns two&mut.Why: std's unsafe cores are reviewed by many people; yours is reviewed by you.
-
2
Name the unsafe operations you need and list each one's documented conditions.
Example
fn pick_two_mut<T>(items: &mut [T], i: usize, j: usize) -> (&mut T, &mut T):addneeds in-bounds offsets; two&mutneedi != j.Why: every condition you list is one your checks must cover.
-
3
Use
assert!, an earlyreturn None, or aResult, so that no argument can reach the block with a condition false.Example
assert!(i != j); assert!(i < items.len() && j < items.len());Why: soundness means "for every input", and only a check covers every input.
-
4
Put only the unchecked operations inside, and argue each condition from the checks above.
Example
// SAFETY: both < len, and i != j, so ...aboveunsafe { (&mut *p.add(i), &mut *p.add(j)) }.Why: the comment is the proof; the reviewer checks it against the lines above.
-
5
Write tests for the edge cases (equal indices, last item, empty slice) and run them with
cargo +nightly miri test(section 7).Why: Miri catches a wrong argument that ordinary tests pass by luck.
Common mistakes
- Checking one condition and forgetting another, such as bounds but not
i != j. - Marking the wrapper
unsafe fn"to be careful": that hands the conditions to every caller instead of checking them once. - Letting safe code change a field the unsafe block relies on (see "The unsound neighbour").