The Rust ownership model

In this post, we will explore one of the key features that make Rust quite an interesting programming language: its memory management approach. 

Unlike other languages that rely on the developer to properly manage the memory (e.g. C, C++) or that completely manage the memory by using some kind of automatic garbage collection (e.g. Java or C#), Rust takes a third approach through what it calls the ownership model. 

Before diving into details, and for those unfamiliar with Rust, it should be noted that Rust is a compiled and strongly typed programming language and (as we will see shortly) it enforces a broad set of safeties by static analysis of the code before generating any binary file. 

With that context in mind, we will now explore how Rust provides memory safety guarantees by embracing this model. 

The first thing we need to understand is that the ownership model is a set of rules that Rust checks at compilation time. Through this model Rust can provide strong memory safety guarantees by knowing exactly when memory will be allocated and when it will be freed before the binary file is generated, it also prevents most data races. 

The ownership rules are the following: 

  • 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 

Let’s see how these rules are applied in practice by looking at some examples. 

				
					{ 
    let hello = String::from("Hello world!"); // Memory allocation 
 
    println!("{hello}"); 
} // Memory de-allocation
				
			

In this trivial example, when ‘hello’ is declared within the current scope, you can imagine that such scope owns ‘hello’. As soon as the scope ends, ‘hello’ is no longer valid and since String values are stored in the heap, the memory allocated when defining the value is automatically freed up. 

Let’s now explore what happens if we call a function. 

				
					fn main() { 
    let hello = String::from("Hello world!"); // Memory allocation 
 
    print_me(hello); // Ownership transferred 
} 
 
fn print_me(some_string: String) { 
    println!("{some_string}"); 
} // Memory de-allocation 

				
			

Here something interesting happens, since the ownership of the ‘hello’ value is transferred to the function when calling it, once the function ends the memory is automatically freed up! 

But wait, what if we try to use ‘hello’ after calling the function? That’d be quite normal in most programming languages… 

				
					fn main() { 
    let hello = String::from("Hello world!"); // Memory allocation 
 
    print_me(hello); // Ownership transfer 
 
    println!("Again, {hello}"); // Error! 
} 
 
fn print_me(some_string: String) { 
    println!("{some_string}"); 
} // Memory de-allocation 

				
			

Well in Rust this is a clear mistake due to the ownership model. And the compiler will clearly show the error when trying to compile this program: 

				
					error[E0382]: borrow of moved value: `hello` 
 --> src/main.rs:6:22 
  | 
2 |     let hello = String::from("Hello world!"); 
  |         ----- move occurs because `hello` has type `String`, which does not implement the `Copy` trait 
3 | 
4 |     print_me(hello); 
  |              ----- value moved here 
5 | 
6 |     println!("Again, {hello}"); 
  |                      ^^^^^^^ value borrowed here after move 
  | 
note: consider changing this parameter type in function `print_me` to borrow instead if owning the value isn't necessary 
 --> src/main.rs:9:26 
  | 
9 | fn print_me(some_string: String) { 
  |    --------              ^^^^^^ this parameter takes ownership of the value 
  |    | 
  |    in this function 

				
			

At this point, it’s worth reviewing the compiler feedback and noting a couple of important things. First, let’s notice that the compiler clarifies that the type String does not implement the Copy trait. In Rust, simple values held in the stack implement the Copy trait, and when calling a function these just get copied over, however values stored in the heap are ‘moved’ (i.e. Their ownership gets transferred over). 

But what if we do want to continue using the value after calling a function? 

In the same way in which we transfer ownership when calling a function, we can also give the ownership back to the calling scope by just returning the value, let’s see how this looks: 

				
					fn main() { 
    let mut hello = String::from("Hello world!"); // Memory allocation 
 
    hello = print_me(hello); // Ownership transferred, and then received 
 
    println!("Again, {hello}"); 
} // Memory de-allocation 
 
fn print_me(some_string: String) -> String { // Takes ownership of some_string 
    println!("{some_string}"); 
 
    some_string // Gives ownership of some_string to caller 
}
				
			

However, the previous example looks suspicious, why do we have to explicitly transfer & receive the ownership of a String value to a function that just reads and prints it? 

Truly we don’t need to, and for that purpose, Rust provides the concept of references. Let’s now explore the following example: 

				
					fn main() { 
    let hello = String::from("Hello world!"); // Memory allocation 
 
    print_me(&hello); // The value is borrowed to print_me 
 
    println!("Again, {hello}"); 
} // Memory de-allocation 
 
fn print_me(some_string: &String) { // The functions takes a reference 
																		// No ownership gets transferred 
    println!("{some_string}"); 
} 
 
				
			

So, are references just pointers? Kind of, with one big caveat, a reference is guaranteed to point to a valid value of a particular type for the life of that reference. 

It’s worth mentioning that as with any other value, references can also be divided into immutable & mutable references, however, Rust enforces some strict rules over these to avoid data races, particularly: 

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

Let’s review a trivial example to see this in action: 

				
					fn main() { 
    let mut hello = String::from("Hello world!"); // Memory allocation 
 
    let ref1 = &hello; // Immutable reference 
    let ref2 = &mut hello; // Mutable reference 
 
    println!("{ref1} & {ref2}"); // This will produce an error! 
} // Memory de-allocation 
 
error[E0502]: cannot borrow `hello` as mutable because it is also borrowed as immutable 
 --> src/main.rs:5:16 
  | 
4 |     let ref1 = &hello; 
  |                ------ immutable borrow occurs here 
5 |     let ref2 = &mut hello; 
  |                ^^^^^^^^^^ mutable borrow occurs here 
6 | 
7 |     println!("{ref1} & {ref2}"); 
  |               ------ immutable borrow later used here 

				
			

Since we tried to utilize both immutable & mutable references at the same time, we violated the first constrain, however, Rust is smart enough to statically validate from which point onwards a given type of reference can be used, to understand what this means let’s examine a different example: 

				
					fn main() { 
    let mut hello = String::from("Hello world!"); // Memory allocation 
 
    let ref1 = &hello; // Immutable reference 
    let ref2 = &hello; // Immutable reference 
 
    println!("{ref1} & {ref2}"); 
     
    let ref3 = &mut hello; // Mutable reference 
 
    println!("{ref3}"); 
} // Memory de-allocation 

				
			

This example will compile and produce the expected results. 

But why? 

Because after we defined ref3 we no longer used ref1 nor ref2! Thus, the immutable and the mutable references don’t coexist at the same time. 

There are more advanced concepts to fully understand the implications that the ownership model has in Rust, particularly if you are really interested in the subject, take a look at lifetimes.  

By now the reader should have a good idea of how Rust enforces proper memory management through the ownership model. 

As of writing this article, we have just been struck by the CrowdStrike issue. It seems that the core issue might have been caused by trying to reference a null pointer (See: https://hackernoon.com/code-smell-260-crowdstrike-null), this is an example of how valuable memory safeties imposed by Rust are, in Rust any value that might not exist typically returns an “Option<T>”, which is an enum with two values, either there’s no value (None) or the value does exists (Some(T)) and is contained within the enum (enums might contain values in Rust!). Whenever the developer tries to access an Option value, the compiler enforces that both cases are always handled. This doesn’t directly solve the issue (As the developer might just panic if a None value is found) but it does force the developer to make a conscious decision on how to handle the situation turning an implicit potential error into an explicit decision (be it right or wrong).

Build your tech team faster 

Scale with senior nearshore experts in your time zone.

Why you should augment your team with Folder IT

Outsourcing or Augmenting your team with Folder IT professionals is a cost effective solution that does not sacrifice on quality nor communication effectiveness. Our teams are qualified for working with all the latest technologies and for joining you right away.

Request a quote now for outsourcing your project or staff augmentation services to Argentina.

Build your
tech team
faster
Scale with senior nearshore experts in your time zone.

Tags

NEWSLETTER
Get tech insights
in your inbox

Related

Access Elite
Software Developers
from Argentina

Get in touch
for expert solutions


«Outsourcing is too risky
and unreliable»


«Outsourcing is too risky
and unreliable»


«Outsourcing is too risky
and unreliable»

Get tech insights in your inbox

Get exclusive news and updates.