Rust memory management

From wikinotes
Revision as of 04:10, 11 February 2023 by Will (talk | contribs) (→‎Drop Early)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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