Rust

The poster child for memory safety

Rust's memory safety relies on two main principles: ownership and borrowing.

Every value in Rust has an owner, meaning that at any given point in the code there is one thing that can have read/write control over the value. Ownership can be borrowed temporarily, but this is tracked by Rust's compiler and the violation of any ownership rules does not compile.

This is a different approach to other programming languages. As we have seen so far, C has no such concept of memory safety. Higher-level managed languages, such as Python, Java or C#, handle memory-safety in the runtime. This compromises speed, as well as the requirement of a runtime.

This articlearrow-up-right has a good explanation, as does this Stack Overflowarrow-up-right post. The official documentation for this topicarrow-up-right is also fantastic.

Ownership

Consider the following code:

fn main() {
    let original = "Hello, World!".to_string();
    let other = original;
    println!("{}", original);
}

While this is valid in many other languages, Rust will throw an error here because the value is moved.

error[E0382]: borrow of moved value: `original`
 --> src/main.rs:4:20
  |
2 |     let original = "Hello, World!".to_string();
  |         -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait
3 |     let other = original;
  |                 -------- value moved here
4 |     println!("{}", original);
  |                    ^^^^^^^^ value borrowed here after move

In the second line, as soon as we define other, it becomes invalid to use original again. This protects against double-frees, use-after-frees and any other sorts of vulnerabilities that can arise due to dangling pointers.

circle-info

Note that the error talks about how the type String does not implement the Copy traitarrow-up-right. If a type does implement this, then an assignment like the second line here would duplicate the variable, copying all of the bits over. This wouldn't cause an issue, as there are two distinct variables owning their respective values. What Rust does not want is two variables refering to the same data in memory, which would occur for String types as they are just pointers to the actual string in memory.

Borrowing

Instead of copying data or transferring ownership, Rust allows you to borrow data. This borrowing can be done immutably or mutably. There are restrictions on borrowing which prevent data races, iterator invalidation and bugs caused by concurrency.

Immutable Borrowing

Immutable borrowing allows for read-only borrows. There can be an any number of these borrows at any one time:

This compiles correctly, and acts as you would expect.

Mutable Borrowing

Mutable borrowing is a read/write borrow.

However, to prevent bugs such as data races, we cannot have two mutable borrows:

Similarly, we actually can't take an immutable reference either!

So we can have either

  • one mutable borrow

  • any number of immutable borrows

But it is not allowed for us to mix and match - the compiler enforces this.

Notably, even if the borrow is immutable we are unable to modify the original mutable variable. Take the following example:

We get the error

Coming from a language like C, we can be forgiven for thinking that we can get a pointer to a variable and, once the variable is modified, read the new value with this pointer. But Rust doesn't allow that!

Lifetimes

A lifetime is what the compiler's borrow checker uses to ensure borrows are valid. A lifetime begins when a variable is created, and ends when it is destroyed. The Rust bookarrow-up-right has a fantastic description of lifetimes, as well as how you can explicitly control them. The idea is that a variable is tied to the existence of other variables, and dies once they die.

Think about this example:

In something like C, x would go out of scope and die once the } ends the scope, while r lives on as a dangling reference to where x would have been. In Rust, the compiler catches this and refuses to compile.

Meanwhile, if we move the println! into the scope defined by the {}, the program compiles fine as x lives long enough for the borrowed value to be used.

No Null Pointers

Rust does not have null pointers - cases must be handled explicitly. This prevents null pointer dereferences and maybe-null bugs.

Out of Bounds

Rust performs out-of-bounds checking at compilation time, but if this is impossible (e.g. the index is provided by the user), it automatically includes runtime OOB checks. Take the following code for example:

If we run the program and input 5, which is OOB, the program panics:

Last updated

Was this helpful?