minami

Understanding Rust’s Memory Model

So I have been working with garbage collected programming languages mostly. I have written decent amount of C++ but that was during my uni days when I was solving LeetCode problems and college assignments. Recently I started learning Rust and here is my understanding of what’s happening beneath the abstractions.

You can see this as a blog where I guide you through what’s memory and how memory is allocated in Rust. This is what I understood so if you think I’ve written something wrong or can be improved please dm me on X/Twitter.

Before that WTF is Memory?

Ok so consider memory as a massive array where each element can hold 8 bits of data. This data is nothing but an address.

Address  | Value (in binary)
---------|------------------
0x1000   | 01110011  (115 in decimal, 's' in ASCII)
0x1001   | 01101100  (108 in decimal, 'l' in ASCII)
0x1002   | 01101111  (111 in decimal, 'o' in ASCII)
0x1003   | 01110000  (112 in decimal, 'p' in ASCII)

The Stack: Fast and Organized

So stack works like you can only add/push or remove/pop from the top. It is extremely fast because the CPU just needs to move a pointer up or down.

When we create a variable in Rust (like below) the all go to the stack memory:

fn main() {
	let x: i32 = 100;
	let flag: bool = true;
	let point: f64 = 0.5;
}

And here is how the memory looks like:

Stack
+---------------------+  <-- Top of Stack (higher address)
|     point = 0.5     |  f64 (8 bytes)
+---------------------+
|     flag = true     |  bool (1 byte + padding)
+---------------------+
|       x = 100       |  i32 (4 bytes)
+---------------------+  <-- Bottom (lowest address for this frame)

Some things to note here:

The Heap: Slower but Spacious

Heap is a dynamic memory region where you can request space at runtime. Instead of storing data directly you get a pointer to the allocated memory.

fn main() {
    let v = vec![65, 54, 57];
    let s = String::from("slop");
}
Stack                      Heap
┌─────────────────┐       ┌─────────────────┐
│ s: String       │       │ "slop" (4B)     │ ← 0x5000
│  ptr: 0x5000 ───┼───────┤                 │
│  len: 5         │       └─────────────────┘
│  capacity: 5    │
├─────────────────┤       ┌────────────────────┐
│ v: Vec<i32>     │       │ 65 │ 54 │ 57 │     │ ← 0x4000
│  ptr: 0x4000 ───┼───────┤    │    │    │     │
│  len: 3         │       └────┴────┴────┴─────┘
│  capacity: 3    │
└─────────────────┘

So here notice one thing, that the stack has a pointer “ptr” that points to the actual data on the heap.

Ownership & Memory

Lets take an example:

let x = String::from("x");
let y = x;

Before the let y=x; here is how the memory layout would look like:

Stack                      Heap
┌─────────────────┐       ┌─────────────────┐
│ x: String       │       │ "x" (1B)        │
│  ptr: 0x1000 ───┼───────┤                 │
│  len: 1         │       └─────────────────┘
│  capacity: 1    │
└─────────────────┘

After let y = x; what happens is x is moved to y so:

Stack                      Heap
┌─────────────────┐       ┌─────────────────┐
│ x: (invalid)    │       │ "x" (1B)        │
├─────────────────┤       │                 │
│ y: String       │       │                 │
│  ptr: 0x1000 ───┼───────┤                 │
│  len: 1         │       └─────────────────┘
│  capacity: 1    │
└─────────────────┘

Why is x invalid!?!?

Rust compiler invalidates x to prevent:

Borrowing & Memory

Lets take one more example (here y will borrow x):

let x = String::from("tpot");
let y = &x;
Stack                      Heap
┌─────────────────┐       ┌─────────────────┐
│ x: String       │       │ "tpot" (4B)     │ ← 0x5000
│  ptr: 0x5000 ───┼───────┤                 │
│  len: 4         │       └─────────────────┘
│  capacity: 4    │
├─────────────────┤
│ y: &String      │
│  ptr: &x        │
└─────────────────┘

What are references?

Box: Explicitly Heap Allocating

Box is a smart pointer that lets you put a value on the heap explicitly

let b = Box::new(5);
Stack                      Heap
┌─────────────────┐       ┌─────────────────┐
│ b: Box<i32>     │       │ 5 (4 bytes)     │ ← 0x6000
│  ptr: 0x6000 ───┼───────┤                 │
└─────────────────┘       └─────────────────┘

Copy in Rust

Some types implement the Copy trait and are duplicated instead of moving them

let x = 100;
let y = x;
// x & y both a valid cause y is an independent copy

Types that implement Copy (there might be more so crosscheck plz):

You noticed one thing!? Only those data types that are allocated on the Stack implement Copy trait. This is what I noticed, might be true/false.

Memory Leaks in Rust

But Rust is memory safe right!? Yes, Rust prevents user after free and double-free but we can still create reference cycles with Rc<T> and RefCell<T>

struct Node {
	next: Option<Rc<RefCell<Node>>>,
}

In the above code, it creates a cycle that won’t be freed.When two Rc pointers reference each other in a cycle, their reference counts never reach zero which means the memory is never freed. Why is that!? Cause Rc uses reference counting and to break cycles it uses Weak<T> for uni directional relationship which does not prevent deallocation when the strong reference is dropped.

TLDR ;)