How to Use std

:ptr for Pointer Operations

Use std::ptr functions inside unsafe blocks to perform low-level memory reads, writes, and copies in Rust.

When safe references hit the wall

You're building a game engine. You have a raw buffer of vertex data. You need to copy a chunk of that buffer to a new location without allocating a fresh vector. Or you're wrapping a C library that expects a pointer to a struct, and you need to initialize that struct in place. Rust's safe references won't let you do this directly. The borrow checker blocks you because it can't track the lifetime of a raw memory address, and it refuses to let you move data out of a struct field without touching the rest of the struct.

You need std::ptr. This module gives you raw pointer operations. It lets you read, write, and copy memory directly. It bypasses the borrow checker. It gives you the power to implement zero-cost abstractions, interface with other languages, and squeeze performance out of inner loops. It also gives you the power to crash your program, corrupt memory, and introduce undefined behavior. The compiler won't stop you inside an unsafe block. You have to stop yourself.

Raw pointers are coordinates, not leases

A safe reference is a tracked lease. The compiler knows who holds the reference, how long it lives, and whether it's mutable. If you try to use a reference after the data is gone, the compiler rejects you. A raw pointer is a coordinate on a map. It's just a number. The compiler doesn't care if that coordinate points to a valid object, a freed allocation, or random noise. It doesn't care if you're allowed to read or write there. You have the coordinate. You're on your own.

Raw pointers come in two flavors: *const T and *mut T. The names suggest mutability, but they don't enforce it. A *const T is a promise that you won't mutate the data through this pointer. A *mut T is a promise that you might. These promises are for you and other humans. The compiler treats both as raw numbers. You can cast between them freely. The safety comes from how you use them, not from the type system.

A raw pointer is a coordinate on a map. It's just a number. The compiler doesn't care if that coordinate points to a house, a lake, or a demolished lot. It doesn't care if you're allowed to go there. You have the coordinate. You're on your own.

Minimal example: read and write

Here's the basic pattern. You convert a safe reference to a raw pointer, perform an operation inside an unsafe block, and let the compiler verify the rest of your code.

use std::ptr;

/// Demonstrates basic read and write operations on a raw pointer.
fn main() {
    let mut x = 42;
    // Convert a safe reference to a raw pointer.
    // This tells the compiler we're stepping outside the borrow checker.
    let raw_ptr = &mut x as *mut i32;

    unsafe {
        // Write a new value to the memory address.
        // This bypasses move and borrow rules.
        // It does not drop the old value at that address.
        ptr::write(raw_ptr, 100);

        // Read the value back from memory.
        // This creates a copy of the data at that address.
        // It does not drop the value currently stored there.
        let val = ptr::read(raw_ptr);

        println!("Value: {val}");
    }
}

The unsafe block is the boundary. Inside, you're the compiler. Outside, the borrow checker takes back control.

How the bytes move

The key insight with std::ptr is that read and write manipulate bytes without running destructors. When you use normal Rust syntax like x = 5, the compiler drops the old value of x before writing the new one. If x is a String, the old string's memory is freed.

ptr::write skips the drop. It just stores the new value at the address. ptr::read skips the drop too. It loads the value from the address and returns a copy. The original value remains in memory, untouched. This is how you implement moves without dropping. Rust's move semantics work by copying bytes and marking the source as invalid. ptr::read and ptr::write give you that byte-copying power directly.

This matters when you're building data structures. If you're implementing Vec::pop, you need to move the last element out of the vector without dropping it. You use ptr::read to copy the element, then decrement the length. The element is now owned by the caller. The vector still holds the bytes, but the length says they're invalid. If you used *ptr, the vector would drop the element, and you'd have a double-free when the caller drops their copy.

Convention aside: the community calls this the "minimum unsafe surface" rule. Keep the unsafe block as small as possible. Put the pointer operations inside, and return safe values outside. This isolates the risk and makes the rest of your code easier to reason about.

Realistic example: safe wrapper with MaybeUninit

Real code rarely uses raw pointers on simple integers. You use them when you need to initialize memory in place, copy slices efficiently, or interface with C. Here's a pattern that combines ptr::write with MaybeUninit to initialize a buffer without zeroing it first. Zeroing memory can be a performance cost. MaybeUninit lets you skip it.

use std::mem::MaybeUninit;
use std::ptr;

/// Initializes a buffer using ptr::write to avoid zeroing overhead.
/// Returns a fully initialized array.
fn init_buffer() -> [u32; 4] {
    // Create uninitialized memory.
    // This allocates space but doesn't write zeros.
    // The memory is invalid until we write to it.
    let mut buf = MaybeUninit::<[u32; 4]>::uninit();

    unsafe {
        // SAFETY:
        // 1. buf.as_mut_ptr() is valid for writes of 4 u32 elements.
        // 2. The memory is uninitialized, so writing is required to make it valid.
        // 3. We write exactly 4 elements, matching the array size.
        // 4. The value [1, 2, 3, 4] is a valid [u32; 4].
        ptr::write(buf.as_mut_ptr(), [1, 2, 3, 4]);

        // Assume init. This tells the compiler the memory is now valid.
        // The compiler trusts us. If we're wrong, this is undefined behavior.
        buf.assume_init()
    }
}

fn main() {
    let data = init_buffer();
    println!("{data:?}");
}

The // SAFETY: comment lists the invariants. Each invariant is a condition that must hold for the code to be correct. If any invariant is false, the code has undefined behavior. The comment is a proof. If you can't write the proof, you don't have the code.

Treat the SAFETY comment as a proof. If you can't write it, you don't have one.

Pitfalls: alignment, overlap, and the double-free trap

Raw pointers introduce three common failure modes. Alignment, overlap, and double-free.

Alignment is the first. Every type has an alignment requirement. A u64 must be aligned to an 8-byte boundary. If you write a u64 to an address that isn't aligned, the program crashes on many CPUs. ptr::write assumes alignment. If you're not sure the address is aligned, use ptr::write_unaligned. It generates slower instructions that work on any address, but they're safe. If you try to dereference a raw pointer without unsafe, the compiler rejects you with E0133 (dereference of raw pointer requires unsafe). You can't accidentally use a raw pointer in safe code.

Overlap is the second. ptr::copy_nonoverlapping copies data from source to destination. It assumes the ranges don't overlap. If they do, the copy can corrupt data. Use ptr::copy when the ranges might overlap. It handles the overlap by moving data in the right direction. It's slightly slower, but it's correct. The compiler can optimize copy_nonoverlapping more aggressively because it knows the ranges are separate.

Double-free is the third. This happens when you use ptr::read on a value that implements Drop. You get a copy of the value. The original value is still in memory. If you drop the original later, the destructor runs twice. The first drop frees the memory. The second drop tries to free it again. The allocator panics or corrupts the heap. The fix is to use ptr::write to overwrite the original value after reading it, or to use std::mem::forget to suppress the drop. This is why ptr::read and ptr::write are used together to move data. You read the value, then you write a dummy value or decrement the length to mark the source as invalid.

Undefined behavior doesn't mean the program crashes. It means the program can do anything. It can crash. It can silently corrupt data. It can appear to work in debug mode and melt your CPU in release mode. Verify your invariants before you dereference.

Decision: when to use std::ptr

Use std::ptr for FFI when you call C or another language and have to cross out of Rust's safety. Use std::ptr for performance-critical inner loops where measured profiling shows safe abstractions are the bottleneck, and you can isolate the unsafe code in a small helper. Use std::ptr when you're implementing a safe abstraction yourself, such as a custom allocator, a ring buffer, or a data structure that needs to move data without dropping. Reach for safe references and slices when lifetimes are simple; the unsafe alternative is rarely worth it.

Reach for safe references when lifetimes are simple; the unsafe alternative is rarely worth it.

Where to go next