Rust Memory Model — The foundation
From last couple of years Rust is growing it’s popularity because of the performance and memory safety gain it has proven in various success stories. Despite being language with steep learning curve lots of tech companies and individual developers are super enthusiastic about this language adoption. I have been using this language more than a year now and witnessed couple of great performance achievements by services migrating in Rust, gaining huge savings in Infra cost and smooth performance with peak loads.
Source: Google
Here I will share my learning about this language and try to learn more in the course of creating a series of blogs to understand the nuances, rules and complexity of this language.
Memory model in Rust runs without a Garbage collector, Rust compiler do the heavy lifting to make sure you don’t end up writing a piece of code which leaks memory once the execution is done. To achieve this it enforces a set of rules at compile time with very descriptive error messaging but to understand those error message you need to know what is going on behind the scene.
To understand the two major concepts of Rust Ownership and Borrowing, it is important to know during the execution of a program where its variable values are stored, Stack or Heap.
A fixed size data in Rust always get stored in Stack, whereas if the size of data is unknown at the compile time it must be stored in the heap.
Stack data allocation is simple last in, first out. In stack, retrieval and creating copy of the data is easy hence in most of the cases Primitive type data stored in Stack get copied on assignment,
fn main() {
let x = 5; // x is stored in stack memory let y = x; // x is copied to y
println!("x: {}, y: {}", x, y); // Both x and y are valid
}// out of scope, x and y are dropped
So as the example showing x and y both has it’s ownership of data and it will compile and run without any error. Now consider this example,
- [derive(Debug)]
struct Axis {
x: i32, y: i32,
}
fn main() {
let axis = Axis { x: 15, y: 14 }; // Axis is on the stack
let axis2 = axis; // Axis is moved to Axis2, Axis is now invalid
println!("{:?}, {:?}", axis, axis2); // error: use of moved value: `Axis`
}
Even though struct Axis has fixed size of data, it is stored in the stack as well but here when we are doing the assignment let axis2 = axis; the data is not copied but moved. Later we will understand what “move” is, but for now let’s understand why it is not copied.
To make the above Axis example work you need to implement a Copy trait for this struct similar to all the Primitive data type which implements this Copy trait by default,
- [derive(Copy, Clone, Debug)]
struct AxisCopy{
x: i32, y: i32,
} fn main() {
let axis = AxisCopy { x: 14, y: 12 }; // AxisCopy implements the Copy trait
let axis2 = axis; // Axis is copied to Axis2, both Axis and Axis2 are valid
println!("{:?}, {:?}", axis, axis2); // prints "AxisCopy { x: 14, y: 12 }, AxisCopy { x: 14, y: 12 }"
}
These simple examples show a very powerful concept of Rust Ownership’s rules,
- There can only be one owner at a time.
- Each value in Rust has an owner.
- When the owner goes out of scope, the value will be dropped
Once the value get assigned to a variable, it’s ownership started. The ownership ends with it’s scope. Here check this out in action,
- [derive(Debug)]
struct DropMe;
impl Drop for DropMe {
fn drop(&mut self) {
println!("Dropping!");
}
}
fn main() {
println!("Outer Scope Starts");
{
println!("Inner Scope Starts");
let x = DropMe;
println!("x: {:?}", x); // Scope of x ends here
}
println!("Outer Scope Ends"); // Scope of main ends here
}
Rust automatically calls the destructor for all the contained fields. Now in Axis example we have used the word “move” for variable assignment, what does that mean? Let’s understand this with example of heap allocated and growable type defined in Rust String,
let str = String::from("hello"); // hello stored in heap memory let str1 = str; // str is moved to str1
println!("{}", str); // This would cause a compile-time error because str is no longer valid
Unlike the stack memory where data can be simply copied, heap memory requires a different approach due to its dynamic nature. In heap memory storing an object is like finding an empty table in a restaurant for fixed number of people and if anyone comes late a host need to guide that person to that table, hence copying the data like stack is not an option here. We need maintain a pointer which will work as a host and the pointer with be store in stack.
As the size of string is unknown at compile time to memory allocation happened at the runtime in the heap like this, where as stack has pointer to the memory, length and capacity.
Stack Heap ────────────────────────────── ───────────────────────
str
┌────────────────────────────┐
│ ptr ───────────────┐ │
│ len: 5 │ │
│ capacity: 5 │ │
└────────────────────┘ ▼
┌────────────────────────┐
│ 'h' 'e' 'l' 'l' 'o' │
└────────────────────────┘
Now when we assigned str to str1 we do not copy like fixed size integers but we copy the data stored in stack for this heap storage which is pointer, length and capacity. As we discussed above when a variable goes out of scope Rust calls the destructor to clean up the memory but in this case str and str1 has pointer to same memory, which means it will try to double free the same memory causing memory corruption. To avoid such situation just after the line let str1= str Rust consider str moved, no longer valid.
Stack Heap ────────────────────────────── ────────────────────────────
str ❌ (moved)
str1
┌────────────────────────────┐
│ ptr ───────────────┐ │
│ len: 5 │ │
│ capacity: 5 │ │
└────────────────────┘ ▼
┌──────────────────────┐
│ 'h' 'e' 'l' 'l' 'o' │
└──────────────────────┘
Here is the error message you get on accessing the variable after it is moved, error[E0382]: borrow of moved value: `str`
--> src/main.rs:15:22 |
11 | let str = String::from("Hello, world!");
| --- move occurs because `str` has type `String`, which does not implement the `Copy` trait
12 | 13 | let str1 = str;
| --- value moved here
14 | 15 | println!("{:?}", str);
| ^^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
13 | let str1 = str.clone();
| ++++++++
Rust error messages are super helpful and self explanatory only if you know what you are doing. Here it is suggesting two ways to fix this issue,
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!
Now clone() is expensive and explicit action and provide a type specific behavior as in this case clone creates a copy of pointed buffer in the heap memory. Call this method when you need an independent ownership or the data need to outlive the original value.
Clone is a supertrait of Copy, so everything which is Copy must also implement Clone. This we have seen above in the Struct copy example, now copy can only take care of things in cases of implicit call like assignment or passing call by value it is not overloadable.
Now the second way to fix the String issue is by passing the reference. In Rust this concept is called Borrowing, it has some strict rules around and a borrow checker implementation is there to make sure that these rules are getting followed at compile time itself. We will deep dive into this topic in my next blog post. Stay tuned and happy coding !
Read the full article here: https://towardsdev.com/rust-01-memory-model-the-foundation-09899c37ba26