Ownership and Borrowing
Rust’s unique approach to memory management
What is memory management?
(and why is it hard?)
Little vs Big Data
Little Data
- fixed size, known at compile time
bool,char,i32,f64,usize, …- copies quickly
Big Data:
- variable size, unknown at compile time
String,Vec<T>,HashMap<K,V>, …- copies slowly
Most PLs manage memory with the stack and heap.
Little Data Lives on the Stack
fn main() {
let n = 5; // L1
let y = inc(n); // L3
println!("n: {}, y: {}", n, y);
}
fn inc(x: i32) -> i32 {
x + 1 // L2
}
Variables live in stack frames mapping variables to values
- At
L1frame formainholdsn = 5 - At
L2frame forincholdsx = 5 - At
L3frame formainholdsn = 5andy = 6
Frames are organized into a stack of currently-called-functions.
- At
L2frame formainabove the frame forinc
Entire frame is freed or deallocated on function return
Little Data Copies Quickly
When an expression reads a little-data variable, its value is copied from its slot in the stack frame
At
L2the value ofnis copied intoyAt
L3the value ofnis left unchanged, even after changingy
Big Data Copies Slowly
Cannot copy big data quickly!
Big data lives on the Heap
Big data is stored on the heap
Can quickly copy references to data!
… but a and b refer to the same data on the heap
Sharing is Hard!
Why?
Sharing with Manual Management (C, C++)
Programmer explicitly alloc and free memory
- Zero runtime overhead
Problem: Unsafe
- Free too early (Dangling references!)
- Forget to free (Leaks!)
- Double-free (Vulnerabilities!)
Sharing with Garbage Collection (Java, Python, Haskell)
Runtime that looks for and reclaims unused memory
- Memory safe (can only use “valid” memory that has not been freed)
as the program runs; in other languages,
Problem: Unpredictable
- GC can kick in and introduce pauses at unpredictable times
Sharing breaks local reasoning
(But my personal reason…)
Ground can change beneath your feet!
(Makes concurrency really hard)
Rust’s Secret Sauce: Ownership
The key idea in Rust’s ownership system is in three rules:
- Each value in Rust has an owner.
- There can only be a single owner at any time.
- Value is dropped (reclaimed) when owner goes out of scope.
Unique value proposition: memory safety without GC
Ownership Ingredients
- Scope
- Drop
- Ownership
- Move/Transfer
1. Scope
Scope is the “area” within a program for which a name is valid.
{ // s not valid here; not yet declared
let s = String::from("hello"); // s is valid from this point on
// ...
// do stuff with s
// ...
} // scope is over; s no longer valid
2. Drop/Free when Owner Goes Out of Scope
Scope is the “area” within a program for which a name is valid.
{ // s not valid here; not yet declared
let s = String::from("hello"); // s is valid from this point on
// ...
// do stuff with s
// ...
} // scope is over; DROP/FREE `s`
3. (Unique) Ownership
Heap data has a single unique owner … assignment transfers or moves ownership.
QUIZ
What will be printed by this code?
fn main() {
let a = String::from("hello"); // `a` is the unique owner
let b = a; // ownership of heap data MOVED to `b`
println!("a is {a}"); // what will be printed?
}
4. Move/Transfers Ownership
Assignment moves ownernship … cannot then use a moved value
fn main() {
let a = String::from("hello"); // `a` is the unique owner
let b = a; // ownership of heap data MOVED to `b`
println!("a is {a}"); // what will be printed?
}Compiler rejects with error:
error[E0382]: borrow of moved value: `a`
--> src/main.rs:20:20
|
18 | let a = String::from("hello");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
19 | let b = a;
| - value moved here
20 | println!("a is {a}");
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
19 | let b = a.clone();
| ++++++++Notes
Stringdoes not implement theCopytrait.- Consider
.clone()if you want to copy data instead of moving it.
4. Clone (if you really can’t move …)
Assignment moves ownernship … cannot then use a moved value
… if you must then you have to explicitly clone the data
fn main() {
let a = String::from("hello"); // `a` is unique owner
let b = a.clone(); // `b` is unique owner of a clone
println!("a is {a}"); // OK
}
Recap: Copy vs Move
Consider these two examples:
Little Data Is Copied
{
let x = 5; // x comes into scope
let y = x; // x is copied into y
println!("x is {x}"); // x and y are both valid
}Big Data is Moved
let x = String::from("hello"); // x comes into scope
let y = x; // x is moved into y
println!("x is {x}"); // ERROR: `x` value is moved!
QUIZ
What happens on assignment?
fn main() {
let s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");
}
Cannot Assign to Immutable Variable
let x creates an immutable variable by default – you cannot re-assign it!
fn main() {
let s = String::from("hello"); // s comes into scope
s = String::from("ahoy"); // ERROR: cannot assign twice to immutable variable
}Produces an error
error[E0384]: cannot assign twice to immutable variable `s`
--> src/main.rs:7:5
|
6 | let s = String::from("hello");
| - first assignment to `s`
7 | s = String::from("ahoy");
| ^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
6 | let mut s = String::from("hello");
| +++Read the error message carefully!
Re-assigning Variables
fn main() {
let mut s = String::from("hello"); // s comes into scope
s = String::from("ahoy"); // s is re-assigned
// old value DROPPED
println!("{s}, world!"); // prints "ahoy, world!"
}
Mutating Data
String can be mutated (i.e. changed)
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{s}"); // prints "hello, world!"
}QUIZ What happens if we replace let mut with just let s?
QUIZ: Function Calls
What happens when we run?
fn main() {
let mut s = String::from("hello");
call_me_string(s);
println!("{s}, world!");
}
fn call_me_string(x: String) {
println!("Thanks for calling me with {x}!");
}
QUIZ: Function Calls
What happens when we run?
fn main() {
let mut s = 42; // s comes into scope
call_me_i32(s);
println!("{s}, world!"); // prints "ahoy, world!"
}
fn call_me_i32(x: i32) {
println!("Thanks for calling me with {x}!");
}
Fn Parameters: Little Data ==> Copy
What happens when we run?
fn main() {
let mut s = 42;
call_me_i32(s); // `s` is COPY into `x`
println!("{s}, world!"); // prints "42, world!"
}
fn call_me_i32(x: i32) {
println!("Thanks for calling me with {x}!");
}Try it out on aquascope
sis copied into parameterxxgets printed …sis still valid after the call
Fn Parameters: Big Data ==> Move
What happens when we run?
fn main() {
let mut s = String::from("hello"); // s comes into scope
call_me_string(s); // s is MOVED into `x`
println!("{s}, world!"); // compile-time error!
}
fn call_me_string(x: String) {
println!("Thanks for calling me with {x}!");
// x goes out of scope and is DROPPED
}Try it out on aquascope
sis moved into parameterxxgets printed …sis not valid after the call!
QUIZ
What happens when we run the following program?
fn burp() -> String {
let s = String::from("burp"); // s comes into scope
s // s is moved out to the caller
}
fn say_length(z: String) {
let n = z.len(); // get length of z
println!("length of z = {n}");
}
fn main() {
let s1 = burp();
println!("(a) s1 is {s1}");
say_length(s1);
println!("(b) s1 is {s1}");
}
Returns Copy (Little) or Move (Big) Too!
fn burp() -> String {
let s = String::from("burp"); // `s` comes into scope
s // `s` is moved out to the caller
}
fn say_length(z: String) {
let n = z.len(); // get length of `z`
println!("length of z = {n}");
} // `z` goes out of scope and is DROPPED
fn main() {
let s1 = burp(); // burp() moves its return into s1
println!("(a) s1 is {s1}");
say_length(s1); // s1 is moved into say_length()
println!("(b) s1 is {s1}"); // ERROR: s1 value is MOVED!
}QUIZ How to modify say_length so we can print s1 after the call?
Don’t drop, return
fn say_length(z: String) -> String {
let n = z.len(); // get length of `z`
println!("length of z = {n}");
z // `z` MOVED to return value
}
fn main() {
let s1 = burp(); // burp() moves its return into s1
println!("(a) s1 is {s1}"); // prints "(a) s1 is burp"
let s1 = say_length(s1); // s1 is moved into say_length()
println!("(b) s1 is {s1}"); // prints "(b) s1 is burp"
}… but seriously, this is rather clunky!
How to simplify say_length?
Rust’s Secret Sauce: Ownership + Borrowing
Unique value proposition: memory safety without GC
- Each value in Rust has an owner.
- There can only be a single owner at any time.
- Value is dropped (reclaimed) when owner goes out of scope.
To “access” data, no need to take ownership, just borrow
Borrowing with References
&T is a reference to a value of type T
fn say(z: &String) { // L2 `z` is a reference to a String
let n = z.len(); // get length of `z`
println!("length of z = {n}");
} // `z` exits scope, but NOT DROPPED, `z` NOT-OWNERSo say_length takes a reference to a String
fn main() {
let s = read_file("moby_dick.txt"); // L1 read_file() moves its return into `s`
say(&s); // `s` BORROWED by say()
// L3 `s` RETURNED by say()
} // `s` exits scope, is DROPPEDYou can borrow from the owner but you have to give it back
Rust’s Secret Sauce: Ownership + Borrowing
Unique value proposition: memory safety without GC
- Each value in Rust has an owner.
- There can only be a single owner at any time.
- Value is dropped (reclaimed) when owner goes out of scope.
To “access” data, no need to take ownership, just borrow
&Tgives a reference to aTi.e. lets you borrow access toT
Mutating through References
Recall that String can be mutated (i.e. changed) e.g.
fn main() {
let mut s = String::from("yo!");
s.push_str(", world!");
println!("{s}"); // prints out "yo!, world!"
}
QUIZ
What happens if we try
fn main() {
let mut s = String::from("yo!");
change(s);
println!("{s}");
}
fn change(z: &String) {
z.push_str(", world!");
}
Mutating Requires Mutable References (Borrows)
We can only mutate data through a mutable reference &mut T
fn main() {
let mut s = String::from("yo!");
change(&mut s);
println!("{s}");
}
fn change(z: &mut String) {
z.push_str(", world!");
}
QUIZ
What about little data? For example, what happens if we try
fn main() {
let x = 42;
say(&x);
println!("haha {x}");
}
fn say(z: &i32) {
println!("hoho {x}");
}
Prints
hoho 42
haha 42QUIZ
What happens if we try
fn main() {
let x = 42;
change(&x);
println!("haha {x}");
}
fn change(z: &i32) {
z = z + 1;
}
Compiler error!
- Can’t do
z + 1aszis a reference; - Can’t re-assign
zas it is an immutable reference!
(Re)-Assignment Requires Mutable References (Borrows)
We can only reassign data through a mutable reference &mut T
fn main() {
let mut x = 42;
change(&mut x);
println!("haha {x}");
}
fn change(z: &mut i32) {
*z = *z + 1;
}
Rust’s Secret Sauce: Ownership + Borrowing
Unique value proposition: memory safety without GC
- Each value in Rust has an owner.
- There can only be a single owner at any time.
- Value is dropped (reclaimed) when owner goes out of scope.
To “access” data, no need to take ownership, just borrow
&Tgives a reference to aTi.e. lets you read access toT&mut Tgives a mutable reference to aTi.e. lets you write access toT
Both references return ownership to whoever they “borrowed” from…
But why two kinds of references?
QUIZ
Why two kinds of references (&T vs &mut T)?
fn quizA() {
let mut s = String::from("yo!");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
}
fn quizB() {
let mut s = String::from("yo!");
let r1 = &s;
let r2 = &mut s;
println!("{} and {}", r1, r2);
}What happens when you run quizA and quizB?
Shared vs Mutable Borrows
At any given time, you can have either
many shared references(
&T) orsingle mutable reference (
&mut T)
Often called the sharing XOR mutability rule
But … why?
Mutation can Invalidate References
i.e. the “ground can change beneath your feet”
fn quizA() {
let mut s = String::from("yo!");
let r = &s; // shared borrow
change(&mut s); // mutable borrow
println!("{}", r); // reuse shared borrow
}What could change do to s that would make r invalid?
e.g. enlarge it, so it needs to be shifted in memory, or …
Mutation can Invalidate References
i.e. the “ground can change beneath your feet”
fn main() {
let mut v = vec![10, 20, 30]; // create a vector of 3 elements
let last = &v[2]; // shared reference to last elem
println!("last = {}", *last); // prints 30
change(&mut v); // mutate vector, removing last element!
println!("last = {}", *last); // YIKES! what does this print?
}
fn change(z: &mut Vec<i32>) {
z.pop();
}After change, the references last refers to ???
The reference is now dangling or invalid
Sharing XOR Mutability rule prevents this!
Multiple shared references are OK, as none of them can change the data!
QUIZ
What happens when we run the following program?
fn main() {
let mut v = vec![10, 20, 30];
let r0 = &v[0];
let r1 = &v[1];
let r2 = &v[2];
println!("elems are {:?}, {:?}, {:?}", *r0, *r1, *r2);
let mr = &mut v;
change(mr);
change(mr);
println!("v is {:?}", v);
}
fn change(z: &mut Vec<i32>) {
z.pop();
}
Sharing XOR Mutability
fn main() {
let mut v = vec![10, 20, 30];
let r0 = &v[0];
let r1 = &v[1];
let r2 = &v[2];
println!("elems are {:?}, {:?}, {:?}", *r0, *r1, *r2);
let mr = &mut v;
change(mr);
change(mr);
println!("v is {:?}", v);
}
fn change(z: &mut Vec<i32>) {
z.pop();
}Q Why is programm accepted by compiler?
A The shared borrows r0, r1, r2 are unused after the first println!,
so the compiler considers them out of scope before the mutable borrow mr is created!
Sharing XOR Mutability
fn main() {
let mut v = vec![10, 20, 30];
let r0 = &v[0];
let r1 = &v[1];
let r2 = &v[2];
println!("elems are {:?}, {:?}, {:?}", *r0, *r1, *r2);
let mr = &mut v;
change(mr);
change(mr);
println!("r1 is {:?}", *r1);
}Q Why is program rejected by compiler?
A The shared borrows r1 overlaps with the mutable borrow mr because r1 is used after mr is created!
Rust’s Secret Sauce: Ownership + Borrowing
Unique value proposition: memory safety without GC
- Each value in Rust has an owner.
- There can only be a single owner at any time.
- Value is dropped (reclaimed) when owner goes out of scope.
To “access” data, no need to take ownership, just borrow
&Tgives a reference to aTi.e. lets you read access toT&mut Tgives a mutable reference to aTi.e. lets you write access toT- At any given time, you can have many shared or single mutable reference
Material Inspired by
- https://rust-book.cs.brown.edu/ch04-01-what-is-ownership.html
- https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html