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

Lifetimes

  • 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

Refs, Ownership, Lifetimes

  • 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

More About Refs

  • 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;
    

Explicit Lifetimes

  • 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

Book Example

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

Last modified: Monday, 1 July 2019, 1:39 AM