Rust in a nutshell : Ownership

How does Rust manage memory ?

Stack & Heap

Stack: It’s a special region of your computer’s memory that stores temporary variables created by each function. When a function exits, all of its variables are popped off of the stack (and hence lost forever). Thus stack variables are local in nature (variable scope, or local).

Heap : Unlike the stack, variables created on the heap are accessible by any function, anywhere in your program. Heap variables are essentially global in scope.

  • Primitive values such as numbers, characters, and true/false values are stored on the stack (stack is LIFO).
  • While the value of more complex objects or types that could grow in size are stored in the heap memory.
let myString = "hello";

myString is stored in stack because we know size of the string literal at compile time.

However sometimes we don’t know the size at compile time and it grows while running the program.

At this level the Heap memory comes to the game.

let s = String::from("frayed knot");

String is allocated on the heap and as such is able to store an amount of text that is unknown to us at compile time.

Data with an unknown size at compile time or a size that might change must be stored on the heap.

When you put data on the heap, you request a certain amount of space :

  • the operating system finds an empty spot in the heap that is big enough.
  • marks it as being in use.
  • and returns a pointer, which is the address of that location.
https://www.oreilly.com/library/view/programming-rust/9781491927274/ch04.html
  • Pushing to the stack is faster than allocating on the heap because the operating system never has to search for a place to store new data; that location is always at the top of the stack.
  • Accessing data in the heap is slower than accessing data on the stack because you have to follow a pointer to get there.

The advantage of using the stack to store variables, is that memory is managed for you. You don’t have to allocate memory by hand, or free it once you don’t need it any more. What’s more, because the CPU organizes stack memory so efficiently, reading from and writing to stack variables is very fast.

The heap is a region of your computer’s memory that is not managed automatically for you, and is not as tightly managed by the CPU. It is a more free-floating region of memory (and is larger).

In a C world, to allocate memory on the heap, you must use malloc() or calloc() (built-in C functions). Once you have allocated memory on the heap, you are responsible for using free() to deallocate that memory once you don’t need it any more. If you fail to do this, your program will have what is known as a memory leak.

How does Rust manage memory ?

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.
fn main() {
// s is not valid here, it’s not yet declared
{
let s = "hello"; // s is valid from this point forward

// do stuff with s
}
// this scope is now over, and s is no longer valid
}
  • When s comes into scope, it is valid.
  • It remains valid until it goes out of scope.

What about Heap allocation ?

{
let s = String::from("hello"); // s is valid from this point forward

// do stuff with s

}
// this scope is now over, and s is no
// longer valid

The same thing happen : when a variable goes out of scope, Rust calls a special function for us. This function is called drop. Rust calls drop automatically at the closing curly bracket.

a. Copy :

let x = 5;
let y = x;

What is happening ?

  • bind the value 5 to x;
  • then make a copy of the value in x and bind it to y.
  • we have two variables, x and y, and both equal 5.
  • ownership isn’t affected (we make a copy).

Because integers are simple values with a known, fixed size, and these two 5 values are pushed onto the stack.

copy is to duplicate a value by only copying bits stored on the stack.

What types are copy ?

  • All the integer types, such as u32.
  • The Boolean type, bool, with values true and false.
  • All the floating point types, such as f64.
  • The character type, char.
  • Tuples, if they only contain types that are also Copy. For example, (i32, i32) is Copy, but (i32, String) is not.
  • And also string literal.

b. Ownership move :

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

What is happening ?

$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move


error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

Instead of trying to copy the allocated memory, Rust considers s1 to no longer be valid and, therefore, Rust doesn’t need to free anything when s1 goes out of scope.

Ownership is moved from s1 to s2.

There can only be one owner at a time.

Representation in memory after s1 has been invalidated

We would say that s1 was moved into s2 (shallow copy).

Rust will never automatically create “deep” copies of your data. If you do want to deeply copy the heap data of the String, not just the stack data, you can use clone.

c. Clone (deep copy)

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

This works just fine but the operation s2 = s1 could be very expensive in terms of runtime performance if the data on the heap were large.

Ownership isn’t moved or affected.

Heap Clone (deep copy)

Function scope :

fn print_padovan() {
let mut padovan = vec![1,1,1]; // allocated here
for i in 3..10 {
let next = padovan[i-3] + padovan[i-2];
padovan.push(next);
}
println!("P(1..10) = {:?}", padovan);
} // dropped here

Give ownership :

fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1


} // s1 goes out of scope and is dropped.

fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it

let some_string = String::from("hello"); // some_string comes into scope

some_string // some_string is returned and
// moves out to the calling
// function

}

Take and give back ownership :

fn main() {
let s2 = String::from("hello"); // s2 comes into scope

let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3


} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was moved, so nothing happens.

// takes_and_gives_back will take a String and return one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into scope

a_string // a_string is returned and moves out to the calling function
}

Tuple :

fn main() {
let s1 = String::from("hello");

let (s2, len) = calculate_length(s1);

println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String

(s, length)
}

Borrowing without loosing ownership :

fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

The &s1 syntax lets us create a reference that refers to the value of s1 but does not own it. Because it does not own it, the value it points to will not be dropped when the reference goes out of scope.

Borrowing without loosing ownership

Likewise, the signature of the function uses & to indicate that the type of the parameter s is a reference.

fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because it does not have ownership of what it refers to, nothing happens.

What happens if we try to modify something we’re borrowing ?

fn main() {
let s = String::from("hello");

change(&s);
}

fn change(some_string: &String) {
some_string.push_str(", world");
}

$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut std::string::String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

Just as variables are immutable by default, so are references. We’re not allowed to modify something we have a reference to.

Mutable References :

fn main() {
let mut s = String::from("hello");

change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

Multiple mutable references :

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14

|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

We can have only one mutable reference to a particular piece of data in a particular scope.

The benefit of having this restriction is that Rust can prevent data races at compile time. A data race is similar to a race condition and happens when these three behaviors occur:

  • Two or more pointers access the same data at the same time (multiple readers).
  • At least one of the pointers is being used to write to the data (only one writers).
  • There’s no mechanism being used to synchronize access to the data.

Rust prevents this problem from happening because it won’t even compile code with data races !

Combining mutable and immutable references :

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM

println!("{}, {}, and {}", r1, r2, r3);
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);
| -- immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

We also cannot have a mutable reference while we have an immutable one. Users of an immutable reference don’t expect the values to suddenly change out from under them !

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

This code is fine : the scopes of the immutable references r1 and r2 end after the println! where they are last used, which is before the mutable reference r3 is created. These scopes don’t overlap, so this code is allowed.

let mut s = String::from("hello");

{
let r1 = &mut s;} // r1 goes out of scope here, so we can make a new reference with no problems.

let r2 = &mut s;

Also this code will works fine : brackets create a new scope, allowing for multiple mutable references, just not simultaneous ones.

Dangling References :

fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String

let s = String::from("hello"); // s is a new String

&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger!
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ help: consider giving it a 'static lifetime: `&'static`
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

error: aborting due to previous error

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

Because s is created inside dangle, when the code of dangle is finished, s will be deallocated. But we tried to return a reference to it. That means this reference would be pointing to an invalid String.

The solution here is to return the String directly :

fn no_dangle() -> String {
let s = String::from("hello");

s
}

This works without any problems. Ownership is moved out, and nothing is deallocated.

In Rust world :

  • At any given time, you can have either one mutable reference or any number of immutable references.
  • References must always be valid.

Conclusion

In this story, I discovered how Rust manages memory : copy, clone, variable scope, function scope, borrowing, mutable & immutable references restrictions, dangling pointers prevention …

Primitives types are Copy and they are stored in stack because there size is known at compile time.

Complex types like vector or String or list have dynamic size at runtime, that’s why they are stored in heap with a pointer to that memory in stack.

Assigning a primitive type to another primitive type is a copy. However, assigning a complex type to another complex type move ownership.

Like in Javascript we have block scope and function scope variables : outside the block or function, variables are automatically deallocated by Rust and are no longer accessible.

We don’t need to wait for a Garbage Collector to free memory but when the variable isn’t used or had loosed ownership, it will be automatically dropped !

I loved how Rust rethought memory management and efficiently do all works not at runtime but at compilation time to prevent as much as possible of runtimes bugs !

Thank you for reading my story.

You can find me at :

Twitter : https://twitter.com/b_k_hela

Github : https://github.com/helabenkhalfallah

I love coding whatever the language and trying new programming tendencies. I have a special love to JS (ES6+), functional programming, clean code & tech-books.