No history yet

Ownership and Stacks

From Garbage Collection to Ownership

Coming from languages like Python or TypeScript, you're used to a garbage collector cleaning up memory for you. It's a convenient safety net. Rust takes a different path. It manages memory through a system of ownership, with a set of rules the compiler enforces at compile time. This means memory safety issues are caught before your code even runs, without the runtime performance cost of a garbage collector.

To grasp this, we first need to understand where your program's data lives: on the stack or on the heap.

The Stack and the Heap

The stack is for fast access. All data stored on the stack must have a known, fixed size. Think of it like a stack of plates: the last one you add is the first one you take off. This Last-In, First-Out (LIFO) model is incredibly efficient for managing function calls and local variables.

Primitive types like integers (i32), booleans (bool), and characters (char) live on the stack. Their size is known when the program is compiled.

Lesson image

The heap is less organized. It's used for data whose size might change or is unknown at compile time. When you need to allocate memory on the heap, you ask the operating system for a certain amount of space. It finds an empty spot big enough and returns a pointer to that location's address. This pointer—a fixed-size address—is stored on the stack. Following the pointer to get to your data is slower than accessing data directly on the stack because it involves an extra step.

In Rust, the String type is a classic example of heap-allocated data because its text content can grow. A String is actually a struct containing a pointer to the text on the heap, along with its length and capacity.

The Rules of Ownership

Rust's memory management revolves around three core rules:

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

These rules are the foundation of Rust's safety guarantees.

Let's see what happens when we try to break rule #2. In Python, assigning a variable to another creates a new reference to the same object. Rust behaves differently.

let s1 = String::from("hello");
let s2 = s1;

// This line will cause a compile-time error!
// println!("{}", s1);

When we assign s1 to s2, Rust doesn't just copy the pointer. It performs a move. Ownership of the String data on the heap is transferred from s1 to s2. After the move, Rust considers s1 to be uninitialized, preventing us from accidentally using it again. This elegantly prevents a common bug called a double free error, where two variables might try to free the same memory.

This is a fundamental shift from the reference counting used in Python. In Python, every variable is a reference, and the runtime keeps track of how many references point to an object. When the count hits zero, the garbage collector frees the memory. Rust's move semantics achieve a similar goal at compile time, without the runtime overhead.

Automatic Cleanup with Drop

So if there's no garbage collector, how does Rust clean up heap memory? This is where rule #3 and the Drop trait come into play.

When a variable that owns heap data goes out of scope, Rust automatically calls a special function called drop. You can implement the Drop trait for your own types to specify what should happen when an instance is no longer needed, like releasing files, network connections, or memory.

struct MySmartPointer {
    data: String,
}

// Implementing the Drop trait for our custom type
impl Drop for MySmartPointer {
    fn drop(&mut self) {
        // This code runs automatically when the pointer goes out of scope
        println!("Dropping MySmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = MySmartPointer { data: String::from("some data") };
    println!("MySmartPointer created.");
    // `c` goes out of scope here, and `drop` is called.
}

This is known as (RAII). It's a powerful pattern that links the lifetime of a resource directly to the lifetime of an object. As soon as the owner goes out of scope, its resources are freed. It’s deterministic and predictable, giving you fine-grained control over when resources are released.

The ownership system is Rust's most unique feature. It might take some getting used to, but it's the key to writing safe, fast, and concurrent code without a garbage collector.