Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 1

Rust Ownership: Memory Safety Without GC

Recommended resources

Hello! Welcome to your first lesson on embedded development with Rust. We're starting with the absolute cornerstone of the language: the ownership system. Your extensive background in C/C++ and Dart gives you a unique perspective, as you've worked with both manual memory management and garbage collection. This lesson will bridge that experience to Rust's unique approach.

Our goal is to explain Rust's ownership model and how it ensures memory safety without a garbage collector. This is not just a language feature; it's the key to writing performant and reliable code, which is paramount in the embedded world you're aiming for. We will explore how Rust enforces memory safety at compile time, eliminating entire classes of bugs that you might have encountered in C/C++ and avoiding the runtime overhead of a garbage collector common in languages like Dart.

The Memory Management Landscape

Before diving into Rust's specifics, let's quickly frame the problem. All programs must manage their use of memory. Historically, there have been two main approaches.

How rust forces you to respect memory

This short video provides an excellent high-level overview of the classic memory management challenges in C and the garbage-collected solution in Java (which is conceptually similar to Dart).

First, watch the segment on C from the beginning. This will be a familiar scenario. Then, watch the explanation of Java's garbage collector from the next segment.

As the video illustrates, and as you know from your C/C++ experience, manual memory management with malloc/free or new/delete is powerful but perilous. It's easy to forget to free memory (a memory leak), free it too early (a dangling pointer/use-after-free), or free it twice (a double-free). These are common sources of crashes and security vulnerabilities.

On the other hand, languages like Dart and Java use a garbage collector (GC). The GC runs in the background, identifying and cleaning up memory that is no longer in use. This simplifies development but comes at a cost:

  • Runtime Overhead: The GC consumes CPU cycles and memory.
  • Non-determinism: The GC can pause the program at unpredictable times to perform a collection, which is often unacceptable for the real-time constraints of embedded systems.

Rust presents a third option that provides the automatic cleanup of a GC with the performance and control of manual management. It achieves this through the ownership model, which is a set of rules checked by the compiler. If the rules are broken, the code simply won't compile.


The Foundation: Stack, Heap, and The Rules of Ownership

To understand ownership, we first need to distinguish between two areas of memory where a program can store data: the stack and the heap. Your experience with C/C++ means you're already familiar with these, but let's review them in the context of Rust.

What is Ownership? - The Rust Programming Language

"The Rust Programming Language" (often called "The Book") provides a clear and concise explanation of the stack and the heap. This will set the stage for why ownership is primarily concerned with heap data.

Please read the section titled The Stack and the Heap. Focus on the characteristics of each: LIFO for the stack, the role of the allocator for the heap, and the performance differences.

In short, the stack is fast, ordered, and for data of a known, fixed size. The heap is for data whose size might change or is unknown at compile time.

This distinction is visualized well in the following diagram.

This diagram shows how simple types like integers (`i32`, `bool`) are stored entirely on the stack. In contrast, a complex, growable type like `String` has a small pointer and metadata on the stack, which points to the actual character data allocated on the heap.

With this memory layout in mind, we can now introduce the three simple but powerful rules that govern ownership in Rust.

What is Ownership? - The Rust Programming Language

The entire ownership system is built upon three core rules. This section from "The Book" states them plainly.

Read the short section titled Ownership Rules. These three rules are the central concept of this lesson.

Let's break down these three rules:

  1. Each value in Rust has an owner. A variable that holds a value is called its owner.
  2. There can only be one owner at a time. This is the "single ownership" principle.
  3. When the owner goes out of scope, the value will be dropped. "Dropped" means its memory is deallocated. Rust automatically calls a special function called drop at the end of the scope (the closing }).

This automatic call to drop is a pattern you might recognize from C++ as Resource Acquisition Is Initialization (RAII). When an object is created, it acquires resources (like memory), and when it is destroyed (goes out of scope), its destructor releases those resources. Rust applies this concept rigorously to memory management.


Ownership in Action: The Move and Copy Semantics

Now, let's see how the compiler enforces these rules. This is where Rust's behavior diverges significantly from C++. In C++, assignment typically means making a copy. In Rust, the default behavior for complex, heap-allocated types is to move.

Rust for C++ developers - What you need to know to get rolling with crates - Pavel Yosifovich

This video segment, aimed at C++ developers, provides a great introduction to how Rust's ownership model differs from what you're used to.

Watch from the introduction to get a feel for the conceptual shift from C++'s flexibility to Rust's compiler-enforced strictness.

The Move Semantic

Let's dig into what a "move" really means. Consider what happens when you assign one String to another.

let s1 = String::from("hello");
let s2 = s1; 
// What happens to s1 now?

In C++, this would typically invoke a copy constructor, creating a new, independent string s2. In Rust, something different happens.

Understanding Memory Management, Part 4: Rust ...

This article does a fantastic job of comparing Rust and C++ directly, which should resonate with your background.

In the article, read the section that starts with the C++ vs. Rust code examples. Pay close attention to the explanation of how Rust's assignment moves the variable and invalidates the original.

As the article explains, Rust transfers ownership from s1 to s2. After the line let s2 = s1;, s1 is no longer valid and cannot be used. The compiler will stop you if you try.

This "move" prevents the double-free error. If Rust simply copied the pointer (a shallow copy), both s1 and s2 would point to the same heap data. When they both went out of scope, they would both try to free the same memory, leading to memory corruption. By invalidating s1, Rust ensures that only one owner (s2) is responsible for dropping the data.

This diagram perfectly illustrates the process:

Step 1 shows `s1` owning the heap data. Step 2 shows the "move" operation: ownership is transferred to `s2`, and `s1` is invalidated. Attempting to use `s1` afterwards results in a compile-time error.

The Copy Trait for Stack-Only Data

Moving ownership for every assignment would be inefficient for simple types that live entirely on the stack, like integers.

let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // This works perfectly!

This code works because integer types have the Copy trait. Instead of moving, the value is bit-wise copied. x remains valid and usable.

Understanding Memory Management, Part 4: Rust ...

The same article provides a great breakdown of the Copy trait.

First, read the section on copying integers. Then, continue through the explanation of the Copy trait, which is a type of "marker trait" that tells the compiler a type is safe to copy bit-for-bit.

A type can have the Copy trait if all of its components are also Copy. This is true for all scalar types (i32, f64, bool, char) and tuples composed of Copy types. A type that requires heap allocation and a drop function (like String or Vec) can never be Copy.

If you do want to perform a deep copy of heap data (like a String), you must do so explicitly by calling the clone() method. let s2 = s1.clone(); creates a completely new copy on the heap, and both s1 and s2 remain valid owners of their respective data.

Decoding the Compiler Error

The Rust compiler's error messages are famously helpful. Now that we understand move and Copy, we can fully dissect a typical ownership error.

Understanding Memory Management, Part 4: Rust ...

Let's revisit the compiler error from the beginning of the article and break it down.

Read the section Decoding our Original Error. This is a great exercise to solidify your understanding of how the compiler applies these rules.

The error borrow of moved value is the compiler telling you that you're trying to use a variable whose ownership has already been moved elsewhere. The message points out exactly where the move occurred and why (Hat does not implement the Copy trait).


Conclusion

In this lesson, we've unpacked Rust's most defining feature. Let's summarize the key points:

  • A Third Way: Rust's ownership model provides compile-time memory safety, avoiding both the risks of manual memory management in C/C++ and the runtime overhead of a garbage collector in languages like Dart.
  • The Three Rules: Every value has one owner; when the owner goes out of scope, the value is dropped. This is enforced by the compiler.
  • Move by Default: For heap-allocated data, assignment transfers ownership (a "move"), invalidating the original variable to prevent double-free errors.
  • Copy for Stack Data: Simple, stack-only types can implement the Copy trait, which makes them copy on assignment instead of moving.

You might be thinking that having to transfer ownership all the time seems restrictive. What if a function just needs to read a value without taking ownership of it? This is a crucial question, and Rust has an elegant answer.

In our next lesson, we will explore borrowing and references, which allow you to create temporary, safe pointers to data without transferring ownership. This is the mechanism that provides flexibility to the strict ownership system, and mastering it is the next step on your journey to becoming a proficient Rust developer.