Skip to Content
Course content

56: Unsafe Rust: When and Why

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was tasked with integrating a legacy C library for high-speed image processing into a new Rust-based tool. The library returned a raw pointer to a buffer of pixels, and I spent an entire afternoon trying to wrap that pointer in a way that would make the borrow checker happy. I kept trying to "trick" the compiler into treating the raw pointer as a safe slice, but every time I did, I hit a wall of lifetime errors that felt impossible to solve. I remember thinking, "Is Rust actually preventing me from doing my job?"

The epiphany came when I stopped treating unsafe as a "failure" to write idiomatic Rust and started treating it as a tool for boundary management. Rust isn't trying to stop you from performing low-level operations; it's just demanding that you draw a clear line around the code that the compiler can't possibly verify. Once I wrapped the raw pointer access in a small, carefully audited unsafe block and exposed a safe API to the rest of my app, the friction vanished.

The Superpowers of the Unsafe Block

When you wrap code in an unsafe { ... } block, you aren't turning off the borrow checker. That's a common misconception. The borrow checker is still running, and the usual rules of ownership still apply. What you're actually doing is granting yourself a set of "superpowers" that the compiler normally forbids because it cannot mathematically prove they are safe.

Specifically, unsafe allows you to:

  • Dereference raw pointers (*const T and *mut T).
  • Call other unsafe functions (including FFI calls to C or C++).
  • Implement unsafe traits.
  • Access or modify mutable static variables.

In my image processing case, dereferencing that raw pointer from the C library was the "unsafe" act. The compiler had no way of knowing if the C library had already freed that memory or if the pointer was null. By using unsafe, I was telling the compiler: "I have read the C documentation, I know this pointer is valid for the next ten milliseconds, and I take full responsibility if this crashes."

Maintaining the Safety Invariant

The goal of a professional Rust engineer isn't to avoid unsafe entirely—that's impossible if you're building low-level primitives or interfacing with hardware. The goal is to encapsulate it. You want to build a "safe abstraction." This means you write a small amount of unsafe code, but you wrap it in a safe function that ensures the caller can't possibly cause a segmentation fault.

// A simplified example of an unsafe abstraction
struct MyBuffer {
    ptr: *mut u8,
    len: usize,
}

impl MyBuffer {
    // The unsafe part is hidden inside this safe method
    pub fn get(&self, index: usize) -> Option<&u8> {
        if index >= self.len {
            return None;
        }
        // SAFETY: We just checked that index is within bounds, 
        // so dereferencing the pointer is safe here.
        unsafe {
            Some(&*self.ptr.add(index))
        }
    }
}


Notice the comment starting with SAFETY:. This is a convention I highly recommend. Whenever you use unsafe, write a brief explanation of why it's actually safe. If you can't articulate why the code won't crash, you shouldn't be using the unsafe keyword. You're essentially signing a contract with the compiler; the SAFETY comment is your evidence that you've done the due diligence to honor that contract.




📋 Practical Task

Build a Fast-Path Array Accessor

In this exercise, you will implement a wrapper around a raw pointer to simulate a "fast-path" memory accessor. Your goal is to create a struct that manages a pointer to a heap-allocated array and provides a method to retrieve an element without the standard bounds checking that Vec or slices perform, while still maintaining a safe public API.

Requirements:

  • Create a struct called FastArray that holds a *const i32 and a size: usize.
  • Implement a new method that takes a Vec<i32>, leaks the vector's memory (using Box::leak or Vec::into_raw_parts), and stores the pointer.
  • Implement a method at(&self, index: usize) -> Option<i32>.
    • Inside this method, first perform a manual bounds check.
    • If the index is valid, use an unsafe block to offset the pointer and dereference it to return the value.
    • Include a // SAFETY: comment explaining why the dereference is valid.
  • In your main function, instantiate the FastArray and verify it can retrieve elements at the start and end of the array, as well as handle out-of-bounds requests gracefully.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.