Skip to content
Lesson 4 of 7

Step 1 of 4 · Reading · ~4 min

Learn

Macros and Unsafe

Unsafe Rust: Raw Pointers and Safe Abstractions

You will be able to use raw pointers, own a block of raw memory, and hide both behind a safe API. Safe Rust enforces ownership and borrowing for you; unsafe hands five specific checks back to you and changes nothing else.

What unsafe unlocks

Inside an unsafe block you may: dereference a raw pointer, call an unsafe fn (including extern "C" functions), read or write a static mut, implement an unsafe trait, and read a union field. Borrow and type checking still run. Outside the block, the first of these is a compile error:

error[E0133]: dereference of raw pointer is unsafe and requires unsafe function or block

A raw pointer (*const T, *mut T) is an address with no lifetime. Creating one is safe; following one is not:

let xs = [10, 20, 30];
let p: *const i32 = xs.as_ptr();
let (first, last) = unsafe { (*p, *p.add(2)) };
println!("{} {}", first, last);          // 10 30
println!("{}", std::ptr::null::<i32>().is_null());   // true

p.add(n) moves by n elements, not bytes. Moving one past the end is allowed; dereferencing it, or moving further, is undefined behaviour (UB): the compiler assumes it never happens, so anything can result. UB includes dangling or misaligned pointers, two live &mut to the same place, reading uninitialised memory, and an invalid bool.

A safe wrapper: split_at_mut by hand

The safe version of a function that returns two &mut into one slice does not compile:

fn split_pair<T>(s: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    (&mut s[..mid], &mut s[mid..])   // error[E0499]: cannot borrow `*s` as mutable more than once
}

The compiler cannot see the halves are disjoint. You can, so you build both slices from the raw pointer:

use std::slice;

fn split_pair<T>(s: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = s.len();
    assert!(mid <= len);              // without this check the unsafe below is UB
    let p = s.as_mut_ptr();
    // SAFETY: both halves lie inside `s` and do not overlap.
    unsafe { (slice::from_raw_parts_mut(p, mid), slice::from_raw_parts_mut(p.add(mid), len - mid)) }
}

With mid = 2 on [1, 2, 3, 4, 5], setting l[0] = 9 and r[0] = 8 gives [9, 2, 8, 4, 5]. The unsafe is one expression; callers use a plain safe function. That ratio is the goal.

Owning raw memory

alloc returns uninitialised bytes described by a Layout; dealloc needs the same layout back. Use ptr::write to fill a slot and ptr::read to take a value out:

use std::alloc::{alloc, dealloc, Layout};
use std::{mem, ptr};

let layout = Layout::from_size_align(4 * mem::size_of::<u32>(), mem::align_of::<u32>()).unwrap();
unsafe {
    let p = alloc(layout) as *mut u32;
    for i in 0..4 { ptr::write(p.add(i), (i as u32 + 1) * 100); }
    let sum: u32 = (0..4).map(|i| ptr::read(p.add(i))).sum();
    println!("{}", sum);                 // 1000
    dealloc(p as *mut u8, layout);
}

For u32 that is the whole story. For a type that owns memory, such as String, two rules matter:

  • Never assign into an uninitialised slot. *p = s first drops the old value, which here is garbage. ptr::write stores without dropping.
  • ptr::read copies bits; it does not move. After let b = ptr::read(&a), a and b both own one heap block and both will free it: a double free, usually an abort. Make exactly one of them stop owning (mem::forget(a)), or make sure the source is never used or dropped again.

A buffer like this needs an invariant you keep by hand, for example "slots 0..len are initialised, the rest are not". Drop is where it pays off: it must destroy exactly the initialised slots (ptr::drop_in_place), then free the buffer. Keep the fields private so outside code cannot break the invariant.

Common mistakes

  • Dropping all cap slots instead of the first len: it drops uninitialised memory.
  • Using ptr::read in peek: it duplicates ownership, so the same text is freed twice.
  • Forgetting to shrink len after taking a value out: the slot is later dropped a second time.
  • Pushing without checking len < cap: the write lands past the allocation and corrupts the heap.

Your exercise

Finish a fixed-capacity stack of items stored in raw memory: push, pop, peek and Drop. The starter allocates the buffer and runs the commands. Watch the live line for double drops.

Up nextLifetimes In DepthLifetimes, Trait Objects, and Async

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…