Rust
If you have not yet done it, install Rust on your machine, cargo (Rust’s package manager) and rust-analyzer for your favorite IDE.
Rustlings
Rustlings is a set of exercise designed to quickly learn Rust by examples. I recommend doing the exercices in parallel to reading the official Rust free book that quickly covers the important concepts.
cargo install rustlings
Then go into any directory of your machine where you want to keep the code examples for this course.
rustlings init
This command would create a directory in the current directory you’re in.
Launch your favorite IDE, go into the directory containing rustlings’
exercise and launch Rustlings’ watchmode by hitting rustlings on your
IDE’s terminal.
You’re all set! Your goal now is to become proficient enough in Rust before the project starts, around week 9. You must work your Rust programming every week.
Why Rust?
As we discussed in classes 01 and 02, system languages like C are not memory safe. This aspect was a feature to let programmers in charge of creating well defined programs. We say that a program is well defined when no possible execution can lead to an undefined behavior. In computer programming, UB (undefined behavior) is the result of potentially arbitrary instructions execution. UB is a feature of system languages and compilers designed to produce fast binary code. We discussed in class 02 examples of undefined behaviors in programs that could be turned to an exploit, execute code that was not meant to run in the program to take its control.
Other languages emerged with the goal to reduce this complexity, and avoiding to make the programmer responsible for producing a well defined program. We say that these languages are type safe: that is, any program written in these languages cannot exhibit undefined behavior. Most of these languages achieve it while seriously reducing the performance of the program’s execution.
Rust however, solves this decades old tension: it is a system language, for which execution is as fast as compiled C programs, and Rust programs are type safe!
Moreover, the approach taken by Rust has surprising benefits for multithreaded programming, which is a notoriously difficult task in any other language, and could be scary in C/C++ due to their unsafety. Rust’s type system and specific rules guarantee that a multithreaded Rust program is free of any data race, catching any misuse of mutexes or synchronization primitives directly at compile time. That is, if your Rust program compiles, it cannot deadlock.
Programming in Rust may give you more assurance that any tool, or product you may contribute to in the future would be free of binary exploitation vectors. Thwarting by design potential malicious third parties hoping to take advantage of our programming mistakes. Everybody makes mistakes, and with Rust we can catch them all early.
What’s the catch then? Well, Rust achieves its benefits by enforcing at compile time the concept and rules of ownership, moves, and borrows onto the programmer. If the programmer breaks any of the rules associated to these concepts, the code would not compile. The difficulty in Rust programming is to understand these concepts, and how they impact your programming. Although this may feel uncomfortable to have restrictions and limitations, it is a no-brainer for most experienced C/C++ programmers, to the point where C/C++ domination is slowly decreasing, eventually being replaced by Rust. As you read these lines, many C/C++ fundamental pieces of software for which efficiency constrains are paramount are being re-written in Rust to eventually move away from memory unsafety and undefined behaviors, and free the programmers from these unreliable tools. Examples such as the Linux kernel, Browsers renderer (like Firefox’s Servo), or mainstream video game engines. This has no precedent in the history of Computer sciences.
Why’s that? Security aspects is not the only motive to migrate to Rust. C/C++ languages and their inherent issues may lead programmers that do not enforce rules on their own programming in disarray. Understanding of a fundamental logical issue that questions the whole program structure can happen significantly far in the programming of a C/C++ program, to the point the programmer feels they are hurting themself, or fighting their own terrible choices when a significant amount of effort was already invested. In Rust, instead of fighting yourself, you’ll be fighting the borrow checker. It is an improvement, one that forces you to think early of several important aspects of your program, because Rust’s rules force you do to so. These important aspects are, among others, ownership and lifetimes.
Ownership and lifetimes
In Rust, any value has a single owner, and the owner decides the lifetime of the value. Moreover, if the owner is dropped (Rust’s terminology that includes freeing memory and losing access to stack-allocated content), then any object owned is dropped as well. In C/C++, a stack allocated value lives as long as its scope exists (in Rust too). A heap allocation in C/C++ requires a deallocation at some point, but there is no constraint by the language on who is responsible for the allocation/deallocation. That is, there is no constraints on the liveness of an object in C/C++.
In Rust, the lifetime of a value is defined (controlled) by the lifetime of its owner.
Example: in Rust, a variable owns its value. When the variable is dropped, the value is dropped.
#![allow(unused)]
fn main() {
fn foo() {
let myvar = vec![0]; // allocation (on the heap) of a variable holding
// a vector containing a single value 0.
for i in 1..=10 {
myvar.push(i);
}
println!("My vector is {:?}", myvar); // print My vector is [0, 1, 2,
// 3, 4, 5, 6, 7, 8, 9, 10]
} // myvar is dropped here because it only exists in the scope of foo().
// Its value is dropped as well as its value (i.e., the heap-allocated
// vector)
}
A variable may own a value or many values that themselves own other elements. Eventually, you may see the ownership system as a tree. Any value in a Rust program is a member of some tree, rooted by a variable that has its own lifetime. When the variable is dropped, all the tree is dropped.
This tree system is a limitation of Rust’s ownership model. Compared to other languages that can have any kind of relationship between values, Rust would only let you have a tree. You can’t build an arbitrary graph of references like you could do in C, so it limits you in that sense. However, this approach has enough flexibility to let us program solutions to any problem well within the restrictions of the language, sometimes with a few twists that have yet to be discovered or learn along the journey ;)
To accommodate to this restriction, Rust supports a move semantic that allows you to move value from one owner to another, technically letting you operating on the ownership trees form and liveness
Moreover Rust’s standard libraries also offers tools to bend a bit the
ownership model. Special types like Rc or Arc give you a reference
counted pointer supporting a single value to have multiple owners. The
value would then be dropped when all the owners are dropped.
And finally, we can ‘borrow’ a reference to a value in Rust. You may think of references like pointers in C, except that the reference in Rust is not owning the value, and has a limited lifetime that is either inferred automatically by the compiler, or needs to be explicitly written in the program. Lifetimes of references is one of the complicated pieces in Rust.
Move
In Rust, many operations like assigning a value to a variable, passing a value to a function through its parameters, or returning a value from a function is not a copy. We move the value. When moving a value, the source looses its ownership. The destination now controls the moved value’s lifetime. The source becomes uninitialized, and the program can’t use it anymore.
Move semantic through an assignment example:
#![allow(unused)]
fn main() {
let dinner = vec!["chicken", "vegie", "eggs"];
let dinner_confirmed = dinner;
// We can't use dinner anymore! It has been moved into
// dinner_confirmed! Any attempt to use dinner would result to a
// compilation error, as it would be against the ownership system.
}
Shadowing in Rust works as well through the ownership model: the old value owned by the variable gets dropped, and the new value is assigned. So the variable is not into an uninitialized state when shadowing is used.
Other move semantics: passing arguments to functions moves ownership to the function’s parameters. Returning a value from a function moves ownership to the caller. Constructing a Tuple moves value to the Tuple. Pushing into a vector moves the value to the vector.
More complicated: Control flow. The idea is that if some variable can be moved inside one of the potential branching, then the compiler will consider it uninitialized afterwards, and it cannot be used anymore:
#![allow(unused)]
fn main() {
let v = vec![42, 42];
if some_condition {
f(v); // v is moved in f.
} else {
...
}
g(v); // Compilation error! v might have been moved if some_condition
// was true
}
Same goes for loops:
#![allow(unused)]
fn main() {
let v = vec![42, 42];
while some_condition {
f(v); // v is moved at the first iteration of the loop, so the
// compiler would not accept to move it more than once, and assume that it
// could happen more than once, hence it would not compile.
}
}
To deal with that kind of problem, we can either clone() the variable, and move the cloned value:
#![allow(unused)]
fn main() {
let v = vec![42, 42];
while some_condition {
f(v.clone()); // ok! We copy v and move v's copy. v is never moved.
}
g(v);
}
This is however quite inefficient, since copying the vector at each loop would be CPU intense. Rust programmers must be careful with clone()’s usage and not take this option to solve every ownership problem that the compiler throws. We have one more tool at our disposal to deal with this sort of issues. Borrowing.
Borrowing & References
There is a mantra in Rust: a reference must never outlive its referent. You will probably cross a similar kind of error often from the compiler when experimenting for the first time in Rust, and sometimes also when you’ll learn to handle lifetimes. Rust refers to creating a reference to a value as borrowing the value. And like in the real life, when you borrow, you must always return what you borrow to its owner.
References are pointers, but with special rules enforced by the language and its compilers to make them safe to use. A reference lets you access a value without changing its ownership. In the previous code example, we could then do:
#![allow(unused)]
fn main() {
let v = vec![42, 42];
while some_condition {
f(&v); // We borrow v (an immutable reference to v).
}
}
We can have as many immutable references to a value as we want. However,
we can borrow mutably (e.g., &mut v) only once at a time, and we
cannot have any immutable borrow while a mutable borrow exists.
-
As many shared reference or immutable reference: &T for any type T.
-
A mutable reference: &mut T for any type T.
Rust decided to enforce the ‘many’ readers or a ‘single’ writer for a
value at any time. This is a cornerstone aspect of Rust’s data race
prevention in multithreaded scenario. Moreover, as long as there are
shared references alive, not even its owner can modify the value. In
our example, nobody can modify v as long as the function f() holds a
shared reference to v.
Note, Rust supports implicit dereference of a reference. That’s it,
unlike in C, when we hold a pointer a to value, we don’t need to
explicitly use the star symbol * to dereference.
Note, Rust references are never NULL. In Rust, if you need a value that
is either a reference or nothing, you would use Option<&T>, an enum
built in into the language. Rust would always force you to check whether
the Option’s value is something or None before using it. If you remember
the Null pointer dereference example from class
01; this kind of problem
cannot happen in Rust. We cannot compile a program that can lead to
dereferencing a NULL value; well, unless we use the unsafe keyword.
Unsafe Rust
Unsafe rust is out of scope of this course. You may wield Rust’s standard library tools internally using unsafe blocks, but you should not use this power yourself in this course.
Warning
The following is a joke modified from the result of a prompt to DeepSeek-R1 run through llama-cpp.
Mufasa: (voice solemn, yet warm, as they stand atop Pride Rock,
overlooking the vast savannah of code) “Simba, there is a place we do
not speak of. A boundary that defines the balance between safety and
chaos. It is called… the unsafe keyword.”
Simba: (curious, looking up at his father) “The unsafe keyword?
What’s that?”
Mufasa: “It is a realm where the compiler’s safeguards fade away. A place where memory is raw, pointers are wild, and undefined behavior lurks in the shadows. You must never go there, Simba. The Rustaceans of the past decreed it forbidden. They said only those with wisdom and discipline may enter, and even then… only when there is no other path.”
Simba: (innocently) “But what’s so dangerous about it?”
Mufasa: (gravely) “Within the unsafe block, the rules that
protect us vanish. Dereference a dangling pointer, and your program
crashes. Misalign a memory access, and undefined behavior consumes
everything. The compiler can no longer guard you. It is a place of great
power, but at great risk.”
Simba: (determined, yet naive) “I’m not scared! I can handle the
unsafe keyword!”
Mufasa: (firm but loving) “It is not the fear of the keyword
itself. It is the fear of what lies beyond its guardrails. Promise me,
Simba, you will never use unsafe lightly. It is not a shortcut, but a
last resort. The safety of your code depends on your restraint.”
Simba: (sighs, looking out at the horizon of rustlings and structs) “Okay, Dad. I promise. But… one day, I want to write really fast code. Like you!”
Mufasa: (nods, proud but cautious) “Speed is not the measure of a
Rustacean. Balance is. Speed and safety, side by side. The unsafe
keyword is a tool, but respect the rules of Rust, and it will respect
you. Remember: the borrow checker watches over us all.”
They gaze at the sunrise over the savannah, the golden light reflecting on the syntax of the code below.
Mufasa: (quietly, echoing the Rust philosophy) “Everything the
light touches is safe. But the unsafe keyword… that is the shadow
where we must tread carefully, and only when we must. A great
responsibility for those who wield it.”
Simba: (whispering, determined) “One day… I’ll use it wisely. But only when I have to.”
Mufasa: (smiling faintly) “That day will come, my son. But until then… we are safe.”
The wind rustles through the grasslands, carrying the faint sound of
clippy warnings in the distance.
Studying & Practicing Rust
It is expected that you learn the language basics and practice it throughout the semester. The following content may help you: