Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

References vs raw pointers

C has one kind of pointer. Whether a T * may be null, who owns the memory behind it, and how long it stays valid are all conventions, written down in documentation if you’re lucky. Rust has references instead, and a reference carries those guarantees in its type.

What a reference guarantees

A reference, &T or &mut T, is always:

  • non-null
  • aligned for T
  • pointing to a live, initialized, valid T
  • valid for its lifetime, which the borrow checker enforces
  • subject to the aliasing rules: either any number of &T, or exactly one &mut T

These guarantees aren’t only there to protect you. The compiler optimizes based on them. Because a &mut T is the only way to reach its value, the compiler can keep that value in a register instead of re-reading it from memory.

That’s also why creating a reference that breaks one of these guarantees is undefined behavior, even if you never read through it.

Raw pointers

Those guarantees can be quite limiting, though, especially when you want to interface with C. For this reason Rust also has raw pointers: *const T and *mut T. They are Rust’s equivalent of C’s const T * and T *, and they come without any of the guarantees Rust has for references. A raw pointer may be null, dangling, misaligned, or pointing to memory that is being mutated through another pointer at the same time.

Creating a raw pointer is a safe operation. Dereferencing one is unsafe, because that’s the moment all those possibilities matter:

let mut x = 10_i32;

// Creating raw pointers is safe.
let p: *mut i32 = &mut x;
let null: *const i32 = std::ptr::null();
assert!(!p.is_null());
assert!(null.is_null());

// SAFETY: `p` comes from `&mut x`, so it's non-null, aligned, and points to a
// live `i32`. Nothing else accesses `x` while we use `p`.
unsafe {
    *p += 1;
    assert_eq!(p.read(), 11);
}

Raw pointers have a small API of their own. The methods you’ll mostly use are:

  • is_null to check for null
  • cast to change the pointee type, the equivalent of a C pointer cast
  • add for pointer arithmetic, which is unsafe because the result must stay within the same allocation
  • read and write to copy a value out or in without creating a reference

Mutability is about where a pointer came from

In C, casting away const is legal as long as the memory itself isn’t const. Rust is stricter. Whether you may write through a pointer depends on how it was created, not only on its type. A *mut T that you got by casting a &T must never be written through, because a &T guarantees that the value doesn’t change.

The compiler catches the most obvious version of this:

let x = 10_i32;
let p = &x as *const i32 as *mut i32;
// error: assigning to `&T` is undefined behavior, consider using an `UnsafeCell`
unsafe { *p = 20 };

It doesn’t catch the less obvious ones. This example compiles, runs, and prints 20, but it is undefined behavior all the same:

let mut x = 10_i32;
let p = &mut x as *mut i32;
let r = &x;
// SAFETY: none! `r` is a shared reference, so `x` must not change while it
// is alive.
unsafe { *p = 20 };
println!("{r}");

The borrow checker only tracks references, so it has no idea p and r point to the same value. Miri does catch it and reports undefined behavior; Dynamic analysis with Miri puts it to work on a real port.

Keep in mind that C’s const is a convention. A C function that takes a const T * can still cast it away and write through it. If you pass it a pointer derived from a &T, you are trusting that it doesn’t.

From raw pointer to reference

Sooner or later you’ll want to turn a raw pointer from C into a reference, so the rest of your Rust code can work with it safely. There are two ways to do that:

  • &*ptr or &mut *ptr dereference the pointer and borrow the result
  • ptr.as_ref() and ptr.as_mut() do the same, but return None for a null pointer

Both approaches are unsafe. The compiler can’t check that the pointer meets the guarantees a reference needs, so you have to promise that it does. It also can’t work out how long the memory stays valid, so you have to assign a lifetime for the reference that matches how long the C side keeps the value alive.

pub struct Config {
    pub verbose: bool,
}

/// Borrows the configuration a C library handed us.
///
/// # Safety
///
/// `ptr` must be null, or point to a valid `Config` that stays alive and
/// unmodified for as long as the returned reference is in use.
pub unsafe fn config_from_c<'a>(ptr: *const Config) -> Option<&'a Config> {
    // SAFETY: the caller guarantees that `ptr` is null or valid for `'a`.
    unsafe { ptr.as_ref() }
}

The # Safety section tells you what 'a means in practice. Without it, a caller could keep the reference around long after the C library freed the Config, and nothing would stop them.

Head to the exercise

The exercise is one function: swap the two i32s that a pair of raw pointers point to.

Exercise

The exercise for this section is located in 01_foundations/02_raw_pointers