🦀 Learning Rust

    đź“– Table of Contents

01 Memory: Ownership, Borrowing, Lifetimes and Smart Pointers


🌱 This is a living document and will be actively contributed to and refined over time. If you spot any errors, please reach out and let me know!

Rust is a non-GC (garbage collected) language that ensures memory safety.

Many languages, such as Ruby, Javascript and Python, run a garbage collection process that manages memory for you. This may make the language easier to work with, but it comes at a cost. Garbage collectors require overhead and can become a bottleneck for application performance. You are also at the mercy of the assumptions it makes about memory usage patterns. While this may be fine for certain domains, the whole point of using a system programming language is to regain fine grained control.

Languages like C++ and C require you to explicitly allocate and deallocate memory using functions like malloc and free. This gives you precise control over your application and it’s performance, but also leaves you vulnerable to many mistakes such as dangling pointers, memory leaks and data races. Rust gives you a safer alternative to manual memory managment that eliminates entire classes of errors found in it’s predecessors.


🗑️ So how does Rust guarantee memory safety, without relying on a garbage collector or explicit memory allocation/deallocation?


The answer is ownership and borrowing.

Ownership


Every value in Rust must have a single owner
Owners are responsible for memory deallocation
Ownership can be moved


The simplest way to begin illustrating this concept is to assign one variable to another. When we set y equal to x, we are telling the compiler to give y ownership over the Rust string that is currently assigned to x. This is called a move of ownership. Since x is no longer pointing to value, it is removed from the stack, memory is deallocated and it is offically dropped from the program.

let x = String::from("Rust"); // x owns "Rust"
let y = x; // Ownership is 'moved' to y  - x is about to be 'dropped'

println!("{}", x); // Error - x no longer exists

Let’s look at another example.
Here we will set x to a struct.
We then call read_val which prints our nested value to the screen.
Notice that the second attempt to access x results in an error.

struct Foo {
    val: i32,
}

fn main() {
    let x = Foo { val: 42 };

    fn read_val(container: Foo) {
        println!("{}", container.val)
    }

    read_val(x); // prints 42
    read_val(x); // Compiler Error - use of moved value 'x'
}

What does use of moved value 'x' mean?

This means there was a move of ownership. Initially x is the owner of Foo. When we pass x to read_val, ownership of Foo is passed to the function argument container. When the scope of read_val comes to an end it drops all local variables, arguments and owned values.

That means that when we go to accessx a second time, it no longer exists in memory. If you do not want your value to be dropped, we can let the function borrow a reference to it.

Borrowing


References can be “borrowed” from an owner.
There can be any number of immutable references.
There can only be one mutable reference.


Immutable References

Rust provides us with the borrow operator &.
This creates an immutable reference.
The value can now be passed around without moving ownership.
We may create as many immutable borrowed references as we need.

struct Foo {
    val: i32,
}

fn main() {
    let x = Foo { val: 42 };

    // update fn signature to expect borrowed reference via `&`
    fn read_val(container: &Foo) {
        println!("{}", container.val)
    }

    // preface variable with '&' to denote a borrowed reference
    read_val(&x); // prints 42
    read_val(&x); // prints 42
}

This is telling the compiler: When I call read_val pass in a reference that is borrowed from the owner, x. read_val does not take ownership You can pass as many immutable references around your application as you need. If we need to mutate the data, we can create a single mutable reference.

Mutable References

Declare a mutable borrowed reference by using &mut.
There can only be one mutable reference at a time.

struct Foo {
    val: i32,
}

fn main() {
    let mut x = Foo { val: 42 };

    // update fn signature to expect borrowed reference via `&`
    fn sum_val(container: &mut Foo, add: i32) {
        container.val += add;
        println!("{}", container.val)
    }

    // preface variable with '&' to denote a borrowed reference
    sum_val(&x, 8); // prints 50
}

Notice we must use an ampersand when declaring the function signature, as well as when we pass our reference inside the function invocation. We also had to explicitly tell the compiler that Foo is mut.

Lifetimes


Lifetime are…


Rust uses lifetimes to describe when the lifecycle of a value’s memory allocation.
The compiler can infer most lifetimes, but sometimes it’s nice or even necessary to get explicit.

Lifetime Annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

At first lifetime annotations will look a bit like hyieroglyphics Here we are using <'a> to say the memory allocated to x and y should be preserved for the duration of 'a regardless of what happens inside the current scope.

Lifetimes and Scope

Recall that varibles in Rust are block scoped. A values lifetime, unless otherwise specified via a lifetime annotation, will always be the duration of it’s blok scope.


{

    }

When a variable goes out of scope, Rust calls the drop function. drop can be implemented for any type as a trait.

🧠 In C++, this pattern of deallocating resources at the end of an item’s lifetime is sometimes called Resource Acquisition Is Initialization (RAII). The drop function in Rust will be familiar to you if you’ve used RAII patterns.

The Stack and Heap

In order to understand ownership in Rust, you must understand the difference between allocating memory to the stack versus the heap. This post won’t go into great detail but I think this document serves as nice primer.

Some key things to keep in mind are as follows:

The Stack
  • Data with known size at compile time are placed on the stack
  • Last In, First Out (LIFO).
  • Memory can be accessed via variable bindings or references.
  • Memory is stored side by side (contiguous) on the physical hardware
  • Fast, but has a rigid structure (i.e. push, pop, fixed sized)
The Heap
  • Data with dynamic size is stored on the heap
  • Memory is dispersed throughout the heap and subject to change when resized
  • Memory must be accessed via a pointer
  • Slow compared to the stack, but far more flexible (i.e. dynamic data allocation)
  • Not to be confused with a “heap” data structure - unrelated

Pointers and References

A raw pointer is a variable that contains an memory address. We have already seen pointers: a borrowed reference & compiles down to a raw pointer.

*const T | *mut T


Raw pointers do not have any of the memory safety guarantees found throughout the rest of the language. In general raw pointers are used when interfacing with C code or when unsafe, low level opitimizations must be made. Limited to specific cases, raw pointers aren’t frequently relied upon, but are important to be aware of and understand. I recommend checking out the MIT guide to raw pointers for a more in depth explanation.

& = reference operand * = dereference operand

let a  = 42;
let r = &a;
let b = a + *r; //dereference

🦀 unsafe is a keyword in Rust that serves as an “escape hatch”. It doesn’t necessarily imply dangerous code, but rather is a way of telling the compiler that you have additional context and need to make use of increased flexbility. Please do take some time to learn about this block >

Smart Pointers

Rc<T> | Arc<T> | Box<T> | Ref<T> | RefCell<T> | Cow<T>


Included in the Rust standard library are a handful of smart pointers that go well above and beyond simple dereferencing. Smart pointers are data structures that act like pointers, but have additional metadata and functionality. There are many smart pointers and it is possible to implement your own. In this article we will touch on the most common examples.

Box<T> - Stores values on the heap - Checked at compile time

Let’s consider this trivial example. We try to add two i32 - but the code will not compile initially. This is because b is a pointer to a memory address on the heap. In order to access the value, we must first use the dereference * operator.

let a: i32 = 22;
let b: Box<i32> = Box::new(60);

println!("{}", a + b); // Will not compile - b is a memory address i.e. 0x55db16f98ba0
println!("{}", a + *b); // Prints 82

Rc<T> - “Reference Count”. Enables multiple owners

Keeps track of all the owners

RefCell<T> - Single Ownership. Allows mutation of ___.

Enforces borrowing rules at runtime. Interior mutability

Copy & Clone


Copy and Clone are traits included in the std module.
All primitives in Rust implement copy.


Remember the first example in this post?
First we set y to equal the value at x.
We then tried to print x to the screen, but were unable because x was dropped? Let’s take a look at the exact same example, except for this time let’s use an i32 instead of a String.

let x: i32 = 33;
let y = x;

println!("{}", x); // prints 33

The reason this example works is because Rust primitives implement the copy trait. We haven’t covered traits in depth yet, but we will see they are the mechanism through which we create polymorphic or shared behavior.

2 + 2 is actually compiled down to 2.add(2)

struct ExampleThing {
    name: String
}

impl ExampleThing {
    fn new(name: String) -> ExampleThing {
        ExampleThing {
            name: String::from(name)
        }
    }
}

#[derive(Debug, Copy, Clone)]
let x = ExampleThing::new("Thing")

Wrap Up


So why is all of this important?

Rust can seem like a lot of work at first. You aren’t wrong! It is a lot of work. But by enforcing rules like ownership, the Rust compiler is able to guide you into building software that is guarunteed to be memory safe. This prevents runtime bottlenecks and entire classes of errors that other non-GC languages are suseptible to. Get over these initial hurdles and you are going to find yourself confidently delivering quality consistently.

Once you’ve gotten the hang of these concepts, you will be well on your way to understanding Rust. Ownership is a very important part of what makes Rust, Rust. It can also be tricky to understand at first. Give yourself some time to adapt to this new paradigm and get ready for some really rewarding moments ahead.

Next up let’s take a closer look at Traits, how they interface with Generics and how they enable shared behavior in our Rust applications.