Unsafe Rust and Me

I’m inspired to have a go at #100DaysToOffload so I’m going to dive into a nice controversial issue: the merits of Rust and its use of unsafe code.

This topic frequently provokes arguments on lobste.rs. Some will make grand pronouncements like “C/C++ is horribly unsafe compared with Rust” or “Rust provides no guarantees at all because any of your dependencies could write unsafe at any time”. As is often the case, both sides have a point but reality lies somewhere in the middle. I’m going to invent a term to describe this: Rust provides a safety gradient.

// Declare contract

/// Safety: `func_ptr` must be a valid function pointer
unsafe fn use_fn_ptr(func_ptr: unsafe extern "C" fn(i32)) {
    // ...
}

// Then call site opts in to contract

let func_ptr = my_c_lib::get_fn();
// Safety: We trust the library to give us a valid `func_ptr`
unsafe {
    use_fn_ptr(func_ptr);
}

To recap quickly, unsafe in Rust plays dual roles: to declare a special contract, and to confirm that you’re following that contract. Used as a modifier on a function, it means the caller must uphold certain requirements that cannot be proven by Rust’s type system. In practice this means things like “This parameter is a raw function pointer. You must ensure this is trustworthy and we can jump to this location.” At the call site you must also use unsafe—this is saying “Yes, I am aware there is a special contract that Rust can’t check. I promise I’m doing the right thing.” Certain primitive operations like dereferencing a raw pointer are implicitly unsafe so you only see the caller side—again, this reflects the existence of a special contract.

Other programming languages seem to get along fine without unsafe. How do they do it? Well, either you can allow users to write this kind of code without extra syntax, like C, or you can move responsibility for all the unsafe parts into the core language or standard library. Java, for example, doesn’t offer pointers or pointer arithmetic at all. You only have references to objects so an entire class of problems disappears.

Well, hang on, why don’t we do that? What if the C library didn’t give us a raw function pointer, but instead an opaque number which could be checked before it was converted to a real function location?

fn use_fn_handle(func_handle: usize) {
    // ...
}

let func_handle = my_c_lib::get_fn();
use_fn_handle(func_handle);

Tada: No unsafe code in this call site! But now we need to convert our integer func_handle to a real location in memory. This likely requires some sort of map. Do we want to pay for that runtime cost? Maybe, maybe not. This is a perfectly acceptable solution but it makes a trade-off. The unsafety has been minimised and we pay for it with slower runtime performance, much like Java. This implementation is a different position on the safety gradient. What’s important is that Rust lets me choose that position depending on my level of confidence and the performance requirements.

I’ve been working with Rust full time for a couple of years. I have never written unsafe in pure Rust code. I can imagine scenarios where I might want to switch off array bounds checks or something for performance but it hasn’t been a bottleneck in any of my code yet. I use it quite a bit on C FFI boundaries like I showed above where it’s contained to that interface and doesn’t infect the rest of my Rust. As a result I have never caused memory corruption errors in Rust.

I have also never experienced memory corruption issues in any of the hundreds of Rust dependencies I’ve used. By comparison, I helped track down a memory corruption bug in an Objective-C library recently where the root cause was faulty pointer arithmetic. Safe Rust would never have permitted such an error, while offering more rigorous lifetime-based solutions which are at least as fast.

Basically I live in safe Rust and I reap its benefits the vast majority of the time. This has enabled me to write very fiddly multithreaded async code without once having a concurrency crash. It’s very rare that I need to use an debugger on a program I’ve written. The thought of having to achieve the same level of performance and correctness in C or C++ terrifies me. Yes, one of my dependencies could pull the rug out from under me with some ill-considered unsafe. The reality is that they don’t.

For all its innovations, Rust doesn’t have the final word here. I fully expect new languages to appear which provide more precise control over that safety gradient, or use clever design to enable more code to be written in “safe” mode but still remain fast. In the meantime I am very much on board.