Rust Explained: Ownership, Borrowing, Safety and Speed

What the Rust programming language is, why Google, Microsoft and the Linux kernel adopted it, and how ownership, borrowing and compile-time safety work, with runnable examples.
Rust Explained: Ownership, Borrowing, Safety and Speed

Rust is a compiled systems programming language with strong static typing and no garbage collector. Its distinguishing idea is that memory safety is checked by the compiler rather than at runtime, so a whole class of bugs that crash C and C++ programs, including use-after-free, double free and data races, cannot be written in safe Rust at all. This guide covers why that matters, what the language looks like and the ownership and borrowing rules that make it work, with short examples you can paste into a main.rs and compile.

A short history

Rust began as a personal project by Mozilla engineer Graydon Hoare in 2006. Mozilla sponsored it from 2009, released version 1.0 in 2015 and shipped Rust components in Firefox from 2017, when parts of the browser’s CSS engine were rewritten in it. Since 2021 the language has been governed by the independent Rust Foundation, with Amazon, Google, Microsoft, Meta and Huawei among its founding members.

Why Rust?

C and C++ have dominated systems programming for decades. The Linux kernel, the Android Open Source Project and Windows internals are all written in them. Rust is the first language to offer comparable performance and control with memory safety enforced by the compiler, and the largest companies in the industry have started to act on that.

  • In 2021, it was announced that Rust adoption for Android is underway, with the statement that about 70 percent of Android’s high-severity security vulnerabilities are related to memory safety.
  • Microsoft is also swiftly transitioning to Rust for Windows core components and business services. Microsoft announced the plan for the transition and released the Rust SDK for Windows. Now, writing Windows applications in Rust is more accessible. Microsoft’s security team has reported speed improvements of 5 to 15 percent in components ported to Rust.
  • In late 2022, Linus Torvalds merged initial Rust support into Linux 6.1, making it the second language, after C, accepted for kernel development.

Main features

  • Safety at compile time. The borrow checker rejects programs that could read freed memory or race on shared data.
  • Fearless concurrency. The same ownership rules that prevent memory bugs also prevent data races, so threads are safe by construction.
  • Speed. No garbage collector, no runtime and zero-cost abstractions: Rust code typically performs like equivalent C or C++.

Variables in Rust

fn main() {
    let x = 18; // Immutable variable
}
fn main() {
    let mut y = 18; // Mutable variable
}

We define a new variable using the let keyword.

Variables are immutable by default, which means we cannot change the value of the variable x unless we explicitly make it mutable. But why is this the case? There are several advantages to having immutable variables.

First, an immutable variable can be shared across multiple threads very easily. Additionally, it is straightforward to store them in memory without the need to perform checks. There is no requirement to validate anything when passing around immutable variables to multiple functions. The decision to make variables immutable is driven by considerations of speed, concurrency and safety. Immutable variables contribute to faster and safer code execution, especially in concurrent programming scenarios.

Safety features in Rust

Rust will find all the following bugs during compile time, so developers do not need to worry about these issues in production. The functions do not even need to be called.

Usage of uninitialized variables

We cannot compile the program. Rust will detect the mutability issue during compile time because we are trying to read the value of an uninitialized variable.

fn use_of_uninitialized_variables_are_prohibited() {
    let x: i8;
    println!("value of x is {}", x);
}

Condition evaluation happens at compile time

The code below won’t compile since the compiler cannot guarantee the state of the condition.

fn conditional_evaluation_err() {
    let x: i8;

    if true {
        x = 10;
    }
    println!("value of x is {}", x);
}

The code below will compile since the compiler can guarantee the state of the condition.

fn conditional_evaluation() {
    let x: i8;

    if true {
        x = 10;
    } else {
        x = 20;
    }
    println!("value of x is {}", x);
}

Cannot assign twice to immutable variable

fn can_not_assign_twice() {
    let x: i8 = 10;
    println!("value of x is {}", x);
    x = 20; // This line will fail; the compiler will detect the issue during compile time.
}

Variables need to be defined as mutable to change the value after declaration

fn mutable_variables() {
    let mut x: i8 = 10;
    println!("value of x is {}", x);
    x = 20; // value changed here
    println!("new value of x is {}", x); // this will print the new value
}

Scope in Rust

fn scope() {
    let x: i8 = 10;
    {
        let y: i8 = 20;
    }
    println!("x = {}, y = {}", x, y);
    // this will fail to compile since
    // `y` is available in a different scope in the same function
}

Variable scopes and variable shadowing

fn scope_redeclaration() {
    let x: i8 = 10;

    { // New block/scope starts here
        let x: i8 = 20; // This is called shadowing
    } // The above x will be dropped here

    println!("value of x = {}", x);

    // The value of x will be 10 because the redeclaration of x is allowed in a different scope,
    // but will be dropped after the scope
}

Rust ownership

Only three rules to master

  • Each value has an owner. No value exists in memory without one.
  • There is exactly one owner at a time. A value cannot be shared, but it can be borrowed.
  • The value is dropped when its owner goes out of scope. There is no garbage collector.

Accessing moved variables will throw an error at compile time

In this example, we are trying to access the moved value. We need to understand the concept of stack and heap memory to understand the concept here. In Rust, the datatype consists of a capacity, length, and pointer to the actual memory in the heap. So whenever we assign the ownership to a new variable as below, Rust actually creates a new item in the stack by copying the length and capacity and moving the data pointer to the new variable. At this moment, the old variable (s1) will get dropped.

fn main() {
    let s1 = "hello world!".to_string();
    let s2 = s1; // ownership moves to s2; s1 is no longer valid
    println!("{}", s1); // error: value borrowed here after move
}

Mutable move operation

Basics:

  • A variable consists of two parts: stack and heap.
  • Stack memory is really fast, but the size must be constant. It is basically a FIFO (First in, first out) data structure.
  • Heap memory is slower but can hold large or variable-sized data, for example strings. But can hold a large amount of data, for example, strings.

This code will work since the variable s1 is mutable. Mutable variables can be reassigned with new values and reused.

fn mutable_move_operation() {
    let mut s1 = "hello world!".to_string();
    let s2 = s1; // owner changed to s2
    // But we can assign a new value to the s1 since it is mutable
    s1 = "something else".to_string();
    println!("{}", s1); // This will work
}

Cloning variables

Then how to make copies of variables? It is simple, clone the variable. Cloning creates a new variable with its own capacity, length and data pointer. But the difference here is that while cloning, data will be copied to a new memory in the heap. Then the variable will be a pointer to the new memory location. Cloning a String copies the heap data, so it costs memory and time in proportion to the string’s length. Use it when you need two independent values, and prefer a reference when you only need to read.

fn clone_data() {
    let s1 = "Hello world".to_string();
    let s2 = s1.clone(); // cloning operation
    println!("s1: {}", s1);
    println!("s2: {}", s2);
}

Copy and clone

  • In Rust, Copy means the data in the stack will be copied, and the pointer to the heap will be updated.
  • In the clone operation, stack values will be copied, and heap data will be copied to a new location, and the pointer will be updated to the new location.

Dropping values

In Rust, there is no garbage collector; then how will it keep track of memory? It is fairly simple and straightforward. When a variable goes out of scope, its destructor runs immediately, the heap memory is freed and the stack slot is popped. There is no collector to schedule and no pause to tune. Garbage-collected languages do this work later and in bulk; Python, for instance, uses reference counting plus a cycle collector to find memory that is no longer reachable.

Moving a variable into a function

fn moving_to_functions() {
    let s1 = "message 1".to_string();
    // In the next line, variable is moved & not returning anything
    another_function(s1);
    // Next line will fail to compile since the ownership moved & variable dropped
    println!("{}", s1);
}

fn another_function(msg: String) {
    println!("msg is {}", msg)
}

One workaround is to have the function return the value and reassign it to s1. There is a better way.

References and borrowing

Instead of moving the value and ownership of a variable, we can pass a reference to the value. That means, we are not actually moving the value to the new function. So, once the new reference goes out of scope, the reference will go out of scope, not the value or ownership. Under the hood whenever we create a new reference Rust creates a pointer in the background and points it to the actual variable.

There are two kinds of references:

  • Immutable reference
  • Mutable reference

The rules:

  • At any given time there can be either one mutable reference or any number of immutable references to a value, never both.
  • References can never be null and can never outlive the value they point to. The compiler enforces this through lifetimes.

Under the hood a reference is a pointer, as in C, but Rust dereferences it for you: the . operator calls methods on the referenced value directly. When you need the value itself, for example to assign through a mutable reference, prefix the reference with *.

Immutable reference

Create an immutable reference with & and accept it with &String in the function signature. Note that is_empty() is called on the reference without any explicit dereference.

fn main() {
    println!("Reference and Borrowing in Rust");
    let currency = "Euro".to_string();
    println!("1. Default currency is {currency}");
    // Passing only the reference to another function
    check_currency(&currency);
    // This still works: ownership never moved
    println!("2. Default currency is {currency}");
}

// Accept an immutable reference
fn check_currency(currency: &String) {
    println!("Checking {currency}");
    if currency.is_empty() {
        println!("Invalid")
    }
}

Mutable reference

Create a mutable reference with &mut variable. The example passes one to update_currency, which dereferences it with * and assigns a new value.


// Mutable reference example
fn mutable_reference() {
    let mut currency = "".to_string();
    println!("1. Default currency is {currency}");
    // Passing the mutable reference to another function
    update_currency(&mut currency);
    // Print the updated value
    println!("2. Default currency is {currency}");
}

fn update_currency(currency: &mut String) {
  println!("Checking {currency}");
  if currency.is_empty() {
    println!("Currency is {currency}");
    *currency = "SEK".to_string();
  }
}

The borrow checker is what makes Rust’s concurrency story work. Because the compiler knows at every point who may read and who may write a value, it can refuse to compile code that would let two threads race on the same data. The rules feel strict at first; after a few weeks they become the way you think about ownership in every language.

Where to go next

All the examples are in the companion GitHub repository. For how these ideas hold up in production, read how I built CVBlender, a web product with a Rust backend, or see how to build private Rust crates in GitLab CI when your first real project needs a pipeline.