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 = sfirst drops the old value, which here is garbage.ptr::writestores without dropping. ptr::readcopies bits; it does not move. Afterlet b = ptr::read(&a),aandbboth 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
capslots instead of the firstlen: it drops uninitialised memory. - Using
ptr::readinpeek: it duplicates ownership, so the same text is freed twice. - Forgetting to shrink
lenafter 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.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…