Rethinking Rust’s unsafe keyword

Sep 4, 2023

In this blog, I will go over the current problems with the unsafe keyword in Rust and propose a new backwards compatible pair of keywords called itsfine and safe to fix the problems.

Adding this might confuse new users with new keywords and existing keywords that do the same thing but slightly differently and bloat the language a bit. So I won’t discuss if this is a necessary change that should be done, but new languages can definitely consider this instead of unsafe.

DISCLAIMER: I have changed my mind about some things that were written here after discussion on Reddit, please read the last section of this blog. I will keep this post up as a useful resource about the unsafe keyword.

How unsafe works currently

In Rust, we use the unsafe keyword in two ways, first way is inside a function body as a block.

fn foobar() {
    unsafe {
        // unsafe code
    }

    // safe code
    // safe code

    unsafe {
        // unsafe code
    }
}

This tells the compiler that the code inside the unsafe {} block breaks some rules, but this is something that can only be checked by a human so there is no need to worry and the author has made sure this does not actually break anything. So the unsafe {} block does not mean the code is definitely unsafe, it means the code could be potentially unsafe, because the safety is checked by a human instead of the compiler.

Note that it is not unsafe to call a function containing unsafe {} blocks

fn baz() {
    foobar(); // no need for unsafe {} block
}

So the author has to make sure the function containing unsafe {} blocks is okay to be called on all scenarios at the call site.

The second way is as a prefix in the function signature

unsafe fn foobar() {
    // unsafe code
    // unsafe code
}

This means that the function contains some unsafe code that breaks some rules, but it is safe if you uphold some contract documented by the author, and it is the caller’s responsibility to uphold the contract. So you need an unsafe {} block like below when calling this function since it is on the person calling the function to uphold the contract.

unsafe { foobar(); }

Problems with unsafe

The are some problems with the way the unsafe works right now

The itsfine keyword

So I propose a new keyword to solve these problems, the itsfine keyword. This keyword would work little differently from the current unsafe keyword.

The first use case is the same, instead of unsafe, you will use itsfine. It also reads nicely, “hey compiler, it’s fine, I know what I am doing, you can’t check this”.

fn foobar() {
    itsfine {
        // unsafe code
    }
}

Now let’s talk about the difference, a function containing an unsafe block can be called without unsafe {} at the call site.

fn foobar() {
    unsafe {
        // unsafe code
    }
}

fn baz() {
    foobar(); // no need for unsafe {} block
}

This won’t be the case for itsfine.

fn foobar() {
    itsfine {
        // unsafe code
    }
}

fn baz() {
    itsfine { foobar(); }   // needs itsfine {} block
}

A function with itsfine {} blocks is assumed to be unsafe unless some contract is upheld by default, unlike a function containing unsafe {} blocks which is assumed safe to call implicitly.

There are two problems with this propagation behavior

To solve these problems, we will introduce another keyword, the safe keyword. This keyword basically stops the propagation of the itsfine {} block

safe fn foobar() {
    itsfine {
        // unsafe
    }
}

fn baz() {
    foobar(); // no need for itsfine {} block
}

We can actually think of safe to be implicitly present for all functions, even for completely safe function with no itsfine {} blocks. Functions with itsfine {} blocks have an implicit itsfine in their function signature, so they need itsfine {} block at the call site. The author can choose to be explicit and add an itsfine or explicitly remove it using safe.

itsfine fn foobar() {       // itsfine here is optional
    itsfine {
        // unsafe code
    }
}

fn baz() {
    itsfine { foobar(); }   // needs itsfine {} block
}

With this, the documentation can leave out the safe in function signatures, but then show itsfine if the function signature has one.

The safe can also be used in the body of the function,

fn foobar() {
    safe {
        itsfine {
            // unsafe code
        }
    }
}

fn baz() {
    foobar(); // no need for itsfine {} block
}

Note that the safe {} block only blocks the propagation of the itsfine {} block, it does not allow you to write unsafe code.

This will be very useful for macros that want to generate itsfine {} blocks but without making the caller of the macro to have to insert safe as part of their function signature to make their function safe to call.

Backwards compatibility

The unsafe keyword and itsfine + safe keywords can co-exist. The table below tells how

Call site Callee has itsfine Callee has safe Callee has unsafe
unsafe {} Allowed Allowed Allowed
itsfine {} Allowed Allowed Allowed
no blocks or safe {} Not allowed Allowed Not allowed

Functions that have unsafe in its signature also has a itsfine in its signature. A function that does not have unsafe in its signature (but may or may not contain unsafe {} blocks) has a safe in its signature. The documentation can also show these.

Migration scripts or commands can also be provided to change unsafe keywords into itsfine and safe keywords.

Every function that does not have an unsafe in its signature but has unsafe {} blocks can be turned into safe functions with itsfine {} blocks instead of unsafe {} blocks.

fn baz() {
    unsafe {
        // unsafe code
    }
}

Will turn into

safe fn baz() {
    itsfine {
        // unsafe code
    }
}

Every function that has unsafe in its signature can be turned into an itsfine function with a itsfine {} block in its body.

unsafe fn baz() {
    // unsafe code
}

Will turn into

fn baz() {      // implicit itsfine
    itsfine {
        // unsafe code
    }
}

Alternate names

I initially started with itsfine and !itsfine (note the !) instead of itsfine and safe at the beginning, taking inspiration from auto traits. The problem with !itsfine is the function signature

!itsfine fn foobar() {
    itsfine {
        // unsafe
    }
}

What !itsfine means here is that this function does not require an itsfine {} block at the call site, but it can also be interpreted as “foobar is not fine”, which will get really confusing for new users. The latter is also what I got from reviews.

Another alternative is to use unsafe and safe, this is of course not backwards compatible with the current way unsafe works. Also the keyword itsfine seems to carry more truthy meaning to when you would actually use unsafe code, writing code beyond the reasoning of the compiler, not for actually writing unsafe code.

Criticism and Disclaimer

This section was added later post discussions on Reddit. While I still do think the name itsfine is much nicer than unsafe, I am not sure if the propagating behavior and the safe keyword is right.

The unsafe keyword is always used in the context of encapsulating unsafe code in safe APIs, so the safe keyword would just be spammed everywhere. This also make sense, there is no point in using a safe language if you don’t encapsulate unsafe code in safe APIs and take advantage of the language. Because encapsulating unsafe code is the most common use case, authors already think about contracts.

Functions being declared unsafe is not very common and most functions declared unsafe don’t even have unsafe blocks in them or are raw C bindings 2.

I think what I was really looking for is a lint error to add a // SAFETY: comment for every occurrence of the unsafe keyword.