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):
- Dereference a raw pointer.
- Call an
unsafefunction (including C functions through FFI). - Read or write a mutable
staticvariable. - Implement an
unsafetrait (likeSend), writtenunsafe impl. - 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
&raw const hpand&raw mut hpcreate raw pointers directly. Older code writes&hp as *const i32; both work.- Having a
*constand a*mutto the same variable at the same time is allowed, unlike&and&mut. The compiler stops tracking aliasing for raw pointers; that's now your job. - The
// SAFETY:comment explains why this particular unsafe block is fine. It's the standard convention in Rust codebases (the next step explains it).
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.