RustArena

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]

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

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:

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