CS3984 Computer Systems in Rust



Stack and Heap

Memory used in programs are allocated in two distinct parts of the available memory space:

Stack

  • Used for local variables
  • Values have fixed size
  • Allocation and deallocation is fast (by moving the stack pointer)

Heap

  • Size of values are determined at runtime
  • Allocation and deallocation is slower (book-keeping needed to manage the contents of the heap)

Stack and Heap: Stack

#include <stdint.h>

int main(void) {
    uint32_t x = 5;
}


fn main() {
    let x: u32 = 5;
}


Stack and Heap: Heap

#include <malloc.h>
#include <stdint.h>

int main(void) {
    uint32_t *x = malloc(sizeof(*x));
    *x = 5;
    free(x);
}


fn main() {
    let x: Box<u32> = Box::new(5);
}


Dynamic Memory Management

  • No garbage collector/runtime system
  • Full control over memory management
  • Manual memory management through malloc and free
#include <malloc.h>
#include <stdint.h>

int main(void) {
  uint32_t *x = malloc(sizeof(*x));
  *x = 5;
  free(x);
}


  • No garbage collector/runtime system
  • Full control over memory management
  • Automatic memory management
fn main() {
    let x: Box<u32> = Box::new(5);
} // x is automatically freed here


This is done with Rust’s ownership model.

Automatic Memory Management In Rust

  • A value is a sequence of bits in memory.
  • A variable is a placeholder for a value, and is a component of a stack frame.
  • A stack frame is a mapping of variables to values on the stack.
fn main() {
    let a = 5; // <---- L1
    let b = add_one(a);
}

fn add_one(x: i32) -> i32 {
    let c = x + 1;
    c
}


Automatic Memory Management In Rust

  • A value is a sequence of bits in memory.
  • A variable is a placeholder for a value, and is a component of a stack frame.
  • A stack frame is a mapping of variables to values on the stack.
fn main() {
    let a = 5;
    let b = add_one(a);
}

fn add_one(x: i32) -> i32 {
    let c = x + 1; // <---- L2
    c
}


Automatic Memory Management In Rust

  • A value is a sequence of bits in memory.
  • A variable is a placeholder for a value, and is a component of a stack frame.
  • A stack frame is a mapping of variables to values on the stack.
fn main() {
    let a = 5;
    let b = add_one(a); // <---- L3
}

fn add_one(x: i32) -> i32 {
    let c = x + 1;
    c
}


Ownership Rules

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

Ownership Rules

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
fn main() {
    let a = 5;
    {
        let b = 6;
        {
            let c = 7;  // <--- L1
        }
        println!("{b}");
    }
    println!("{a}");
}


Ownership Rules

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
fn main() {
    let a = 5;
    {
        let b = 6;
        {
            let c = 7;
        }
        println!("{b}"); // <--- L2
    }
    println!("{a}");
}


Ownership Rules

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
fn main() {
    let a = 5;
    {
        let b = 6;
        {
            let c = 7;
        }
        println!("{b}");
    }
    println!("{a}"); // <--- L3
}


Ownership Rules (2)

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
fn main() {
    let a = 5;
    {
        let b = Box::new(6);
        println!("{b}"); // <--- L1
    }
    println!("{a}");
}


Ownership Rules (2)

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
fn main() {
    let a = 5;
    {
        let b = Box::new(6);
        println!("{b}");
    }
    println!("{a}"); // <--- L2
}


Ownership Rules (3)

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
  • Assignment moves ownership of the value.
  • This invalidates the original variable.
fn main() {
    let a = Box::new(5); // <--- L1
    let b = a;
}


Ownership Rules (3)

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.
  • Assignment moves ownership of the value.
  • This invalidates the original variable.
fn main() {
    let a = Box::new(5);
    let b = a; // <--- L2
}


Ownership Moved (1)

  • Assignment moves ownership.
fn main() {
    let s1 = String::from("Hello world!");
    let s2 = s1;
    println!("{} {}", s1, s2);
}


Ownership Moved (2)

  • Cloning avoids moving ownership.
fn main() {
    let s1 = String::from("Hello world!");
    let s2 = s1.clone();
    println!("{} {}", s1, s2);
}


  • Types that implement the Clone trait are clonable.

Ownership Moved (3)

  • Structs can automatically implement the Clone trait through derive if the contents of the struct are also Clone.
#[derive(Debug, Clone)]
struct User {
    active: bool,
    username: String,
    sign_in_count: u64,
}

fn main() {
    let user_1 = User {
        active: true, username: String::from("crab"), sign_in_count: 0
    };
    let user_2 = user_1.clone();
    println!("{user_1:?} and {user_2:?}");
}


Q/A

Q: Why are structs not Clone by default?
A: Flexibility. Clone can be manually implemented to provide different functionality (eg. for reference counting, see std::sync::Rc)

Q: Why do assignments not .clone() by default?
A: Performance. Cloning may be expensive, so we want explicit intention.

Q: Is using .clone() a lot bad practice?
A: Possibly, however always measure.

We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.

Donald Knuth

Q: For types like integers, having to always .clone() seems like a big hassle… Is there a better way?
A: There is!

Copy

  1. By default, variable assignment has move semantics.
  2. However, if a type implements the Copy trait, it has copy semantics.
  3. Types that are Copy are implicitly duplicated on assignment.
fn main() {
    let a = 5; // <---- L1
    let mut b = a;
    b += 1;
}


Copy

  1. By default, variable assignment has move semantics.
  2. However, if a type implements the Copy trait, it has copy semantics.
  3. Types that are Copy are implicitly duplicated on assignment.
fn main() {
    let a = 5;
    let mut b = a; // <---- L2
    b += 1;
}


Copy

  1. By default, variable assignment has move semantics.
  2. However, if a type implements the Copy trait, it has copy semantics.
  3. Types that are Copy are implicitly duplicated on assignment.
fn main() {
    let a = 5;
    let mut b = a;
    b += 1; // <---- L3
}


Copy (2)

  • Structs can automatically implement the Copy trait through derive if the contents of the struct are also Copy.
  • Any struct that is Copy (”cheap to duplicate”) must also be Clone (”maybe expensive to duplicate”).
#[derive(Debug, Clone, Copy)]
struct AnonymousUser {
    active: bool,
    sign_in_count: u64,
}

fn main() {
    let user_1 = AnonymousUser {
        active: true, sign_in_count: 0
    };
    let user_2 = user_1;
    println!("{user_1:?} and {user_2:?}");
}


Q/A

Q: You mentioned explicit intention earlier, why are Copy types implicitly duplicated on assignment?
A: Ergonomics, having to .clone() everywhere creates clutter.

Q: Can structs with non-Copy fields implement Copy?
A: No. Copy indicates that the type can be duplicated with a simple memcpy, and complicated implementations for more functionality are not allowed.

Q: Why do structs not default to being Copy if all of its fields are Copy?
A: Explicit intention. Accidentally adding a non-Copy field to a struct may break code that relies on the struct implicitly being Copy.

Q: ?

Ownership and Functions

Passing a value to a function is similar to performing variable assignment.

fn main() {
    let s = String::from("crab");
    takes_ownership(s);
    // `s` is no longer valid here

    let x = 5;
    makes_copy(x);
    // `x` can still be used here
}

fn takes_ownership(some_string: String) {
    println!("{some_string}");
} // `s` is dropped here and memory is freed

fn makes_copy(some_integer: i32)
    println!("{some_integer}");
}


Ownership and Functions (2)

Q: When is the memory for "crab" freed?

fn main() {
    let s1 = returns_ownership();
    let s2 = takes_and_returns(s1);
}

fn returns_ownership() -> String {
    String::from("crab")
}

fn takes_and_returns(some_string: String) -> String {
    some_string
}