Storage Locations

• Registers: (and pseudo-registers) Contain 64-bit values that are being used in the current computation. Are nowhere in memory (notionally). Not really visible in Rust

• Global Storage: Allocated and initialized at program startup. Rust doesn't use this much. The static keyword denotes global storage

• The Stack: Each time a function is called, a "stack frame" is allocated. Things that are temporary to that function are stored there. When the function returns, it is all lost. let variables and function parameters are stack-allocated

• The Heap: Area of dynamically allocated memory. OS and language runtime cooperate to allocate enough to run and to reclaim unused storage. Rust's Box<T> type has its contents heap-allocated: other Rust types such as String and Vec are built out of Boxes.

Standard Memory Management

• Programming without dynamic memory allocation and deallocation?

• Two standard dynamic memory models

• Automatic: Garbage collector or reference counting system

• Garbage Collector: When low on memory, trace out all accessible memory, free non-accessible

• Reference Counting: Keep track of how many references to a particular chunk of memory. When count goes to zero, free it

• Manual: Programmer keeps track of which memory should be preserved, allocates new memory, frees old

Rust Memory Management

• Invisible Manual: Compiler issues code to allocate memory and free memory where needed

• This is restrictive: programmer must ensure that memory is not allocated too late or freed to early, in the presence of pointers

• Rust compiler ensures that memory is allocated before use, statically unavailable at time of free

• Key is lexical scope: when a variable statically leaves scope, its value is no longer reachable, so freed

• Example

Memory Allocation

• By default, a value must be allocated in memory unless it is small enough to fit in a register and marked copyable and bleah bleah bleah

• Choices are stack allocation or heap allocation: default is stack

• Heap allocation is ultimately done in unsafe code

• Drop trait allows explicit actions during deallocation

Copyable Values

• If a type has the Copy trait (e.g. the integer types) the compiler will feel free to make a (deep) copy of it whenever convenient

• If a type has the Clone trait (e.g. most built-in types) the compiler will make a (deep) copy whenever the type's clone() method is called

• Otherwise there will be no user-visible copying

Moves

• The compiler may choose to insert code to (notionally) move a thing to a different place in memory

• If this happens, it will not make a copy: it will leave the old thing uninitialized and unreferenceable, and then reclaim its storage

• Example

Ownership

• Net effect of all this: at any given time a value is "owned" by a particular name

• The value is given to the owning name when it is created (Resource Acquisition Is Initialization = RAII)

• The value is freed when the owning name leaves scope

• Ownership can be transferred by a move

Mutability

• By default, an owned thing is expected to be left alone

• To change its insides, the owned thing must be marked mut. This is essentially just a compiler sanity check

• A mut thing is deep-mutable: any of its owned insides can be changed

• Rust tracks ownership via explicit or implicit lifetime specifiers

• Format is 'a, read "tick a"

• Compiler's "borrow checker" tracks lifetime and ownership of values, throws a static error when can't work

• Usually implicit, but can be made explicit when needed / wanted

  struct S<'a> {
field: T<'a>,
}


The Takeaway

• Need to develop an operational mental model of ownership and lifetimes

• When confused, refer to that model or get help

• Can take a reference to an owned thing

  let y = 5;
let ry = &y;

• Now you have an obligation: ry must not outlive y

• C/C++ will happily let you write things like

int y = 5; int *ry = &y; return ry;

even though the returned pointer is now pointing into whereever on the stack the now-lost y was

• Rust compiler tracks this for you, so that e.g.

  let y = 5;
let ry = &y;
return ry;


will give a compile-time error

Mutable Refs

• Refs come in two varieties: "mutable" and "immutable" (really "exclusive" and "shared")

• Can only take mutable ref to mutable thing

  let mut y = 5;
let ry = &mut y;

• Once you take a mutable ref, you've essentially "borrowed" the value referred to

• Must not go out of scope
• Cannot take any more references while it is live
• Owner can't do anything with the value while it is live (read it, change it, move it, drop it)

Immutable Refs

• An immutable ref is essentially "shared". You can take lots of them if you want

  let y = 5;
let ry1 = &y;
let ry2 = &y;

• You are still restricted for safety

• Owner cannot drop or move value (nor mutate, duh) while refs are live

• A lot of automatic derefing happens

• With the "." structure / enum operator
• With arithmetic operators
• etc
• playground
• There's no such thing as a "null reference" (in safe code): no way to produce one, no need to guard against them

• If you need a "nullable" value, use the Option type

let y = 10; let ory = Some(&y);

• This is true for refs or anything else

• You can get a reference to an anonymous variable implicitly defined by an expression

  let ry = &10;


• The machinery that the compiler uses to check lifetimes is by default "under the hood": does the checking for you without intervention

• Sometimes, though, you need (or want) to get explicit access to that machinery to allow a program to compile that is safe but won't by default

• "Named lifetimes" start with a tick, e.g. 'a ("tick-a")

• In many contexts, explicit lifetime names can be declared

  fn f<'a, 'b>(x: &u64, y: &u64) -> &u64

• These names can then be used to describe lifetime constraints for referenced data

  fn f<'a, 'b>(x: &'a u64, y: &'b u64) -> &'a u64


In this example, the returned data must not be used after x dies

  fn f<'a, 'b: 'a>(x: &'a u64, y: &'b u64) -> &'a u64


We can also require that result data not be used after y dies

  fn f<'a, 'b: 'a>(x: &'a u64, y: &'b u64) -> &'b u64


https://play.rust-lang.org/?gist=59636f6153699652df21d05ea61f3428&version=stable

Rust Parametric Types

• Actually "monomorphic" or "template" types, but…

• Just like we can provide lifetime variables, can provide type variables to get "generic" thingies

  fn id<T>(x: T) -> T {
x
}


Can now call with whatever type as long as they match

  assert_eq!(id(5u32), 5u32);
assert_eq!(id("hello"), "hello");

• Much more to say on this topic later in the quarter