In my previous post, I spent some time talking about the idea of
unsafe abstractions. At the end of the post, I mentioned that Rust
does not really have any kind of official guidelines for what kind of
code is legal in an unsafe block and what is not.What this means in
practice is that people wind up writing what seems reasonable
and
checking it against what the compiler does today. This is of course a
risky proposition since it means that if we start doing more
optimization in the compiler, we may well wind up breaking unsafe code
(the code would still compile; it would just not execute like it used
to).
Now, of course, merely having published guidelines doesn’t entirely
change that dynamic. It does allow us to assign blame
to the unsafe
code that took actions it wasn’t supposed to take. But at the end of
the day we’re still causing crashes, so that’s bad.
This is partly why I have advocated that I want us to try and arrive
at guidelines which are human friendly
. Even if we have published
guidelines, I don’t expect most people to read them in practice. And
fewer still will read past the introduction. So we had better be sure
that reasonable code
works by default.
Interestingly, there is something of a tension here: the more unsafe
code we allow, the less the compiler can optimize. This is because it
would have to be conservative about possible aliasing and (for
example) avoid reordering statements. We’ll see some examples of this
as we go.
Still, to some extent, I think it’s possible for us to have our cake
and eat it too. In this blog post, I outline a proposal to leverage
unsafe abstaction boundaries to inform the compiler where it can be
aggressive and where it must be conservative. The heart of the
proposal is the intution that:
- when you enter the unsafe boundary, you can rely that the Rust type
system invariants hold;
- when you exit the unsafe boundary, you must ensure that the Rust
type system invariants are restored;
- in the interim, you can break a lot of rules (though not all the
rules).
I call this the Tootsie Pop model: the idea is that an unsafe
abstraction is kind of like a Tootsie Pop. There is a gooey candy
interior, where the rules are squishy and the compiler must be
conservative when optimizing. This is separated from the outside world
by a hard candy exterior, which is the interface, and where the rules
get stricter. Outside of the pop itself lies the safe code, where the
compiler ensures that all rules are met, and where we can optimize
aggressively.
One can also compare the approach to what would happen when writing a
C plugin for a Ruby interpreter. In that case, your plugin can assume
that the inputs are all valid Ruby objects, and it must produce valid
Ruby objects as its output, but internally it can cut corners and use
C pointers and other such things.
In this post, I will elaborate a bit more on the model, and in
particular cover some example problem cases and talk about the grey
areas that still need to be hammered out.