Rust - A memory-safe language

About resource ownership

Why does C++ have a const keyword?

Type systems for safety

  • Type modifiers such as const can prevent mistakes without runtime overhead
    • Sometimes const is even faster than non-const!
  • const is about mutability: When is something allowed to change?
  • Rust introduces borrow checking: Who references a resource?
    • To understand it, we need to understand resource ownership

Resource lifecycle

  • Systems programming is about managing (hardware) resources
  • Every resource undergoes the resource lifecycle:
  • How do we ensure this lifecycle?

Resource ownership

  • Every resource must have at least one owner
    • The one responsible for releasing the resource back into the resource pool
  • Resources that do not get released leak
  • Keep leaking resources and you encounter resource exhaustion
    • Kind of like the environment…

Why is resource exhaustion a bigger problem for systems programming than for applications programming?

Resource ownership strategies

  • Manual: C, C++ (malloc, free, fopen, fclose etc.)
  • Managed at runtime: Java, Python, Go (garbage collection)
  • Managed at compile-time: Rust (borrow checking)

Revisiting invalidating iterators

fn main() {
    let mut v = vec![1, 2, 3, 4];
    let p = &v[0]; 

    v.push(41); 
    println!("{}", p);
}
  • This didn’t compile with:
cannot borrow `v` as mutable because it is also borrowed as immutable
  • Rust borrows are similar to C++ references
  • Translation: “You must not change (mutate) something while someone is looking at (borrowing) it”

The rule of one

  • Rust values have exactly one owner
  • This is limiting, so we can borrow values
    • Immutably: &T
    • Mutably: &mut T
  • These are mutually exclusive
    • Either exactly one mutable borrow
    • Or arbitrarily many immutable borrows

Why the code didn’t compile

fn main() {
    let mut v = vec![1, 2, 3, 4];
    let p = &v[0]; 

    v.push(41); 
    println!("{}", p);
}
  • Take an immutable borrow here

Why the code didn’t compile

fn main() {
    let mut v = vec![1, 2, 3, 4];
    let p = &v[0]; 

    v.push(41); 
    println!("{}", p);
}
  • Take an immutable borrow here
  • Now mutable borrows are disallowed
    • Signature of Vec::push:
    • fn push(&mut self, element: T)

Understanding borrow checking - 1

fn first<T>(v: &Vec<T>) -> &T {
    &v[0]
}

fn main() {
    let mut v = vec![1, 2, 3, 4];
    let p = first(&v);

    v.push(41); 
    println!("{}", p);
}

Compiles or doesn’t compile?

Understanding borrow checking - 1

fn first<T>(v: &Vec<T>) -> &T {
    &v[0]
}

fn main() {
    let mut v = vec![1, 2, 3, 4];
    let p = first(&v);

    v.push(41); 
    println!("{}", p);
}

Doesn’t compile:

cannot borrow `v` as mutable because it is also borrowed as immutable

Understanding borrow checking - 2

fn first<T>(arg1: &Vec<T>, arg2: &Vec<T>) -> &T {
    &arg1[0]
}

fn main() {
    let mut vec1 = vec![1, 2, 3, 4];
    let mut vec2 = vec![1, 2, 3, 4];
    let p = first(&vec1, &vec2);

    vec2.push(41);
    println!("{}", p);
}

Compiles or doesn’t compile?

Understanding borrow checking - 2

fn first<T>(arg1: &Vec<T>, arg2: &Vec<T>) -> &T {
    &arg1[0]
}

fn main() {
    let mut vec1 = vec![1, 2, 3, 4];
    let mut vec2 = vec![1, 2, 3, 4];
    let p = first(&vec1, &vec2);

    vec2.push(41);
    println!("{}", p);
}

Doesn’t compile, but for a different reason:

missing lifetime specifier
this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `arg1` or `arg2`

Lifetimes

  • Lifetimes are generic types that tell the compiler two things:
    1. How long a specific borrow lives
    2. How it ties to other borrows
fn first<'a, T>(arg1: &'a Vec<T>, arg2: &'a Vec<T>) -> &'a T {
    &arg1[0]
}
  • Translation: “first returns a borrow that comes from both arg1 and arg2 and lives as long as both arg1 and arg2
  • What does that even mean?

Lifetimes visualized

fn first<'a, T>(
    arg1: &'a Vec<T>, 
    arg2: &'a Vec<T>
) -> &'a T {
    &arg1[0]
}

fn main() { // 'm (lifetime of main) ------
    let mut vec1 = vec![1, 2, 3, 4];    // |
    let mut vec2 = vec![1, 2, 3, 4];    // |
    let p = first(&vec1, &vec2);        // |  
                                        // |   
    vec2.push(41);                      // |  
    println!("{}", p);                  // |  
} // --------------------------------------

Lifetimes visualized

fn first<'a, T>(
    arg1: &'a Vec<T>, 
    arg2: &'a Vec<T>
) -> &'a T {
    &arg1[0]
}

fn main() { // 'm (lifetime of main) ------
    let mut vec1 = vec![1, 2, 3, 4];    // |
    let mut vec2 = vec![1, 2, 3, 4];    // |
    let tmp1 = &vec1;                   // |
    let tmp2 = &vec2;                   // |
    let p = first(tmp1, tmp2);          // |  
                                        // |   
    vec2.push(41);                      // |  
    println!("{}", p);                  // |  
} // --------------------------------------

Lifetimes visualized

fn first<'a, T>(
    arg1: &'a Vec<T>, 
    arg2: &'a Vec<T>
) -> &'a T {
    &arg1[0]
}

fn main() {
    let mut vec1 = vec![1, 2, 3, 4];
    let mut vec2 = vec![1, 2, 3, 4];
    let tmp1 = &vec1;
    let tmp2 = &vec2;
    let p = first(tmp1, tmp2);

    vec2.push(41);
    println!("{}", p);
}

Lifetimes visualized

fn first<'a, T>(
    arg1: &'a Vec<T>, 
    arg2: &'a Vec<T>
) -> &'a T {
    &arg1[0]
}

fn main() {
    let mut vec1 = vec![1, 2, 3, 4];
    let mut vec2 = vec![1, 2, 3, 4];
    let tmp1 = &vec1;
    let tmp2 = &vec2;
    let p = first(tmp1, tmp2);

    vec2.push(41);
    println!("{}", p);
}

Fixed code

fn first<'a, 'b, T>(
    arg1: &'a Vec<T>, 
    arg2: &'b Vec<T>
) -> &'a T {
    &arg1[0]
}

fn main() {
    let mut vec1 = vec![1, 2, 3, 4];
    let mut vec2 = vec![1, 2, 3, 4];
    let tmp1 = &vec1;
    let tmp2 = &vec2;
    let p = first(tmp1, tmp2);

    vec2.push(41);
    println!("{}", p);
}

Inferred lifetimes

  • We only need manual lifetime annotations in ambiguous situations
  • Often, the Rust compiler can figure things out for us:
fn first<T>(v: &Vec<T>) -> &T {
    &v[0]
}

// equivalent to:
fn first<'a, T>(v: &'a Vec<T>) -> &'a T {
    &v[0]
}

When to add lifetime annotations

  • When borrows are ambiguous:
// -> &T comes from arg1 or arg2? Ambiguous -> annotation required!
// Note: The compiler only looks at the function SIGNATURE!
fn first<T>(arg1: &Vec<T>, arg2: &Vec<T>) -> &T {
    &arg1[0] 
}
  • Borrows inside struct and enum:
struct Foo {
     // For which lifetime is inner valid? Ambiguous -> annotation required
    inner: &i32,
}

When to add lifetime annotations

  • When borrows are ambiguous:
// Resolved:
fn first<'a, 'b, T>(arg1: &'a Vec<T>, arg2: &'b Vec<T>) -> &'a T {
    &arg1[0] 
}
  • Borrows inside struct and enum:
// Resolved:
struct Foo<'a> {
    inner: &'a i32,
}

More on borrow checking once we talk about memory