3 min read
On this page

Raw Pointers & Memory

Raw pointers are Rust's escape from the ownership system. They do not have lifetimes, they do not enforce aliasing rules, and dereferencing them is unsafe. They exist because some problems — FFI, custom allocators, intrusive data structures — require direct memory manipulation. This topic covers how to use them correctly.

*const T and *mut T

Rust has two raw pointer types:

  • *const T — a read-only raw pointer
  • *mut T — a mutable raw pointer

Creating raw pointers is safe. Dereferencing them is not:

fn main() {
    let x = 42;
    let ptr: *const i32 = &x;

    // Creating the pointer: safe
    println!("Pointer: {:?}", ptr);

    // Dereferencing the pointer: unsafe
    unsafe {
        println!("Value: {}", *ptr);
    }
}
Pointer: 0x7ffeefbff5dc
Value: 42

You can also create pointers from mutable references:

let mut x = 42;
let ptr: *mut i32 = &mut x;

unsafe {
    *ptr = 100;
    println!("Value: {}", *ptr);
}
Value: 100

Unlike references, raw pointers can be null, can be dangling, and can alias. The compiler does not check any of this.

Creating Raw Pointers

Several ways to obtain raw pointers:

// From references
let x = 42;
let ptr: *const i32 = &x;
let mut_ptr: *mut i32 = &mut x as *mut i32;

// From Box
let boxed = Box::new(42);
let ptr: *const i32 = Box::into_raw(boxed);
// Must reconstruct the Box to free the memory
unsafe { drop(Box::from_raw(ptr as *mut i32)); }

// From Vec
let v = vec![1, 2, 3];
let ptr: *const i32 = v.as_ptr();
let len = v.len();
// ptr is valid as long as v is alive and not reallocated

// Null pointers
let null: *const i32 = std::ptr::null();
let null_mut: *mut i32 = std::ptr::null_mut();

The critical rule: a raw pointer is only valid as long as the memory it points to is valid. If the original Vec is dropped or reallocated, the pointer is dangling.

std::ptr Operations

The std::ptr module provides safe-to-call functions for pointer manipulation:

use std::ptr;

fn main() {
    let src = [1, 2, 3, 4, 5];
    let mut dst = [0i32; 5];

    unsafe {
        // Copy memory (like memcpy, but checks for overlap)
        ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), 5);
    }

    println!("{:?}", dst);
}
[1, 2, 3, 4, 5]

Key operations:

use std::ptr;

// Read a value from a pointer
let value = unsafe { ptr::read(ptr) };

// Write a value to a pointer
unsafe { ptr::write(ptr, 42); }

// Copy with possible overlap (like memmove)
unsafe { ptr::copy(src, dst, count); }

// Copy without overlap (like memcpy, faster)
unsafe { ptr::copy_nonoverlapping(src, dst, count); }

// Set memory to zero (like memset with 0)
unsafe { ptr::write_bytes(ptr, 0, count); }

// Check for null
if !ptr.is_null() {
    unsafe { process(*ptr); }
}

ptr::read is important when you need to move a value out of a location without running its destructor on the original. ptr::write is the counterpart for placement.

Pointer Arithmetic

Raw pointers support offset operations:

fn main() {
    let data = [10, 20, 30, 40, 50];
    let ptr = data.as_ptr();

    unsafe {
        // offset by N elements (not bytes)
        let third = *ptr.add(2);
        println!("Third element: {}", third);

        // Equivalent using offset
        let fourth = *ptr.offset(3);
        println!("Fourth element: {}", fourth);
    }
}
Third element: 30
Fourth element: 40

add takes a usize and moves forward. offset takes an isize and can move backward. Both are in units of T, not bytes. Going out of bounds is undefined behavior.

ManuallyDrop

ManuallyDrop<T> wraps a value and prevents its destructor from running:

use std::mem::ManuallyDrop;

fn main() {
    let mut data = ManuallyDrop::new(String::from("hello"));

    // The String is not dropped when data goes out of scope.
    // We must drop it manually or we leak memory.

    // Take ownership back
    let s = unsafe { ManuallyDrop::take(&mut data) };
    println!("{}", s);
    // s is dropped normally here
}

ManuallyDrop is useful when building data structures that manage their own memory. You control exactly when destructors run:

struct MyVec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
}

impl<T> Drop for MyVec<T> {
    fn drop(&mut self) {
        for i in 0..self.len {
            unsafe {
                // Drop each element explicitly
                ptr::drop_in_place(self.ptr.add(i));
            }
        }
        if self.cap > 0 {
            unsafe {
                // Free the allocation
                let layout = std::alloc::Layout::array::<T>(self.cap).unwrap();
                std::alloc::dealloc(self.ptr as *mut u8, layout);
            }
        }
    }
}

Transmute

std::mem::transmute reinterprets the bits of one type as another. It is the most dangerous function in Rust:

use std::mem;

// Convert between same-size integer types
let x: u32 = 42;
let y: i32 = unsafe { mem::transmute(x) };

// View f32 as its bit representation
let f: f32 = 3.14;
let bits: u32 = unsafe { mem::transmute(f) };
println!("3.14 as bits: {:#010x}", bits);
3.14 as bits: 0x4048f5c3

You almost never need transmute. Safer alternatives exist for nearly every use case:

// Instead of transmute for integer conversion:
let x: u32 = 42;
let y: i32 = x as i32;

// Instead of transmute for float bits:
let bits = f32::to_bits(3.14);

// Instead of transmute for byte slice to str:
let bytes = b"hello";
let s = std::str::from_utf8(bytes).unwrap();

// Instead of transmute for enum to integer:
#[repr(u8)]
enum Color { Red = 0, Green = 1, Blue = 2 }
let value = Color::Green as u8;

The only legitimate uses of transmute are FFI type conversions and certain zero-copy parsing scenarios. If you think you need it, you probably do not.

Writing Safe Wrappers Around Unsafe Code

The goal is always to minimize the surface area of unsafe code:

pub struct AlignedBuffer {
    ptr: *mut u8,
    len: usize,
    align: usize,
}

impl AlignedBuffer {
    pub fn new(len: usize, align: usize) -> Self {
        assert!(align.is_power_of_two(), "alignment must be a power of two");
        assert!(len > 0, "length must be positive");

        let layout = std::alloc::Layout::from_size_align(len, align).unwrap();
        let ptr = unsafe { std::alloc::alloc_zeroed(layout) };

        if ptr.is_null() {
            std::alloc::handle_alloc_error(layout);
        }

        AlignedBuffer { ptr, len, align }
    }

    pub fn as_slice(&self) -> &[u8] {
        // SAFETY: ptr is valid for len bytes, allocated in new()
        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
    }

    pub fn as_mut_slice(&mut self) -> &mut [u8] {
        // SAFETY: ptr is valid for len bytes, &mut self ensures exclusive access
        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
    }
}

impl Drop for AlignedBuffer {
    fn drop(&mut self) {
        let layout = std::alloc::Layout::from_size_align(self.len, self.align).unwrap();
        // SAFETY: ptr was allocated with this layout in new()
        unsafe { std::alloc::dealloc(self.ptr, layout); }
    }
}

The public API is entirely safe. Raw pointer manipulation is confined to the implementation. Every unsafe block has a SAFETY comment.

Common Pitfalls

  • Dereferencing dangling pointers. The most common unsafe bug. A pointer becomes dangling when the data it points to is freed or moved. Always ensure the source outlives the pointer.
  • Forgetting to free memory. Box::into_raw transfers ownership to a raw pointer. If you never call Box::from_raw, the memory leaks.
  • Using transmute for type punning. Transmuting between incompatible types is instant undefined behavior. Use as casts, to_bits/from_bits, and safe conversion methods instead.
  • Pointer arithmetic off the end. Going more than one element past the end of an allocation is UB, even without dereferencing. Use add and offset carefully.
  • Forgetting alignment. Dereferencing an unaligned pointer is UB. Use read_unaligned and write_unaligned when alignment is not guaranteed.

Key Takeaways

  • *const T and *mut T are raw pointers. Creating them is safe; dereferencing is unsafe.
  • std::ptr provides functions for copying, reading, writing, and comparing pointers.
  • ManuallyDrop prevents automatic destructor calls, giving you explicit control.
  • transmute reinterprets bits between types and should almost never be used. Safer alternatives exist for nearly every case.
  • Always wrap unsafe pointer code in safe abstractions with documented invariants.
  • A raw pointer is only valid as long as the memory it references is allocated and alive.