Rust Introduction: Cargo, Crates, Rust project, Hello Word, (2h on computers),
Pierre Cochard, Tanguy Risset
This course has been set up for students of the Telecommunication Department at INSA-Lyon (5th year), it is vastly inspired by the Rust book and many other resources on the web. It assumes that students do not have any programming experience in Rust, but have a strong programming experience in other languages (C/C++ and object-oriented languages in particular).
- Setting up the environnement for using Rust
- Rust Hello World
- Variables, Types and Mutability
- Functions in Rust
- Generic types, traits and
#derivedirective - First example of Move semantics: the cube
- Introduction to Visual Studio Code
In addition to these documents, some others are available on Moodle https://moodle.insa-lyon.fr/course/view.php?id=10386, presenting the concepts covered in this course. Don't forget to check it before you start. Many of the information listed here come from https://www.rust-lang.org/learn/.
The course is organized in sections that have questions. In addition, you will find text boxes labeled Course:, in which important concepts are presented.
Course: What is Rust and Why Rust
Why should computer science engineers always learn new languages?
The use of the Rust programming language is growing exponentially, the number of useful libraries and projects that are available to developers is already huge. The main reason for that is that Rust provides safe memory management without a garbage collector, ensuring both performance and security. Its ownership system eliminates data races and segmentation faults. It enables efficient and concurrent programming while guaranteeing memory safety. It is associated with a powerful modern ecosystem and is suited for embedded systems, system programming as well as high-performance applications. Adopted by major industry players, Rust is emerging as a reliable alternative for secure and system-level development.
Setting up the environnement for using Rust
We're going to start by setting up the environment that will enable you to program in Rust. We recommend that you use Rust on your own machine, but the environment is already installed on the department computers.
This environment simply consists in having:
-
An editor for programming, we strongly recommend Visual Studio Code that is available on all OS, for instance here: https://visualstudio.microsoft.com/fr/downloads/. Together with the rust-analyzer extension. See Appendix below for an introduction to Visual Studio Code.
-
Install both the rust compiler (
rustc) andcargo, with therustupinstaller script.
cargo is Rust's package manager and build system. Its installation and versioning are managed
by rustup.
Below is a summary for installing Rust and cargo on your laptop. The original complete instructions can be found here:
https://doc.rust-lang.org/book/ch01-01-installation.html.
As a summary:
-
On linux, use the following command:
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh -
On (recent) macOS:
1) curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh -s -- --no-modify-path
2) Choose 1)
3) . "$HOME/.cargo/env"
4) rustc --version
cargo --version
- on Windows, go to https://www.rust-lang.org/tools/install and follow the instructions for installing Rust.
Course: What is cargo used for?
cargo manages many aspects that you need to understand to use Rust correctly:
-
Dependency Management: Cargo manages the dependencies of a Rust project. It automatically downloads and builds the required libraries and dependencies, making it easier for developers to include external code in their projects.
-
Project Configuration: Cargo uses a file called
Cargo.tomlto configure a Rust project. This file includes information about the project, its dependencies, and various settings. -
Building and Compilation: Cargo handles the compilation process of Rust code. It can build the project, manage dependencies, and generate executable binaries. Developers can use Cargo commands like
cargo buildto compile the project orcargo runto build and run it in one step. -
Testing: Cargo provides built-in support for testing Rust code. Developers can use the
cargo testcommand to run tests defined in the project. -
Documentation: Cargo can generate and serve documentation for the project using the
cargo doccommand. This is useful for both internal and external documentation (the HTML file that you are reading has been generated bycargo doc) -
Publishing Packages: Cargo facilitates the process of publishing Rust packages to the official package registry, called "
Crates.io." This makes it easy for others to discover and use Rust libraries and projects.
Rust Hello World
A Rust project is contained in a directory which has the name of the project. From now on, we suggest making a project in your home directory and keeping all your Rust code there.
We will use the cargo command to build our first hello_world
project. Note that this is not mandatory, everything can be built by
hand, the Rust compiler can be invoked without cargo by using the command
rustc.
Execute the following command:
cargo new hello_worldCheck the generated files:
Cargo.tomlis the project configuration file written in the TOML (Tom's Obvious, Minimal Language) format1.src/main.rsis the Rust main file (the program entrypoint).Now, build your project with the
cargo buildcommand.
- Where is the generated executable file?
- What is the bang (
!) afterprintln?
Correction
The hello_cargo executable is located in the target/debug directory,
because cargo's default build configuration is debug. As the soon as a
program is ready for release, we can call cargo with the --release
option, which will optimize the build.
Course: What about println!
As you can see on the Rust Standard Library
documentation : println! is not a
function, it is a macro.
Macros are called with a trailling bang (such as println!), they are a
way of writing code that writes other code, which is known as
metaprogramming. Understanding and declaring Macros is quite complex and
will be seen later. But using them (for instance, println!) is usually
very straightforward.
Variables, Types and Mutability
What is the problem with the program below? Does it compile?
#![allow(unused)] fn main() { let x = 5; println!("The value of x is: {x}"); x = 6; println!("The value of x is: {x}"); }
Correction
No it does not compile, x is not declare mutable: message
| let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
What are the values printed by the program below?
#![allow(unused)] fn main() { let x = 5; { let x = x + 1; { let x = x * 2; println!("The value of x in the inner scope is: {x}"); } } println!("The value of x is: {x}"); }
Correction
The first printed value is 12, then 5, because it is
outside the scope of the definition of the second x.
The notion of scope is quite important in Rust, a scope can be manipulated in the language as an object, we will see it in more details in TD4.
Functions in Rust
Functions in Rust have little difference with functions in other programming languages.
They are declared with the fn keyword, and their parameters (arguments) are passed by value or reference.
Write a function fibonacci:fibonacci(n: i32) -> i32, which computes elementnof the fibonacci sequence.We will not use a recursive solution, but rather a
forloop whose syntax will be:for i in 2..n+1( half-open range) and mutable variables.We recall the definition of the fibonacci function
fib:fib(0)=1 fib(1)=1 fib(i)=fib(i-1)+fib(i-2) for i >= 2
Correction
#![allow(unused)] fn main() { fn fibonacci(n: i32) -> i32 { let mut next = 1; let mut prev = 1; let mut old; if n<=1 { return 1; } else { for _i in 2..n+1 { old = next; next = next + prev; prev = old; } return next; } } }
Generic types, traits and #derive directive
Just like in C++ with class and struct,
Rust types can be defined and be implemented by using different
methods. For instance, the following code defines the type Complex as
a struct of two floats (type names begin with an uppercase character by
convention).
#![allow(unused)] fn main() { struct Complex { re: f32, im: f32, } fn build_complex(re: f32,im:f32)-> Complex { Complex {re,im} } let mut a = build_complex(2.3, 4.0); println!("a = ({}, {})", a.re, a.im); a.re = a.re+1.; println!("a = ({}, {})", a.re, a.im); }
Course: Generic Types
Like templates in C++, Rust enables the use of generic
types in functions or struct, enums or method definitions. Here is a
simple definition of a Point structure using integer or float
coordinates:
#![allow(unused)] fn main() { struct Point<T> { x: T, y: T, } let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; }
Modify the program of Complexcreation above by using a typestruct Complex<T>which uses a generic type, Test it withprintln!
Correction
#![allow(unused)] fn main() { struct Complex<T> { re: T, im: T, } fn build_complex<T>(re: T,im: T)-> Complex<T> { Complex {re,im} } let a = build_complex(2.3, 4.0); println!("a = ({}, {})", a.re, a.im); }
Course: Traits: Defining common behaviour
A trait defines a functionality or a behaviour that a particular type has and can share with other types. Traits are similar to a feature often called interfaces in other languages.
Many natural methods can be defined for any -- or at least many --
types. For instance the copy or clone methods (rust primitives) or
the fmt method (of the trait std::fmt::Display, rust standard
library) that enables to use println!. These methods are not defined
by default when a new type is defined.
Traits are defined by the trait keyword. By
convention they are named starting with an upper case, e.g. the
Clone trait, it usually defines a method with the same name in lower case
(here: clone())
If you want to clone a Complex, you juste have to write the
implementation of the clone method:
impl Clone for Complex {
fn clone(&self) -> Self {
Complex{re: self.re, im: self.im}
}
// Now a.clone() can be used on Complex variables
}
Implement the Clone trait for the struct Complex<T>type defined before. You will have to useimpl<T: Clone>to ensure that theTgeneric type implements theClonetrait.
Correction
impl<T: Clone> Clone for complex<T> {
fn clone(&self)->Self{
complex {re: self.re.clone(),im: self.im.clone()}
}
}
By the way, do you know the difference between Copy and Clone?
Select your preferred answer:
-
Cloneis a supertrait ofCopy2 -
Copyis implicit, inexpensive, and cannot be re-implemented (memcpy).Cloneis explicit, may be expensive, and may be re-implemented arbitrarily. -
The main difference is that cloning is explicit. Implicit notation means move for a non-Copy type.
Course: Deriving traits
For certain traits3:
(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, etc.),
the compiler is capable of providing basic implementations for some
traits via the #[derive] attribute (In Rust, an attribute is metadata applied to code elements like functions, structs, modules, or crates, attributes are prefixed with # and enclosed in square brackets []).
These traits can still be manually implemented if a more complex behavior is required.
for instance, the Clone trait can be automatically derived for
Complex type:
#[derive(Clone)]
struct Complex {
re: f32,
im: f32,
}
[... no need to implement Clone ...]
let a = build_complex(2.3,4.0);
let _c = a.clone()
[...]
How could we use println!to displayComplexvariables? Test the two following methods:
- Implement the
std::fmt::Displaytrait for typeComplex.This will require to:
- use
std::fmt- search for the prototype of the
Displaytrait- use the macro
write!to print fields
- Derive the
Debugtrait that includes thefmt::Displaytrait and use the"{:?}"format.
Correction
We would have to implement the fmt::Display trait for type Complex
use std::fmt;
[...]
impl fmt::Display for Complex {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.re, self.im)
}
}
[...]
let mut a = build_complex(2.3, 4.0);
println!("a = {}", a);
First example of Move semantics: the cube
In this first example we define a very simple data structure, a Cube
with a single field c that indicates its size.
Write a Rust program that defines a structure Cubeand prints its size
Correction
#![allow(unused)] fn main() { struct Cube {c:f32} println!("My cube: {}", Cube{c:0.5}.c); }
Can you print the cube itself? Can you write:
println!("My cube: {}", Cube{c:0.5});If not, how can you make the cube printable?
Hint: You can derive the
std::fmt::Displaytrait or theDebugtrait
Correction
If you just use println!("My cube: {}", Cube{c:0.5}); then you get the
following message:
error[E0277]: `Cube` doesn't implement `std::fmt::Display`
--> src/main.rs:4:29
|
4 | println!("My cube: {}", Cube{c:0.5});
| ^^^^^^^^^^^ `Cube` cannot be formatted\
with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Cube`
= note: in format strings you may be able to use `{:?}` \
(or {:#?} for pretty-print) instead
One solution:
#![allow(unused)] fn main() { #[derive(Debug)] struct Cube { c: f32, } println!("My cube size is: {}", Cube{c:0.5}.c); println!("My cube is: {:?}", Cube{c:0.75}); }
Define a variable xassigned to a given cube, print it and then define a second variableydefined bylet y = x;. Then printxagain, what is the problem?
Correction
#![allow(unused)] fn main() { #[derive(Debug)] struct Cube { c: f32, } let x = Cube { c: 0.75 }; println!("My cube x size is: {}", x.c); let y = x; println!("My cube x is: {:?}", x); println!("My cube y is: {:?}", y); }
After the let y = x; statement, the content of x is invalid because it
has been moved to y and it can't be accessed anymore. The program will
not compile anymore because the content of x has already been moved when
we try to use it.
error[E0382]: borrow of moved value: `x`
--> src/main.rs:10:36
|
7 | let x = Cube { c: 0.75 };
| - move occurs because `x` has type `Cube`, which does not\
implement the `Copy` trait
8 | println!("My cube x size is: {}", x.c);
9 | let y = x;
| - value moved here
10 | println!("My cube x is: {:?}", x);
| ^ value borrowed here after move
|
Course: Move semantics
In Rust, move semantics refers to the ownership transfer of data from one variable to another. Rust enforces a strict ownership model where each piece of data has a single owner at a time, and ownership can be transferred (or "moved") when a value is assigned to another variable or passed as an argument to a function.
By default, an assignment such as let y = x implies a transfer of
ownership of the content of x to y. The ownership concept will be
studied further during next course.
In order to duplicate the cube (as it would be done in any language),
one has to clone it or to implement the Copy trait. Deriving the
Copy trait for Cube changes the semantic of the assignment: the
assignment is now a copy, not a move.
Modify your program by cloning xintoy
Correction
#![allow(unused)] fn main() { #[derive(Debug, Clone)] struct Cube { c: f32, } let x = Cube { c: 0.75 }; let y = x.clone(); println!("My cube size is: {}", x.c); println!("My cube is: {:?}", y); }
Introduction to Visual Studio Code
Visual Studio Code (often abbreviated as VS Code) is a cross-platform source code editor developed by Microsoft. It is compatible with Windows, macOS, and Linux, offering great flexibility to developers working in diverse environments. This lightweight yet powerful editor is designed to meet the needs of modern developers, providing a wide range of features.
Key Features of Visual Studio Code
Visual Studio Code stands out due to the following features:
-
Built-in support for multiple programming languages: VS Code supports a wide array of languages such as Python, JavaScript, C++, Java, and more, thanks to its extension system.
-
Extensions and customization: A vast library of extensions is available to add functionalities like debugging, version control, and language-specific tools.
-
Integrated debugger: VS Code provides an interactive debugging environment to simplify error correction in the code.
-
Version control integration: Seamless integration with Git and other version control systems allows developers to track code changes directly within the editor.
-
Integrated terminal: A terminal is available inside the editor, enabling command execution without leaving the application.
-
IntelliSense: This feature offers intelligent code completion and contextual suggestions based on syntax and variable types.
-
Cross-platform compatibility: VS Code works consistently on Windows, macOS, and Linux, ensuring a uniform user experience regardless of the operating system.
Thanks to its intuitive interface and powerful tools, Visual Studio Code has become one of the most popular editors among developers, whether they are beginners or experienced professionals. Its active community and frequent updates make it a reliable choice for addressing the evolving needs of software development.
How to use VS code efficiently for Rust
Installation with snap (Linux):
sudo snap install code --classic
-
Open the project directory:
code myproject -
Add the Rust Analyzer extension (left bar, small squares), search
rust→ install rust-analyzer -
Go to Explorer
-
Ctrl-Shift-P: command palette
-
Ctrl-P: search for a file
-
Ctrl-Shift-E: explorer
-
Ctrl-J: command line inside VS Code
-
Ctrl-Shift-I: indent the whole Rust file
To compile:
-
Either use the Run button above
main(Rust Analyzer) -
Or Ctrl-Shift-P and type
Run, then select -
Or Ctrl-J and type:
cargo build
You can have more documentation about the TOML format here: https://toml.io/en/ or here in french: https://toml.io/fr/. However, it is probably not necessary, TOML is quite simple to understand
list of derivable traits: https://doc.rust-lang.org/rust-by-example/trait/derive.html?highlight=derive#derive
Ownership, borrowing, mutability, heap and stack in Rust (2h on computers)
Pierre Cochard, Tanguy Risset
- Move semantics and Copy semantics
- References and borrowing
- Mutable Reference
- Heap and Stack: the String example
- Smart Pointers
- Rust Lifetimes
- Recalls on Heap and Stack {#appStack}
Course: Ownership (from https://doc.rust-lang.org/book/)
Ownership is a set of rules that govern how a Rust program manages memory. All programs have to manage the way they use a computer's memory while running. Some languages have garbage collection that regularly looks for no-longer-used memory as the program runs; in other languages, the programmer must explicitly allocate and free the memory.
Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks.
If any of the rules are violated, the program won't compile. None of the features of ownership will slow down your program while it's running.
Because ownership is a new concept for many programmers, it does take some time to get used to. When you understand ownership, you'll have a solid foundation for understanding the features that make Rust unique.
Here are the Ownership Rules:
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
The scope notion is (for the moment) the same as in traditional languages such as C.
Move semantics and Copy semantics
As we have seen in previous course, the following program will compile because:
-
default semantics of assignement is for type
Cubeis move. -
But the derivation of the
Copytrait turns it into a copy semantics, hencexandyrepresent two different values.
#[derive(Debug, Clone, Copy)] struct Cube { c: f32, } fn main() { let x = [Cube{c:0.5},Cube{c:0.75},Cube{c:1.0}]; let y = x; println!("x is: {:?}", x); println!("y is: {:?}", y); }
Try this program: fn main() { let x = [(10,20),(30,40),(50,60)]; let y = x; println!("x is: {:?}", x); println!("y is: {:?}", y); }Why does it work?
Correction
In Rust, we have Copy Semantics for primitive types like numbers, Boolean, tuples and arrays. Here x is an array of pairs of numbers hence it has value semantics
References and borrowing
There is an alternative to moving or duplicating (i.e. cloning) a
value: you can borrow it. Borrowing in Rust is done with the
reference operator: ’&’.
in the original Cube program which has not derived the Copytrait, create a reference toxby usinglet y = &x. Can you printxafter that?
Correction
#[derive(Debug)] struct Cube { c: f32, } fn main() { let mut x = Cube { c: 0.75 }; let y = &x; println!("My cube size is: {}", x.c); println!("My cube is: {:?}", y); }
Yes we can print x and y because both are referencing the same
object that is x. The cube is still owned by x
Course: Reference in Rust
References in Rust are equivalent to references in any language: a pointer to the same content, except that, because of the strong static verifications performed by the compiler, a reference is always guaranteed to point to a valid value of a particular type for the life of that reference1.
References are indicated by the ’&’ operator. As in C, the opposite of
referencing is dereferencing, which is accomplished with the dereference
operator: ’*’. However, in practice, the ’*’ operator can be
omitted; this is called deref coercion or autoderef (it is
implemented in a trait Deref that is implemented for all references).
This autoderef is implemented in almost all cases, except when you assign a value to a dereferenced mutable reference:
#![allow(unused)] fn main() { let mut x = 10; let y = &mut x; *y = 20; // explicit dereferencing is required here }
Borrowing is extremely useful in function calls. Each time you call a function with a parameter, the ownership of the object passed as a parameter is transferred to the function (actually, it is transferred to the formal parameter of the function). If, instead, you pass a reference to the object, the ownership does not change, so you can call many functions that only use an object without modifying it by using references.
Mutable Reference
Sometimes, you wish to have a function call that modifies an object.
For that, you can use a mutable reference with the syntax:
let y = &mut x. Mutable references in Rust do not change ownership.
They only provide exclusive access to a value for mutation while
ensuring that the ownership of the value remains unchanged.
By using a mutable reference to x ( let y = &mut x), write a function calleddoublethat double the size of your cubex.
Correction
#[derive(Debug)] struct Cube { c: f32, } fn double(y : &mut Cube) { y.c = 2.*y.c; } fn main() { let mut x = Cube { c: 0.75 }; let y = &mut x; double(y); println!("My cube is: {:?}", x); }
Course: Mutable reference
Ownership in Rust means having full control over a value (here a value is to be understand as L-value, i.e. a value which is stored in a memory box). The owner is responsible for managing the value's lifetime (we will talk later about lifetimes) and cleaning up its resources when it goes out of scope. Ownership can be transferred (moved) but is unique at any given time (except in very special cases that we will see).
Borrowing (via references, either &T or &mut T) allows you to
access a value without transferring ownership. Immutable borrow (&T):
Grants read-only access to a value. Mutable borrow (& mut T): Grants
exclusive, write-access to a value.
Rules of Mutable References:
-
You can only have one mutable reference to a value at a time.
-
While a mutable reference exists, no other references (mutable or immutable) to the same value are allowed.
It is important to understand that the Rust compiler evaluate very precisely the scope of variable.
In the two codes below, only one of them is correct, which one and why?
#[derive(Debug)]
struct Cube {
c: f32,
}
fn double(y : &mut Cube) {
y.c = 2.*y.c;
}
fn main() {
let mut x = Cube { c: 0.75 };
let y = &mut x;
double(y);
println!("My cube is: {:?}", x);
println!("My cube is: {:?}", y);
}
#[derive(Debug)]
struct Cube {
c: f32,
}
fn double(y : &mut Cube) {
y.c = 2.*y.c;
}
fn main() {
let mut x = Cube { c: 0.75 };
let y = &mut x;
double(y);
println!("My cube is: {:?}", y);
println!("My cube is: {:?}", x);
}
Correction
Only the code of the right side is correct: on the left side, the scope
of y include the use of x in the println, which means access to
the value of x while it is still borrowed by y. On the right side,
the scope of y does not extend after it is printed (it is not used
anymore), hence x can be used again.
Heap and Stack: the String example
Many programming languages don't require you to think about the stack and the heap very often. But in a systems programming language like Rust, whether a value is on the stack or the heap affects how the language behaves and why you have to make certain decisions.
Section 6 recalls the basics that everyone should know about the heap and the stack; please read it if you are not very familiar with these concepts.
The following code manipulates a string that contains hello:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; }
As you know, if s1 were set to an integer (say 5), then s2 would
have been set to a copy of 5, because int32 has copy semantics by
default. But here, s1 is assigned to a String. We will study strings
in more detail later, but this is a good example to understand the
difference between the heap and the stack.
A String is made up of three parts, shown in the left figure 2 (taken from the Rust book): a pointer to the memory that holds the contents of the string, a length, and a capacity. This group of data is stored on the stack. On the right is the memory on the heap that holds the contents. The reason for this is that a string might contain an arbitrarily long character string, but the size used to store the structural information (i.e., pointer, length, and capacity) does not change from one string to another; it is known statically.
(a) (b)
(a) Representation in memory of a String holding the value "hello" bound to s1.
(b) Representation in memory of the variable s2 that has a copy of the pointer, length, and capacity of s1
When we assign s1 to s2, the String data is copied, meaning we copy
the pointer, the length, and the capacity that are on the stack. We do
not copy the data on the heap that the pointer refers to. In other
words, the data representation in memory looks the right of like
Figure above.
Note that the effective content of the string (i.e. the hello
characters) is not duplicated, moreover it cannot be reached anymore
with s1 string have move semantics so s1 is moved to s2 (data is
now owned by s2)2.
Write a function fn append_world(s: & mut String)which appends " world" to string s. Call it giving a mutable reference tos2. you can use the functionpub fn push_str(&mut self, string: &str)
Correction
fn append_world(s:& mut String){ s.push_str(" world!") } fn main() { let s1 = String::from("hello"); let mut s2 = s1; append_world(&mut s2); println!("my string: {:?}", s2); }
Smart Pointers
Smart pointers are inherited from other language such as C++. Smart
pointers are data structures that act like a pointer but also have
additional metadata and capabilities. Rust has a variety of smart
pointers defined in the standard library that provide functionality
beyond that provided by references. To explore the general concept,
we'll look at a couple of different examples of smart pointers,
including a reference counting smart pointer type (Rc) and a unique
pointer on the heap (Box).
The most straightforward smart pointer is a Box, whose type is
written Box<T>. Boxes allow you to store data on the heap rather than
the stack with a Unique pointer. What remains on the stack is the
pointer to the heap data. This is usefull for instance to create
recursive type.
Create a type Listbased on the following structure: a list is either (the "either" correspond to anenum) the constantNilor the concatenation of an integer and aList:Cons(i32, List). Try without usingBoxthen usingBox.
You will have to declare the use of the created symbols after the definition of List by writing: use crate::List::{Cons,Nil};
Correction
Si on écrit ça:
enum List{ Cons(i32,List), Nil, } use crate::List::{Cons,Nil}; fn main() { let l1 = Cons(4,Cons(3,Nil)); }
Compilation error is:
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List{
| ^^^^^^^^^
2 | Cons(i32,List),
| ---- recursive without indirection
But this works:
#[derive(Debug)] enum List{ Cons(i32,Box<List>), Nil, } use crate::List::{Cons,Nil}; fn main() { let l1 = Cons(4,Box::new(Cons(3,Box::new(Nil)))); println!("L1 = {:?}",l1); }
In the majority of cases, ownership is clear: you know exactly which variable owns a given value. However, there are applications where a single value might have multiple "owners". For example, in graph data structures, multiple edges might point to the same node, and that node is conceptually owned by all of the edges that point to it. A node shouldn't be cleaned up unless it doesn't have any edge pointing to it and so has no owners.
You have to enable multiple ownership explicitly by using the Rust type
Rc<T>, which is an abbreviation for reference counting. We use
the Rc<T> type when we want to allocate some data on the heap for
multiple parts of our program to read and we can't determine at compile
time which part will finish using the data last. Note that Rc<T> is
only for use in single-threaded scenario, other constructs are used in
multithreaded programs.
Consider the scheme below where a list ( a) is shared by two other lists (bandc).Write a program that creates this object by using
Rc<T>instead ofBox<T>. For that you will have to:
use std::rc::Rc;- Provide 2 clones (for
bandc) of references toa:Rc::clone(&a)
Correction
#[derive (Debug)] enum List { Cons(i32,Rc<List>), Nil, } use crate::List::{Cons,Nil}; use std::rc::Rc; fn main() { let a=Rc::new(Cons(5,Rc::new(Cons(10,Rc::new(Nil))))); let b = Cons(3,Rc::clone(&a)); let c = Cons(3,Rc::clone(&a)); println!("a={:?}",a); println!("b={:?}",b); println!("c={:?}",c); drop(a); //b and c still exist println!("b={:?}",b); println!("c={:?}",c); drop(b); drop(c); }
Rust Lifetimes
Rust’s memory model aims to guarantee safety (no dangling pointers, no data races) without a garbage collector. To achieve this, Rust enforces a system of ownership, borrowing, and finally, the essential but sometimes confusing concept of lifetimes.
Lifetimes don’t control allocation or deallocation. Instead, they allow the compiler to reason about the validity of references. Their purpose is to ensure that no reference outlives the data it points to.
Why Rust Needs Lifetimes
Consider a function that returns a reference:
#![allow(unused)] fn main() { fn get_ref<'a>(s: &'a String) -> &'a str { &s[..] } }
Rust must ensure that:
- the returned reference is valid, and
- it never points to data that might be freed or moved before it is used.
Rust cannot always infer the relationships between the lifetimes of multiple references. When inference is too ambiguous, it requires explicit lifetime annotations.
A lifetime is therefore a static constraint, used at compile time, to avoid errors like dangling references.
Course: Lifetimes
A lifetime indicates how long a reference must remain valid. It is typically written as 'a, 'b, etc.
Example:
#![allow(unused)] fn main() { fn demo<'a>(x: &'a i32) { /* ... */ } }
This means:
- The reference
xmust stay valid for the entire duration of lifetime'a.
Lifetimes correspond to regions of code. Importantly, they are not stored at runtime. They exist only for the compiler’s static analysis.
Rust can often infer lifetimes automatically. Three lifetime elision rules apply to function signatures, which cover most cases. For instance:
#![allow(unused)] fn main() { fn len(s: &str) -> usize { s.len() } }
We do not annotate anything, but Rust implicitly understands:
- the input reference has a lifetime
'a, and - the return value does not depend on the input’s lifetime.
Rust only requires explicit lifetimes when there are multiple input references and the function returns one of them.
Why is the following Rust code invalid? Try to correct it.
#![allow(unused)] fn main() { fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } }
Correction
Rust rejects this function. It cannot determine whether `x` or `y` lives longer, and returning one of them is ambiguous.The correct version is:
#![allow(unused)] fn main() { fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } }
- The returned reference cannot outlive either
xory. - Its lifetime is the minimum of the two input lifetimes.
With this annotation, the compiler can verify the function’s safety.
Lifetimes in Structs
A struct containing references must almost always define lifetimes:
#![allow(unused)] fn main() { struct Holder<'a> { value: &'a str, } }
This means:
- A
Holderinstance cannot outlive the value it references.
This is essential for safety: if a struct contains a reference, Rust ensures that the struct is destroyed before the data it refers to.
Lifetimes in Methods: self and Lifetime Propagation
In method implementations, lifetimes apply to self:
#![allow(unused)] fn main() { impl<'a> Holder<'a> { fn get(&self) -> &str { self.value } } }
Rust infers that the returned reference has the same lifetime as &self, which is 'a.
No explicit annotation is needed because of elision rules.
The most famous lifetime is 'static. It indicates that the data:
- is available for the entire program (e.g., string literals), or
- is stored in a location that will never be freed prematurely.
Recalls on Heap and Stack
Although knowing the exact memory management is generaly not necessary to a programmer, in many case (system programming or embedded programming for instance, often done in Rust), it is crucial to understand how memory is handle by the compiler/OS. From the programmer point of view, and thanks to virtual memory system, everything happens as if we had all the memory available.
The memory management is more or less the same for every language and
system, what differ is what is visible for the programmer: explicit
memory management (malloc/free) or garbage collecting etc. This memory
is organized in different section, almost allways in the following way:
The "code" section contains the assemble code of the program. The "static" section contains all the "static" variables (i.e. variables that are available during the whole execution of the program). The two other section are managed dynamically during execution:
-
The heap is used for dynamic memory allocation:
malloc(in C) ornew(in object languages). The object stored in the heap have a lifetime that is independent of function execution, they can survive after the function that created them has finished. The heap can be managed explicitely (as is C withmallocandfree) or implicitely (using a garbage collector as in Python for instance). -
The stack is used to manage the execution of functions (or procedures in general) which includes in particuly the allocation and management of functions local variables.
The stack start from big adresses and grows downward, although it is often represented upside-down as below: small adresses up, big addresses down. The heap grows upward, when the two bounds meet, the system is out of memory.
The stack execution principle is important to know. when a function is called, a space is allocated on the stack to store its local variables: this space is called the function frame. When the function ends, its frame is freed and the stack goes back to the frame of the calling function.
Below is an illustration of the evolution of the stack during a function call, two registers of the processor are indicated: the stack pointer (SP) that indicate the top of the stack and the frame pointer that indicate the beginning of the frame of the current fonction. The frame contains all the information needed to the execution of the function, including room for local variables.
    
    
(a) before call (b) during call (c) after call
-
before the call, the frame pointer FP points to the frame of the calling function
-
during the call, the stack is increase (i.e. SP is decreased as the stack is upside-down) to have room for the frame of the called function. This includes room for local variable of the function, parameter given to the function and information for returning from the function (return address in the code because a given function can be called from many places in the code), room for the function result as well as some bookeeping information such as saved values of the processor registers.
-
after the call, the called fonction frame has disappeared. Actually its content is still there but cannot be accessed anymore because the stack pointer SP has been put back to its location before the call
Important to remember: The function variables whose size are known
at compile time are usually stored in the stack. The variable whose
size are know during execution, such as String or object created by
new are usually stored on the heap.
It is a major difference between Rust and other languages: there are no "null pointers", interestingly enough, the decision of authorizing Null pointer was taken by Tony Hoare place during the 60's, it is known as his "billion dollar mistake": https://news.ycombinator.com/item?id=12427069
It is important to know that the pointer used in a String has the
"Unique<T>" type, which forbid the object pointed by this pointer
to have two Owner at the same time. Hence the String type cannot
have a copy semantics
Advanced types Compound and collection types
Pierre Cochard, Tanguy Risset
Compound types
Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
Course: Tuples
'Tuples' are fixed-size collections of arbitrary-typed values,
they are defined with the (Type, Type, ...) syntax:
#![allow(unused)] fn main() { // Explicit type: let mut tup: (i32, i32, f32, &str) = (31, 16, 47.27, "hello!"); // Inferred type: let mut tup = (31, 16, 47.27, "hello!"); }
Accessing individual values within a Tuple can be done by either:
- referring to its index
- destructuring the tuple and bind it to individually-named variables:
#![allow(unused)] fn main() { let mut tup = (31, 16, 47.27, "hello!"); // Access by index: tup.0 = 22; tup.3 = "world!"; // 'Destructuring' a tuple: let (t0, t1, t2, t3) = tup; println!("y = ({t0}, {t2})"); }
'Tuples' are stored on the stack unless Box<...> is explicitely used.
Take a mutable reference to the third element ( f32) of the tupletupand pass it to a function that multiplyies it by2:
let mut tup = (31, 16, 47.27, "hello!");
// The function's prototype (to be implemented):
fn mul2(t: &mut f32);
mul2(...);
assert!(tup == (31, 16, 94.54, "hello!"));
Correction
#![allow(unused)] fn main() { let mut tup = (31, 16, 47.27, "hello!"); fn mul2(t: &mut f32) { *t += *t; } mul2(&mut tup.2); assert!(tup == (31, 16, 94.54, "hello!")); }
Rust refuse les mutations implicites. l'appel à mul2 a besoin de &mut:
& → reference (not a value)
mut → reference is exclusive and mutable
Tuples can be conviently used in a function in order to return multiple values, which can then be assigned to distinct variables in a same expression:
#![allow(unused)] fn main() { // A function returning a pair of signed integers: fn return_tuple(x: i32) -> (i32, i32) { return (x+1, x+2); } // Calling a function, and storing its result: let y: (i32, i32) = return_tuple(8); println!("y = {:?}", y); println!("y = ({:?}, {:?})", y.0, y.1); // Passing a tuple as an argument to a function: fn print_tuple(x: &(i32, i32)) { println!("x = ({:?}, {:?})", x.0, x.1); } print_tuple(&y); }
Write a function that transforms a (i32, i32)tuple by swapping its two values:
#![allow(unused)] fn main() { // The function prototype to be implemented: fn swap(tup: &mut(i32, i32)); let mut x = (31, 27); swap(&mut x); assert!(x == (27, 31)); }
Correction
#![allow(unused)] fn main() { fn swap(tup: &mut(i32, i32)) { *tup = (tup.1, tup.0); } let mut x = (31, 27); swap(&mut x); assert!(x == (27, 31)); }
Course: Arrays (primitive type)
Arrays are fixed-size groups of values of the same type, and can be defined in Rust with the syntax:
[Subtype; Length], for instance[i32; 10]
Arrays can be defined and initialized to a specific value in a single statement. They cannot be used without being initialized before.
#![allow(unused)] fn main() { // Explicit type: let a: [i32; 3] = [31, 16, 47]; // Inferred type: let b = [0, 1, 2, 3, 4]; // #[i32; 5] // Create and zero-initialize an array: let mut a: [usize; 10] = [0; 10]; // same as: let mut a = [0 as usize; 10]; // same as: let mut a = [0usize; 10]; // Writing at a specific index: // Note: in Rust, as in C, array indices start at 0 a[0] = 2; println!("a[0] = {}", a[0]); }
'Arrays' are stored on the stack unless Box<...> is explicitely used.
As in most programming languages, multidimensional/nested arrays are also supported in rust, and can be declared as follows:
#![allow(unused)] fn main() { // 2-dimensional array, 2 arrays of `i32` with a length of 10 each: let mut multi_array = [[0 as i32; 10]; 2]; [...] multi_array[0][0] = 1; }
What would be the type of the following arrays?
#![allow(unused)] fn main() { let a1 = [(1, 2), (3, 4), (5, 6)]; let a2 = [(1, 2), (3, 4), (5, (6, 7))]; }
Correction
The type of a1 would be:
#![allow(unused)] fn main() { let a1: [(i32, i32); 3] = [(1, 2), (3, 4), (5, 6)]; }
a2, on the other hand, does not compile: all values must have the same type in an array.
Course: Ranges & Iterators
Arrays are convenient for storing and processing a set of contiguous data on the stack, for instance through the use of loops, ranges and iterators.
A range represents an interval of values between a start and an end point.
In rust, they can be conveniently used with the start..end construct (here excluding the end value),
or with start..=end (here including the end value).
Examine the following assert!statements, will this program compile?
let a = 0..10;
let b = 1..=10;
// 'a' range:
assert!(a.contains(&0));
assert!(!a.contains(&10));
// 'b' range:
assert!(!b.contains(&0));
assert!(b.contains(&10));
// The 'a' and 'b' ranges have the same number of elements:
assert_eq!(a.count(), b.count());
Correction
Yes, all the assert!() statements are true.
Course: Iterators
Iterators allow to go through an array, a range or a collection, and access each element one-by-one.
#![allow(unused)] fn main() { let r = 0..10; // Iterate over a range: for n in r.into_iter() { print!("{n} "); // -> 0 1 2 3 4 5 6 7 8 9 } println!(); // Iterate over an array: let mut a = [0; 10]; // Basic for-loop iteration: for x in a { println!("{x}"); } // From a 'range': for n in (0 .. a.len()) { println!("{}", a[n]); } // As mutable, changing the values of the array: for x in &mut a { *x += 1; } // Equivalent to (using a 'closure'): a.iter_mut().for_each(|x| *x += 1 ); // Iterate with both element and index: for (i, x) in a.iter_mut().enumerate() { *x += i; } }
Using ranges and/or iterators, write in the following multidimensional array's first sub-array values that incrementally go from 1 to 10, and in the second, decrement the values from 10 to 1, as shown below:
#![allow(unused)] fn main() { let mut multi_array = [[0 as i32; 10]; 2]; // The following must be true: assert_eq!(multi_array, [ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] ]); }
Correction
#![allow(unused)] fn main() { let mut multi_array = [[0 as i32; 10]; 2]; for n in 1..=10 { multi_array[0][n-1] = n as i32; } for n in (1..=10).rev() { multi_array[1][10-n] = n as i32; } // The resulting 2D-array assert_eq!(multi_array, [ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] ]); }
Collections
In addition to primitive compound types, the Rust standard library includes a number of very useful data structures called collections. Unlike the built-in array and tuple types, the data these collections point to is stored on the heap, which means the amount of data does not need to be known at compile time and can grow or shrink as the program runs.
Course: Vectors
'Vectors' are a collection of multiple values of a same type stored on the heap. Unlike arrays, they have a dynamic size: they can grow, or shrink.
A Vec object has ownership over the data located in its underlying heap-allocated buffer,
which means that the buffer will be deallocated whenever the owning object goes out of scope.
#![allow(unused)] fn main() { // The easiest way to create a vector is to use the 'vec!()' macro: let mut v = vec![0, 1, 2, 3, 4, 5]; // Vec<i32> println!("Value: {:?}", v); // 'Pushing' (appending) a new value at the end: v.push(6); println!("Value: {:?}", v); // 'Popping' (removing) its last value: let last = v.pop(); println!("Last value: {:?}, Vector: {:?}", last, v); // removing value at index i: v.remove(i) }
Examine the following v1,v2andv3vectors and their underlying heap buffer pointers.
#![allow(unused)] fn main() { let mut v0: Vec<i32> = vec![0, 1, 2, 3, 4]; // get a pointer to the underlying heap memory buffer: let v0_ptr: *const i32 = v0.as_ptr(); // Create another vec 'v1' from 'v0', and get its heap pointer again: // i.e. move v0 to v1 let mut v1: Vec<i32> = v0; let v1_ptr = v1.as_ptr(); // Create another vec 'v2' from 'v1'using clone(): let mut v2: Vec<i32> = v1.clone(); let v2_ptr = v2.as_ptr(); }
Which of the following assertions are
true:
#![allow(unused)] fn main() { // Assertion A: the address of pointer 'v0' is the same as pointer 'v1' assert_eq!(v0_ptr.addr(), v1_ptr.addr()); // Assertion B: the address of pointer 'v1' is the same as pointer 'v2' assert_eq!(v1_ptr.addr(), v2_ptr.addr()); // Assertion C: the address of pointer 'v0' is the same as pointer 'v2' assert_eq!(v0_ptr.addr(), v2_ptr.addr()); }
Correction
Assertion A is correct:
v0_ptrandv1_ptrare the same, sincev0was moved intov1its memory was not re-allocated at any other address, butv1has now exclusive ownership over this address, makingv0unaccessible.Assertion B and C are incorrect:
v2has been explicitly cloned fromv1: this means that all of its contents (including its heap-allocated memory buffer) have been deep-copied: another memory zone has been allocated and has been filled with the same contents.
Iterating over a vector is the exact same process as for an array (most operations are inter-compatible!).
#![allow(unused)] fn main() { // Initializing from a range and iterator: let mut v = Vec::from_iter((0..6).map(|i| i+1 )); println!("Value: {:?}", v); // Iterate/increment: for x in &mut v { *x += 1; } println!("Value: {:?}", v); // General operations: v.rotate_left(1); println!("Value: {:?}", v); // etc. }
Using a single loop, move the contents of vector vto arrayasuch as vectorvis equal tovec and arrayais equal to the inverse ofv:[5, 4, 3, 2, 1, 0]:
#![allow(unused)] fn main() { let mut v = vec![0, 1, 2, 3, 4, 5]; let mut a = [0; 6]; (...) assert_eq!(v, vec![]); assert_eq!(a, [5, 4, 3, 2, 1, 0]); }
Correction
#![allow(unused)] fn main() { let mut v = vec![0, 1, 2, 3, 4, 5]; let mut a = [0; 6]; for n in (0..v.len()) { a[n] = v.pop().unwrap(); } assert_eq!(v, vec![]); assert_eq!(a, [5, 4, 3, 2, 1, 0]); }
Course: Hash-maps
HashMap are heap-allocated collections of same-type values indexed by a unique key.
Like vectors, they can grow, or shrink. They make a convenient choice for representing indexes, dictionaries, or any other type of database-like objects:
#![allow(unused)] fn main() { // Unlike Vec, the HashMap data structure need to be explicitly included! use std::collections::HashMap; // Inferred type: let mut departments = HashMap::new(); // HashMap<i32, str> departments.insert(85, "Vendée"); departments.insert(31, "Haute-Garonne"); departments.insert(44, "Loire-Atlantique"); // We use the ampersand(&) and the key (&1) as the argument // because [..] returns us a reference of the value. It is not //the actual value in the HashMap. let d31 = departments[&31]; assert_eq!(d31, "Haute-Garonne"); // Removing a key: departments.remove(&85); // Iterating over all values: for department in departments { // We get a tuple! println!("Key: {}, Value: {}", department.0, department.1); } }
Move the contents of the following Vecobject into aBTreeMap(which behaves the same as aHashMap, but will sort its contents by key) in order to get these athlete names sorted by their score in points.Note: some of them have the same score, which should appear in the same
key.
#![allow(unused)] fn main() { use std::collections::BTreeMap; let vec = vec![ ("Y. Horigome", 281), ("N. Huston", 279), ("M. Dell", 153), ("J. Eaton", 281), ("S. Shirai", 278), ("K. Hoefler", 270), ("C. Russell", 211), ("R. Tury", 273), ]; let mut map = BTreeMap::new(); [...] for score in map { println!("{:?}", score); } }
Hints:
- build a BTreeMap with the key being the score
- use the
entry(key)method of HashMap that return the HashMap entru corresponding to key- apply
or_defaulton the result of entry (it creates an entry with no value for this key, if the key does not exists in the HashMap)
The last
forloop should print:
(153, ["M. Dell"])
(211, ["C. Russell"])
(270, ["K. Hoefler"])
(273, ["R. Tury"])
(278, ["S. Shirai"])
(279, ["N. Huston"])
(281, ["Y. Horigome", "J. Eaton"])
Correction
#![allow(unused)] fn main() { use std::collections::BTreeMap; let vec = vec![ ("Y. Horigome", 281), ("N. Huston", 279), ("M. Dell", 153), ("J. Eaton", 281), ("S. Shirai", 278), ("K. Hoefler", 270), ("C. Russell", 211), ("R. Tury", 273), ]; let mut map: BTreeMap<i32, Vec<&str>> = BTreeMap::new(); for v in vec { map.entry(v.1) .or_default() .push(v.0); } for score in map { println!("{:?}", score); } }
'string' types (str and String)
Course: str primitive
The str primitive type can be used to represent a string literal:
#![allow(unused)] fn main() { // String literal: let s = "Hello, World!"; }
As a literal, a str has a static lifetime which can be also explicitly stated in its type declaration.
A static lifetime means that the object is valid throughout the entire duration of the program.
The type &str is usually called a string slice
#![allow(unused)] fn main() { // Here, the three syntaxes are equivalent: let s = "Hello, World!"; // Inferred type & lifetime let s: &str = "Hello, World!"; // Explicit type, inferred lifetime let s: &'static str = "Hello, World!"; // Explicit type & lifetime }
Unlike const char* in the C programming language, &str in Rust is not null-terminated, but relies on a slice, which is composed of a pointer and a size in bytes:
#![allow(unused)] fn main() { let s = "Hello, World!"; println!("Pointer: {:?}, Length: {} bytes", s.as_ptr(), s.len()); for (n, char) in s.chars().enumerate() { println!("Char {n}: {char}"); } }
For safety reasons, Rust doesn't allow modifying the actual contents (the characters) of a &str, thus the following does not compile:
#![allow(unused)] fn main() { let s: &mut str = "Hello, World!"; }
Run the following code:
#![allow(unused)] fn main() { let s1 = "It's not about the bunny \t"; // trim() Returns a string slice with leading and trailing whitespace removed. let s2 = s1.trim(); println!("{s1}"); println!("Address: {:?}, Length: {}", s1.as_ptr(), s1.len()); println!(); println!("{s2}"); println!("Address: {:?}, Length: {}", s2.as_ptr(), s2.len()); }
- Since Rust forbids modifying the contents of a
strliteral, why are we in this case allowed to use the.trim()function? What is truly happening in this code?
Correction
In this specific case, since we can clearly see that the underlying pointer is the same in both s1 and s2, we can deduce that the underlying buffer has not been modified, but instead we instantiated another slice object, which points to the same address as s1, but has a different length, which omits the trailing whitespaces and tab.
- What would happen if we modified
s1as follows?
#![allow(unused)] fn main() { let s1 = " It's not about the bunny \t"; let s2 = s1.trim(); }
Correction
Here, both the pointer and length would be different in s2, the pointer would be indeed offset by the number of whitespaces leading the string literal (which is here equal to 5):
#![allow(unused)] fn main() { let s1 = " It's not about the bunny \t"; let s2 = s1.trim(); // s2 pointer address minus 5 (removing the whitespaces) is equal to s1 pointer's address assert_eq!(s2.as_ptr().addr()-5, s1.as_ptr().addr()); }
Course: String
A String, on the other hand, is a standard library collection type that can be basically seen as a vector of char, dynamically stored on the heap. Just like a Vec, it can grow, shrink, and has ownership over its own underlying buffer, which makes it an easier object to manipulate. While it inherits all of the str methods, it does not have a static lifetime.
#![allow(unused)] fn main() { // Create from a string literal: let mut s = String::from("Owls are not what they seem"); // Append a 'char': s.push('!'); println!("Value: {:?}", s); // Append another string: s.push_str(" Really?"); println!("Value: {:?}", s); // Other ways of appending to the String: s = s + " Yes, "; s += "really!"; // Iterate over every 'char' for c in s.chars() { print!("{c} "); } println!(""); // Example of transformation: s = s.chars().rev().collect(); println!("Value: {:?}", s); }
Examine the following code:
#![allow(unused)] fn main() { let s0: &str = "That gum you like is going to come back in style"; // Build a 'String' object from the previous '&str': let mut s1: String = String::from(s0); // Now modify 'string': s1 = s1.to_ascii_uppercase(); println!("{s1}"); }
Is
s0still accessible? If yes, what is now its value? Is it the same ass1and why?
Correction
s0 is still accessible, and its value remains the same. The contents of s0 has been copied into a heap buffer owned by s1. The two are completely separate from one another.
We now call the
as_str()method ons1, and store the resulting&strvalue in a new variable calleds2. Can you guess what is the lifetime ofs2?
#![allow(unused)] fn main() { let s0: &str = "That gum you like is going to come back in style"; // Build a 'String' object from the previous '&str': let mut s1: String = String::from(s0); let s2: &str = s1.as_str(); }
Correction
s2 has the same lifetime as s1, since its referring to the same underlying heap buffer. In this case, this &str is not a string literal, which would have a 'static lifetime, but is a slice that points to dynamically-allocated memory.
Slices
A slice in rust can be considered as a bounded pointer or reference to a contiguous sequence of elements in an array,
a collection, or a string of characters, as we saw earlier. It is declared with the &[T] syntax. Since it works like a reference, it does not have ownership over its contents.
#![allow(unused)] fn main() { // Create a byte buffer: let mut buffer = [0 as u8; 16]; // Get a slice on half the buffer: // (notice how the slice itself is not mutable, // but instead points to a mutable sequence in the buffer) let slice: &mut[u8] = &mut buffer[0..8]; // we use the 'range syntax' here to capture the slice // Iterate on the slice to change values: for (i, n) in slice.iter_mut().enumerate() { *n = i as u8; } println!("{:?}", buffer); }
Take a slice out of the stringobject, starting from character25until the end, and use the.make_ascii_lowercase()method on the captured slice.
#![allow(unused)] fn main() { let mut string = String::from("YOU REMIND ME TODAY OF A SMALL MEXICAN CHIHUAHUA"); let slice = ...; slice.make_ascii_lowercase(); println!("{string}"); }
Correction
#![allow(unused)] fn main() { let mut string = String::from("YOU REMIND ME TODAY OF A SMALL MEXICAN CHIHUAHUA"); let slice = &mut string[25..]; slice.make_ascii_lowercase(); println!("{string}"); }
What is the inferred type of the
slicevariable?
Correction
It is a &mut str.
Struct, enums, Traits and Object definitions
Pierre Cochard, Tanguy Risset
- Struct in Rust
- Enum and pattern matching
- Generic Types
- Traits: Defining Shared Behavior
- Programming paradigms in Rust
Struct in Rust
Course: Structs (from https://doc.rust-lang.org/book/)
We have already seen the Struct concept: Structs are similar to tuples in that they both hold multiple related values.
Unlike tuples, you can name each field member so it’s clear what the types mean. Adding these names means that structs are more flexible than tuples: you don’t have to rely on the order of the data to specify or access the values of an instance.
Here is an example of Struct definition:
#![allow(unused)] fn main() { struct User { active: bool, username: String, email: String, sign_in_count: u64, } }
We create an instance by stating the name of the struct and then add curly brackets containing key: value pairs:
#![allow(unused)] fn main() { let user1 = User { active: true, username: String::from("someusername123"), email: String::from("someone@example.com"), sign_in_count: 1, }; }
To get a specific value from a struct, we use dot notation. For example, to access this user’s email address, we use user1.email.
When creating instances from other instances The syntax .. specifies that the remaining fields not explicitly set should have the same value as the fields in the given instance:
#![allow(unused)] fn main() { let user2 = User { email: String::from("another@example.com"), ..user1 }; }
Unit struct (i.e. struct without fields) and Tuple struct (i.e. struct with no named fields) are special Rust features that might be useful in some cases (see https://practice.course.rs/compound-types/struct.html)
Consider the following structdefinition (replacingstringby string slicestr):#![allow(unused)] fn main() { struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } }Try to create an instance of this structure in a main program.
You will not be able because the lifetime of the
strslice is not known (it depends on the lifetime of the string it points to). In aStructall fields must have the same lifetime.Try to solve the problem by adding lifetime in your definition, using the compiler error message.
Correction
$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 ~ username: &'a str,
#![allow(unused)] fn main() { struct User<'a> { active: bool, username: &'a str, email: &'a str, sign_in_count: u64, } }
Methods in Rust
As in any object-oriented langage, methods can be defined within the context of a struct making a struct a regular object as defined in the object-oriented programming paradigm.
(In Rust, methods can be also defined in the context of an enum or a trait object. The definition of methods use the keyword fn as function but the are preceded by the keyword impl (eg: impl MyStruct { fn [...] }) to specify that this function is only defined in the context of a particular type object.
For a method, the first parameter is always &self (or self if the method need to take ownership of self). &self is a short cut for &self: MyObjectType whatever this type is.
For example, if we want to defined a methode name() for the previous structure User (the first on, with String) in order to obtain the username of a User, we will use the following syntax:
impl User {
fn name(&self)->&String{
&self.username
}
}
When calling this method, the self argument is never written, it is implicit (e.g. : user1.name()). The above method is ofter called a getter. Getters are not implemented by default for Rust Struct, it is a good habit to name the getter after the field name they are getting (hence we should have called this method username())
Define a structRectanglewith two integer fieldswidthandheight. Then implement two methods forRectangle:area(&self)(which computes the surface of the rectangle) andfits_in(&self, other_rect: &Rectangle)which indicate ifselffits completely inside theother_rectrectangle.
Correction
#![allow(unused)] fn main() { struct Rectangle { height: i32, width: i32, } impl Rectangle { fn area(&self)->i32{ self.height * self.width } } impl Rectangle { fn fits_in(&self,other_rect: &Rectangle)->bool{ if other_rect.width>self.width && other_rect.height>self.height { return true; } else { return false; } } } }
Enum and pattern matching
Course: Enum and option Enum (from https://doc.rust-lang.org/book/)
As in C, Enum gives you a way of saying a value is one of a possible set of values. For instance, An IP address can be either an IPv4 address or an IPv6 address, but not both at the same time:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } }
This allows to store the two possible kind in "the same" memory location given that they will never be active together in one instance.
A new feature comparing to C is that we can put data directly into each enum variant.
#![allow(unused)] fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); }
Rust has an extremely powerful control flow construct called match that allows you to compare a value against a series of patterns and then execute code based on which pattern matches. Pattern matching in an important area of computer science and compilation, we just show a very simple example here:
#![allow(unused)] fn main() { enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } }
The Option Enum
As explained in TD1, the Null value was invented to represent the "no value" value and it was a bad invention. With the Option Enum Rust proposes a mecanism that explicitely distinguish the cases where a variable has a value or no value. Rust defines (in the standard library, in the prelude) a particular Option<T> enum:
#![allow(unused)] fn main() { enum Option<T> { None, Some(T), } }
The <T> syntax is a feature of Rust called a generic type parameter (similar to template in C++) that will be explained hereafter.
Write a function that computes the square root of a f32number and return None if the number is negative.
Correction
fn square_root(a: f32) -> Option<f32>{ match a { n if n >= 0.0 => Some(a.sqrt()), _ => None, } } fn main() { println!("square root of N={:?}: {:?}",5,square_root(5.0)); println!("square root of N={:?}: {:?}",-5,square_root(-5.0)); }
Of course, a Some(T) object cannot be "casted" or "simplified" in a T object. The only way to get the Tobject is to unwrap the option.
unwrap will be explained in next course (for Result type), here it will return the Tobject or panic in case of None. The Option<T> enum has a large number of methods that are useful in a variety of situations https://doc.rust-lang.org/std/option/enum.Option.html.
Try to unwrap (i.e. apply .unwrap()) your square root for positive and negative number.
Correction
fn square_root(a: f32) -> Option<f32>{ match a { n if n >= 0.0 => Some(a.sqrt()), _ => None, } } fn main() { println!("square root of N={:?}: {:?}",5,square_root(5.0).unwrap()); println!("square root of N={:?}: {:?}",-5,square_root(-5.0).unwrap()); //fails }
Generic Types
Every programming language has tools for effectively handling the duplication of concepts. In Rust, one such tool is generics. Generic Types, Traits and Lifetimes are three kind of generics.
Generic Data Types can be used in the definition of functions, struct or enum. Generic Data Type are very close to the template concept in C++.
Write a function that will be able to find the minimum of a Vectormay this vector be a vector ofi32,f32or characters or on any type that has the possibility of comparing elements. Use the following function signature:smallest<T: PartialOrd> (v: &[T])-> &T
Correction
fn smallest<T: PartialOrd> (v: &[T])-> &T { let mut min=&v[0]; for e in v{ if e < min { min = e; } } min } fn main() { let v1 = vec![2,7,9,1]; println!("min v1= {}",smallest(&v1)); let v2 = vec![2.0,7.7,9.9,1.1]; println!("min v2= {}",smallest(&v2)); let v3 = vec!['a','t','4','j']; println!("min v3= {}",smallest(&v3)); }
Define a structure Pointwith two fieldsxandythat can be integer or floating point. IsPoint{x:2,y:2.3}a valid instance of the structurePoint?
Correction
No, because the types of the two fields are not some same:
either Point{x:2, y:3} or Point{x:2.0, y:2.3}.
#![allow(unused)] fn main() { struct Point<T> { x: T, y: T, } }
Traits: Defining Shared Behavior
A trait defines the functionality or behaviour that a particular type has and can share with other types. We have already seen some very common traits: Clone or Debug. The trait Clone for instance represents the fact that a variable of a type can be duplicated (i.e. cloned) to a second instance of the type, identical to the variable but refering to a different memory location. Traits are similar to a feature often called interfaces in other languages, although with some differences.
Defining a trait consists in the following syntax:
impl TheTrait for TheType {...}.
In general, trait names should be defined in UpperCamelCase (e.g. IsEven) and traits methods should be defined with snake_case name (e.g. is_even(&self)). Implementing a trait for a particular type is done using impl name_of_trait for name_of_type{[...]}.
Define a trait IsEventhat is composed of the methodis_even(&self). Implement the trait for theRectangletype defined préviously (a Rectangle is even if both height and width are even)
Correction
trait IsEven { fn is_even(&self)->bool; } #[derive(Debug)] struct Rectangle { height: i32, width: i32, } impl IsEven for Rectangle { fn is_even(&self)->bool { return (self.height%2 == 0) && (self.width%2 == 0) } } fn main() { println!("Hello, world!"); let r1=Rectangle{height:20,width:30}; println!("Rectangle {:?} is even: {}",r1,r1.is_even()); }
You can specify a default implementation of each method in the definition of the trait (then an explicit implementation will hide the default implementation)
Trait as parameter: the Trait Bound Syntax
Trait can be used as function parameter, the function will be valid for any type implementing the trait. There are two possible syntaxes, the impl Trait syntax for simple cases and the Trait Bound Syntax for the general case:
fn myFunction(a_variable: &impl TheTrait) { [..]}
fn myFunction<T: TheTrait>(a_variable: &T) { [..]}
Define a function notifyEventhat takes as parameter a type that implements the traitIsEvenand notify (i.e. print) the fact that the object is even. Note that you can implementIsEvenandDebugtraits together by specifyingIsEven+Debug.
Correction
fn notify_even<T: IsEven+Debug> (o: &T){
println!("This object: {:?} is even? {}",o,o.is_even());
}
Sometimes, this syntax can be heavy and you can use the where Clause: instead of writing this:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
we can use a where clause, like this:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
The impl trait syntax can be used to specify that a result of a function must implement a trait.
Using Trait Bounds to Conditionally Implement Methods
The following example (from https://doc.rust-lang.org/book/ch10-02-traits.html) illustrates the fact the trait can be used to conditional implementation of methods
#![allow(unused)] fn main() { use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } } }
Programming paradigms in Rust
This section is largely inspired by https://corrode.dev/blog/paradigms/.
Rust is a multi-paradigm programming language, accommodating imperative, object-oriented, and functional programming styles. It is important to be aware that the programming paradigm is an important design choice when you start a new Rust program. The choice of style often depends on a developer’s background and the specific problem they’re addressing. but there are also many "known habits" of Rust developpers.
A specificity compared to other recent languages is the important and growing influence of functional programming in Rust.
A simple example: integer Vector sum.
Consider the problem of suming all the component of vector with i32values. Write a program to do it in aniterative/imperativeway (i.e. a loop accumulating in a temporary variable)
Correction
#![allow(unused)] fn main() { let vector = vec![9,6,-2,7]; let mut sum = 0; for i in 0..vector.len() { sum += vector[i]; } println!("Hello, world sum = {}!",sum); }
Do the same thing in a more functionnal way: Use the iter()method ofVectortype andsum()method of iterators
Correction
#![allow(unused)] fn main() { let vector = vec![9,6,-2,7]; let sum2 :i32 = vector.iter().sum(); println!("Hello, world sum2 = {}!",sum2); }
The second formulation is, of course, much more concise, as is often the case in functional programming, but it is less suited to certain types of processing (matrix calculations, for example).
A More Complete Example
Consider the following Rust code, which defines a list of several languages along with the paradigms they are associated with. You will start from this code. The task will be to find the top five languages that support functional programming and have the most users.
#![allow(unused)] fn main() { #[derive(PartialEq,Clone,Debug)] enum Paradigm { Functional, ObjectOriented, } #[derive(Clone,Debug)] struct Language { name: &'static str, paradigms: Vec<Paradigm>, nb_users: i32, } impl Language { fn new(name: &'static str, paradigms: Vec<Paradigm>, nb_users: i32) -> Self { Language { name, paradigms, nb_users } } } let languages = vec![ Language::new("Rust", vec![Paradigm::Functional,Paradigm::ObjectOriented], 100_000), Language::new("Go", vec![Paradigm::ObjectOriented], 200_000), Language::new("Haskell", vec![Paradigm::Functional], 5_000), Language::new("Java", vec![Paradigm::ObjectOriented], 1_000_000), Language::new("C++", vec![Paradigm::ObjectOriented], 1_000_000), Language::new("Python", vec![Paradigm::ObjectOriented, Paradigm::Functional], 1_000_000), ]; }
Give an imperative solution to the task using nested forloops
Correction
#![allow(unused)] fn main() { fn five_best_func_lang1(lang_list: &Vec<Language>)->Vec<Language>{ let mut fun_lang: Vec<Language> = vec![]; for l in lang_list { if l.paradigms.contains(&Paradigm::Functional){ fun_lang.push(l.clone()); } } //buble sort to sort the vector let mut changed = true; while changed { changed = false; for i in 1..fun_lang.len(){ if fun_lang[i].nb_users > fun_lang[i-1].nb_users { fun_lang.swap(i,i-1); changed = true; } } } //Select the first five while fun_lang.len()>5 { fun_lang.remove(fun_lang.len()-1); } fun_lang } }
Give a more functional implementation by transforming the vector in an iterator and using the following method (see https://docs.rs/itertools/latest/itertools/trait.Itertools.html):
use itertools::Itertools;
into_iter()method transforms a Vector into an iterator
filter()method can keep only element with a property (use a lambda as an argument to filter)
sorted_by_key()sorts all iterator elements into a new iterator in ascending order (useReverse())
collect()transform an iterator into a collection.
Correction
#![allow(unused)] fn main() { fn five_best_func_lang2(lang_list: &Vec<Language>)->Vec<Language>{ lang_list.into_iter() .sorted_by_key(|lang| lang.nb_users) .filter(|lang| lang.paradigms.contains(&Paradigm::Functional)) .rev() .take(5) .cloned() .collect() } }
Which implementation is more efficient in terms of computation time?
-use std::time::Instant;
- Or (more complex) use
criterioncrate:
#![allow(unused)] fn main() { [dev-dependencies] criterion = "0.5" }
Correction
let start = Instant::now();
let l2 = five_functionnal_lang2(&languages);
let t2 = start.elapsed();
println!("func2: {:?}", t2);
Rust as a safe language: Pattern Matching, Handling Results/Errors, Options and Simple macros
Pierre Cochard, Tanguy Risset
Pattern matching in Rust
Pattern matching is the act of checking a given sequence of tokens or expressions for the presence of one or more specific patterns. The concept is implemented in many programming languages (Rust, Haskell, Swift, etc.) and tools, for various purposes, such as: regular expressions, search and replace features, etc.
In Rust, patterns and pattern matching constitute a specific syntax, that is used in different places in the language (match statements, if let expressions, function parameters, simple macros, etc.)
A pattern consists of some combination of the following:
- Literals
- Destructured arrays, enums, structs, or tuples
- Variables
- Wildcards and Placeholders (most used:
_meaning Anything, and..meaning The rest). A wildcard matches values; a placeholder stands in for something not yet specified (often a type).
Course: match statements
The primary, and most explicit, use of pattern matching in Rust is done through the match statement, which can be perceived as the Rust-equivalent of a C switch, but with additional features. Its syntax is also a little bit different. For instance, let's take a look at this simple C program:
// C basic switch case:
enum Colors {
Red, Blue, Green, Yellow, Orange
};
bool match_color_orange(Colors color) {
switch (color) {
case Orange: {
printf("Orange!\n");
return true;
}
case Red:
case Blue: {
printf("Not orange :(\n"));
return false
}
default: {
printf("Still not orange\n");
return false;
}
}
}
In Rust, we would have the following equivalent:
#![allow(unused)] fn main() { enum Colors { Red, Blue, Green, Yellow, Orange } fn match_color_orange(color: Colors) -> bool { match color { // 'case' statements are replaced by the // 'PATTERN => EXPRESSION' syntax: Colors::Orange => { println!("Orange!"); true } // We use '|' operators here, instead of having // multiple 'case' statements: Colors::Red | Colors::Blue => { println!("Not orange :("); false } // Anything else (equivalent to 'default'): _ => { println!("Still not orange..."); false } } } }
Once a pattern mach is found, the corresponding instruction are executed and the match instruction terminates (it does not check for other matching patterns, the first matching pattern is choosen).
match statements can be directly bound to variables:
#![allow(unused)] fn main() { enum Colors { Red, Blue, Green, Yellow, Orange } let color = Colors::Red; let is_color_warm = match color { Colors::Orange => true, Colors::Red => true, Colors::Yellow => true, _ => false }; }
Matching ranges is also supported, for instance:
#![allow(unused)] fn main() { fn match_number(number: i32) { match number { 50..=99 => println!("Between 50 and 99"), 100..=1000 => println!("Between 100 and 1000"), _ => println!("Other value") } } }
And, as a matter of fact, any other type of expression can be matched! from string types:
#![allow(unused)] fn main() { fn match_str(s: &'static str) { match s { "Orange" => println!("Orange!"), "Yellow" => println!("Not orange"), _ => println!("Something else...") } } }
to other kinds of collections:
#![allow(unused)] fn main() { fn match_tup(tup: (i32, i32)) { match tup { (0, 0) => println!("Zeroes!"), (1, 1) => println!("Ones"), _ => println!("Something else...") } } match_tup((1, 1)); match_tup((0, 1)); fn match_array(arr: [u8; 3]) { match arr { [0, 1, 2] => println!("Array match!"), _ => println!("No match") } } match_array([0, 1, 2]); match_array([4, 5, 6]); fn match_slice(sl: &[i32]) { match sl { &[0, 1, 2] => println!("Slice matches!"), _ => println!("No match") } } match_slice(&[0, 1, 2]); }
Write a matchstatement which applies to anyi32number to check if it is lower than 100. It should only have the two following patterns:
- The value is below
100(including negative numbers);- The value is equal or higher than
100.
Correction
#![allow(unused)] fn main() { fn match_number(number: i32) { match number { ..100 => println!("Below a hundred"), _ => println!("A hundred or above") } } }
Course: match statements: "flexible" patterns
As we saw earlier, the match statement can test any kind of value, and it also extends to custom and composite types, including struct instances. A custom struct can be indeed either matched by its contents in a very precise manner:
#![allow(unused)] fn main() { struct Point { x: isize, y: isize } let point = Point {x: 0, y: 100}; match point { // Only match Point if its 'x' member is equal to 0 // and 'y' is equal to 100: Point {x: 0, y: 100} => println!("Match!"), _ => println!("No match!") } }
Or, in a more flexible way, using, for instance, ranges for its member values:
#![allow(unused)] fn main() { struct Point { x: isize, y: isize } let point = Point {x: 25, y: 100}; match point { // Only match if 'x' is between 0 and 100, // and 'y' is between 50 and 100 Point {x: 0..=100, y: 50..=100} => println!("Match!"), _ => println!("No match!") } }
Finally, the _ => expression can be extended to any kind of value (or field value) that we want to ignore. This can also be done using the .. syntax, which will ignore all the following values or field values:
#![allow(unused)] fn main() { struct Point { x: isize, y: isize } let p = Point {x: 0, y: 100}; match p { // Only match if 'x' is between 0 and 100, // and ignore the 'y' field: Point {x: 0..=100, y: _} => println!("Match!"), // Only match if 'x' is between 101 and 1000, // Similarly, the '..' syntax will ignore all the struct fields after 'x': Point {x: 101..=1000, ..} => println!("Match!"), _ => println!("No match!") } }
Note: for compound/collection types, the .. syntax may be followed by other patterns:
#![allow(unused)] fn main() { let tup = (0, 1, 2, 3, 4); match tup { // The first element of the tuple should be '0', and the last should be '4', // we ignore the values in-between: (0, .., 4) => println!("Match!"), _ => println!("No match") } }
Implement a matchstatement on a&[i32]slice. It should match all of the following patterns:
- The slice's first value should be
0;- The slice's second value should be either
10or20;- The slice's final value should be
100;- The slice can have an arbitrary size.
You can use the following assert! statements to test your code:
#![allow(unused)] fn main() { fn match_slice(s: &[i32]) -> bool; assert_eq!(match_slice(&[1, 20, 20, 30, 100]), false); assert_eq!(match_slice(&[0, 5, 20, 30, 100]), false); assert_eq!(match_slice(&[0, 10, 20, 30, 99]), false); assert!(match_slice(&[0, 10, 20, 30, 40, 100])); assert!(match_slice(&[0, 20, 20, 30, 50, 60, 70, 80, 90, 100])); }
Correction
#![allow(unused)] fn main() { fn match_slice(s: &[i32]) -> bool { match s { &[0, 10 | 20, .., 100] => { println!("Match!"); true } _ => { println!("No match!"); false } } } assert_eq!(match_slice(&[1, 20, 20, 30, 100]), false); assert_eq!(match_slice(&[0, 5, 20, 30, 100]), false); assert_eq!(match_slice(&[0, 10, 20, 30, 99]), false); assert!(match_slice(&[0, 10, 20, 30, 40, 100])); assert!(match_slice(&[0, 20, 20, 30, 50, 60, 70, 80, 90, 100])); }
Note that the &[0, 10 | 20, .., 100] can be replaced by [0, 10 | 20, .., 100].
Course: Pattern summary
As a summary, pattern can be used
- In
letstatements:let (x,y)=(1,2) - In
if letstatments (while letand for loops)if let Some(color) = favorite_color {[...]} else {[...]} - In function parameters
fn print_coordinates(&(x, y): &(i32, i32)) {[...]}
In let, functions and for loops, patterns must be irrefutable (i.e. cannot fail)
The Result enum type
Course: Returning a Result from a function
To handle and propagate runtime errors, Rust relies on a simple but efficient mechanism based on an enum: the Result<T, E> enum, which is defined as:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E) } }
The templated T and E types have no trait implementation predicate whatsoever, they could be anything, for instance:
#![allow(unused)] fn main() { // This function returns a u8 slice if there is no error, a Vec<f32> if there is. // This is probably not very useful, but it's still perfectly valid code: fn my_function(i: i32) -> Result<&[u8], Vec<f32>> {...} // This too (empty tuple type for both): fn my_function(i: i32) -> Result<(),()> { Ok(()) } }
The unwrap() method can be used to extract the Ok argument from a Result<...>. This will be explained in more detail further, but you will need it to test your function:
let u = my_function(3).unwrap();
Write a function called positive()that takes ani32as argument and checks whether it is (strictly) positive. It should return the same argument value if it is positive, or aStringwith an error message if the argument is negative or equal to zero.
Correction
#![allow(unused)] fn main() { fn positive(i: i32) -> Result<i32, String> { if i > 0 { Ok(i) } else { Err(String::from("Argument is negative or == 0")) } } }
Course: propagating Errors
An Error in Rust can be propagated down the call stack by using the question mark operator: ?
#![allow(unused)] fn main() { fn my_function(i: i32) -> Result<i32, ()> {...} fn my_other_function() -> Result<i32, ()> { // Append the '?' operator right after the function call: let mut i = my_function(1)?; // Do something with 'i': i += 1; // Return an 'Ok' result with the modified 'i' value: Ok(i) } }
Here, the my_function(1)? function call means:
- if the result enum value is
Err(an error), then propagate the error now, by returning the sameErrfrommy_function(), otherwise, continue with the rest of the code.
This code could also be implemented with an equivalent match statement, but is a bit more verbose:
#![allow(unused)] fn main() { fn my_other_function() -> Result<i32, ()> { let mut i = match my_function(1) { Ok(i) => i, Err() => return Err(()) }; i += 1; Ok(i) } }
Implement the same mechanism for the previous positive(i: i32)example, using the?syntax, and test it with both positive and negative values in a new function with the same return type, in order to see what happens.
// Your 'positive' function: fn positive(i: i32) -> Result<i32, String> {...} // Define a new function, and call 'positive(...)' from here: fn check_positive() -> Result<i32, String> { ... // <- test positive & negative values here using the '?' syntax } // Check the return type from main: fn main() { println!("{:?}", check_positive()); }
Correction
fn positive(i: i32) -> Result<i32, String> { if i > 0 { Ok(i) } else { Err(String::from("Argument is negative or == 0")) } } fn check_positive() -> Result<i32, String> { let mut i = positive(1)?; i = positive(-1)?; Ok(i) } fn main() { println!("{:?}", check_positive()); }
Course: returning a Result from the main() function
In general, returning with or without error from a main function, such as in the C or C++ programming languages, is done by returning an integer exit code (0 for success, error otherwise):
int main(void) {
// No error, return 0:
return 0;
}
In Rust, the main() function has no return type by default, and returning a i32 is not accepted by the compiler. For instance, the following code is invalid:
fn main() -> i32 { 0 }
In this case, the compiler prints the following:
error[E0277]: `main` has invalid return type `i32`
--> src/main.rs:3:14
|
3 | fn main() -> i32 {
| ^^^ `main` can only return types that implement `Termination`
|
= help: consider using `()`, or a `Result`
The Termination trait documentation indeed indicates that only the following types are valid:
#![allow(unused)] fn main() { impl Termination for Infallible; impl Termination for !; impl Termination for (); impl Termination for ExitCode; impl<T: Termination, E: Debug> Termination for Result<T, E>; }
Therefore, we can see that propagating a Result down to main() is possible, but is still a bit of a specific case. The type T held by the Ok enum value must implement the Termination trait, and the type held by the Err enum value must implement the Debug trait. For instance, the following still does not work because i32 does not implement the Termination trait:
fn main() -> Result<i32, String> { Ok(0) }
But the following works:
// Using an empty tuple as the 'Ok' result: fn main() -> Result<(), String> { Ok(()) } // Or using the ExitCode type: use std::process::ExitCode; fn main() -> Result<ExitCode, String> { Ok(ExitCode::from(0)) }
Result type in the main() function, propagate the Result of our positive() function down to the main function which returns it.
fn positive(i: i32) -> Result<i32, String> {...} fn check_positive() -> Result<i32, String> {...} // Use a valid Result type here: fn main() -> Result<?, ?>{ // Call the 'check_positive' function here, and propagate its Result as the main() return type: }
Correction
fn positive(i: i32) -> Result<i32, String> { if i > 0 { Ok(i) } else { Err(String::from("Argument is negative or == 0")) } } fn check_positive() -> Result<i32, String> { let mut i = positive(1)?; i = positive(-1)?; Ok(i) } fn main() -> Result<(), String> { check_positive()?; Ok(()) }
Course: Handling Result types immediately
.unwrap(), .expect()
In some cases - when propagating an error is not possible, or unconvenient - dealing immediately with a Result type is preferrable. This is why certain methods, such as .unwrap() or .expect() are natively implemented, and quite commonly used:
- The
.unwrap()method, for instance, will induce apanic!call and will exit the program immediately when encountering an error. Otherwise it will return theOkvalue safely:
#![allow(unused)] fn main() { // Get the current working directory: let dir: std::path::PathBuf = std::env::current_dir().unwrap(); println!("{:?}", dir); }
- The
.expect()method is really similar, but will allow the user to print a custom&strmessage on error, which will be prepended to the actual display of theErrcontents:
#![allow(unused)] fn main() { fn my_function() -> Result<(), i32> { Err(1) } my_function().expect("Error! Now exiting program with error code"); }
- Other similar helper methods also exist, with different behaviors:
.unwrap_or(other: T)returns the valueotherin case of anErr.unwrap_or_default()returns the type's default value in case of anErr.unwrap_or_else(func: Fn)executes a custom function in case of anErr- etc.
panic!, assert! and other macros
In addition to Result types, other simple tools, in the form of macros, are provided:
- The
panic!(msg)macro interrupts the program immediately and prints a custom error message:
fn main() { use std::io; // Read input from stdin: let mut buffer = String::new(); println!("Please enter password:"); io::stdin().read_line(&mut buffer).unwrap(); // Panic if password is not long enough: if buffer.len() < 8 { panic!("Password should be at least 8 characters"); } else { println!("{buffer}"); } }
- The
assert!(bool),assert_eq!(lhs, rhs), andassert_ne!(lhs, rhs), verify a boolean statement or check equality between two elements:
fn main() { use std::io; // Read input from stdin: let mut buffer = String::new(); println!("Please enter password:"); io::stdin().read_line(&mut buffer).unwrap(); // We assert that the password is at least 8 characters // This will cause a 'panic' if the assertion is false: assert!(buffer.len() >= 8, "Password should be at least 8 characters"); // Here, we forbidden choosing 'password' as a password: assert_ne!(buffer.trim(), "password", "Choosing 'password' as password is unsafe."); }
The Option enum type
The Option enum in Rust is somewhat similar to the Result enum, but its main purpose is to indicate the presence or absence of a value, rather than an error. It has the following definition:
#![allow(unused)] fn main() { pub enum Option<T> { None, Some(T) } }
As an example, let's suppose we want to build a list of people to contact, with various information, such as the contact's name and address, and optionally phone number and/or e-mail, we could for instance define the following struct:
#![allow(unused)] fn main() { struct MyContact { name: &'static str, address: &'static str, email: Option<&'static str>, phone: Option<&'static str>, } let mut list: Vec<MyContact> = Vec::new(); list.push(MyContact { name: "Marlo Stanfield", address: "2601 E Baltimore St, Baltimore, MD 21224", email: None, phone: Some("+1 410-915-0909") }); }
By doing this, we can then take advantage of the Option enum and pattern matching to decide of the best way to contact each person in the list:
#![allow(unused)] fn main() { for contact in list { match (contact.email, contact.phone) { // Both e-mail and phone are available: (Some(email), Some(phone)) => { if is_email_correct(email) { contact_by_email(email); } else { contact_by_phone(phone); } } // Only e-mail is available: (Some(email), None) => { contact_by_email(email); } // Only phone is available: (None, Some(phone)) => { contact_by_phone(phone); } // Neither phone nor email: (None, None) => { send_mail_to_address(contact.address); } } } }
As for the Result type, Option can be checked and handled immediately using the same .unwrap(), .expect() methods.
#![allow(unused)] fn main() { fn money_left() -> Option<i32>; money_left().expect("No money left :("); }
Using the previous contact list example, implement a small database of books that would be used by a library. It should have a search_book(...)function which searches for a specific book using its name and/or the name of the author (we assume here that there's only one book per author). The function should return a reference to theBookobject if it has been found, orNoneotherwise.
#![allow(unused)] fn main() { struct Book { name: String, author: String, } #[derive(Default)] struct LibraryDatabase { books: Vec<Book> } impl LibraryDatabase { // The function to implement: fn search_book(&self, name: Option<&'static str>, author: Option<&'static str> ) -> Option<&Book> {...} } }
You can use the following #[test] function to verify your code:
#![allow(unused)] fn main() { #[test] fn test() { let mut database = LibraryDatabase::default(); database.books.push(Book { name: String::from("Peter Pan"), author: String::from("Barrie")}); assert!(database.search_book(Some("Peter Pan"), None).is_some()); assert!(database.search_book(None, Some("Barrie")).is_some()); assert!(database.search_book(None, None).is_none()); assert!(database.search_book(Some("Barrie"), None).is_none()); assert!(database.search_book(None, Some("Peter Pan")).is_none()); assert!(database.search_book(Some("Alice in Wonderland"), Some("Barrie")).is_none()); assert!(database.search_book(Some("Peter Pan"), Some("Lewis Carroll")).is_none()); } }
Correction
#![allow(unused)] fn main() { impl LibraryDatabase { fn search_book(&self, name: Option<&'static str>, author: Option<&'static str>) -> Option<&Book> { match (name, author) { (Some(n), Some(a)) => { self.books.iter().find(|e| e.name == n && e.author == a) } (Some(n), None) => { self.books.iter().find(|e| e.name == n) } (None, Some(a)) => { self.books.iter().find(|e| e.author == a) } (None, None) => None } } } }
Using Result,Optionand all the previous examples, create a program which parses a password, with the following rules:
- Password must:
- be at least 8 characters long;
- should contain at least one of these special characters:
!,?or_;- should contain at least one number;
- should not contain any whitespace.
- A specific error message should be displayed for each rule.
hint: in order to chain the tests in a functionnal way, one can use the
then_some(self, value T)called on aboolthat returnsvalueif theboolis true, orok_or<E>(self, err: E)that can transform anOptionin aResult.
Note: in order to verify your program, you can implement a
#[test]function, such as:
#![allow(unused)] fn main() { #[test] fn password_test() { let pwd0 = String::from("pass"); let pwd1 = String::from("password"); let pwd2 = String::from("password!"); let pwd3 = String::from("pass word!"); let pwd4 = String::from("password!1"); assert!(parse_password(&pwd0).is_err()); assert!(parse_password(&pwd1).is_err()); assert!(parse_password(&pwd2).is_err()); assert!(parse_password(&pwd3).is_err()); assert!(parse_password(&pwd4).is_ok()); } }
and then run
cargo teston your program.
Correction
use std::io; fn parse_password(buffer: &String) -> Result<(), &'static str> { buffer .len().ge(&8) .then_some(&buffer).ok_or( "Password should at least contain 8 characters" )? .chars().all(|c| !char::is_whitespace(c)) .then_some(&buffer).ok_or( "Password should not contain any whitespace" )? .contains(|c: char| c.eq(&'?') | c.eq(&'!') | c.eq(&'_')) .then_some(&buffer).ok_or( "Password should contain at least 1 special character" )? .contains(char::is_numeric) .then_some(&buffer).ok_or( "Password should contain at least 1 number" )?; Ok(()) } fn main() -> Result<(), &'static str> { let mut buffer = String::new(); println!("Please enter password:"); io::stdin().read_line(&mut buffer).expect( "Error reading password!" ); // Remove the ending character: buffer.pop(); parse_password(&mut buffer)?; println!("Success!"); Ok(()) }
Simple macros with macro_rules!
Unlike other programming languages, such as C or C++, Rust's macro system is based on abstract syntax trees (AST), instead of string preprocessing, which makes them a bit more complex to use, but also more reliable and powerful. macros are expanded before the compiler interprets the meaning of the code. The difference between a macro and a function is that macro definitions are more complex than function definitions because you’re writing Rust code that writes Rust code. Due to this indirection, macro definitions are generally more difficult to read, understand, and maintain than function definitions.
Throughout this course, we have already encoutered a few of them, including vec!, panic!, assert!, and of course println!. These macros are defined by the macro!(...) syntax (don't forget the trailling exclamation mark) and are called simple macros, as opposed to Rust's more complex macro systems, such as attribute macros (for instance the #[test] function attribute), and derive macros (the #[derive(Debug)] statement on top of a struct), which we have already both seen as well.
Course: Basic macro_rules! usage
Simple macros can be defined anywhere in our code, using the macro_rules! syntax:
#![allow(unused)] fn main() { macro_rules! hello_world { () => { println!("Hello World!") }; } hello_world!(); }
Here, we defined a macro! that takes no argument, which is indicated by the () statement. Our hello_world!() macro call will be under the hood replaced by the contents that we defined within the => { ... } block.
Advantages of using macros
Our hello_world!() example is of course not very useful, and in fact adds unnecessary noise to a very simple piece of code, but think of the vec! macro for instance:
#![allow(unused)] fn main() { let v1 = vec![1, 2, 3, 4, 5]; let v2 = vec!(); let v3 = vec![1]; }
Defining the three different vectors by hand would actually mean writing the following code:
#![allow(unused)] fn main() { let v1 = <[_]>::into_vec(Box::new([1, 2, 3, 4, 5])); let v2 = Vec::new<i32>(); let v3 = std::vec::from_elem(1, 1); }
Notice how these three vectors are each time created in a very different way? In this case, the vec! macro allows defining a more practical and unified way of instantiating a Vec object, without having to remember all the (sometimes complex) underlying code. Furthermore, as you can see with this example, a macro! can also accept a variable number of arguments, which is not the case with a Rust function.
The different types of arguments (or fragment specifiers)
As you may already have guessed with our first basic macro example, which uses the => operator, macro_rules! relies on pattern matching to parse its arbitrary number of arguments.
macro_rules! can parse different kinds of patterns, including:
(): the empty pattern, which means no argument (our previous example);block: a block expression, surrounded by{ };expr: any kind of Rust expression;ident: an identifier (the name of a variable, function, etc.);literal: a number/string or other kind of litteral;ty: a typett: a TokenTree- etc. (see the full list here).
Matching a specific pattern
Let's now try an example with an actual argument. Here, we will use the ident designator in order to create functions from a simple macro call:
#![allow(unused)] fn main() { macro_rules! define_fn { ($fn_name:ident) => { fn $fn_name() { println!( "This function's name (ident) is: '{}()'.", stringify!($fn_name) ); } } } define_fn!(foo); define_fn!(bar); foo(); bar(); }
Let's break this code piece-by-piece:
#![allow(unused)] fn main() { ($fn_name:ident) => { }
→ Instead of an empty pattern (), we use the pattern ($fn_name:ident), in which $fn_name would be the name of the argument, and ident its type. The dollar sign ($) is used to declare a variable in the macro system that will contain the Rust code matching the pattern.
#![allow(unused)] fn main() { fn $fn_name() { }
→ Within our generated code block, we declare a function with the name taken from our $fn_name ident argument.
#![allow(unused)] fn main() { println!( "This function's name (ident) is: '{}()'.", stringify!($fn_name) ); }
→ We then define the function's body, with a println! call, in which we print the ident's name using a utility macro called stringify!. This very useful macro will transform our $fn_name identifier into a &'static str object;
What would be the generated code for the define_fn!(foo)macro call?
Correction
#![allow(unused)] fn main() { fn foo() { println!( "This function's name (ident) is: '{}()'.", stringify!(foo) ); } }
Create a similar macro, but this time the generated code should define a structand itsimplblock like the following:
#![allow(unused)] fn main() { // All of this code should be generated by our new macro, // but the name 'Foo' should be made variable: struct Foo { print: &'static str } impl Foo { fn new() -> Foo { Foo { print: "Foo" } } } }
#![allow(unused)] fn main() { // The macro to implement: macro_rules! define_struct { ... } // Use the following to verify that the macro is correct: define_struct!(Foo); let bar = Foo::new(); assert_eq!(bar.print, "Foo"); }
Correction
#![allow(unused)] fn main() { macro_rules! define_struct { ($name:ident) => { struct $name { print: &'static str } impl $name { fn new() -> $name { $name { print: stringify!($name) } } } } } define_struct!(Foo); let bar = Foo::new(); assert_eq!(bar.print, "Foo"); }
Course: pattern overloading
macro_rules! definitions can accept an arbitrary number of patterns, in a very simple way. Let's try it out on our define_fn! macro. We will add another pattern allowing to add arbitrary code expressions to the created fn:
#![allow(unused)] fn main() { macro_rules! define_fn { ($fn_name:ident) => { fn $fn_name() { println!( "This function's name (ident) is: '{}()'.", stringify!($fn_name) ); } }; // <-- pattern blocks must end with a semicolon if they're followed by other blocks // Our new pattern: ($fn_name:ident, $additional_code:expr) => { fn $fn_name() { println!( "This function's name (ident) is: '{}()'.", stringify!($fn_name) ); // Append the additional code 'expr' at the end of the defined fn: $additional_code } } } define_fn!(foo); define_fn!(bar, println!("Additional code")); foo(); bar(); }
Here, we added the ($fn_name:ident, $additional_code:expr) pattern, which is composed of two arguments: the same ident argument, followed by an expr argument, which can be any valid Rust expression. The two arguments are separated by a comma , but the choice of a comma is completely arbitrary, it could be (almost) any symbol.
In our previous example, try replacing the ',' symbol between $fn_name:identand$additional_code:exprwith another symbol (e.g. + or *). Then, call thedefine_fn!macro with two arguments separated by the same new symbol, and see what it does.
Correction
#![allow(unused)] fn main() { macro_rules! define_fn { ($fn_name:ident) => { fn $fn_name() { println!( "This function's name (ident) is: '{}()'.", stringify!($fn_name) ); } }; // Here, we replace it with the '+' symbol: ($fn_name:ident + $additional_code:expr) => { fn $fn_name() { println!( "This function's name (ident) is: '{}()'.", stringify!($fn_name) ); $additional_code } } } define_fn!(foo); define_fn!(bar + println!("Additional code")); foo(); bar(); }
Course: pattern matching for macro_rules!
Pattern matching for macro_rules! is quite different from pattern matching used in the match keyword. Macros can also easily deal with pattern repetition by using a special syntax, which resembles the one used for regular expressions. In particuler, the usual operators of regular expressions can be used: '_', '+' or '*' (one object, a repetition of objects -- at least one, a repetition of objects - possibly 0). In the following example, we want to replace the std::cmp::max() function to take an arbitrary number of arguments:
#![allow(unused)] fn main() { let mut max = std::cmp::max(1, 2); max = std::cmp::max(max, 3); max = std::cmp::max(max, 4); max = std::cmp::max(max, 5); }
In this case, having a macro like the following could prove useful, and would lighten the code a lot:
#![allow(unused)] fn main() { max!(1, 2, 3, 4, 5, 6*7, 3*4); }
The way to do this is to use the $(...),+ syntax, as follows:
#![allow(unused)] fn main() { macro_rules! max { // Only one argument 'x', return 'x': ($x:expr) => {$x}; // At least two arguments, // - 'x' being the first, // - 'y' being one or more additional argument(s), // which is defined by the '$(...),+' syntax: ($x:expr, $($y:expr),+) => { // We recursively call 'max!' on the tail 'y' std::cmp::max($x, max!($($y),+)) } } }
The + in the $($y:expr),+ syntax means one or more instances of the ($y) expression, separated by a comma ,.
Note: The
*symbol also exists, and means zero or more instances of the pattern.
Now, if we were to call the max! macro the following way, we would only match the first pattern ($x:expr) => {$x}:
#![allow(unused)] fn main() { max!(1); }
- With two arguments, we would match the second pattern
($x:expr, $($y:expr),+)with a single additional argument:
#![allow(unused)] fn main() { max!(1, 2); // expands to: std::cmp::max(1, max!(2)); // expands to: std::cmp::max(1, 2); }
- And with more arguments recursively:
#![allow(unused)] fn main() { max!(1, 2, 3); // expands to: std::cmp::max(1, max!(2, 3)); // expands to: std::cmp::max(1, std::cmp::max(2, max!(3))); // expands to: std::cmp::max(1, std::cmp::max(2, 3)); }
As a summary:
$varcaptures a value in a pattern.$var:identspecifies a type (could beident,expr,ty, etc.).$( $(var:pat),*)or$( $(var:pat),+)captures repetitive sequences separated by commas.$varis replaced during macro expansion.
Bonus: Transform our previous define_struct!example into a more elaborated macro. It should now have the following interface: at least one member (bar), 0 ore some methods
#![allow(unused)] fn main() { // Define the struct 'Foo': define_struct!( name: Foo, members: { bar: i32 } methods: { fn hello() { println!("hello!") } fn bar(&self) { println!("{}", self.bar); } } ); // Instantiate the struct 'Foo': let f = Foo::default(); // Call its bar method: f.bar(); // Or its static 'hello' method: Foo::hello(); }
Correction
#![allow(unused)] fn main() { macro_rules! define_struct { ( name: $name:ident, members: { $($field:ident : $type:ty),* } methods: { $($meth:tt)* } ) => { #[derive(Default)] struct $name { $($field : $type),* } impl $name { $($meth)* } }; } }
Bonus: Add a nested macro_rules!definition intodefine_struct!which allows to copy the members and methods of the defined struct into a new different one, such as:
#![allow(unused)] fn main() { // Define Foo struct: define_struct!( name: Foo, members: { bar: i32 } methods: { fn hello() { println!("hello!") } fn bar(&self) { println!("{}", self.bar); } } ); // The define_struct! macro should define a new 'Foo!' macro, // which allows copying the members and methods of 'Foo' into another new struct: Foo!(FooCopy); let c = FooCopy::default(); c.bar(); FooCopy::hello(); }
Correction
#![allow(unused)] fn main() { macro_rules! define_struct { ( name: $name:ident, members: { $($field:ident : $type:ty),* } methods: { $($meth:tt)* } ) => { #[derive(Default)] struct $name { $($field : $type),* } impl $name { $($meth)* } macro_rules! $name { ($copy:ident) => { #[derive(Default)] struct $copy { $($field : $type),* } impl $copy { $($meth)* } } } }; } }
Pierre Cochard, Tanguy Risset
- Introduction
- Using Threads and Closures to Run Code Simultaneously
- Sharing data safely between threads
- Asynchronous programming
Introduction
Modern CPU architectures offer many ways to run high-performance programs. The most obvious is threading: because most CPUs have multiple cores, threads can be executed in parallel. A thread is a standard abstraction available in most programming languages and managed by the operating system. The Rust thread API is presented in Section 2, and the common tools used for distributed programming are presented in Section 3.
Another way to handle parallel tasks is to use concurrency, i.e., having a single process or thread switch between different tasks, so that no time is wasted when a task is idle. Rust provides the async mode to support concurrency. In async mode, Rust launches a runtime (sometimes called an executor) that can interrupt idle tasks and schedule tasks that are ready to run. The async model may use threading if the runtime is multithreaded by it is transparent to the programmer, conceptually it runs sequentially and implements a hidden state machine that regularly polls suspended tasks to determine whether they can resume. Several runtimes exist that support the async model. The most widely used is Tokio, which is introduced in Section 3.
Using Threads and Closures to Run Code Simultaneously
Course: Closures
Closures in Rust are anonymous functions (sometimes called lambda functions) that can be stored in variables, or passed to other processes as arguments. They can be found in a lot of places in the language, in order to allow functional-style programming, behavior customization or to provide concurrent/parallel code execution. They are defined by the following syntax |arguments| -> return_type { body }, for instance:
#![allow(unused)] fn main() { // Define a closure and store it into a variable: let my_closure = |x: i32| -> i32 { println!("my_closure"); x*2 }; // Execute the closure like you would normally do with a function: let y = my_closure(2); println!("{y}"); }
Borrowed captures
Just like in C++, closures can capture the environment it originates in and use external data in its own internal scope. By default, captured data, whether it is mutable or not, will be borrowed:
#![allow(unused)] fn main() { let x = 2; let my_closure = || -> i32 { // Use a borrowed (immutable) capture of 'x', // and return two times its value: x*2 }; println!("{}", my_closure()); }
#![allow(unused)] fn main() { // Same, but this time, we modify 'x' directly in the closure: let mut x = 2; // The closure itself also has to be made 'mutable' // in the case of a mutable borrow: let mut my_closure_mut = || x *= 2; my_closure_mut(); println!("{x}"); }
Moved captures
Instead of being borrowed, data can also be moved into a closure scope, using the move keyword before declaration. The semantic is the same as for assignment: if the type of the data implements the Copy Trait, the data is transmitted by value (as shown below for i32 type). if it does not, the data is moved (as shown in next question)
#![allow(unused)] fn main() { let mut x = 2; // Capturing 'x' by value. Here, it is made with a simple copy: let mut my_closure_mut = move || { x *= 2; println!("x (closure): {x}"); }; my_closure_mut(); println!("x: {x}"); }
Why is the following code invalid? How can we solve the issue?
#![allow(unused)] fn main() { let mut x = vec![31, 47, 27, 16]; let mut my_closure_mut = move || { x.push(32); println!("{:?}", x); }; my_closure_mut(); println!("{:?}", x); }
Correction
A Vec object does not implement the copy trait, so it is moved instead into the closure. The last println! macro call refers to an object that is not in scope anymore. Depending on what the user wants to eventually do with this code, there would be two ways to fix this, either:
1. Remove the move keyword, the Vec would be borrowed instead;
#![allow(unused)] fn main() { let mut x = vec![31, 47, 27, 16]; let mut my_closure_mut = || x.push(32); my_closure_mut(); println!("{:?}", x); }
2. Clone (deep copy) the Vec object and move it into the closure. This way, the first one would still be valid in the main scope.
#![allow(unused)] fn main() { let mut x = vec![31, 47, 27, 16]; let mut x2 = x.clone(); let mut my_closure_mut = move || { x2.push(32); x2 }; x = my_closure_mut(); println!("{:?}", x); }
Course: Passing closures as objects or arguments
One big specificity of closures is that they have a unique, anonymous type that cannot be written out. This can for instance be demonstrated by running the following piece of code:
#![allow(unused)] fn main() { // Utility function that prints out the type of a variable: fn print_type_of<T>(_: &T) { println!("Type of my closure: {}", std::any::type_name::<T>()); } let my_closure = |x: i32| -> i32 { x*2 }; print_type_of(&my_closure); }
Therefore, passing a closure as an argument to a function using a specific type is not possible in Rust. Instead, in order to do that, one would have to use a trait. Indeed, all closures implement one or several of the following traits, depending on their nature and properties:
FnOnce: applies to closures that can be called once. All closures implement this trait;Fn: applies to closures that don't move captured values out of their body and that don't mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.FnMut: applies to closures that don't move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.
In general:
-
If a closure moves a captured variable : we use
FnOnce. -
If a closure modifies a captured variable : we use
FnMut. -
If it only reads : we simply use
Fn.
A closure can then be passed as an argument the same way we do for passing trait-implementing objects, by using the Fn/FnOnce/FnMut(argument_type) -> return_type:
#![allow(unused)] fn main() { // All of the 'Fn' traits have the format: // 'Fn(argument-types) -> return types' fn exec_closure(x: i32, closure: impl Fn(i32) -> i32) -> i32 { closure(x) } let c = |x: i32| x + 27; let r = exec_closure(31, c); println!("{r}"); }
The generic form also works (and is usually preferrable):
#![allow(unused)] fn main() { fn exec_closure<T>(x: i32, closure: T) -> i32 where T: Fn(i32) -> i32 { closure(x) } let c = |x: i32| x + 27; let r = exec_closure(31, c); println!("{r}"); }
Using the generic form, store the following chirp_fnclosure as a member of thestructBird, with a valid type signature:
let chirp_fn = |times: i32| {
for _ in 0..times {
println!("chirp!");
}
}
// The struct to implement (use generics!):
struct Bird<...> {
chirp: ...
}
// Create a new instance of 'Bird' with the chirp_fn closure:
let bird = Bird { chirp: chirp_fn };
// Call the 'chirp' closure from inside the struct:
// operator precedence priority can be found here: https://doc.rust-lang.org/reference/expressions.html
(bird.chirp)(10);
Correction
#![allow(unused)] fn main() { struct Bird<F> where F: Fn(i32) -> () { chirp: F, } let chirp_fn = |times: i32| { for _ in 0..times { println!("chirp!"); } }; let bird = Bird { chirp: chirp_fn }; (bird.chirp)(10); }
Course: Spawning Threads using Closures
A std::thread object in Rust will execute a given closure in an independent (or parallel) context of execution. In the following example, the std::thread::spawn call will only return when it's done creating the thread, but not when the thread has actually finished executing:
#![allow(unused)] fn main() { // Spawn a new thread which will execute its given closure: std::thread::spawn(|| { println!("Thread 1: chirp!"); }); // At the 'same time', print something from the main thread: println!("Main thread: chirp chirp!"); }
In this case, the main function returns before the independent thread's println! call happens. This is why we can only see the "main thread" print output. Usually, a thread is bound to a local variable, and is waited upon before the parent context of execution finishes. This can be done by calling the .join() method on the thread handle:
#![allow(unused)] fn main() { // Bind the thread's "handle" to a variable: let th = std::thread::spawn(|| { println!("Thread 1: chirp!"); }); println!("Main thread: chirp chirp!"); // Wait for 'th' to finish executing, and re-synchronise both threads: th.join().unwrap(); }
The following code prints a modified value of variable varfrom 3 different threads, running independently from one another. Is the code safe? Can you guess what will be the resulting output?
use std::thread;
let mut var = 32;
let t1 = thread::spawn(move || {
var += 1;
println!("Thread 1: reading value {}!", var);
});
let t2 = thread::spawn(move || {
var += 2;
println!("Thread 2: reading value {}!", var);
});
var += 3;
println!("Main thread: reading value {}", var);
// Re-synchronise both threads:
t1.join().unwrap();
t2.join().unwrap();
Correction
Since we capture var by value in the two additional threads t1 and t2, a local copy of the variable is made in their respective scopes. This means that there is no actual concurrency at play in this code, which is perfectly safe. The output will be (in no particular order):
Main thread: reading value 35
Thread 1: reading value 33!
Thread 2: reading value 34!
What would happen if we removed all the movekeywords from the code?
Correction
move captures would be replaced by borrow captures, which wouldn't work: there would indeed be a a data race, since 3 different threads would be simultaneously overwriting the same data.
Sharing data safely between threads
Using Shared State data sets
Course: Exclusive access with Mutexes
Mutual exclusion, or mutex is a mechanism which prevents accessing the same data from multiple threads running at the same time. It relies on a locking system in order to do so: a thread must first ask to acquire the mutex's lock before being able to access the underlying protected data. The lock is a data structure that keeps track of whichever thread has exclusive access to the data. If the mutex happens to be locked at the time a thread tries to access the data, it will stall until the lock is eventually released, and the data is free to acquire.
#![allow(unused)] fn main() { use std::sync::Mutex; // Instantiate a new Mutex<i32> instance with the value '32': let var: Mutex<i32> = Mutex::new(32); { // Acquire the mutex's lock (and panic in case of failure): let mut v = var.lock().unwrap(); // Modify the value safely: *v += 32; } // Print the result: println!("var = {var:?}"); }
In the following example, we want to try to use a Mutexto get both threads to use and modifyvar, but the compiler doesn't allow it, what is the underlying issue here?
#![allow(unused)] fn main() { use std::thread; let mut var = 32; let t1 = thread::spawn(move || { var += 1; }); let t2 = thread::spawn(move || { var += 2; }); // Re-synchronise both threads: t1.join().unwrap(); t2.join().unwrap(); println!("Result: {var}"); }
Correction
The compiler complains that mtx is already moved in t1, and cannot be moved in another closure.
Here, the Rust move and ownership semantics apply, and using a Mutex makes no exception.
#![allow(unused)] fn main() { use std::thread; use std::sync::Mutex; let mtx = Mutex::new(32); let t1 = thread::spawn(move || { let mut v = mtx.lock().unwrap(); *v += 1; }); let t2 = thread::spawn(move || { let mut v = mtx.lock().unwrap(); *v += 2; }); // Re-synchronise both threads: t1.join().unwrap(); t2.join().unwrap(); println!("Result: {mtx:?}"); }
Course: Reference Counted Mutexes
As we could see in our previous example, a Mutex in itself is not sufficient to implement viable thread-safe data sharing:
- First is the issue of ownership, which could be solved using, for instance, a shared pointer.
- Second would be the issue of concurrency in accessing this shared pointer from multiple threads simultaneously.
In the Rust programming language, Atomic Reference Counting Arc<T>, which can be seen as an atomic shared pointer, is designed to remedy this very specific problem. It firstly solves the ownership issue by being "reference-counted", just like a standard Rc object, but also solves the concurrency issue by being atomic, meaning that is guaranteed to execute as a single unified transaction. When an atomic operation is executed on an object by a specific thread, no other threads can read or modify the object while the atomic operation is in progress. In other words, other threads will only see the object before or after the operation, there would be no intermediary state.
Our previous example can then be replaced by the following:
#![allow(unused)] fn main() { use std::thread; use std::sync::{Arc, Mutex}; // We wrap the Mutex in a Atomically Reference-Counted object: let arc = Arc::new(Mutex::new(32)); // We prepare two clones, for moving into the two distinct closures: let rc1 = Arc::clone(&arc); let rc2 = Arc::clone(&arc); let t1 = thread::spawn(move || { let mut v = rc1.lock().unwrap(); *v += 1; }); let t2 = thread::spawn(move || { let mut v = rc2.lock().unwrap(); *v += 2; }); // Re-synchronise both threads: t1.join().unwrap(); t2.join().unwrap(); println!("Result: {arc:?}"); }
If an Arcobject is sufficient to provide multiple ownership and thread-safe access to data, why do we still need aMutexguarding our data?
Correction
The atomic nature of an Arc means that the underlying reference-counted shared pointer (necessary in order to solve the ownership issue) (materialized by the Rc object), is thread-safe, but that does not extend to the data it points to. We still need a Mutex to guarantee thread-safe read/write accesses to the data itself. As we will see in the next subsection, we could also make the data itself atomic.
Course: Lock-free data sharing with Atomics
Another way of making data thread-safe is by directly using atomic data structures. The Rust standard library provides a few of them in its std::sync::atomic module. The main difference with using Mutexes is that atomics are what we call lock-free: unlike Mutexes, its underlying mechanism never sleeps, but it spins (it will check data availability in a continuous loop), that's why we usually call them spinlocks. In real-time contexts, where thread sleep is not an option, it is always preferrable to use lock-free data structures.
Our previous example would for instance look like this with atomics:
#![allow(unused)] fn main() { use std::thread; use std::sync::Arc; use std::sync::atomic::AtomicI32; use std::sync::atomic::Ordering; // We replace 'Mutex::new()' with the following: let arc = Arc::new(AtomicI32::new(32)); let rc1 = Arc::clone(&arc); let rc2 = Arc::clone(&arc); let t1 = thread::spawn(move || { // Acquire underlying data as a copy: let mut v = rc1.load(Ordering::Acquire); // Modify the copy: v += 1; // Update the value atomically, release the lock: rc1.store(v, Ordering::Release); }); let t2 = thread::spawn(move || { // Another (more compact) way of doing this: rc2.fetch_add(2, Ordering::AcqRel); }); // Re-synchronise both threads: t1.join().unwrap(); t2.join().unwrap(); println!("Result: {arc:?}"); }
Using Message passing to transfer data between threads
Another approach to multiple ownership and thread-safety in Rust would be using an mpsc::channel or mpsc::sync_channel, which are asynchronous/synchronous FIFO queues that store all the updated states of a value in a shared infinite buffer.
An mpsc::channel() call will return a tuple containing a handle to a Sender object and a Receiver object, which are by convention respectively named tx and rx. These two objects are positioned at each end of a FIFO which tunnels data between the two:
#![allow(unused)] fn main() { // Create a 'data channel' between a Sender `tx`, and a Receiver `rx`: let (tx, rx) = std::sync::mpsc::channel::<i32>(); // Send the values `32` and then `16` through the channel: tx.send(32).unwrap(); tx.send(16).unwrap(); // Poll the channel, read data if available: println!("n = {}", rx.recv().unwrap()); println!("n = {}", rx.recv().unwrap()); }
Example of sending from a separate thread:
#![allow(unused)] fn main() { use std::thread; use std::sync::mpsc; let (tx, rx) = mpsc::channel(); // Do the same thing in a separate thread: let th = thread::spawn(move || { tx.send(32).unwrap(); tx.send(16).unwrap(); }); th.join(); // Poll the channel, read data if available: println!("n = {}", rx.recv().unwrap()); println!("n = {}", rx.recv().unwrap()); }
Or from multiple threads simultaneously:
#![allow(unused)] fn main() { use std::thread; use std::sync::mpsc; let (tx, rx) = mpsc::channel(); let mut vec = Vec::new(); for n in 0..8 { // Clone the Sender `tx` for each thread: let tx = tx.clone(); vec.push(thread::spawn(move || { tx.send(n).unwrap(); })); } for t in vec { t.join().unwrap(); } for _ in 0..8 { // Consume the FIFO value-by-value: let value = rx.recv().unwrap(); println!("Received value: {}", value); } }
Using an mpsc::channel, anArc<T>and aMutex(or anAtomic), implement a program which creates and run two independent threads:
- A producer thread which continuously counts from 0 to infinity.
- A consumer thread which continuously reads and prints the count produced by the producer thread.
- The two threads should run for 5 seconds and then stop.
- Hint: you can use
thread::sleepto pause a thread for a certain amount of time.
Correction
#![allow(unused)] fn main() { use std::thread; use std::sync::mpsc; use std::sync::Arc; use std::sync::atomic::Ordering; use std::sync::atomic::AtomicBool; use std::time::Duration; let (tx, rx) = mpsc::channel(); let running = Arc::new(AtomicBool::new(true)); let run_arc1 = running.clone(); let run_arc2 = running.clone(); let pth = thread::spawn(move || { let mut n = 0; while run_arc1.load(Ordering::Acquire) { n += 1; tx.send(n).unwrap(); println!("Sending value: {n}"); } }); let cth = thread::spawn(move || { while run_arc2.load(Ordering::Acquire) { match rx.try_recv() { Ok(v) => { println!("Received value: {v}"); } Err(..) => () } } }); thread::sleep(Duration::from_secs(5)); running.store(false, Ordering::Release); pth.join().unwrap(); cth.join().unwrap(); }
Asynchronous programming
While programming with threads is a perfectly valid way of implementing concurrent programming, it also has a few disadvantages, such as having to rely on operating system scheduling, as well as sometimes making the code difficult to re-use or modify. To address these issues, programmers came up with a new way of structuring a program in a different set of tasks (whether they are independent, concurrent, or sequential), which has been called asynchronous programming.
Code within a thread is written in sequential style and the operating system executes them concurrently. With asynchronous programming, concurrency happens entirely within a program: the operating system is not involved, making context switch faster, and memory overhead also lower. By being natively integrated into a programming language, which is the case with Rust, it also makes control flow more flexible and expressive.
Futures and await: the Async syntax
Rust relies on two keywords async and await, as well as a few underlying concepts (such as futures) to implement asynchronous programming within the language.
Futures and .await
A future is a placeholder data structure for a future value, which is not ready/accessible when it is declared and defined in the first place. In Rust, a Future trait is provided as a building block for implementing async operations.
#![allow(unused)] fn main() { // in std::future module pub trait Future { type Output; // Required method: fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } }
Its core method poll() is called in attempt to resolve the future into a final value. The method is non-blocking, i.e. it will not block scheduling if its value is not yet 'ready'. Instead, it will be rescheduled to be called at a certain time in the future. A Poll result consists in two different value states:
Poll::Pending: the future is for now still unresolved.Poll::Ready(value): the future is resolved and its value accessible.
There are two other types associated with the poll(..) method: Pin<&mut Self>, which ensure that the object will not be moved in memory, and Context, which allows you to give information about when to reschedule a poll(). Both are both linked to the underlying async runtime context of execution and task scheduling. Fortunately, Rust provides an easier way to handle on all of that, through the .await suffix keyword, everything else is hidden behind the scenes:
#![allow(unused)] fn main() { let result = some_future.await; }
would be roughly expanded to something like this:
#![allow(unused)] fn main() { let result = match unsafe {Pin::new_unchecked(&mut some_future)}.poll(cx) { Poll::Ready(val) => val, Poll::Pending => return Poll::Pending } }
Under the hood, the process is the following:
- Future is passed to an executor (such as
tokio) - Executor calls
poll(future, cx)with its underlyingContextinstance - If
Poll::Ready, future is completed, value is retrieved - If
Poll::Pending: executor stops polling - When external events occur (I/O readiness, timers, etc.) future is woken up
- Executor polls future again
- Process repeats until
Ready
Async execution contexts
await expressions are only valid within a certain context of execution: they can be used within blocks and functions that are marked with the async keyword, to specify that they can be interrupted and resumed.
#![allow(unused)] fn main() { async fn some_fn() { let result = some_future.await; } }
When the Rust compiler encounters a block marked with async, it compiles it into a unique, anonymous data type that implements the Future trait. When it sees a function marked with async, it compiles it into a non-async function whose body is an async block. An async function's return type is the type of the anonymous data type the compiler creates for that async block.
Thus, writing async fn is equivalent to writing a function that returns a future of the return type.
#![allow(unused)] fn main() { fn some_fn() -> impl Future<Output = ()> { async move { let result = some_future.await; } } }
Async runtimes
While the async/await features are fully integrated to the Rust programming language, and fully understood by its compiler, Rust doesn't provide any standard runtime to execute an async program. Thus calling our function from main wouldn't work:
async fn some_fn() { let result = some_future.await; } async fn main() { some_fn().await; }
Instead, Rust relies on third-party implementations, such as:
tokio: most popular and used.smol: more lightweight and easy to understand.embassy: for embedded systems.glommio: for I/O-bound workloads.
For the rest of this course, we will use the tokio framework as our async runtime.
async fn some_fn() { // async code... } // The main function has to be annotated // in order to be async compatible: #[tokio::main] async fn main() { some_fn().await; }
Try to implement the "equivalent" program with async/await:use std::thread::{sleep, spawn}; use std::time::Duration; fn count_to(N: i32) { for n in 1..=N { println!("{n}"); sleep(Duration::from_secs(1)); } } fn main() { let t1 = spawn(|| { count_to(5) }); t1.join().unwrap(); println!("joined!"); }
Correction
use tokio::{spawn, time::{sleep, Duration}}; async fn count_to(N: i32) { for n in 1..=N { println!("{n}"); sleep(Duration::from_secs(1)).await; } } #[tokio::main] async fn main() { count_to(5).await; println!("joined!"); }
Spawning concurrent tasks
The example above works with a single task, but is not really interesting as it is.
In order to get parallel tasks running concurrently, tokio provides the spawn() function,
which takes an async closure as argument:
#[tokio::main] async fn main() { tokio::spawn(async move { my_async_fn("first task").await; }); tokio::spawn(async move { my_async_fn("second parallel task").await; }); }
async blocks and closures allow the move keyword, much like normal closures. An async move block will take ownership of the variables it references, allowing it to outlive the current scope, but giving up the ability to share those variables with other code.
Try now to wrap our previous count_to(5).awaitfunction call insidetokio::spawnas a closure. Notice anything? What is the problem here? How can we fix it?
Correction
the tokio::spawn function returns a future, wrapped in a JoinHandle, so we still need to join, by calling .await (and .unwrap()):
use tokio::{spawn, time::{sleep, Duration}}; async fn count_to(N: i32) { for n in 1..=N { println!("{n}"); sleep(Duration::from_secs(1)).await; } } #[tokio::main] async fn main() { tokio::spawn(async move { count_to(5).await; }).await.unwrap(); println!("joined!"); }
Let's now improve our count_tofunction by setting thesleepduration as a variable parameter, and add a start offset as well. We could do this for instance by using astd::ops::Rangeinstead of ai32for the parameterN:#![allow(unused)] fn main() { async fn count_to(R: Range<i32>, sleep_dur: Duration) {...} }Implement the modified version of the function, and its usage inside the
mainfunction. Try to also spawn another function call with different parameters. Are the two tasks now running in parallel?
Correction
No, they're still running sequentially. We need something extra to make them parallel, like the join! macro:
use std::ops::Range; use tokio::{join, spawn, time::{Duration, sleep}}; async fn count_to(R: Range<i32>, sleep_dur: Duration) { for n in R { println!("{n}"); sleep(sleep_dur).await; } } #[tokio::main] async fn main() { // We remove the .await.unwrap() calls, // so that the tasks can be joined afterwards together: let task_1 = tokio::spawn(async move { count_to(0..5, Duration::from_secs(2)).await; }); let task_2 = tokio::spawn(async move { count_to(5..10, Duration::from_secs(1)).await; }); let (t1, t2) = join!(task_1, task_2); t1.unwrap(); t2.unwrap(); println!("joined!"); }
Shared states & Message passing
The process of sharing states & passing messages in async contexts is similar to the one seen above for threads, with a few specificities, which can be dependent on the runtime that is used. tokio, for instance, provides a few primitives, such as async Mutex or mpsc channels (multiple-producer, single consumer), the tutorial explains well how to use them:
Transform the counter example by using tokio(with a mutex or with channels) to share the count of one task with another
Correction
use tokio::{join, sync::mpsc, time::{Duration, sleep}}; #[tokio::main] async fn main() { let (tx, mut rx) = mpsc::channel::<i32>(10); let task_1 = tokio::spawn(async move { for n in 0..5 { tx.send(n).await.unwrap(); sleep(Duration::from_secs(1)).await; } }); let task_2 = tokio::spawn(async move { loop { if rx.is_closed() { break; } match rx.recv().await { Some(n) => { println!("Counting: {n}"); } _ => () } } }); let (t1, t2) = join!(task_1, task_2); t1.unwrap(); t2.unwrap(); println!("joined!"); }
Async and networking
Networking is an ideal candidate for async-based programs, since there's usually no certainty on the timing of their inputs and overall order of events. tokio has in its libraries everything needed for basic TCP and UDP exchanges:
use std::error::Error; use tokio::{net::{TcpListener, TcpStream}}; #[tokio::main] async fn main() -> Result<(), Box<dyn Error>> { let addr = "127.0.0.1:8047"; // Bind a TCP listener to addr: let listener = TcpListener::bind(addr).await?; let streams = tokio::spawn(async move { // Connect to same addr: TcpStream::connect(addr) .await .unwrap(); println!("Connected!"); }); // Wait for a client to connect: let (..) = listener.accept().await?; streams.await?; Ok(()) }
With help from the example above and the tokiotutorial and documentation, write a simple echo example between a TCP server and a single client.
Correction
use std::error::Error; use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}}; #[tokio::main] async fn main() -> Result<(), Box<dyn Error>> { let addr = "127.0.0.1:8047"; // Bind a TCP listener to addr: let listener = TcpListener::bind(addr).await?; let streams = tokio::spawn(async move { let mut stream = TcpStream::connect(addr) .await .unwrap(); let mut buf = vec![0; 128]; loop { let n = stream.read(&mut buf).await.unwrap(); if n == 0 { break; } println!("Received {:?}", String::from_utf8_lossy(&buf[..n])); } }); // Wait for a client to connect: let (mut socket, ..) = listener.accept().await?; tokio::spawn(async move { socket.write_all(b"echo!\n").await.unwrap(); }); streams.await?; Ok(()) }
Pierre Cochard, Tanguy Risset
Project Rules
- The project can be for a single student, or a group of at most 2 students
- The Rust code should be written by you, it means:
- no copy/paste from existing code
- LLM help is OK but only for correcting a code that you wrote
- The projet should illustrate one or several Rust concept or mechanism
- An open source github repository should be set up for the project and communicated to the teacher. This git repository will be used by the teacher to follow project evolution
Project life
- The project is first validated by teachers
- Teachers will not answer questions by mail, they will answer questions during course slots.
- During the project period, students are free to develop whenever and wherever they want: teacher will be present at course slots but student presence is not mandatory, except for Final Project presentation : Wednesday 21/01/2026 14h-18h
- There will be a Mid-term project check on Wednesday 14/10/2026 14h-16h at that point you should have send us your github site and a roadmap (text of 1/2 page) for your project
Evaluation
-
The final presentation (Wednesday 21/01/2026 14h-18h) should take 15mn per project:
- 4-6 mn slide presentation
- 4-6 mn demo
- 4-6 mn questions (from teachers and other students)
-
The project will be evaluated following criteria:
- Amount of work estimated
- Overall quality of the code and the demo
- Code readability and documentation
- Adequation between the announced project and the final result
- Quality of the final presentation
-
During the slide presentation, we do not need a very complete presentation of the code (we can see it on the github site), we need a description of the application, of the Rust crates used and we except you to give you feeling about what's different with Rust:
- What helped you?
- What were the obstacles?
- What did you like?
- What did you not like?
- Will Rust become you favourite language?
A few audio examples:
- Audio example with the
jackcrate: https://gitlab.inria.fr/emeraude/5tc-rust/-/tree/main/audio-examples?ref_type=heads - Audio & network example with
cxx-juce: https://gitlab.inria.fr/emeraude/5tc-rust/-/tree/main/network-examples?ref_type=heads
Example of Project ideas
Networking (Crates: tokio, websocket, ...)
- Chat system in Rust
- peer-to-peer in Rust
Audio (Crates: cpal, jack (linux), cxx-juce, portaudio, ...ask us!)
- Audio streaming in Rust
- Synthesizer/effect in Rust
GUI/TUI (Crates: egui, dioxus, cxx-qt, ratatui, cursive)
- Web app with UI
- Gome with UI
Benchmark (Crates: criterion, divan, hyperfine, ...)
- compare Rust and C performance
- efficiency of thread vs. Async
Explore Rust Crates
- provide pedagogical use of original Crates
Document design
- inspired from typst
Useful Rust Audio Crates
Audio backend crates
How audio works (in a nutshell...)
Usually, when working with audio, whether it's with Rust or any other programming language, you need an interface with OS audio drivers, which are usually wrapped into higher-level libraries, such as JACK, or PortAudio. They all have usually similar APIs, based on audio callbacks.
Audio callbacks
Real-time audio is tightly time-constrained, a continuous audio stream in stereo for instance usually needs 44100 (or 48000, or higher) audio samples (values) per second in order to be viable. In order to achieve this, audio is usually processed or synthesized in buffers (usually 512 samples or lower), to be more efficient. The APIs of most audio backend libraries provide buffer-based audio callback interfaces that will give you an input buffer, an output buffer, or both (duplex). In this callback, you can for example read a buffer of audio inputs (usually a f32 slice) that's given to you by the backend, and write it back to the audio output buffer after you've done your processing:
#![allow(unused)] fn main() { // a passthrough example, // If our sample-rate is 48000 Hz, and our buffer-sizer is 128 samples, // then our function is going to be called 375 times in one second: let my_callback = |inputs: &[f32], outputs: &mut[f32]| { for s in inputs.len() { outputs[s] = inputs[s]; } } }
Which backend crate should I use?
If you're on Linux, JACK is probably the easiest and most efficient in terms of performance. Otherwise, for other platforms, cpal is probably safer, but a little harder to use, especially if you need both audio inputs and outputs, because the audio processing callbacks are not "duplex", meaning you need to have 2 separate audio callbacks (one for inputs, one for outputs), running in two different contexts of execution. The difficulty with this is that, if you need to exchange audio data between the two callbacks, you will have to use a shared ringbuffer (mpsc-like). You can find a very good crate for this here:
JACK (Jack Audio Connection Kit)
-
Use case: recommended for low-latency audio on Linux.
-
Duplex callback: YES
-
Platforms:
- Linux (recommended)
- Should be harder to make it work on macOS and Windows.
-
Dependencies:
- Linux:
jack2package on Linux, orpipewire-jack - macOS and Windows: install from here https://jackaudio.org/downloads/
- Linux:
-
Documentation: https://rustaudio.github.io/rust-jack/
-
Code examples:
- https://github.com/RustAudio/rust-jack/blob/main/examples
- See also the audio example in the 5TC gitlab repository
-
Utilities:
qjackctl, to dynamically patch/connect the audio clients in a GUI graph.qpwgraph, same but works withpipewire-jack
-
Comments:
- If using with PipeWire (with
pipewire-jack), runpw-jack cargo-runin order to enable PipeWire to emulate a jack client. - Otherwise, don't forget to start the JACK server before running your executable (manually in the terminal, or with
qjackctl)
- If using with PipeWire (with
cpal
- Use case: generic use, multi-platform
- Git: https://github.com/RustAudio/cpal
- Duplex callback: NO, need to share a ringbuffer between input/output callbacks
- Platforms: multi-platform
- Dependencies:
- Linux: ALSA (
libasound2-dev) or JACK (jack2,libjack2-dev) - Windows: WASAPI (default), or ASIO (or JACK) optionally
- macOS/iOS: CoreAudio (or JACK)
- Android: AAudio
- Emscripten, WebAssembly
- Linux: ALSA (
- Documentation:
- Code examples:
cxx-juce (bindings)
- Use case: generic use, multi-platform (?), more recent and probably less stable than the others...
- Git: https://github.com/JamesHallowell/cxx-juce
- Duplex callback: YES
- Platforms: Supposed to be multi-platform, but only tested on Linux...
- Dependencies: JUCE (fetched and built automatically)
- Documentation: see examples
- Code examples:
- https://github.com/JamesHallowell/cxx-juce/tree/main/examples
- see network example in 5TC gitlab repository.
project presentation: Wednesday 21/01/2026 14h-18h Room TD-A (Ground floor, Hedy Lamarr)
Schedule:
14h00-14h15: Granular Simulation
- Student(s): PEYRIE Pierre-Angelo
- Description:
- Simple granular simulation using the ggez library. It simulates 2D colliding particles subject to gravity using a simple verlet integration. Inspired by pezzza's work.
- Git: https://github.com/Pierre-AngeloPeyrie/rust_project
- Crates used:
- ggez
14h15-14h30: Task TUI
- Student(s): VAUDEY Rémy
- Description: Simple Task Manager made with ratatui.
- Git: https://gitlab.com/RVaudey21/rust-task-tui
- Crates used:
- ratatui
- crossterm
- color-eyre
- serde (json)
14h30-14h45: Whac'A'Whole
- Student(s): LIN Hanqi
- Description: Simple 2D Game
- Git: https://github.com/Hanqi-b/whac_a_whole
- Crates used:
- macroquad
- rand
14h45-15h00: McDoSim
- Student(s): ZHAN Xuan
- Description: Fast-food scheduling simulation
- Git: https://github.com/duckduckxuan/McDoSim
- Crates used:
- tokio
- rand
15h00-15h15: Pacman
- Student(s): ROUQUET Gabriel
- Description: Simple pacman game on terminal
- Git: https://github.com/grouquet/RUST_PROJECT
- Crates used:
- crossterm
15h15-15h30: Bugglers
- Student(s): HAUTBOIS Guewen, VATAN Hélio
- Description: Imagification : generate an image from a sound.
- Git: https://github.com/MadMuses/Bugglers
- Crates used:
- hound (soundfiles)
- image
- num (numeric types)
- rustfft
15h30-15h45: Radiooooo CLI
- Student(s): MORAES Augusto
- Description: A command-line client made on RUST for radiooooo.com.
- Git: https://github.com/augusto-moraes/radiooooo-rust-cli
- Crates used:
- clap
- tokio
- reqwest (http requests)
- serde (json)
- colored
- log, env_logger
- inquire (terminal prompts)
- chrono
15h45-16h15: Pause
16h15-16h30: Password Manager
- Student(s): LEBLANC Lucas
- Description:
- Git: https://github.com/LSLeblanc/password-manager-rust
- Crates used:
- aes-gcm (crypto)
- rand
- serde (json)
- sha2
16h30-16h45: Audio Chat
- Student(s): GIRARD Hippolyte, LAMBERT Thibaud
- Description: un chat audio en temps réel entre plusieurs clients sur Rust.
- Git: https://github.com/HippG/Rust_Audio_Chat
- Crates used:
- jack (audio)
- opus (audio codec, VoIP, ...)
- ringbuf
- tokio
- quinn (async impl. of IETF QUIC transport protocol)
- rustls
- axum (tokio http routing and request handling)
- serde (json)
- tower-http
16h45-17h00: Rust-networking
- Student(s): SEVERIN Stefan, SCHROTH Stephan
- Description: create a simple TCP-based chat server and client in Rust
- Git: https://github.com/Multi-5/Rust_networking
- Crates used: tokio, mini-redis
17h00-17h15: Tuner
- Student(s): MÜLLER Nadine, VOINCHET Lena
- Description:
- develop a high-performance musical instrument tuner. It uses Rust for Digital Signal Processing (DSP) to ensure memory safety and execution speed, and CPAL the cross-platform audio library.
- Git: https://github.com/LenaVcht/RUST_Tuner
- Crates used: cpal
17h15-17h30: Snake
- Student(s): RENAUD Valentin
- Description: Simple Snake Game.
- Git : https://github.com/Valdyzer/Snake.git
- Crates used: dioxus