00.0 - Intro to Rust
We will use the Rust programming language for the labs.
Resources
- The Rust Programming Language, Chapters 1, 2, 3 and 5
- Tour of Rust step by step tutorial
- Let's Get Rusty - The Rust Lang Book
This lab is rather long, but it tries to be a really quick intro to Rust. We suggest going directly to the Exercises, solve one by one, and read the required documentation as you go.
Standard library
The standard library is divided into three levels:
| Level | Description | Needs |
|---|---|---|
core | Provides the required language elements that Rust needs for compiling, like the Display and Debug traits. Data can only be global items (stored in .data) or on the stack. | Hardware |
alloc | Provides everything from the core level plus heap allocated data structures like, Box and Vec. The developer has to provide a memory allocator, like embedded_alloc. | Memory Allocator |
std | Provides everything from the alloc level plus a lot of features that depend on the platform, including threads and I/O. This is the default level for Windows, Linux, macOS and similar OSes applications. | Operating System |
This course will mostly use the core level of the standard library, as the software has to run on a Raspberry Pi Pico.
By default, Rust has a set of elements defined in the standard library that are imported into the program of each application. This set is called the prelude, and you can look it up in the standard library documentation.
If a type you want to use is not in the prelude, you must bring that type into scope explicitly with a use
statement. Using the std::io module gives you a number of useful features, including the ability to accept
user input.
use std::io;
The main function
The main function is the entry point of our program.
fn main() {
println!("Hello, world!");
}
We use println! macro to print messages on the screen.
To print more complex variables, you can use {:?}, which ensures that any type that implements the Debug trait can be printed.
To insert a placeholder in the println! macro, use a pair of braces {} . We provide the variable name or
expression to replace the provided placeholder outside the string.
fn main() {
let name = "Mary";
let age = 26;
println!("Hello, {}. You are {} years old", name, age);
// if the replacements are only variables, one can use the inline version
println!("Hello, {name}. You are {age} years old");
}
Variables and mutability
We use the let keyword to create a variable.
let a = 5;
By default, in Rust, variables are immutable , meaning once a value is tied to a name, you cannot change that value.
Example:
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
In this case, we will get a compilation error because we are trying to modify the value of x from 5 to 6, but
x is immutable, so we cannot make this modification.
Although variables are immutable by default, you can make them mutable by adding mut in front of the
variable name. Adding mut also conveys intent to future readers of the code by indicating that other parts
of the code will modify the value of this variable.
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Now the value of x can become 6.
Constants
Like immutable variables, constants are values that are tied to a name and have a value known at compile time.
You are not allowed to use mut with constants. Constants are not only immutable by default,
they are always immutable. You declare constants using the const keyword instead of the let keyword.
Constants's data type has to be specified at declaration.
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
For a better understanding, please read chapter 3 of the documentation.
Data Types
Scalar types
A scalar type represents a single value. Rust has four main scalar types: integers, floating point numbers, booleans, and characters.
Integer → Each variant can be signed or unsigned and has an explicit size.
let x: i8 = -2;
let y: u16 = 25;
| Length | Signed | Unsigned | Java Equivalent | C Equivalent1 |
|---|---|---|---|---|
| 8-bit | i8 | u8 | byte/ Byte2 | char / unsigned char |
| 16-bit | i16 | u16 | short / Short2 | short / unsigned short |
| 32-bit | i32 | u32 | int / Integer2 | int / unsigned int |
| 64-bit | i64 | u64 | long / Long2 | long long / unsigned long long |
| 128-bit | i128 | u128 | N/A | N/A |
| arch | isize | usize | N/A | intptr_t / uintptr_t |
Floating Point → Rust's floating point types are f32 and f64, which are 32-bit and 64-bit in size, respectively. The default type is f64 because on modern CPUs it is about the same speed as f32 but is capable of more precision. All floating point types are signed.
| Length | Floating point | Java Equivalent | C Equivalent |
|---|---|---|---|
| 32-bit | f32 | float | float |
| 64-bit | f64 | double | double |
| 128-bit | f128 | N/A | N/A |
fn main() {
let x = 2.0; // f64
let y1: f32 = 3.0; // f32
let y2 = 3.0f32; // f32
}
Boolean → Booleans are one byte in size. Boolean type in Rust is specified using bool.
let t = true;
let f: bool = false; // with explicit type annotation
Character → The Rust char type is the most primitive alphabetic type in the language.
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
Structures
Structs are a data type construct that contain other data types in the form of fields. Rust structures are similar to C structs and Java classes.
To define a structure, we enter the struct keyword and name the entire structure. Then, within curly brackets, we define the names and types of the data, which we call fields.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
To use a structure after having defined it, we create an instance of this structure by specifying concrete values for each of the fields. We create a stack allocated instance by specifying the structure name , then add curly braces containing key: value pairs , where the keys are the field names and the values are the data we want to store in those fields.
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
To access a certain member of the structure we use this syntax:
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com")
}
Note that the entire instance must be mutable ; Rust doesn't allow us to mark only certain fields as mutable!
We can construct a new instance of the structure as the last expression in the function body to implicitly return this new instance.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
Structure Implementation
Structures in Rust are similar to classes in OOP. Besides the operations mentioned above, structures can also be implemented and have methods specific to a structure. The methods are defined in the structure's implementation.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
impl User {
// static method (no self parameter)
// called with User::new()
fn new() -> User {
// ...
}
// method
// called user.is_active()
fn is_active (&self) -> bool {
return self.active;
}
}
Printing Structures
If we try to print an instance of User using the println! macro as we have seen early, it will not work.
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
println!("User is: {}", user1);
}
We will get the following error message:
error[E0277]: `User` doesn't implement `std::fmt::Display`
In order to print a structure, we need to use {:?} instead of {}, and implement Debug trait for the structure with #[derive(Debug)].
We use Debug trait to print structures, arrays, enums or any other type that doesn't implement Display.
#[derive(Debug)]
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
println!("User is: {:?}", user1);
}
Output:
User is: User { active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1 }
To format the Debug nicely use {:#?}.
Tuple structures
Tuples are the same as structures, just that instead of using names for their fields, they use numbers (indexes).
struct Color(i32, i32, i32);
struct Device(String, u8);
fn main() {
let black = Color(0, 0, 0);
let device = Device(String::from("Raspberry Pi Pico"), 2);
println!("The device type is {} and the version is {}", device.0, device.1);
}
Tuples can be named (as the one shown above) or can be anonymous. The following example shows how functions use anonymous tuples to return multiple values.
fn get_item_and_index(value: &str) -> (String, usize) {
// usually search the value here
(String::from("the name"), 0)
}
let value = get_item_and_index("...");
// use value.0 and value.1
For a better understanding, please read chapter 5 of the documentation.
Enums
Enumerations, also referred as enums, allow you to define a type by enumerating its possible variants.
How to define an enum:
enum IpAddrKind {
V4,
V6,
}