Aaaah, the borrow checker: the dreaded enemy lurking within the Rust compiler, ready to make its move to bring pain to your life by preventing your code from compiling. Or that’s what everyone seems to say, which is one of the reasons I put off learning Rust for so long. In reality… the borrow checker is a blessing, but it is true that getting past its gates is difficult at first.
What is the borrow checker anyway? The borrow checker is the component in the Rust compiler that enforces data ownership rules, and it enforces these to prevent data races. And what is a data race? According to a random definition:
There is a “data race” when two or more pointers access the same memory location at the same time, where at least one of them is writing, and the operations are not synchronized.
So how does the borrow checker prevent data races? Not with magic!
The borrow checker’s job is to enforce a set of very simple rules. To demystify what these are, let me quote directly from the “What is Ownership?” section of The Rust Programming Language book, second edition:
- Each value in Rust has a variable that is called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
You can think of the borrow checker as a validator for a locking system: immutable references are shared read locks and mutable references are exclusive write locks. Under this mental model, accessing data via two independent write locks is not a safe thing to do, and modifying data via a write lock while there are readers alive is not safe either.
That doesn’t mean that appeasing the borrow checker is easy, at least at the beginning. One must be well-aware of where data lives and who owns which part(s) of such data, but even if you know these, it’s tricky to write the right code constructs. There are cases in which the borrow checker may make the code harder to write because satisfying its rules may require introducing seemingly-artificial scopes. Fighting the Borrow Checker is a pretty good post that covers many of these cases.
Rest assured, however, that the problems the borrow checker catches are always a symptom of potential bugs in the code. Once you have experienced the benefits it brings, you’ll grow increasingly paranoid of potential bugs in code written in other languages. (Happened to me: I cannot longer think of sandboxfs’s Go implementation as robust.) And what’s worse: no matter how paranoid you may have been in writing a piece of non-Rust code, it only takes a single one-line change by a less-paranoid person to “ruin everything”.
Everything I said above applies to single-threaded code where you pass references across objects and functions. But the surprising thing is that everything in here also applies to multi-threaded code: in fact, the borrow checker is the critical thing to ensure multi-threaded code is safe. We’ll explore this area next.