A brief introduction about Rust.
Digital nomad 🎒 | Mentor 👨🏫 | Speaker 🗣️ | Developer 👨💻 | OSS Contributor 🍫 | Creator of @proyecto26 🧚
Founding Full-Stack Engineer 👷
- Build reliable and efficient systems software:
• networking (web servers, mails servers, web browsers).
• compilers and interpreters.
• build games.
• command-line programs.
• web-assembly programs.
• applications for embedded devices.
Rust strikes a unique balance among performance, safety, and implementation expressions.
- Type safe: the compiler ensure the type of the variables.
- Memory safe: Rust pointers always refer to valid memory.
- Data race free: a value can't be mutated from multiple parts at the same time.
- Zero-cost abstractions: Rust allows the use of high-level concepts, like iteration, interfaces, and functional programming.
- Minimal runtime: no garbage collector to manage memory efficiently.
- Targets bare metal: write an operating system kernel or device drivers
Memory Allocation
The new variable has the ownership of the value; x can't be used again.
This transfers the reference string to a string variable; x is the new owner.
Immutability
fn main() {
let x = String::from("hi");
let y = &x; // <-- Immutable borrow
println!("{}", x);
println!("{}", y);
}
The new variable has the right to read
The Lifetime
- RAII (Resource Acquisition Is Initialization) - An analysis mechanism that can accurately calculate resource and variable life.
- C-Own, Java-GC, Rust-Ownership & Borrowing.
- Advantages of Rust: Fast as C & Safe as Java.
The Compiler
- The compiler does the job;
Source
Code
HIR
>
>
MIR
LLVM
IR
>
Machine
Code
>
Borrow checking,
Optimizations,
Code generation
Printing a string with params
fn main() {
let mut userId = 1;
println!("The user id is {}", userdId);
userId = 2;
println!("The new user id is {}", userdId);
}
Printing a hex value
fn main() {
let color: i32 = 0xff0000;
println!("The color red is #{:06x}", color);
}
Floats
fn main() {
let price = 5.0; // Float 64 bits
let version: f32 = 1.0 // Float 32 bits
}
fn main() {
let enabled = true;
let loading: bool = false;
}
Booleans
fn main() {
let environment = 'development';
let cool_emoji = '🤘'; // One character in Rust like 'a', 'A', etc
}
Strings
Arrays
fn main() {
let numbers = [1, 2, 3, 4, 5];
let fixed_size: [i32; 3] = [-1, 2, 3]; // same type
let first = fixed_size[0]; // signed integer (negative numbers)
}
fn main() {
let names = ("Juan", "David", "Nicholls Cardona");
println!("First name is {}", names.0);
let size = (1024, 768);
let (width, height) = size; // destructured when doing an assignment
println!("width: {}, height: {}", width, height);
// define the types
let pair: (char, i32) = ('a', 17);
}
Tuples
Tuples are fixed-length collections of values of different types
fn main() {
// reference string, you can't change it once you declare it
let name = "Juan";
let api_url: &'static str = "https://myapi.com"; // equivalent
}
Inmutable
fn main() {
let mut message = String::new();
message += "Testing";
let mut strng = String::from("ABC");
strng.push('D'); // one character
strng.push_str("EFG");
// assertion failed, expected 'G'
assert_eq!(strng.pop(), Some('F'));
}
Mutable
mut makes a variable binding mutable
#![allow(dead_code, unused)]
pub struct User {
username: String,
email: String,
active: bool,
}
fn main() {
// Initialized using struct literals
let mut user = User {
username: String::from("jdnichollsc"),
email: String::from("jdnichollsc@hotmail.com"),
active: true,
};
user.email = String::from("jdnichollsc@gmail.com");
println!("user email: {}", user.email);
}
Define a structure
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let blackColor = Color(0, 0, 0);
let origin = Point(0, 0, 0);
Define a tuple structure
#![allow(dead_code, unused)]
// enum with implicit discriminator starts at 0
enum Status {
Active,
Pending,
Inactive
}
struct User {
username: String,
status: Status,
}
fn main() {
let user = User {
username: String::from("jdnichollsc"),
status: Status::Active,
};
if matches!(user.status, Status::Active) {
println!("The user is active");
} else {
println!("The user is not allowed");
}
}
Define an enumeration
#![allow(dead_code, unused)]
// enum with explicit discriminator
enum Color {
Red = 0xff0000,
Green = 0x00ff00,
Blue = 0x0000ff,
}
struct User {
username: String,
theme: Color,
}
fn main() {
let user = User {
username: String::from("jdnichollsc"),
theme: Color::Red,
};
// Casting enum to integer (formatting)
println!("The user theme color is #{:06x}", user.theme as i32);
}
Define an enumeration
struct QuitMessage;
struct MoveMessage {
x: i32,
y: i32,
};
struct WriteMessage(String);
struct ChangeColorMessage(i32, i32, i32);
enum Message {
Quit(QuitMessage),
Move(MoveMessage),
Write(WriteMessage),
ChangeColor(ChangeColorMessage),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Combine enum with structs
Destruct an enum with structs
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Blue", 10);
scores.insert("Red", 10);
scores.insert(String::from("LOL"), 0);
for (color, value) in scores.iter() {
// ...
}
# Using get method (Option<&T>)
let name = String::from("Testing");
let value = scores.get(&name).copied().unwrap_or(0);
HashMaps store values by key, the keys can be booleans, integers, strings, or custom types with traits.
Optional value when there’s no an entry for the key
If Else condition
fn main() {
let age = 18
if age > 17 {
println!("Allowed");
} else {
println!("Not allowed");
}
}
While condition
fn main() {
let mut counter = 0;
while counter < 10 {
println!("Count: {}", counter);
counter += 1;
}
}
For condition
fn main() {
let colors = ["#A76B00", "#FFAF1E", "#126136"];
for color in colors.iter() {
println!("Color {}", color);
}
}
Loop condition
fn main() {
let mut counter: i32 = 0;
let arr = [1, 2, 3, 4];
let index: i32 = loop {
let count = counter as usize;
if arr.len() == count {
break -1;
}
if arr[count] == 26 {
break counter;
}
counter += 1;
};
println!("The index of 26 number is {}", index);
}
Declaring a void function
fn track_event(name: &str) {
// do something
}
fn main() {
track_event("main");
}
Returns a number (32-bit signed integer)
pub fn bigger(a: i32, b: i32) -> i32 {
if a > b {
return a;
}
b // omitting the semicolon at the end of a fn is the same as returning
}
fn main() {
let big_num = bigger(10, 20);
println!("Bigger num bwt 10 & 20 is {}", big_num);
}
Slice
fn main() {
let hello_world = String::from("Hello world");
let hello = &hello_world[0..5];
let world = &hello_world[6..11];
println!("{} {}", hello, world); // hello world
}
Declare methods on your own types
struct Number {
value: i32,
}
impl Number {
// using "&" to avoid moving the ownership of the value
fn is_odd(&self) -> bool {
self.value % 2 == 0
}
}
fn main() {
let num = Number { value: 10 };
if num.is_odd() {
println!("{} is odd", num.value);
} else {
println!("{} is even", num.value);
}
}
Declaring a block with a pair of brackets
fn main() {
let x = "out";
{
// has its own scope
let x = "in"; // this is a different `x`
println!("{}", x); // print "in"
}
println!("{}", x); // print "out"
}
Blocks are also expressions
fn main() {
let project = 26;
println!("{}", x); // print 26
}
fn main() {
let project = { 26 };
println!("{}", x); // print 26
}
They are equivalent
Declaring a block with a pair of brackets
fn main() {
let x = 2;
{
let a = &x;
assert_eq!(*x, 2);
}
println!("{}", x); // print "2"
}
Anonymous functions that can capture values from the scope in which they're defined
fn main() {
let sum = |x:i32| x + 1;
println!("{}", sum(10)); // print "11"
}
fn twice<F: Fn(i32) -> i32>(x: i32, f: F) -> i32 {
f(x) + f(x)
}
fn main() {
let square = |x: i32| { x * x };
println!("{}", twice(5, square)); // prints 50
}
#![allow(dead_code, unused)]
struct Number {
odd: bool,
value: i32,
}
fn main() {
let one = Number { odd: true, value: 1 };
let two = Number { odd: false, value: 2 };
print_number(one); // Odd number: 1
print_number(two); // Even number: 2
}
fn print_number(n: Number) {
match n.odd {
true => println!("Odd number: {}", n.value),
false => println!("Even number: {}", n.value),
}
}
A match has to be exhaustive: at least one arm needs to match.
fn main() {
let one = Number { odd: true, value: 1 };
let two = Number { odd: false, value: 2 };
let three = Number { odd: false, value: 3 };
print_number(one);
print_number(two);
print_number(three);
}
fn print_number(n: Number) {
match n {
Number { value: 1, .. } => println!("One"),
Number { value: 2, .. } => println!("Two"),
Number { value, .. } => println!("{}", value),
// if that last arm didn't exist, we would get a compile-time error
}
}
fn print_number(n: Number) {
match n.value {
1 => println!("One"),
2 => println!("Two"),
3 ..= 5 => println!("Number between 3 and 5"),
_ => println!("{}", n.value), // "_" can be used as a "catch-all" pattern
}
}
trait Signed {
fn is_strictly_negative(self) -> bool;
}
Abstract definition of shared behavior with types
Orphan rules:
- one of your traits on anyone's type.
- anyone's trait on one of your types.
- but not a foreign trait on a foreign type
impl Signed for Number {
fn is_strictly_negative(self) -> bool {
self.value < 0
}
}
fn main() {
let n = Number { odd: false, value: -44 };
println!("{}", n.is_strictly_negative()); // prints "true"
}
NOTE:
You can't use "n" after printing the value (Ownership)
Your trait on a foreign type
impl Signed for i32 {
fn is_strictly_negative(self) -> bool {
self < 0
}
}
fn main() {
let n: i32 = -44; // primitive type
println!("{}", n.is_strictly_negative()); // prints "true"
}
Some traits are markers - they don't say that a type implements some methods, they say that certain things can be done with a type.
i32 implements trait Copy
Foreign trait on your type
#![allow(dead_code, unused)]
struct Number {
value: i32,
is_positive: bool,
}
// An "impl" block is always for a type
impl std::ops::Neg for Number {
type Output = Self; // Self means the current type (Number)
fn neg(self) -> Self {
let new_value = -self.value;
let is_positive = new_value > 0;
Self {
value: new_value,
is_positive: is_positive,
}
}
}
fn main() {
let n = Number { is_positive: true, value: 1 };
let m = -n; // this is only possible because we implemented `Neg`
println!("{}", m.is_positive); // prints "false"
}
A test in Rust is a function that’s annotated with the test attribute
#[cfg(test)] mod tests {
#[test]
fn simple_example() {
let result = 3 + 5;
assert_eq!(result, 8);
}
}
The #[cfg(test)]
annotation on the tests module tells Rust to compile and run the test code only when you run cargo test
, not when you run cargo build
.
// avoid warnings about unused values
let _x = 26;
// call a function but throws away its result
let _ = get_something();
// Re-binding variables
let x = 5;
let x = x + 5; // now x is equal to 10
// Destructuring a Tuple ignoring the first field
let (_, right) = slice.split_at(middle);
// Semi-colon marks the end of a statement
let x = vec![1, 2, 3, 4, 5, 6, 7, 8]
.iter()
.map(|x| x + 3)
.fold(0, |x, y| x + y);
// Multiple statements inside a block
let x = {
let y = 1; // first statement
let z = 2; // second statement
y + z // return the result of the operation
};
// Dots are used to access fields of a value
let user = get_some_struct();
user.username; // this is "jdnichollsc"
// Call a method of a value
let username = "jdnichollsc";
username.len(); // this is 11
// Use double-colon "::" to call external functions
let least = std::cmp::min(3, 8);
// std is a library (crate)
// cmp is a module (source file)
// min is a function
// Import names from other namespaces
use std::cmp::min;
use std::cmp::max;
let min_value = min(7, 1);
let max_value = max(7, 1);
// Use "globs" to import multiple names
use std::cmp::{min, max};
// Use a "wilcard" to import every symbol from a namespace
use std::cmp::*;
// Call methods as regular functions using types (namespaces)
let x = str::len("baxus"); // this is 5
// Rust inserts some non-primitive types at the beginning of every module
let v = Vec::new(); // The full path is std::vec::Vec
// Declaring a struct using double precision values
struct Vec2 {
x: f64,
y: f64
}
// Initializing a struct from another struct
let v3 = Vec2 {
x: 14.0,
..v2 // "struct update" can only happen in last position (without comma)
};
// Destructuring a struct
let v = Vec2 { x: 3.0, y: 6.0 };
let Vec2 { x, .. } = v; // ignore certain values
println!("X is {}", x); // X is 3
- Hardware with limited resources (Energy, Memory, Space, etc)
- Limited connection between devices.
Regulators
- Stable APIs (Qualified toolchain for Rust)
Architectures
- ARM Cortex-A/M
- RISC-V
- XTensa
It includes header files, build systems (Yocto, CMake), Chip/Manufacturer specific IDEs
- SVD2Rust
- PALs
- HALs
- Board Crates
- Peripheral Crates
- RTOS; RTIC, Tock, Hubris
- Async; Embassy
Memory Allocation
fn main() {
let x = String::from("hi");
let y = x; // <-- value moved here
println!("{}", x); // Error
}
fn main() {
let y = {
let x = String::from("hi");
&x
// ^^ does not live long enough
};
println!("{}", y);
}
- Take your first steps with Rust by Microsoft.
- "A half-hour to learn Rust" by amos.
- Comprehensive Rust 🦀 by Google.
- Matthew Perez - Embedded Systems Primer with Rust