Skip to main content

Command Palette

Search for a command to run...

Exploring Rust's Approach to Memory Management: Deep Dive into Ownership and Borrowing

Updated
4 min read
Exploring Rust's Approach to Memory Management: Deep Dive into Ownership and Borrowing
S

Safiul Kabir is a Lead Software Engineer at Cefalo. He specializes in full-stack development with Python, JavaScript, Rust, and DevOps tools.

Ownership System in Rust

Ownership is a fundamental concept in Rust's memory management system. It defines the rules governing how memory is managed and deallocated within a Rust program. At its core, ownership ensures that each piece of data has a single owner responsible for deallocating it when it's no longer needed.

Explanation of Ownership

In Rust, every value has a single owner at any given time. The owner is the variable that holds the value. Ownership can be transferred from one owner to another owner through assignment or function calls. When ownership is transferred, the original owner loses access to the value. This prevents issues like dangling pointers and double frees.

Ownership is tied to the scope of a variable. When a variable goes out of scope, Rust automatically deallocates the memory associated with its value. Functions in Rust can return ownership of values to the caller, allowing them to transfer ownership of dynamically allocated resources back to the calling code.

Certain types in Rust, know as copy types (types that implement the Copy trait), are copied rather than moved when assigned to another variable or passed to a function. This allows the original owner to retain access to the value.

Rules of Ownership

Let's take a look at the ownership rules:

  • 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.

Borrowing and References

In Rust, borrowing and references are mechanisms for allowing code to temporarily access data without taking ownership of it. This allows multiple parts of the code to read or modify data without the need for expensive copying and without violating Rust's ownership rules.

Understanding Borrowing and References

References are a type of pointer that refers to a value stored somewhere in memory. In Rust, references are denoted by the & symbol followed by the type of the value being referenced. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.

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()
}

In the above code snippet, note that we pass &s1 into calculate_length and, in its definition, we take &String rather than String. These ampersands represent references, and they allow you to refer to some value without taking ownership of it.

Rust calls the action of creating a reference borrowing. As in real life, if a person owns something, you can borrow it from them. When you're done, you have to give it back. You don't own it.

When a value is borrowed, the borrower has a limited scope and lifetime, determined by the borrowing rules. Here are the rules:

  • At any given time, you can have either one mutable reference or any number of immutable references.

  • References must always be valid.

Mutable vs. Immutable References

There are two types of references in Rust: immutable references (&T) and mutable references (&mut T). Immutable references allow read-only access to the data, while mutable references allow read-write access.

Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value.

Rust's Compile-Time Memory Safety

The following code attempts to create two mutable references to s and will fail:

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

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

    println!("{}, {}", r1, r2);

Here's the error:

$ 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

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

This error says that this code is invalid because we cannot borrow s as mutable more than once at a time. The first mutable borrow is in r1 and must last until it’s used in the println!, but between the creation of that mutable reference and its usage, we tried to create another mutable reference in r2 that borrows the same data as r1.

The restriction preventing multiple mutable references to the same data at the same time allows for mutation but in a very controlled fashion. 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.

  • At least one of the pointers is being used to write to the data.

  • There's no mechanism being used to synchronize access to the data.

Data races cause undefined behavior and can be difficult to diagnose and fix when you're trying to track them down at runtime; Rust prevents this problem by refusing to compile code with data races!

More from this blog

Safiul Kabir's blog

8 posts

Safiul Kabir is a Lead Software Engineer at Cefalo. He specializes in full-stack development with Python, JavaScript, Rust, and DevOps tools.