RustArena

Step 1 of 6

What unsafe means, and raw pointers

Everything you've written so far is safe Rust: the compiler proves there are no dangling references, no data races and no out-of-bounds reads. But some correct programs can't be proven by the compiler. Calling a C library, talking to hardware, or implementing Vec itself needs operations the compiler can't check. For those, Rust has the unsafe keyword.

The unsafe keyword unlocks exactly five extra abilities (the Rust book calls them the unsafe superpowers):

  1. Dereference a raw pointer.
  2. Call an unsafe function (including C functions through FFI).
  3. Read or write a mutable static variable.
  4. Implement an unsafe trait (like Send), written unsafe impl.
  5. Read a field of a union.

All but the fourth happen inside an unsafe { ... } block. (Edition 2024 also uses the keyword to mark declarations the compiler can't check, like unsafe extern "C" blocks and #[unsafe(no_mangle)]; step 3 shows them. They unlock nothing new: you're vouching that the declaration is right.)

That's the whole list. unsafe does not turn off the borrow checker, type checking or bounds checks on normal indexing. It marks a spot where you promise to uphold rules the compiler can't verify. If you break them, the result is undefined behavior (UB): the program may crash, compute garbage or seem to work until it doesn't. Step 5 is all about UB.

Raw pointers

Rust has two raw pointer types: *const T (read) and *mut T (read and write). Unlike references, a raw pointer may be null, dangling or unaligned, several *mut may point to the same place, and nothing tracks how long the data lives. Creating one is safe; dereferencing one is not.

fn main() {
    let mut hp = 100;
    let read_ptr = &raw const hp; // *const i32
    let write_ptr = &raw mut hp; // *mut i32

    // SAFETY: both pointers point to `hp`, which is alive for all of main.
    unsafe {
        *write_ptr -= 30;
        println!("hp = {}", *read_ptr);
    }

    let nothing: *const i32 = std::ptr::null();
    println!("null? {}", nothing.is_null());
    println!("same address? {}", read_ptr == write_ptr as *const i32);
}
hp = 70
null? true
same address? true

Pointer arithmetic

ptr.add(i) moves a pointer forward by i elements (not bytes). It's an unsafe function itself: the result must stay inside the same array (or point just past its end). Combined with as_ptr() on a slice, it walks through the elements:

fn main() {
    let scores = [10, 20, 30, 40];
    let p = scores.as_ptr();
    let mut total = 0;
    for i in 0..scores.len() {
        // SAFETY: i < scores.len(), so p.add(i) points inside the array.
        total += unsafe { *p.add(i) };
    }
    println!("{total}");
}
100

This is roughly what scores.iter().sum() does under the hood, except that the iterator's safe API guarantees the pointer never leaves the array. In real code you'd write the iterator version: it's just as fast and can't go wrong.

Common mistakes

Dereferencing outside an unsafe block:

fn main() {
    let x = 5;
    let p = &raw const x;
    println!("{}", *p);
}
error[E0133]: dereference of raw pointer is unsafe and requires unsafe block

Keeping a pointer longer than the data. This compiles, because creating and returning a raw pointer is safe, but x is gone when the function returns, so any later dereference is UB:

fn dangling() -> *const i32 {
    let x = 42;
    &raw const x // x is dropped here; the pointer now points at nothing
}

References would never allow this. With raw pointers, lifetimes are your responsibility.

Your task

Fill in the blanks: create a *mut i32 pointing at gold, then add 25 through it inside an unsafe block. The program should print gold = 75.

Challenge 2

For practice (real code would use values.iter().sum()), write sum_raw, which adds up a slice by reading each element through the raw pointer p = values.as_ptr() and p.add(i). Don't use iterators or indexing on values. Add a // SAFETY: comment to your unsafe block.

Challenge 3

Write reverse_raw, which reverses a slice in place using raw pointers: take p = v.as_mut_ptr(), keep two indexes i (from the front) and j (from the back), and swap the elements they point to with std::ptr::swap(p.add(i), p.add(j)) until they meet. Careful: an empty slice has no last element, so check the length before computing len - 1.

Next: Unsafe functions and safe abstractions