Skip to content
Lesson 4 of 7

Step 1 of 4 · Reading · ~2 min

Learn

Macros and Unsafe

unsafe lets you do things the compiler cannot verify. It shows up in:

  • calling C libraries (FFI)
  • implementing data structures (linked lists, raw pointers)
  • hardware and OS interfaces
  • performance-critical paths where you have checked the invariant by hand

The 5 unsafe superpowers:

  1. Dereference a raw pointer (*ptr)
  2. Call an unsafe function
  3. Access or modify a mutable static
  4. Implement an unsafe trait
  5. Access fields of a union

That list is the whole of it. unsafe is a permission, not an escape hatch: inside the block, ownership, borrowing and the type system are enforced exactly as they are outside it. What changes is that five operations whose soundness the compiler cannot prove become legal - and proving them becomes your job.

Raw pointers:

let x = 5;
let r1: *const i32 = &x;      // safe: this is just an address

unsafe {
    println!("{}", *r1);        // unsafe: following it is the risky part
}

Note where the boundary falls. Building a raw pointer reads and writes nothing, so it needs no unsafe even if the pointer is null or dangling. Dereferencing is the moment the compiler can no longer prove the pointee is live, initialised and correctly aligned, so that is the operation that is gated.

FFI to C:

extern "C" {
    fn abs(x: i32) -> i32;        // declared from libc
}

fn main() {
    let result = unsafe { abs(-5) };
    println!("{}", result);       // prints: 5
}

The compiler has no idea what abs does; the unsafe is you promising the declaration matches the real symbol.

Calling unsafe Rust functions:

unsafe fn dangerous() {
    println!("trust me");
}

fn main() {
    unsafe { dangerous(); }
}

Block discipline:

  • Wrap as small a region as possible - a large unsafe block hides which line is load-bearing.
  • Write a // SAFETY: comment saying which invariant makes it sound.
  • Provide a safe wrapper API; callers should not need unsafe of their own.
// SAFETY: `buf` was just allocated with `len` bytes, so `ptr` is valid for the write.
unsafe { ptr::write_bytes(ptr, 0, len); }

std::mem::transmute reinterprets one type's bits as another and is the sharpest tool here - it can invent invalid values of any type. Prefer to_ne_bytes, from_bits or a union when one exists.

Avoid unsafe when you can. Sound safe wrappers already exist for most of what people reach for it: slice::from_raw_parts has a checked cousin, Cell::as_ptr hands you a pointer without one. Reach for raw unsafe only when there is genuinely no safe API.

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…