How Rust achieves memory safety is, at its core, actually quite simple. It hinges mainly on two principles: ownership and borrowing.
Ownership
The compiler uses an affine type system to track the ownership of each value: a value can only be used at most once, after which the compiler refuses to use it again.
fn main() {
let original = "Hello, World!".to_string();
let other = original;
println!("{}", original);
}
yields an error:
error[E0382]: use of moved value: `original`
--> src/main.rs:4:20
|
3 | let other = original;
| ----- value moved here
4 | println!("{}", original);
| ^^^^^^^^ value used here after move
|
= note: move occurs because `original` has type `std::string::String`, which does not implement the `Copy` trait
This, notably, prevents the dreaded double-free regularly encountered in C or C++ (prior to smart pointers).
Borrowing
The illumination that comes from Rust is that memory issues occur when one mixes aliasing and mutability: that is, when a single piece of memory is accessible through multiple paths and it is mutated (or moved away) leaving behind dangling pointers.
The core tenet of borrow checking is therefore: Mutability XOR Aliasing. It's similar to a Read-Write Lock, in principle.
This means that the Rust compiler tracks aliasing information, for which it uses the lifetime annotations (those 'a
in &'a var
) to connect the lifetime of references and the value they refer to together.
A value is borrowed if someone has a reference to it or INTO it (for example, a reference to a field of a struct
or to an element of a collection). A borrowed value cannot be moved.
Mutability (without aliasing)
You can obtain only a single mutable reference (&mut T
) into a given value at any time, and no immutable reference into this value may exist at the same time; it guarantees that you have exclusive access to this tidbit of memory and thus you can safely mutate it.
Aliasing (without mutability)
You can obtain multiple immutable references (&T
) into a given value at any time. However you cannot mutate anything through those references (*).
(*) I am lying; there are structs like RefCell
which implement "interior mutability"; they do respect the Mutability XOR Aliasing principle, but defer the check to run-time instead.
That's it?
Nearly so ;)
It is already quite complicated to implement for the compiler-writers, and may unduly constrain the users (some programs that would be safe cannot be proven safe using this system, requiring to jump through hoops), however the core principles are really that simple.
So what's left?
Bounds-checking. It is not rocket-science, but may induce a performance penalty though. Most languages have some degree of support for it, C being the big exception, and C++ having some support for it, although it is optional.