Rust memory management
Rust uses ownership semantics for memory management.
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/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 endsWhy/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 ellision1. 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);