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_rawtransfers ownership to a raw pointer. If you never callBox::from_raw, the memory leaks. - Using transmute for type punning. Transmuting between incompatible types is instant undefined behavior. Use
ascasts,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
addandoffsetcarefully. - Forgetting alignment. Dereferencing an unaligned pointer is UB. Use
read_unalignedandwrite_unalignedwhen alignment is not guaranteed.
Key Takeaways
*const Tand*mut Tare raw pointers. Creating them is safe; dereferencing is unsafe.std::ptrprovides functions for copying, reading, writing, and comparing pointers.ManuallyDropprevents automatic destructor calls, giving you explicit control.transmutereinterprets 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.