Rust 91 🦀 Raw Pointers (*const T and *mut T) Dereferencing
Raw pointers are Rust’s mechanism for expressing memory access that the compiler cannot verify. A *const T is a pointer that permits reading; a *mut T is a pointer that permits reading and writing. Creating either is safe. Dereferencing either requires an unsafe block because the compiler cannot prove that the pointer is non-null, aligned, points to initialized memory, or respects the aliasing rules that references require . The distinction between creating and dereferencing is the first thing to internalize: the type system allows you to hold a raw pointer, but it does not allow you to use it without taking responsibility for the invariants the compiler normally checks.
This chapter covers the two pointer types, the five safety conditions that must hold before a dereference, the standard library methods that make dereferencing explicit, and the unaligned-pointer case that a plain & reference cannot express.
Key point: Casting a reference to a raw pointer is safe. Dereferencing it is unsafe. The unsafe block does not make the code wrong; it moves the proof obligation from the compiler to the programmer.
Why raw pointers exist
The FFI boundary problem. Every call into a C library passes pointers. The C function signature declares *const c_char or *mut c_void, and Rust must be able to represent those types. The std::ffi module provides the C-compatible types, and the pointer types are the bridge between Rust’s references and C’s untyped addresses . Without raw pointers, FFI would be impossible.
The self-referential structure problem. A linked list or a tree that owns its nodes through Box is difficult to express because the borrow checker cannot prove that the pointers remain valid when the structure is moved. Raw pointers allow the structure to hold addresses that the compiler does not track, with the safety proof moved into the implementation of the data structure itself. The unsafe block is where the invariant “this pointer always points to a live node” is asserted .
The performance problem. A reference carries a non-null and alignment guarantee that the compiler uses to optimize. A raw pointer does not, which means the compiler cannot assume anything about it. In some cases, this is a disadvantage. In other cases—when the pointer comes from C or from a memory-mapped region—the raw pointer is the only type that accurately describes the data.
a. The Two Types and the Cast
The two raw pointer types differ in mutability, not in layout. Both are the same size as a usize on the target platform. A *const T is a pointer through which the pointee cannot be written; a *mut T is a pointer through which it can.
let x = 5;
// Coerce a shared reference to a const raw pointer
let p: *const i32 = &x;
// Coerce a mutable reference to a mut raw pointer
let mut y = 10;
let q: *mut i32 = &mut y;
// Cast between pointer types
let r: *mut i32 = p as *mut i32;
The coercion from &T to *const T and from &mut T to *mut T is implicit in some contexts and explicit in others. The as cast is the general mechanism. A *const T can be cast to *mut T and back, but the resulting pointer inherits the provenance of the original, which means a write through a pointer derived from a shared reference is undefined behavior even if the pointer type is *mut.
The std::ptr module provides ptr::null() and ptr::null_mut() for the null pointer of each type. A null pointer is never dereferenceable, and dereferencing it is undefined behavior. The ptr::is_null() method checks for it.
b. The Five Safety Conditions
Dereferencing a raw pointer is safe only if five conditions hold. The compiler does not check them; the programmer must .
| Condition | Meaning |
|---|---|
| Non-null | The pointer is not null |
| Aligned | The address is a multiple of align_of::<T>() |
| Live | The pointer refers to an allocation that has not been freed |
| Initialized | The memory contains a valid T |
| Aliasing | The access does not conflict with any live reference |
The aliasing condition is the one that is most often violated. Creating a &mut T from a raw pointer while another reference to the same memory exists is undefined behavior, even if the other reference is not used. The UnsafeCell type is the mechanism for interior mutability; a raw pointer into an UnsafeCell can be written through, but a raw pointer into a plain T cannot be written through while a &T exists.
let mut value = 42;
let ptr = &mut value as *mut i32;
// SAFETY: ptr is non-null, aligned, points to initialized memory,
// and no other reference to value exists at this point.
unsafe {
*ptr = 100;
}
assert_eq!(value, 100);
The SAFETY comment is not a formality. It is the place where the programmer writes the proof that the five conditions hold. A reviewer can read the comment and verify the reasoning. Without it, the unsafe block is an unverified assertion.
c. Explicit Methods and Unaligned Access
The std::ptr module provides methods that make the intent of a dereference explicit. ptr.read() reads a value out of the pointer without dropping the old value. ptr.write(value) writes a value into the pointer without reading or dropping the old value. These methods have the same safety requirements as *ptr, but their names say what they do .
fn first_or_none(values: &[u32]) -> Option<u32> {
if values.is_empty() {
None
} else {
let ptr = values.as_ptr();
// SAFETY: values is non-empty, ptr came from values,
// and u32 is Copy so there is no double-drop.
Some(unsafe { ptr.read() })
}
}
The read and write methods require the pointer to be aligned. For unaligned access, the read_unaligned and write_unaligned methods exist. They are the correct methods for fields in a #[repr(packed)] struct, where the field’s offset may not be a multiple of its alignment.
The addr_of! macro is the mechanism for creating a pointer to a field without creating a reference to it. This matters for packed structs, where taking a reference to a field is undefined behavior because the reference would be unaligned. The macro produces the pointer directly, and the pointer can be dereferenced with read_unaligned .
#[repr(packed)]
struct Packed {
f1: u8,
f2: u16,
}
let packed = Packed { f1: 1, f2: 2 };
// ✅ Create pointer without a reference
let raw_f2 = std::ptr::addr_of!(packed.f2);
// ✅ Unaligned read
assert_eq!(unsafe { raw_f2.read_unaligned() }, 2);
The &raw const and &raw mut syntax is the newer form of addr_of! and addr_of_mut!. It creates a raw pointer to a place without creating an intermediate reference, which is the correct way to obtain a pointer to a potentially unaligned field.
Complete Example Session
// ============================================
// PART 1: CREATING RAW POINTERS (SAFE)
// ============================================
let x = 5;
let p: *const i32 = &x;
let mut y = 10;
let q: *mut i32 = &mut y;
// ============================================
// PART 2: DEREFERENCING REQUIRES unsafe
// ============================================
// let v = *p; // ❌ E0133: requires unsafe
let v = unsafe { *p }; // ✅ SAFETY: p is valid
assert_eq!(v, 5);
// ============================================
// PART 3: WRITING THROUGH *mut
// ============================================
unsafe {
// SAFETY: q is non-null, aligned, initialized,
// and no other reference to y exists.
*q = 20;
}
assert_eq!(y, 20);
// ============================================
// PART 4: ptr.read() — DOES NOT DROP OLD VALUE
// ============================================
fn first_or_none(values: &[u32]) -> Option<u32> {
if values.is_empty() {
None
} else {
let ptr = values.as_ptr();
// SAFETY: values is non-empty, ptr from values,
// u32 is Copy.
Some(unsafe { ptr.read() })
}
}
// ============================================
// PART 5: ptr.write() — DOES NOT DROP OLD VALUE
// ============================================
let mut slot = std::mem::MaybeUninit::<u32>::uninit();
let slot_ptr = slot.as_mut_ptr();
unsafe {
// SAFETY: slot_ptr is valid, uninitialized memory
slot_ptr.write(42);
assert_eq!(slot.assume_init(), 42);
}
// ============================================
// PART 6: NULL POINTERS
// ============================================
let null: *const i32 = std::ptr::null();
assert!(null.is_null());
// unsafe { *null } // ❌ UB: null dereference
// ============================================
// PART 7: ADDR_OF! FOR PACKED STRUCTS
// ============================================
#[repr(packed)]
struct Packed {
f1: u8,
f2: u16,
}
let packed = Packed { f1: 1, f2: 2 };
// ❌ UB: &packed.f2 would create an unaligned reference
let raw_f2 = std::ptr::addr_of!(packed.f2);
assert_eq!(unsafe { raw_f2.read_unaligned() }, 2);
// ============================================
// PART 8: ALIASING RULES
// ============================================
let mut data = 1;
let ptr1 = &mut data as *mut i32;
// ❌ Do not create a &mut and a & at the same time
// unsafe { *ptr1 = 2; &data; } // UB
// ============================================
// PART 9: CASTING REFERENCE TO POINTER IS SAFE
// ============================================
let safe_cast = &x as *const i32;
// No unsafe needed to cast; only to dereference
// ============================================
// PART 10: FFI BOUNDARY
// ============================================
extern "C" {
fn strlen(s: *const std::ffi::c_char) -> usize;
}
// The *const c_char is a raw pointer from the FFI signature
The ten parts trace creation, dereferencing, explicit methods, null pointers, packed-struct access, aliasing, and the FFI boundary. Part 7 is the case that most often surprises developers: a & to a packed field is undefined behavior, and addr_of! is the correct way to obtain the pointer.
Quick Reference
Pointer Types
| Type | Permits | Created From |
|---|---|---|
*const T | Reading | &T, ptr::null() |
*mut T | Reading and writing | &mut T, ptr::null_mut() |
NonNull<T> | Reading, non-null guarantee | NonNull::new(ptr) |
Safety Conditions
| Condition | Check |
|---|---|
| Non-null | !ptr.is_null() |
| Aligned | (ptr as usize) % align_of::<T>() == 0 |
| Live | Allocation not freed |
| Initialized | Valid T in memory |
| Aliasing | No conflicting references |
Standard Library Methods
| Method | Effect | Alignment |
|---|---|---|
ptr.read() | Read value, no drop | Required |
ptr.write(v) | Write value, no drop | Required |
ptr.read_unaligned() | Read from any address | Not required |
ptr.write_unaligned(v) | Write to any address | Not required |
ptr.is_null() | Null check | N/A |
Pointer Creation
| Syntax | Purpose |
|---|---|
&x as *const T | Reference to const pointer |
&mut x as *mut T | Reference to mut pointer |
ptr::null() / null_mut() | Null pointer |
addr_of!(place) | Pointer to place, no reference |
&raw const place | Newer form of addr_of! |
&raw mut place | Newer form of addr_of_mut! |
Best Practices
✅ Do This:
// Write a SAFETY comment explaining the invariants
unsafe {
// SAFETY: ptr is non-null, aligned, initialized,
// and no other reference exists.
*ptr = 42;
} // ✅
// Use read/write for clarity
unsafe { ptr.read() } // ✅
// Use addr_of! for packed struct fields
let p = std::ptr::addr_of!(packed.field); // ✅
// Use read_unaligned for packed fields
unsafe { p.read_unaligned() } // ✅
❌ Don’t Do This:
// Dereference without unsafe
let v = *ptr; // ❌ E0133 // ❌
// Dereference a null pointer
unsafe { *std::ptr::null::<i32>() } // ❌ UB // ❌
// Create a reference to a packed field
let r = &packed.field; // ❌ UB: unaligned reference // ❌
// Write through a pointer derived from a shared reference
let p = &x as *const i32 as *mut i32;
unsafe { *p = 1; } // ❌ UB: write through shared-derived pointer // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Dereferencing freed memory | Pointer outlives allocation | Ensure allocation is live |
| Unaligned dereference | Pointer from packed struct | Use read_unaligned |
| Null dereference | Pointer from FFI or null() | Check is_null() |
| Aliasing violation | &mut created while & exists | Use UnsafeCell |
| Write through shared-derived pointer | Cast *const to *mut | Ensure origin was &mut |
Real-World Examples
1. Cast Reference to Pointer
let p = &x as *const i32;
2. Dereference with Safety Comment
unsafe {
// SAFETY: p is valid, aligned, initialized.
*p
}
3. Read Without Drop
unsafe { ptr.read() }
4. Write Without Drop
unsafe { ptr.write(42) }
5. Null Check
if !ptr.is_null() { unsafe { *ptr } }
6. Packed Struct Field
let p = std::ptr::addr_of!(packed.field);
7. Unaligned Read
unsafe { p.read_unaligned() }
8. Mutable Pointer from Mutable Reference
let p = &mut x as *mut i32;
9. FFI Pointer
extern "C" { fn strlen(s: *const c_char) -> usize; }
10. NonNull Wrapper
let nn = NonNull::new(ptr).expect("non-null");
Visual
Creating vs. Dereferencing
┌──────────────────────────────────────────────────────────────┐
│ CREATING (safe) DEREFERENCING (unsafe) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ &x as *const T │ │ unsafe { *ptr } │ │
│ │ │ │ │ │
│ │ No invariants │ │ 5 conditions │ │
│ │ checked │ │ must hold │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ The type system allows the pointer to exist. │
│ The unsafe block allows it to be used. │
│ │
└──────────────────────────────────────────────────────────────┘
The Five Safety Conditions
┌──────────────────────────────────────────────────────────────┐
│ BEFORE DEREFERENCING: │
│ │
│ ┌─────────────────┐ │
│ │ Non-null │ ptr != null │
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ Aligned │ addr % align_of::<T>() == 0 │
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ Live │ allocation not freed │
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ Initialized │ valid T in memory │
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ Aliasing │ no conflicting references │
│ └─────────────────┘ │
│ │
│ The compiler checks none of these. │
│ The SAFETY comment is where the proof lives. │
│ │
└──────────────────────────────────────────────────────────────┘
Packed Struct Access
┌──────────────────────────────────────────────────────────────┐
│ #[repr(packed)] struct Packed { f1: u8, f2: u16 } │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Memory layout: ││
│ │ [f1: 1 byte][f2: 2 bytes] ││
│ │ ▲ ││
│ │ └── offset 1, not aligned for u16 ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ ❌ &packed.f2 → unaligned reference → UB │
│ │
│ ✅ addr_of!(packed.f2) → raw pointer, no reference │
│ ✅ raw_f2.read_unaligned() → safe unaligned read │
│ │
│ The macro exists because references require alignment. │
│ │
└──────────────────────────────────────────────────────────────┘
Aliasing Rule
┌──────────────────────────────────────────────────────────────┐
│ VALID INVALID │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ &mut x │ │ &x and &mut x │ │
│ │ ↓ as *mut │ │ simultaneously │ │
│ │ *ptr = 1 │ │ (UB) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ UnsafeCell<T> │ │ *const cast to │ │
│ │ interior mut │ │ *mut, then │ │
│ │ allowed │ │ written │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ The provenance of the pointer determines what is allowed. │
│ A pointer derived from & cannot be written through. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
*const T | Read-only raw pointer |
*mut T | Read-write raw pointer |
| Creating | Safe; cast from reference or null |
| Dereferencing | Requires unsafe |
ptr.read() | Read, no drop, aligned |
ptr.write(v) | Write, no drop, aligned |
ptr.read_unaligned() | Read, no alignment required |
addr_of!(place) | Pointer to place, no reference |
| Five conditions | Non-null, aligned, live, initialized, aliasing |
| SAFETY comment | Where the proof is written |
Key takeaways:
- Creating a raw pointer is safe; dereferencing it is unsafe. The type system allows the pointer to exist because holding an address does not violate any invariant. Using the address requires the programmer to prove the five conditions that the compiler normally checks .
- The five conditions are the contract. Non-null, aligned, live, initialized, and aliasing. Every
unsafedereference must satisfy all five. TheSAFETYcomment is where the proof is written, and it is what a reviewer reads to verify the reasoning. read()andwrite()are clearer than*ptr. They have the same safety requirements, but their names state the intent and their documentation lists the invariants. Theread_unalignedandwrite_unalignedvariants handle the packed-struct case.- Never create a reference to a packed field. A reference requires alignment, and a packed field may not be aligned. The
addr_of!macro and the&raw constsyntax create a pointer without creating a reference, which is the correct way to access the field . - Provenance matters for aliasing. A
*mut Tcast from a*const Tthat was derived from a shared reference cannot be written through, even though the type permits it. The write is undefined behavior because the underlying provenance is a shared borrow. UseUnsafeCellfor interior mutability, and ensure that a*mutoriginates from a&mutor an allocation.
Remember: Raw pointers are the boundary between Rust’s safety guarantees and the untyped memory that the rest of the world uses. The FFI boundary, the self-referential structure, and the memory-mapped region all require a type that does not carry the compiler’s assumptions. Raw pointers are that type. The cost is that the compiler’s proof obligation moves to the programmer, and the unsafe block is where the programmer accepts it. The block is not a warning to be suppressed; it is a statement that the invariants have been verified. When the five conditions hold and the SAFETY comment explains why, the code is as correct as safe code. When they do not, the code is undefined behavior even if it compiles.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!