Rust Memory Model — Borrowing and References
In this we will try to further deep dive into Reference and Borrowing a crucial key concept in learning Rust.
Let’s start with the previous example,
let str = String::from("Hello, world!");
let str1 = str.clone(); // Cloning str to create a new owned String let str2 = &str; // Borrowing str, no move occurs
println!("{:?}", str); // Hello, world! println!("{:?}", str1); // Hello, world! println!("{:?}", str2); // Hello, world!
We understood about Clone for fixing this issue but also got to know how expensive it is, now the second approach to tackle this very problem with help of references, called Borrowing. Here we pass the reference of value to the borrower, when we are passing the reference it is not pointer to the value but reference to the pointer stored for that value.
Now this example proves the above theory, fn calculate_len(s: &String) -> usize { // s is a reference to a String
s.len()
}
fn main() {
let str = String::from("hello");
let str1 = &str; let len = calculate_len(str1);
println!("{} and {}", len, str1);
} Since str1 is pointing to a pointer stored in stack memory, the move concept didn’t applied to it when it is passed in calculate_len function as this doesn’t own any heap data, moreover it is interesting that immutable reference also implement Copy trait, hence it is still accessible in the last print statement.
This type of referencing called Immutable borrow or aliasing and you can have multiple immutable references at any given time, on its own this is not a problem. But when it is combined with Mutation, it gets complicated. Consider this example in JavaScript,
let arr = [1, 2, 3, 4];
let arr1 = arr;
arr.push(5); console.log("arr", arr); // Prints [1, 2, 3, 4, 5] console.log("arr1", arr1); // Prints [1, 2, 3, 4, 5] Now same in Rust, fn main() {
let mut arr = vec![1, 2, 3];
let arr1 = &mut arr;
arr.push(4);
println!("{:?}", arr1);
} It will give you this error, error[E0499]: cannot borrow `arr` as mutable more than once at a time
--> src/main.rs:15:5 |
13 | let arr1 = &mut arr;
| -------- first mutable borrow occurs here
14 | 15 | arr.push(4);
| ^^^ second mutable borrow occurs here
16 | 17 | println!("{:?}", arr1);
| ---- first borrow later used here
For more information about this error, try `rustc --explain E0499`. The other type of borrowing what we are seeing here let arr1= &mut arr; is called Mutable borrow, as the name suggest mutable where you can changes the referenced data. To ensure Pointer Safety Principal Rust has strict rules around the borrowing and to make sure it is followed at compile time it has Borrow checker.
In JavaScript arr got modified and arr1 has no clue that its value has been changed. In Rust this is not possible, the borrow checker will flag this at compile time itself.
Let’s understand why this is an issue, in above example reason for not allowing this is simple, arr is stored in heap memory with fixed size 3 and capacity 3 but the movement you push a new element in this it might need to search for new memory location with size 4 and if some other variable it pointing to this memory location, they might end up pointing to freed memory.
As per the Rust borrow checker principles to ensure “Pointer Safety Principle”,
- Multiple immutable borrows or one mutable borrows at a time
- Never both simultaneously
Just to summarize what we have learned so far checkout this example, fn print_length(s: &String) {
println!("The length of '{}' is {}.", s, s.len());
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
fn main() {
let mut s1 = String::from("hello");
// aliasing (storing reference)
let mutable_borrow = &mut s1;
// function call(temporary use, scope ends after call)
change(mutable_borrow);
// aliasing (storing reference)
let immutable_borrow = &s1;
// function call (temporary use)
print_length(immutable_borrow);
// reference still valid here
println!("immutable_borrow: {}", immutable_borrow);
println!("{}", s1);
let s1_moved_here = s1;
// use after move of s1
println!("immutable_borrow: {}", immutable_borrow);
println!("s1_moved_here: {}", s1_moved_here);
let will_be_dangling: &String;
{
let temp_string = String::from("temporary");
will_be_dangling = &temp_string;
} // temp_string is dropped here
// Dangling reference use
println!("will_be_dangling: {}", will_be_dangling);
}
It is important to understand the lifetime of the borrower before using it, a function call ends the lifetime of variable when function returns hence after that it is possible to create further references. This is how borrow checker thinks,
- It tracks the lifetime of the borrows( how long a reference lives)
- Ensuring that while a borrow is active, - If an immutable(&T) borrow — you can not mutate the underlying data, only have read/own permission so that you can move/drop it. - If a mutable(&mut T) borrow — you will have permission to read/own/write but you can not have any other active borrow while this is active.
- Ensures that you can not use the moved values (once ownership is transferred)
- Ensures that references don’t outlive the data they refer to prevent the use of dangling pointers.
This book uses a nice analogy of assigning the permission to variables for explaining how borrow checker works, check out if you interested in further deep dive.
Let’s understand a bit about references here, which will help clarifying what points to which memory. This will be helpful to understand the borrowing rules, fn main() {
let mut x = Box::new(2); // store integer 2on the heap
let r1 = &x; // reference to the box
let a= **r1; // dereference the reference to get the box, then dereference the box to get the value
let r2 = &*x; // reference to the value inside the box
let b= *r2; // dereference the reference to get the value
print!("{} {} {}", a, b);
}
In this example if you see even though r1 borrowed x but to reach till the value a has to de-reference two times. Similarly to get the reference of value r2 has to de-reference it and then borrowed the pointer to value. I hope this blog post gives you a basic understanding of borrowing and reference in Rust, next blog we will discuss about the Lifetime concept in Rust.