When references hit a wall
You are implementing a custom memory allocator. You need to track free blocks of memory. You try to mutate a block's metadata while holding a reference to the next block. The borrow checker rejects you. You are trying to create a reference that aliases mutable state, which Rust forbids to prevent data races.
Or you are wrapping a C library. The library hands you a pointer to a buffer and expects you to manage its lifetime. Rust references require a clear owner and a defined scope. The C library doesn't care about Rust's scopes. It just gives you an address.
References won't work here. You need a raw pointer.
Raw pointers are the escape hatch. They let you bypass the borrow checker and manipulate memory directly. They are dangerous. They are also the foundation of every safe data structure in the standard library. Vec, HashMap, and String all use raw pointers internally. The goal is not to use raw pointers everywhere. The goal is to use them to build safe abstractions that the rest of your code can use without fear.
What a raw pointer actually is
A raw pointer is a memory address. Under the hood, it is just a number. It points to a location in memory. It carries no information about ownership, lifetime, or validity.
Rust has two kinds of raw pointers:
*const T: A pointer to data of typeT. Theconstsuggests you intend to read, not write. It does not guarantee immutability. If you have another pointer to the same location, you can still mutate the data.*mut T: A pointer to mutable data of typeT. This signals intent to modify. It also carries a strict aliasing rule. If you have a*mut T, you must be the only one accessing that memory. Aliasing*mutpointers and writing through them is undefined behavior.
Raw pointers implement the Copy trait. This is a critical difference from references. When you clone a reference, the borrow checker tracks the relationship. When you copy a raw pointer, you get an independent handle. There is no tracking. There is no coordination. You can have a million copies of the same raw pointer. The compiler does not care.
Creating and using raw pointers
You create a raw pointer by casting a reference. The as keyword performs the cast. This does not move the value. It just takes the address.
fn main() {
let mut value = 42;
// Create a raw pointer from a reference.
// This takes the address of `value`. It does not move or copy the data.
let ptr = &value as *mut i32;
// Raw pointers are Copy. You can duplicate them freely.
let ptr2 = ptr;
unsafe {
// Dereferencing requires unsafe.
// The compiler trusts you that the pointer is valid and aligned.
println!("Value: {}", *ptr);
// Mutating through the pointer requires unsafe.
// This bypasses Rust's aliasing rules.
*ptr = 100;
}
// The change is visible through the original variable.
println!("Updated: {}", value);
}
Dereferencing a raw pointer outside an unsafe block is a compile error. The compiler rejects this with E0133 (dereference of raw pointer requires unsafe block or function). The unsafe block is your promise to the compiler that you have verified the pointer is valid.
Convention aside: When creating a raw pointer to a struct field, use the std::ptr::addr_of! or std::ptr::addr_of_mut! macros instead of &field as *const T. The macro prevents the compiler from creating a temporary reference, which can trigger undefined behavior if the field is uninitialized or if the struct is !Unpin. The macro is the safe way to get an address.
The Copy trait and aliasing
Because raw pointers are Copy, you can pass them around without restrictions. This makes them ideal for FFI and for building data structures where multiple parts need to point to the same node.
The danger lies in aliasing. The *mut T type has a strict rule: you must not have two *mut T pointers pointing to the same memory and write through both. This is undefined behavior. The compiler cannot enforce this. You must enforce it.
*const T does not have this restriction. You can have multiple *const T pointers to the same location. You can also have a *const T and a *mut T to the same location, but only if you never write through the *const T while the *mut T is active. The rules are subtle. Violating them can cause the optimizer to reorder memory accesses in ways that corrupt your data. The hardware might not crash. The program might just produce wrong results.
Raw pointers are tools for building invariants. If you cannot prove the invariants, you do not have a safe abstraction.
Real-world pattern: FFI and abstractions
Raw pointers shine in two scenarios: crossing FFI boundaries and implementing safe abstractions.
When calling C code, you often need to pass pointers. C functions expect raw pointers. Rust references cannot cross the FFI boundary safely because C does not respect Rust's lifetime rules. You cast references to raw pointers, call the function, and handle the result.
use std::ffi::c_char;
// Declare an external C function.
extern "C" {
fn strlen(s: *const c_char) -> usize;
}
fn main() {
let message = b"Hello, C!\0";
// Pass a raw pointer to the C function.
// We use unsafe because we are calling external code.
let len = unsafe { strlen(message.as_ptr()) };
println!("Length: {}", len);
}
When building abstractions, raw pointers let you manage memory manually. The std::ptr module provides functions that are safer than direct dereferencing in specific contexts.
ptr::write assigns a value without dropping the old value. This matters when you are writing to uninitialized memory. If you use *ptr = value, Rust tries to drop whatever is currently at ptr. If that memory is garbage, dropping it is undefined behavior. ptr::write skips the drop. It is the standard tool for Vec growth and manual memory management.
fn main() {
// Allocate uninitialized memory for a single i32.
let mut storage = std::mem::MaybeUninit::<i32>::uninit();
// Get a raw pointer to the storage.
let ptr = storage.as_mut_ptr();
unsafe {
// Use ptr::write to initialize the memory.
// This avoids dropping uninitialized data.
std::ptr::write(ptr, 42);
// Read the value back.
// ptr::read moves the value out, leaving the memory uninitialized.
let value = std::ptr::read(ptr);
println!("Value: {}", value);
}
}
Convention aside: The community prefers NonNull<T> over *mut T for internal state in data structures. NonNull is a wrapper that asserts the pointer is never null. It makes the intent explicit. If you see a NonNull, you know you do not need to check for null before dereferencing. It also helps with debug assertions. Use NonNull when building linked lists, trees, or caches. Use *mut when null is a valid state, like an optional link or an FFI result.
Pitfalls and the cost of freedom
Raw pointers remove safety guarantees. You are responsible for everything.
- Dangling pointers: The pointer points to memory that has been freed. Dereferencing it is undefined behavior. The compiler will not warn you.
- Null pointers: The pointer is null. Dereferencing it is undefined behavior. Use
NonNullor explicit checks to avoid this. - Alignment: The pointer must be aligned to the type's alignment. Dereferencing a misaligned pointer is undefined behavior. Use
ptr::align_offsetto check alignment. - Out-of-bounds: The pointer points outside the allocated object. Accessing it is undefined behavior. Rust does not perform bounds checks on raw pointers.
- Aliasing violations: Writing through aliased
*mutpointers is undefined behavior. Track your pointers carefully.
The compiler error E0133 protects you from accidental dereferences. Once you are inside unsafe, the compiler steps back. You are on your own.
Treat every unsafe block as a contract. If you cannot write the invariants, do not write the code.
Decision matrix
Use *const T when you need a pointer that can be null or might dangle, but you only intend to read. Use *const T for FFI inputs where the C side provides data you will not modify. Use *const T when you need to store a pointer in a data structure and null is a valid sentinel value.
Use *mut T when you need to mutate memory through a pointer and you are implementing a safe abstraction or crossing FFI boundaries. Use *mut T when you need to bypass the borrow checker to mutate a node while traversing a graph. Use *mut T only when you can prove that no other pointer aliases the memory during writes.
Use NonNull<T> when you are building a data structure and the pointer must never be null. Use NonNull<T> for internal links in linked lists, trees, and caches. Use NonNull<T> to make your invariants explicit and to enable debug assertions.
Reach for &T or &mut T when lifetimes allow it. Raw pointers should be rare in application code. If you find yourself using raw pointers in business logic, you are likely fighting the borrow checker instead of working with it. Trust the borrow checker. It usually has a point.