Step 2 of 6
Unsafe functions and safe abstractions
unsafe fn: a contract the caller must keep
An unsafe fn is a function with a precondition the compiler can't check. Calling it is only allowed inside an unsafe block, which is the caller saying "I've checked the precondition". std has many: slice::get_unchecked (the index must be in bounds), String::from_utf8_unchecked (the bytes must be valid UTF-8).
Every unsafe fn documents its contract in a # Safety section, and every unsafe block gets a // SAFETY: comment explaining why the contract holds right there:
/// Returns the element at `index` without checking bounds.
///
/// # Safety
///
/// `index` must be less than `values.len()`.
unsafe fn get_fast(values: &[u32], index: usize) -> u32 {
// SAFETY: the caller promises index < values.len().
unsafe { *values.as_ptr().add(index) }
}
fn main() {
let v = [7, 8, 9];
for i in 0..v.len() {
// SAFETY: i < v.len() because of the loop range.
print!("{} ", unsafe { get_fast(&v, i) });
}
println!();
}
7 8 9
Notice the unsafe { } block inside the unsafe fn. Since edition 2024, an unsafe fn only means "callers must be careful"; its body is checked like safe code, and each unsafe operation inside still needs its own block. Leave it out and the compiler warns: dereference of raw pointer is unsafe and requires unsafe block (lint unsafe_op_in_unsafe_fn). That keeps the dangerous lines small and visible.
Reviewers read SAFETY comments carefully, and clippy's undocumented_unsafe_blocks lint can require one on every block. A missing or vague comment ("this is fine") is a red flag in code review.
Safe abstractions
The most important idea in this module: wrap unsafe code in a safe function that checks everything the unsafe code relies on. Then no caller, however careless, can cause UB. A function with that property is sound.
The classic example is split_at_mut, which turns one mutable slice into two that don't overlap. The borrow checker can't see that the halves are disjoint:
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
(&mut values[..mid], &mut values[mid..])
}
fn main() {
let mut v = [1, 2, 3];
let (a, b) = split_at_mut(&mut v, 1);
println!("{a:?} {b:?}");
}
error[E0499]: cannot borrow `*values` as mutable more than once at a time
We know the halves don't overlap, so we can build them from raw parts:
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len, "mid out of range");
// SAFETY: mid <= len, so [0, mid) and [mid, len) are inside the slice and
// don't overlap. Both results borrow `values`, so nothing else can use it
// while they're alive.
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut v = [1, 2, 3, 4, 5];
let (left, right) = split_at_mut(&mut v, 2);
left[0] = 10;
right[0] = 30;
println!("{:?} {:?}", left, right);
println!("{:?}", v);
}
[10, 2] [30, 4, 5]
[10, 2, 30, 4, 5]
slice::from_raw_parts_mut(ptr, len)is an unsafe fn: it makes a slice from a pointer and a length, trusting you that they describe valid, unaliased memory.- The
assert!is what makes the function sound. Without it,split_at_mut(&mut v, 99)would create a slice reaching past the array: UB from a safe call, which is a bug insplit_at_mut, not in the caller. - The signature ties both output lifetimes to the input, so the borrow checker still protects callers.
You'll never need to write this one: std's values.split_at_mut(mid) is exactly this code. But the pattern (check, then a small commented unsafe block, behind a safe signature) is how all of std, Tokio and other foundation crates use unsafe.
Common mistakes
- Trusting input you don't control. Unsafe code must not rely on safe code behaving well: a caller's
Ordimpl might be inconsistent, a closure might panic halfway. Sound code stays sound even then. - Making the wrapper
unsafe fn"to be safe". If your function checks everything, it should be a normalfn. Mark itunsafeonly if callers must uphold something.
Your task
Write get_two_mut, which returns mutable references to two different elements of a slice. It must panic if i == j or either index is out of range (check with assert! before any unsafe code). Then get the two references from values.as_mut_ptr() with &mut *ptr.add(i) inside an unsafe block with a // SAFETY: comment.
Challenge 2
Before writing unsafe code, always look for a safe API. Write get_two_mut again without unsafe, using the slice method split_at_mut: split at the larger index, so one element is in the left half and the other is the first element of the right half. It must still panic if i == j or an index is out of range (slice indexing panics for you). The same tests run.
Challenge 3
Write the "unsafe core, safe wrapper" pair that std uses everywhere:
unsafe fn sum_at_unchecked(values: &[i64], indexes: &[usize]) -> i64adds upvalues[i]for everyiinindexesusingget_unchecked, with a# Safetydoc section saying every index must be in bounds.fn sum_at(values: &[i64], indexes: &[usize]) -> Option<i64>is the safe API: it returnsNoneif any index is out of range, and otherwise callssum_at_unchecked.
Remember: inside an unsafe fn, the get_unchecked call still needs its own unsafe { } block.
Previous: What unsafe means, and raw pointers Next: Calling C with FFI