This article is meant to prove something to my friends about the borrow checker.
The problem: is it believed the borrow checker is unnessecary and is constantly being fought against.
Today, I will use the rust crate you_can
to demonstrate why this is not true.
An example of why the borrow checker is fought against:
1fn main() {
2 let a = vec!["hello,", "world!"];
3 let b = a;
4 println!("{:?}", a);
5}
This will cause a compiler error:
This is for a good number of reasons:
- What happens to
a
if we madeb
mutable and mutated it? - Should
a
be passed intob
by value or by reference? - Should
a
be cloned intob
, in order to have 2 instances instead?
Rust solves this by making everything explicit. If we did let b = &a;
we
explicitly told the compiler to keep a reference of a
. By the way, if T
is
printable, then &T
is, too. If we wanted to have the data in 2 places, without
any connection between them, we could clone it:
1let b = a.clone();
In the first case though, rust passes the ownership of a
into b
. This
makes a
invalid until redefined. This is the heart of the rust borrow checker.
The rust borrow checker defines simple ownership rules to decide how memory should be managed, without a garbage collector.
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
From the rust book, chapter 4.1
Some other rule is the borrowing rule: You can have either multiple immutable references, or a singular mutable reference at a time.
Thus, the following is not possible:
1let mut a = "some string";
2let b = &mut a;
3let c = &mut a;
This is for a good reason: when doing this concurrently, you will end up with
data races (also known as Undefined Behaviour or UB). For this reason Mutex
es
exist.
If you struggle with the ownership problem in rust, I highly recommend you thoroughly (re)reading the chapter.
What I wanted to show you today is, you don't have to fight against the borrow checker.
Just add to your Cargo.toml
:
1[dependencies]
2you-can = "0.0.14"
Next, change your code. This time, we produce 2 mutable references to 1 value, then use them after free to demonstrate the consequences.
1// main.rs
2#[you_can::turn_off_the_borrow_checker]
3fn main() {
4 let mut a = vec!["hello,", "world!"];
5 let b = &mut a[0];
6 let c = &mut a[1];
7 drop(a);
8 // UB
9 println!("{}{}", b, c);
10}
This will not produce any compiler errors, but the you_can
crate does warn
us (on nightly):
And yes, this produces UB. In this case, it just segfaults. But there's no
unsafe
or raw pointers, right? Well, this is why
#[you_should::leave_the_borrow_checker_on]
. Memory safety is lost when
suppressing the borrow checker. And no, in the end, when you have learned Rust's
memory model, you are not "fighting" the borrow checker. It's a helping hand,
telling you everything that is wrong with your code regarding memory safety.
This will enable the programmer to write fast, safe, reliable code without a garbage collector or manual allocations.