Rust memory management

From wikinotes

Rust uses ownership semantics for memory management.
You may also be interested in rust pointers (specifically it's smart pointers, that let you break these rules).

TL;DR:

  • by default, all objects can only have a single owner
    (unless abstracted by an Rc pointer)
  • passing instances, hands over ownership
  • passing references, accesses without transferring ownership
  • references use read/write lock semantics.
    multiple immutable references are fine,
    but when a mutable reference exists no other reference can exist (mutable or immutable)
  • lifetimes can be assigned to your objects, to extend/shorten your objects lifetime to match another

Documentation

ownership tut docs https://doc.rust-lang.org/stable/book/ch04-00-understanding-ownership.html
lifetime tut docs https://doc.rust-lang.org/stable/book/ch10-03-lifetime-syntax.html

General

Stack

The stack is

  • a LIFO
  • push=add, pop=remove (from the top)
  • only supports fixed-size datatypes
  • fast

Heap

  • access provided through pointers (a fixed-size, usable on stack)
  • slower

Ownership

Copy Trait

  • Objects have a single owner at once
  • When owner goes out of scope, value is dropped (with drop())
  • When an object is passed as a function-parameter,
    unless it is a reference or implements the Copy Trait,
    ownership is transferred to the function
    (and it cannot be referenced in current context).

See example of ownership in action.

fn print_i32(i: i32)    { println!("{}", i); }
fn print_str(s: String) { println!("{}", s); }

fn main() {
    let i = 123;
    print_i32(i);       // `i` has `Copy` trait, `print_i32` gets a shallow copy
    println!("{}", i);  // valid! `i` is still owned by `main()`

    let s = String::from("abc");
    print_str(s);       // `s` does not have `Copy` trait, pointer passed to function, which now owns it
    println!("{}", s);  // <-- BANG! not allowed to use `s` anymore
}

References

Passing references enables passing objects to arguments without transferring ownership.
However:

  • only a single mutable reference can exist for the same object at a time.
  • if a mutable reference for an object exists, there cannot be immutable references to that object

I'm thinking of this as a read/write lock (multiple readers allowed, nobody can read/write while writing).

fn print_str(s: &String) { println!("{}", s); }

fn main() {
    let s = String::from("abc");
    print_str(&s);
    println!("{}", s);  // <<-- valid! `main` still owns the object
}

In rust, because of lifetime semantics, you can't return a reference from an object created in a function.
You need to return the object itself, so that it's ownership is transferred.

// INVALID!
fn foo() -> &String {
    let s = String::from("hi");
    &s
}

// VALID
fn foo() -> String {
    let s = String::from("hi");
    s
}

Lifetimes

Syntax

fn foo<'a>(i: &str) -> &str {}            // reference
fn foo<'a>(i: &'a str) -> &'a str {}      // reference, w/ lifetime 'a
fn foo<'a>(i: &'a mut str) -> &'a str {}  // mutable reference, w/ lifetime 'a

let s: &'static str = "foobar"            // variable not dropped until program ends

Why/When?

Lifetimes prevent dangling references.
If you return a reference to an object created within an inner scope (on the stack?),
that object would normally be dropped when it's scope ends.

from rust book

{                  //  --+ outer_scope
    let r;         //    |
    {              //    | -+ inner_scope
        let x = 5; //    |  |
        r = &x;    //    |  |
    }              //    | -+
}                  //  --+

You can instead attach a lifetime to it, that effectively says:
don't drop this object, until this other object is dropped.

Function Example

use rand::Rng;

// tell the compiler not to drop the returned reference
// until the earliest dropping of either params `a` or `b`.
//
fn choose_random<'a>(a: &'a str, b: &'a str) -> &'a str {
    let list = vec![a, b];
    let i = rand::thread_rng().gen_range(0..=1);
    list[i]
}

let val = choose_random("abc", "def");
println!("{}", val);

Struct Example

In addition to prolonging an object's lifetime, you can shorten it.
structs that use references must also be assigned a lifetime.

// instances of 'Foo' must not live longer than it's 'bar' value
struct Foo<'a> {
    bar: &'a str,
}

Static Lifetime

The static lifetime means that a reference will live for the full duration of the program's execution.

let s: &'static str = "invulnerable";

Lifetime Ellision

Rust has rules to automatically infer/assign lifetimes based on a method signature.
These rules are referred to as lifetime ellision

1. a lifetime is assigned to every parameter that is a reference


fn foo(one: &str, two: &str) {}

// is compiled as

fn foo<'a, 'b>(one: &'a str, two: &'b str)


2. if there is only one lifetime param, that lifetime is assigned to the return-value


fn foo(one: &str) -> &str {}

// is compiled as

fn foo<'a>(one: &'a str) -> &'a str {}


3. if there are multiple lifetime params, and one is &self/&mut self, that lifetime is assigned to return-value


impl MyStruct {
    fn foo(&self, one: &str) -> &str {}
}

// is compiled as

impl MyStruct {
    fn foo<'a, 'b>(&'a self, one: &'b str) -> &'a str {}
}


Drop Early

You can manually drop an object before it falls out of scope with:

drop(somevar);

Traits

The majority of the traits affecting ownership are covered in rust pointers