Skip to Content
Course content

198: Calling Unsafe Functions

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

By now, you've seen how Rust's borrow checker keeps you from shooting yourself in the foot. But here's the reality: sometimes you have to shoot yourself in the foot. Or, more accurately, you need to tell the compiler, "Trust me, I know what I'm doing," because you're doing something the compiler can't possibly verify—like talking to a C library or manipulating raw memory addresses.

Dealing with a raw pointer

Let's imagine we're writing a driver for a piece of hardware that gives us a raw pointer to a memory-mapped buffer. In a real scenario, this pointer would come from an external C library, but for this example, I'll just create a local array and grab a raw pointer to it. We want to convert this raw pointer into a Rust slice so we can actually use it with the tools we love, like iterators.


fn main() {
    let data = [10, 20, 30, 40, 50];
    let ptr = data.as_ptr(); // This is a *const i32
    let len = data.len();

    // I want to turn this pointer back into a slice...
    let slice = std::slice::from_raw_parts(ptr, len);
    println!("Buffer content: {:?}", slice);
}

If you try to compile this, Rust is going to stop you immediately. You'll see an error saying that std::slice::from_raw_parts is an unsafe function. Rust isn't saying the function is "broken"; it's saying that the compiler cannot possibly guarantee that ptr is valid or that len is correct. If I passed a random memory address here, the program would crash or, worse, leak sensitive data.

Entering the unsafe block

To call an unsafe function, we have to wrap it in an unsafe block. This is essentially a marker that says, "I've checked the invariants manually, and I'll take the blame if this segfaults."


fn main() {
    let data = [10, 20, 30, 40, 50];
    let ptr = data.as_ptr();
    let len = data.len();

    let slice = unsafe {
        std::slice::from_raw_parts(ptr, len)
    };
    
    println!("Buffer content: {:?}", slice);
}

Now it compiles. But notice that only the function call is inside the block. I generally recommend keeping unsafe blocks as small as humanly possible. The smaller the block, the easier it is to audit when something goes wrong.

Where I messed up the bounds

Here is where things get dangerous. When I first wrote a similar utility for a project a few years ago, I made a classic mistake: I assumed the length of the buffer was a constant based on the hardware spec, rather than verifying the actual allocated size.

Let's simulate that mistake here. Watch what happens when I lie to the compiler about how much memory is actually there:


fn main() {
    let data = [10, 20, 30, 40, 50];
    let ptr = data.as_ptr();
    
    // I'm claiming there are 100 elements here, but there are only 5.
    let len = 100; 

    let slice = unsafe {
        std::slice::from_raw_parts(ptr, len)
    };
    
    // This might print garbage, or it might crash. 
    // Either way, it's Undefined Behavior (UB).
    println!("Buffer content: {:?}", slice);
}

This is why unsafe is scary. The compiler didn't warn me. It trusted me. I've now created a slice that reads past the end of my array and into memory I don't own. To fix this, I have to ensure that the len variable is strictly tied to the actual allocation size. In a real FFI (Foreign Function Interface) scenario, this means you must be absolutely certain that the C code providing the pointer also provided the correct length.

Wrapping unsafe in a safe abstraction

You should almost never ask your users to write unsafe blocks. Instead, the "Rust way" is to wrap the unsafety inside a safe function. You provide a safe interface, and you handle the dangerous parts internally, ensuring that no matter what the user passes in, the unsafe block remains sound.


fn get_buffer_slice(ptr: *const i32, len: usize) -> &'static [i32] {
    // We assume the caller has provided a valid pointer and length.
    // In a real app, we'd add as many checks as possible here.
    unsafe {
        std::slice::from_raw_parts(ptr, len)
    }
}

fn main() {
    let data = [10, 20, 30, 40, 50];
    let ptr = data.as_ptr();
    let len = data.len();

    // The caller doesn't need to know about 'unsafe' anymore.
    let slice = get_buffer_slice(ptr, len);
    println!("Safe access: {:?}", slice);
}

By doing this, you've isolated the danger. If the program crashes during a memory read, you know exactly which function to look at.




📋 Practical Task

Exercise: Summing a Raw Memory Buffer

Your task is to create a safe wrapper function that interacts with a raw pointer to calculate the sum of an integer array.

  • Create a function named sum_raw_buffer that takes two arguments: a raw pointer ptr: *const i32 and a length len: usize.
  • Inside this function, use an unsafe block to convert the raw pointer and length into a Rust slice using std::slice::from_raw_parts.
  • Calculate the sum of the elements in that slice and return it as an i32.
  • In your main function, create a standard array, get its raw pointer and length, and call your sum_raw_buffer function to print the result.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.