Welcome to your second lesson. In our previous session, we established Rust's foundation: the ownership system. You saw how its strict rules around single ownership and move semantics provide memory safety at compile time, a stark contrast to both the manual management in C/C++ and the garbage collection in Dart. We ended by asking how to access data without the cumbersome process of transferring ownership back and forth.
Today, we will answer that question by exploring borrowing and references. This is the mechanism that brings flexibility and ergonomics to Rust's strict ownership model. Our goal is to apply Rust's borrowing and reference rules to share data without transferring ownership. You'll learn about the different kinds of borrows and the rules the compiler—specifically, the "borrow checker"—enforces to guarantee safety. This concept is fundamental to writing idiomatic and efficient Rust, especially in the embedded context where predictable performance and reliability are non-negotiable.
The Problem with Moving Everywhere
Let's recall the issue from last lesson. If we have a function that needs to inspect a String, we had to pass ownership to the function, and the function had to return ownership along with its result.
// The cumbersome way from last lesson
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // s is now owned by the function
(s, length) // Return ownership of s
}
This is clumsy. We don't want the function to own the string, just to read it. In C++, you would solve this by passing a pointer (*) or a reference (&). Rust uses a similar idea, but with much stronger safety guarantees.
References: Safe Pointers
In Rust, a reference is like a pointer in that it’s an address we can follow to access data stored elsewhere. However, unlike a C/C++ pointer, a reference is guaranteed by the compiler to point to a valid value of a particular type for its entire life. The act of creating a reference is called borrowing.
Let's see how this simplifies our calculate_length function.
[Rust Guide] 4.4. Reference and Borrowing - DEV Community
This article from DEV Community provides a clear before-and-after example, showing how references streamline our code.
Read from the beginning of the article through the section titled References. Compare the two versions of the calculate_length function and notice how using &s1 to pass a reference means we no longer need to return the string.
By changing the function signature to fn calculate_length(s: &String), we are now "borrowing" the String instead of taking ownership. The & symbol creates a reference. Inside main, s1 remains the owner of the String data, and we can continue to use it after the function call without any special handling. This is far more ergonomic.
Your experience with C/C++ pointers is valuable here, but it's crucial to understand the distinction. Rust's references are a core part of its safety promise.
Intro to Embedded Rust Part 4: Ownership and Borrowing | DigiKey
The presenter in this DigiKey video gives an excellent explanation of how Rust's references are "safety wrappers" around raw pointers, providing compile-time checks without runtime cost.
Watch the segment from this timestamp to understand the conceptual difference between Rust's references and raw pointers. The discussion on stack vs. heap will also reinforce our previous lesson's concepts.
The Rules of Borrowing: Immutable vs. Mutable
The borrow checker's primary job is to prevent data races, which can occur when:
- Two or more pointers access the same data at the same time.
- At least one of the pointers is being used to write to the data.
- There’s no mechanism being used to synchronize access to the data.
To prevent this, Rust enforces a simple but powerful set of rules via two types of references.
1. Immutable References (&T)
By default, references are immutable. You can have as many immutable references to a particular piece of data as you want, because they don't pose a risk of data races. Think of them as read-only access.
let s = String::from("hello");
let r1 = &s; // immutable borrow
let r2 = &s; // another immutable borrow is fine
println!("{}, {}", r1, r2); // Perfectly valid
2. Mutable References (&mut T)
If you need to change the data you're borrowing, you need a mutable reference, using &mut. To create a mutable reference, the original variable must also be declared as mut.
let mut s = String::from("hello");
let r1 = &mut s; // mutable borrow
r1.push_str(", world");
println!("{}", r1); // Prints "hello, world"
Now for the most important rule in this lesson, which combines these two concepts.
The Core Borrowing Rule
At any given time, within a particular scope, you can have either:
- One mutable reference (
&mut T), - OR any number of immutable references (
&T).
You cannot have both. The compiler will stop you. This rule is the key to how Rust prevents data races at compile time.

Let's see what happens when we break this rule.
let mut s = String::from("hello");
let r1 = &s; // immutable borrow
let r2 = &s; // another immutable borrow
let r3 = &mut s; // BIG PROBLEM: cannot mutably borrow while immutably borrowed
// println!("{}, {}, and {}", r1, r2, r3); // This line is what makes the borrows active
The compiler would produce an error, because you can't have r3 trying to change the data while r1 and r2 have read-only access.

Why This Matters: A C++ Comparison
Your C++ background provides a perfect context for appreciating why this rule is so critical. In C++, it's possible to write seemingly innocent code that contains a hidden time bomb related to memory reallocation.
Rust for C++ developers - What you need to know to get rolling with crates - Pavel Yosifovich
This segment from Pavel Yosifovich's talk, "Rust for C++ developers," is one of the best demonstrations of Rust's safety guarantees. He shows a subtle but common bug in C++ involving vector reallocation and how Rust's borrow checker prevents it entirely.
Watch from this segment. Pay close attention to the C++ code that crashes at runtime and the explanation of why the equivalent Rust code fails to compile. This is a powerful illustration of the borrow checker's value.
As the video demonstrates, the C++ std::vector might reallocate its memory when a new element is added. This invalidates any existing references to elements within it, leading to a use-after-free bug that crashes the program at runtime.
The Rust compiler, by enforcing the "one mutable OR many immutable" rule, sees that v.push() requires a mutable borrow of v, which cannot coexist with the immutable borrow held by the value reference. It correctly identifies this as a potential data race and stops the compilation. What is a subtle runtime bug in C++ becomes a straightforward compile-time error in Rust.
Preventing Dangling References
Another common C-family bug that the borrow checker eliminates is the dangling reference—a reference that points to memory that has been deallocated.
Consider a function that tries to return a reference to a variable created inside it:
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory is gone.
// Danger!
In C++, this would compile but would create a dangling pointer, leading to undefined behavior when the caller tries to use it. Rust prevents this at compile time.
[Rust Guide] 4.4. Reference and Borrowing - DEV Community
This article section explains how Rust's compiler ensures that data outlives any references to it, thus preventing dangling references.
Please read the section titled Dangling References. The example code shows exactly the scenario described above, which the Rust compiler will reject.
The compiler enforces that the data a reference points to must be valid for the entire "lifetime" of that reference. While we will dedicate a future lesson to explicit lifetimes, for now, you can trust that the borrow checker is tracking these scopes and ensuring no reference can outlive the data it points to.
Conclusion
In this lesson, we've unlocked the flexibility of the ownership system through borrowing. This is a concept you will use constantly in Rust.
Here are the key takeaways:
- Borrowing with References: Using
&creates a reference, allowing you to access data without taking ownership. - Two Kinds of Borrows: References can be immutable (
&T) for reading or mutable (&mut T) for writing. - The Cardinal Rule: In any given scope, you can have either one mutable reference OR any number of immutable references, but not both.
- Compile-Time Safety: The borrow checker enforces these rules to prevent data races and dangling references at compile time, eliminating entire classes of bugs common in languages like C/C++.
Now that you have a solid grasp on how Rust manages data and state through ownership and borrowing, we are ready to build more complex and expressive programs. In our next lesson, we will explore Rust's powerful algebraic data types (enum) and use exhaustive pattern matching to model the various states of a system—a crucial skill for writing robust embedded software.