Ownership in Rust
A quick rundown of concepts in Rust ownership system.
Memory management in Rust
The runtime memory of a Rust program consists of two parts: a stack and a heap
Stack:
- All variables are stored in the stack.
- Variables are organized using frames in the stack. Each frame contains mapping from variables to values within a single scope.
- After a function returns (or scope ends), the frame corresponding to that function is dropped.
- Last frame added is dropped first (LIFO)
- We can store scalar types, references (pointers), collections of scalar types with fixed size as values in the stack.
Heap
- Heap is used to store values with unknown size or values whose size can change.
- We can use
Box
es to store values in the heap. For example, let i = Box::new(5), stores the integer value 5 in the heap and saves its reference in variable i. - A value in heap is deallocated when Rust deallocates the frame containing variable which owns that value.
Ownership system
Rust uses a set of rules at compile time to check for possible undefined behaviours in the program. This check ensures that the program can be run safely and efficiently.
Ownership rules
-
Each value in Rust has a variable that’s called its owner.
-
There can only be one owner at a time.
let s1: String = String::from("Hello"); // `s1` is the owner of the string "Hello". let s2: String = s1; // `s2` is now the owner of the string "Hello". // println!("s1 was {s1}"); // Cannot use `s1` as it does not point to anything. println!("s2 is {s2}"); let x: i32 = 5; // `x` is the owner of the value 5. // Variables stored in stack are copied instead of moving. let y: i32 = x; // `y` is now the owner of copy of value 5. println!("x is {x} and y is {y}");
-
Variables cannot be used after being moved.
let s1: String = String::from("Hello"); let s2: String = s1; let s3: String = s2.clone(); // Following line will throw an error // Value of s1 was moved to s2, so s1 does not point to any value. // println!("s1 is {s1}"); println!("s2 is {s2} and s3 is {s3}"); // this works as `clone` does not move value of s2 let x: i32 = 5; // x is the owner of the value 5. // Variables stored in stack are copied instead of moving. let y: i32 = x; // y is now the owner of copy of value 5. println!("x={x} and y={y}"); // This works as value of x was never moved.
-
When the owner goes out of scope, the value will be dropped.
fn greet(){ // name is the owner of "John". // Heap contains the string "John" // Stack contains the variable `name` which stores reference to "John" in the heap let name: String = String::from("John"); println!("Hello {name}!"); } // Frame for `greet` function with `name` variable is dropped here. // As `name` was the owner of "John" string in the heap, the string value is also dropped.
References in Rust
-
References can be used to pass variables without moving or cloning. References can be created by adding
&
as prefix to variable names.fn main(){ let s1 = String::from("Hello"); let s2 = String::from("John"); // With references greet_with_references(&s1, &s2); println!("s1={s1}, s2={s2}"); // this works as `s1` and `s2` are still owners of "Hello" and "John" strings. // Without references greet(s1, s2); // println!("s1={s1}, s2={s2}"); // This line will throw an error as s1 and s2 are empty pointers } fn greet(greeting: String, name: String) { // `s1` and `s2` will be moved to `greeting` and `name` respectively. println!("{} {} from greet!", greeting, name); } // "Hello" and "John" strings are deallocated with their owners `greeting` and `name`. fn greet_with_references(greeting: &String, name: &String) { // `greeting` and `name` point to references of `s1` and `s2` values respectively. // Ownership of "Hello" and "John" strings is not transferred to `greeting` and `name`. println!("{} {} from greet_with_references!", greeting, name); }
-
References, like normal variables, are immutable by default. We can create mutable reference by using
mut
keyword. -
Dereference operator can be used to get the value of a reference.
Reference permissions
-
Rust has a borrow checker that runs at compile time and looks for permission violations for variables.
-
Each variable can have three types of permissions at compile time:
- Read - Allows data to be read and copied to another location.
- Write - Allows data to be mutated in-place.
- Own - Allows data to be moved and dropped.
-
By default, a variable has “Read” and “Own” permissions and mutable variable has additional “Write” permission.
-
Creating references can temporarily remove these permissions:
-
Creating an immutable reference to a variable removes its “Write” and “Own” permissions.
-
Permissions removed by creating a reference are restored at the end of reference’s lifetime.
We can check this by simply changing the order of last two lines in the above example.
let s1: String = String::from("Hello"); // `s1` has Read and Own permissions let s2: &String = &s1; // Own permission is removed from `s1` println!("{} world from s2!", s2); // `s2` is used for the last time here. let s3: String = s1; // This move works as Own permission is restored for `s1`. println!("{} world from s3!", s3);
-
Creating a mutable reference to a variable removes all its permissions and the reference is given unique and non-owning access to variable’s data.
let mut s1: String = String::from("Hello"); // `s1` has Read and Own permissions let s2: &mut String = &mut s1; // Own permission is removed from `s1`. // println!("{} world from s1!", s1); // won't work as `s1` does not have Read permission s2.push_str(" world from s2!"); // `s2` has Read and Write permissions for "Hello" String println!("{s2}");
-
-
Permissions can also be defined on paths and not just variables. A path is anything you can put on the left-hand side of an assignment.