Introduction
Purpose
This site is a collection of my personal Rust notes. I made it to help me understand Rust better and to have a quick place to review important topics. It also works as a learning guide for others who want to learn Rust. The book covers basic to advanced topics with short explanations and examples that are easy to follow.
Motivation
While I was learning Rust, I wrote down useful tips and examples. Later, I thought it could also help other developers. Rust has some hard parts like ownership and lifetimes, and I wanted to make those easier to understand. Writing this book also helped me remember what I learned.
Todos
Here are some things I plan to add:
- Add more real-world examples, especially in advanced topics like concurrency and unsafe Rust
- Share more common Rust patterns and best practices in the appendix
- Add diagrams to explain difficult ideas like ownership and traits
- Keep the content up to date with the newest Rust versions
1.1 Rust Toolchain
Core Tools
Tool | Description |
---|---|
rustc | The Rust compiler. It compiles .rs source code into binary executables or libraries. |
cargo | The Rust package manager and build tool. It handles project creation, building, dependency management, and running tests. |
rustup | The Rust toolchain installer and version manager. It lets you install and manage multiple Rust versions and components. |
Project & Dependency Tools
Tool | Description |
---|---|
crates.io | The official package registry for Rust libraries. It hosts reusable packages called "crates". |
Cargo.toml | The manifest file for a Rust project. It lists metadata, dependencies, and configuration for cargo . |
Testing & Formatting Tools
Tool | Description |
---|---|
cargo test | Runs unit tests or integration tests for your Rust project. |
cargo fmt | Automatically formats your code using Rust’s official style guide. |
cargo clippy | A linter that provides suggestions to improve code style and catch common mistakes. |
Documentation
Tool | Description |
---|---|
rustdoc | Generates HTML documentation from your Rust code using doc comments (/// ). |
cargo doc | Builds documentation for your project and its dependencies using rustdoc . |
Nightly & Advanced Tools
Tool | Description |
---|---|
cargo bench | Runs benchmark tests to measure performance. Available on nightly. |
cargo expand | Expands macros to show generated code. Useful for debugging procedural macros. |
miri | An interpreter for Rust programs to detect undefined behavior. Works on nightly. |
Toolchain Channels
Channel | Description |
---|---|
stable | Most reliable, updated every 6 weeks. |
beta | Next release candidate for stable. Used for testing. |
nightly | Updated daily, includes experimental features and tools. Needed for advanced or unstable features. |
1.2 Variables, Data Types, Constants
Variables
// main.rs
fn main() {
// creation
let a = 5;
// ^ here type is annotated implicitly
let b: i16 = 10;
// ^^^^ here type is explicitly annotated as i16.
// if we try to assign 5.0 float point to b, it will
// be an error because of type annotation.
// mutability
let c = 10; // variables are immutable as default in Rust
let mut d = 20;
// ^^^^ 'mut' keyword is used to make variable mutable.
d = 120; // now we can change the value because of mutability
// shadowing
let e = 10;
let e = 110; // the variable 'e' is shadowed.
println!("{e}"); // 110 will be printed out to the terminal.
}
Data Types
// main.rs
fn main() {
// Scalar Data Types which store single data type
// boolean
let b1: bool = true;
// unsigned integers
let i1: u8 = 1;
let i2: u16 = 1;
let i3: u32 = 1;
let i4: u64 = 1;
let i5: u128 = 1;
// signed integers
let i6: i8 = 1;
let i7: i16 = 1;
let i9: i32 = 1;
let i10: i64 = 1;
let i11: i128 = 1;
// floating point numbers
let f1: f32 = 1.0;
let f2: f64 = 1.0;
// platform specific integers
let p1: isize = 1; // represents pointer sized signed integer
let p2: usize = 1; // represents pointer sized unsigned integer
// characters, &str, and String
let c1: char = 'c';
let s1: &str = "Rust";
let s2: String = String::from("Rust");
// Compound Data Types which store multiple data types
// arrays
let a: [i32, 5] = [1, 2, 3, 4, 5]; // all members have the same type
let a1 = a[4]; // reach the any element of array by "variable[<index>]"
// tuples
let t1: (i32, i32, i32) = (5, 10, 15);
// each member can have different type
let t2: (i32, f64, &str) = (5, 10.5, "rust");
let s1 = t2.2; // reach the any element by "variable.<index>"
let (i1: i32, f1: f64, s1: &str) = t2; // the tuple can also be destructured
let t1 = (); // empty tuple is the special tuple type called "unit type"
// Type Aliasing
type model = u16;
let model_year: model = 1996;
}
Constants
Constant | Static |
---|---|
SCREAMING_SNAKE_CASE | SCREAMING_SNAKE_CASE |
Explicit type annotation | Explicit type annotation |
Can not be mutated | Can be marked as mutated |
Value of it will be inlined at compile time | It occupies location in the memory |
// main.rs
const MAX_LIMIT: u8 = 100; // inlined where it is used
// ^^^^ explicit type annotation needed
static MIN_NUMBER: i32 = -5; // occupy location in the memory
// ^^^^ explicit type annotation needed
// ^^^^^^^^^^^ SCREAMING_SNAKE_CASE needed
fn main() {
}
Operators in Rust
Category | Operators | Description | Example |
---|---|---|---|
Arithmetic | + , - , * , / , % | Basic math operations | 5 + 3 , 10 % 4 |
Comparison | == , != , < , > , <= , >= | Compare values, return bool | a == b , x < y |
Logical | && , ! | Boolean logic operations | true && false , !is_valid |
Bitwise | & , ^ , << , >> | Bit-level operations on integers | x & y , 1 << 3 |
Assignment | = , += , -= , *= , /= , %= , etc. | Assign and update values | count += 1 , x = 42 |
Reference | & | Borrowing (create reference) | let r = &value; |
Dereference | * | Follow a reference to access value | let x = *ptr; |
Range | .. , ..= | Range expressions (exclusive/inclusive) | for i in 0..5 {} , 1..=3 |
Error Propagation | ? | Return early if Result::Err or Option::None | let v = might_fail()?; |
Casting | as | Type conversion | let x = y as f64; |
1.3 Functions, Control Flow, Comments
ℹ️ Rust coding rules suggestions:
Item Type | Case Style | Example |
---|---|---|
Project Name | snake_case or kebab-case | my_project or my-project |
Variables | snake_case | my_variable |
Functions | snake_case | calculate_sum() |
Structs | PascalCase | MyStruct |
Enums | PascalCase | MyEnum |
Enum Variants | PascalCase | SomeVariant |
Constants | SCREAMING_SNAKE_CASE | MAX_LIMIT |
Statics | SCREAMING_SNAKE_CASE | MAX_LIMIT |
Modules | snake_case | my_module |
Crates | snake_case | my_crate |
Traits | PascalCase | Display |
Type Aliases | PascalCase | MyType |
Lifetimes | lowercase with ' | 'a , 'static |
Functions
Function definition syntax of Rust
fn function_name(parameter1: Type1, parameter2: Type2, ...) -> ReturnType {
// function body
}
- The return type comes after the arrow ->.
- If the last expression in the function block is returned implicitly, you omit the semicolon.
- If you use return, you must end the line with a semicolon.
When a generic type has to be used in the function definition
-
Basic generic function:
fn identity<T>(value: T) -> T { value }
-
Generic with constraints (trait bounds)
fn print_value<T: std::fmt::Display>(value: T) { println!("{}", value); }
-
Multiple generic parameters
fn pair<T, U>(a: T, b: U) { // do something }
-
With return type and trait bounds
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T { a + b }
-
Syntax with "where" keyword
fn function_name<T, U>(param1: T, param2: U) -> ReturnType where T: Trait1 + Trait2, U: Trait3, { // function body }
Control Flows
Conditional statements
Syntax | Description |
---|---|
if / else if / else | Branches execution based on a condition |
match | Pattern matching control flow |
fn main() {
if x > 0 {
println!("Positive");
} else if x < 0 {
println!("Negative");
} else {
println!("Zero");
}
match number {
1 => println!("One"),
2 | 3 => println!("Two or Three"),
_ => println!("Something else"),
}
}
Loops
Syntax | Description |
---|---|
loop | Infinite loop (exit with break ) |
while | Loop while a condition is true |
for | Loop over an iterator or range |
fn main() {
loop {
if some_condition {
break;
}
}
while i < 10 {
i += 1;
}
for x in 0..5 {
println!("{}", x);
}
}
Loop control
Syntax | Description |
---|---|
break | Exit the loop |
continue | Skip the rest of current loop iteration |
break 'label | Break out of a named loop |
'label: loop {} | Label a loop to break from nested loops |
fn main() {
'outer: for i in 0..3 {
for j in 0..3 {
if i == j {
break 'outer;
}
}
}
}
Comments
Comment Type | Syntax | Purpose |
---|---|---|
Line Comment | // | For quick notes or disabling lines |
Block Comment | /* ... */ | Multi-line, can be nested |
Doc Comment (item) | /// | Public API docs for items |
Doc Comment (module) | //! | Docs for the current file/module |
Examples for Doc comments
-
Documenting a function
/// Adds two numbers and returns the result. /// /// # Arguments /// /// * `a` - The first integer /// * `b` - The second integer /// /// # Example /// /// ``` /// let sum = add(2, 3); /// assert_eq!(sum, 5); /// ``` fn add(a: i32, b: i32) -> i32 { a + b }
-
Documenting a struct
/// A simple structure to hold a point in 2D space. struct Point { /// The x-coordinate. x: f64, /// The y-coordinate. y: f64, }
-
Crate level documentation (usually in lib.rs)
//! # My Math Crate //! //! This crate provides basic math utilities. //! //! ## Features //! - Addition //! - Subtraction /// Adds two numbers. pub fn add(a: i32, b: i32) -> i32 { a + b }
-
Module level documentation (usually in mod.rs)
//! This module handles geometry-related computations. /// Computes the area of a rectangle. pub fn area(width: f64, height: f64) -> f64 { width * height }
✅ Notes:
- Markdown works in both
///
and//!
. - Use triple backticks
```
for code blocks. - Run
cargo doc --open
to generate and view documentation in your browser.
2.1 Ownership
Ownership is a strategy for managing memory(and other resources) through a set of rules checked at compile time.
Rules
- Each value in Rust has a variable that is called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Which Problems Ownership solves?
- Memory/Resource Leaks
- Double free
- Usage after free
Examples
1. Scope
fn main() {
{
let s1 = String::from("Hello Rust!");
}
// ^ s1 is dropped, because it goes out of scope
println!("s1 is: {s1}");
// ^^^ error: can not find value s1 in this scope
}
2. Ownership Moved
There can only be one owner at a time
fn main() {
let s1 = String::from("Hello Rust!");
let s2 = s1;
// ^^^^^^^^^^^^ value of s1 is moved to s2
// s1 is not owner of the value anymore
println!("s1 is: {s1}");
// ^^^^ error: s1's value is moved,
// It can not be borrowed.
}
ℹ️ INFO
Ownership is not being moved for primitive types. In Rust, primitives that are entirely stored on the stack, such as integers, floating point numbers, booleans, or characters are cloned by default.
These types are cheap to clone. So there is no material difference between cloning and moving the values.
3. Ownership Moved to an Argument of the Function
fn main() {
let s1 = String::from("Hello Rust!");
print_string(s1);
// ^^^ ownership of the value moved here
println!("s1 is: {s1}");
// ^^^ error: value is moved before
// It can not be borrowed.
}
fn print_string(p1: String) {
println!("String in function: {p1}");
}
//^ p1 is dropped. It was the borrowed value of s1
2.2 Borrowing
The act of creating a reference.
- References are pointers with rules/restrictions
- References do not take ownership.
Why borrow?
- Performance
- When ownership is not needed/desired.
Rules
- At any given time, you can have either one mutable reference or any number of immutable reference.
- References must always be valid.
Which problems borrowing solves
- Data races.
- Dangling references.
Examples
1. Referenced Argument
fn main() {
let s1 = String::from("Hello Rust!");
print_string(&s1);
// ^^^ Instead of moving ownership
// Reference of the value is used
println!("s1: {s1}")
// ^^^ No error: the value is not moved
}
fn print_string(p1: &String) {
println!("String in function: {p1}");
}
2. Mutable Reference
Here in the example mutable and immutable references are used together. Normally it violates the first borrowing rules. But Rust borrow checker is smart enough to understand that immutable reference is used before the mutable reference. This is a feature in rust called non-lexical lifetimes.
fn main() {
let mut s1 = String::from("Hello Rust");
// ^^^ mut keyword is used because mutable reference is needed.
let r1 = &s1;
print_string(r1);
let r2 = &mut s1;
add_string(r2);
println!("s1: {s1}");
}
fn add_string(p1: &mut String) {
p1.push_str(" World");
// ^^^ here we can call p1 reference directly.
// Because it is automatically dereferenced.
// Otherwise (*p1).push_str() would be used.
}
fn print_string(p1: &String) {
println!("String in function: {p1}");
}
Borrow Checker
The borrow checker is part of Rust's compiler that ensures that references are always valid. It is tasked with enforcing of a number of properties:
- All variables must be initialized before used.
- You can't move same value twice.
- You can't move a value while it is borrowed.
So it checks to see that there is no ambiguity in the code that would point to an invalid reference in memory.
2.3 Structs
Structs and Implementation Block
Structs allow us group related data together.
#[derive(Debug)]
struct Product {
name: String,
price: f32,
in_stock: bool,
}
impl Product {
// Associated function
fn new(name: String, price: f32, in_stock: bool) -> Self {
Product {
name,
price,
in_stock,
}
}
// Associated function
fn get_tax_rate() -> f32 {
0.1
}
// immutable borrow of self
fn calculate_sales_taxes(&self) -> f32 {
self.price * Product::get_tax_rate()
// ^^^^^^^^^^^^^^^^^^^^^^
// Usage of the associated function
}
// mutable borrow of self
fn set_price(&mut self, val: f32) {
self.price = val;
}
// owned form of self
fn buy(self) -> i32 {
let name = self.name;
println!("{name} was bought");
123
}
}
fn main() {
// must be set as mutable because of the set_price()
let mut phone = Product::new(String::from("Iphone"), 999.99, true);
// ^^^^^^^^^^^^^^^^^
// Most common usage of the associated function
let sales_tax = phone.calculate_sales_taxes();
println!("Sales tax: {sales_tax}");
phone.set_price(100.00);
println!("Sales tax: {}", phone.calculate_sales_taxes());
println!("Invoice number: {}", phone.buy());
// ^^^ The value of phone is moved
phone.set_price(200.00);
// ^^^^^^ error: the value of phone is dropped
}
Here in the example above we can see some definitions:
- Immutable borrowing of self: It borrows self as immutable
- Mutable borrowing of self: It borrows self as mutable. So, we can change the value of the attributes.
- Self owned: It moves ownership to the member function.
- Associated function: It is the function does not use the self as an argument but it is still the member function. Double column is used to call the function.
Tuple Structs
The maximum number of elements allowed in a tuple or tuple struct is 12 for most standard trait implementations like Debug, Clone, PartialEq, etc., in the standard library.
fn main() {
struct RBG(i32, i32, i32);
struct CMYK(i32, i32, i32, i32);
let color1 = RGB(255, 255, 255);
let color2 = CMYK(0, 58, 100, 0);
//unit-like struct
struct MyStruct;
}
Enums and Matching
enum ProductCategory {
Books,
Clothing,
Electrics
}
enum Command {
Undo,
Redo,
AddText(String),
MoveCursor(i32, i32),
Replace{
from: String,
to: String
}
}
impl Command {
fn serialize(&self) -> String {
match self {
Command::Undo => String::from(
"{\"cmd\": \"undo\"}"
),
Command::Redo => String::from(
"{\"cmd\": \"undo\"}"
),
Command::AddText(s) => format!(
"{{\
\"cmd\": \"add_test\",\
\"text\": \"{s}\" \
}}"
),
Command::MoveCursor(x, y) => format!(
"{{\
\"cmd\": \"move_cursor\",\
\"line\": {x} \
\"column\": {y} \
}}"
),
Command::Replace(from, to) => format!(
"{{\
\"cmd\": \"replace\",\
\"from\": \"{from}\" \
\"to\": \"{to}\" \
}}"
),
}
}
}
fn main() {
let category = ProductCategory::Books;
}
Match Expression:
- It should cover all branches.
_
can be used to cover rest of branches.- For enums in Rust, each attribute can have different types.
Special Enums in Rust
Option
enum Option<T> {
Some(t),
None
}
fn main() {
// if-let syntax is Rust-based feature
// and it is useful pattern if you don't want to
// implement None branch with `match` pattern
if let Some(value) = username {
println!("username: {value}");
}
}
Result
ℹ️ Info
ok()
method of Result enum converts Result to Option
enum Result<E, T> {
Err(E),
Ok(T)
}
2.4 Generics
We use generics to create definitions for items like function signatures or structs, which we can then use with many different concrete data types.
struct BrowserCommand<T> {
name: String,
payload: T,
}
impl<T> BrowserCommand<T> {
fn new(name: String, payload: T) -> Self {
BrowserCommand { name, payload }
}
}
/// Implement the Generic T for String
impl BrowserCommand<String> {
fn print_payload(&self) {
println!("payload: {}", self.payload);
}
}
Most common generic definitions in Rust
enum Option<T> {
Some(T),
None
}
enum Result<T, E> {
Ok(T),
Err(E)
}
Monomorphization
- This means that compiler stamps out a different copy of the code of a generic function for each concrete type needed.
- For example, if the BrowserCommand struct is called for T=String and T=i32, the compiler will generate different copies of the struct for each concrete type needed.
- This process of monomorphization also happens with structs, enums, and implementation blocks
2.5 Traits
A trait defines the functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic type can be any type that has certain behavior.
ℹ️ Info
Rust does not support classical inheritance like other OO programming languages. Instead, it uses traits similar to interfaces in Java.
trait Park {
fn park(&self);
}
trait Paint {
// default implementation in trait
fn paint(&self, color: String) {
println!("Painting object: {}", color);
}
}
struct VehicleInfo {
make: String,
model: String,
year: u16,
}
struct Car {
info: VehicleInfo,
}
impl Park for Car {
fn park(&self) {
println!("Parking car");
}
}
// no need to implement the paint function because of default implementation
// in the trait
impl Paint for Car {}
struct Truck {
info: VehicleInfo,
}
impl Truck {
fn unload(&self) {
println!("Unloading truck");
}
}
impl Paint for Truck {
fn paint(&self, color: String) {
println!("Painting truck: {}", color);
}
}
impl Park for Truck {
fn park(&self) {
println!("Parking truck");
}
}
struct House {}
impl Paint for House {
fn paint(&self, color: String) {
println!("Painting house: {}", color);
}
}
Polymorphism
- Polymorphism is a concept in programming that allows objects to be treated as instances of their parent class rather than their actual class.
- This means that a single function can operate on objects of different classes.
- In Rust, polymorphism is achieved through traits and generics, which are powerful tools for writing flexible and reusable code.
Trait Bounds
Three types of trait bound as it is seen below:
fn paint_red_zero<T>(object: &T)
where
T: Paint + Park,
{
object.paint("red".to_string());
}
fn paint_red_one<T: Paint>(object: &T) {
object.paint("red".to_string());
}
fn paint_red_two(object: &impl Paint) {
object.paint("red".to_string());
}
Super Trait
Here is the paint is the super trait.
- Anything that implements Vehicle also implements Paint
- The functionality of the trait relies on the super trait.
trait Paint {
fn paint(&self, color: String) {
println!("Painting object: {}", color);
}
}
trait Vehicle: Paint + AnotherTrait {
fn park(&self);
// Traits can have associated functions
fn get_default_color() -> String {
"black".to_owned()
}
}
Trait Objects
dyn stands for dynamic dispatch
fn create_paintable_object(vehicle: bool) -> Box<dyn Paint> {
if vehicle {
Box::new(Car {
info: VehicleInfo {
make: "Toyota".to_owned(),
model: "Camry".to_owned(),
year: 2018,
},
})
} else {
Box::new(House {})
}
}
let paintable_objects: Vec<&dyn Paint> = vec![&car, &house];
Static Dispatch
Where the compiler knows which concrete methods to call at compile time.
Dynamic Dispatch
Where the compiler can not figure out which concrete methods to call at compile time. So, instead, it inserts a little bit of code to figure that out at runtime
- Advantage of dynamic dispatch is flexibility.
- it adds runtime performance cost
ℹ️ Info
To convert from Box smart pointer to reference we can use as_ref() method.
2.6 Lifetimes
Concrete Lifetime
Borrow checker tracks the lifetime of the values and checks to make sure that references are valid at compile time.
- A concrete life time is the time during which a value exists at a particular memory location.
- A life time is started when a variable is created or moved into a particular memory location, and ends when a value is dropped or moved out of a particular memory location.
Generic Lifetime
- lifetime specifiers are also known as generic lifetime annotations are a way to describe the relationship between lifetime of references.
fn first_turn<'a>(p1: &'a str, p2: &'a str) -> &'a str
// life time of the return value is equal the lifetime of the argument
// which has shortest lifetime. life time of p1 or life time of p2
ℹ️ Info
Life time of return value must be tied to the life time input parameters in general. It is because if a function returns reference that reference has to point to something passed in.
- ‘static lifetime
- string slices has a static lifetime. This means we can return string slices created in the function as a return value
fn first_turn2(_p1: &str, _p2: &str) -> &'static str {
let s1: &'static str = "Lets Get Rusty";
s1
}
Struct and Life time Elision
Basically, when it is evident that we want certain lifetimes to be available, the compiler can do that for us automatically. It has some rules as we listed below:
-
Rust compiler follows three lifetime elision rules:
- Each parameter that is a reference gets its own lifetime parameter
- If there is only one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
- If there are multiple input lifetime parameters, but one of them &self or &mut self, the lifetime of self is assigned to all output lifetime parameters
-
When storing references in struct we must add a generic lifetime annotation.
// lifetime elision
fn take_and_return_content(content: &str) -> &str {
content
}
2.7 Smart Pointers
Box Smart Pointer
- Use cases of box smart pointer:
Box::new(<object>);
- First one is to avoid copying large amount of data when transferring ownership.
- Box smart pointer is in combination with trait objects.
- When you have a type of unknown size, you want to use box smart pointer in a context where the exact size is required
- Box smart pointer is similar to unique smart pointer in C++
let ui_components: Vec<Box<dyn UIComponent>> =
vec![Box::new(button_a), button_b, Box::new(label_a)];
// button_b has already defined as Box smart pointer
Rc Smart Pointer (Reference Counter)
It provides shared ownership.
Rc<object_name>
. When it is created the reference counter is 1. Then each time it is cloned asRc::clone(&variable)
, reference counter is increased by 1.- Rc smart pointer is similar to shared smart pointer in C++
- It can only be used in single-threaded applications. For multi-threaded applications you must use the atomically reference counted smart pointer
RefCell Smart Pointer
- Rc smart pointer only allows immutable shared ownership of a value. With RefCell we can borrow mutably.
- RefCell uses unsafe rust code to work around of borrowing rules at compile time.
- RefCell must use carefully because responsibility of ownership is on programmer.
Custom Smart Pointer
- Two important traits implemented by smart pointers
std::ops::Deref
immutable dereferencing operationsstd::ops::Drop
when a value goes out of scope you can run code also known as destructor.
use std::ops::Deref;
// Define a generic smart pointer
struct MyBox<T>(T);
// Implement new method
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// Implement Deref to allow `*` operator
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
// Implement Drop for custom cleanup
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
println!("Dropping MyBox with value!");
}
}
// Usage example
fn main() {
let x = 5;
let y = MyBox::new(x);
println!("x = {}", x);
println!("*y = {}", *y); // works because of Deref
}
2.8 Error Handling
Error handling is complicated by its nature. It might have a few steps as follows:
- Defining
- Propagating
- Handling or Discarding
- Reporting
- Developer & End User
Recoverable Errors
- Represented by the
Result<T, E>
enum. - Can be handled by the program gracefully.
- Common for operations like file I/O or network access
fn read_file() -> Result<String, std::io::Error> {
std::fs::read_to_string("data.txt")
}
ℹ️ Use
match, ?, or .unwrap_or() to handle them.
Unrecoverable Errors
- Represented by the
panic!
macro - Program stops execution immediately
- Used when a bug is detected or something truly unexpected happens.
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("division by zero");
}
a / b
}
ℹ️ Result to Option or Option to Result
github/novafacing/CONVERSATIONS
Resource for helpful conversations
Idiomatic Errors in Rust
pub trait Error: Debug + Display {
pub fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
Implementation Error trait on our custom error types achieves a few things:
- Semantically marks types as errors
- Standardizes
- Checking the source of the error
- User facing reporting
- Developer facing reporting
ℹ️ Note
Error trait is defined in the standard library. Error handling infrastructure and third party libraries are built on top of this trait. So, it is important that your types implement this trait so that they work with the error handling ecosystem.
Basic Error Handling (using thiserror and anyhow crates)
Here you can find a working example created to show how to use thiserror and anyhow crates to handle errors
- Project structure
my_example
├── Cargo.toml
├── config.yaml
└── src
├── lib.rs
└── main.rs
Cargo.toml
file
[package]
name = "my_example"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0"
thiserror = "2.0.12"
serde = { version = "1.0", features = ["derive"] }
config = "0.15.11"
lib.rs
file
use serde::Deserialize;
use thiserror::Error;
/// Our application wide configuration
#[derive(Debug, Deserialize)]
pub struct Config {
pub db_url: String,
pub max_connection: u32,
}
#[derive(Debug, Error)]
pub enum AppError {
#[error("Failed to load configuration: {0}")]
ConfigError(#[from] config::ConfigError),
#[error("Failed to connect to Database: {0}")]
DatabaseError(String),
}
pub fn load_config() -> Result<Config, AppError> {
let config = config::Config::builder()
.add_source(config::File::with_name("config.yaml"))
.build()?
.try_deserialize::<Config>()?;
Ok(config)
}
pub fn connect_to_db(url: &str) -> Result<(), AppError> {
if url.starts_with("postgres://") {
Ok(())
} else {
Err(AppError::DatabaseError(format!(
"Unsupported database url: {url}"
)))
}
}
main.rs
file
use anyhow::{Context, Result};
use my_example::{connect_to_db, load_config};
fn main() {
if let Err(e) = run() {
eprintln!("❌ Application error: {e}");
for cause in e.chain().skip(1) {
eprintln!("🔎 Caused by: {cause}")
}
std::process::exit(1);
}
}
fn run() -> Result<()> {
let config = load_config().context("Could not load config.yaml")?;
println!("✅ Loaded config: {:#?}", config);
connect_to_db(&config.db_url).context("Failed to connect to database")?;
println!("✅ Connected to database!");
Ok(())
}
Notes for example:
- The config crate makes it very ergonomic to load and parse configuration files (like YAML, TOML, JSON, etc.)
thiserror
crate is used at the library side to define custom error typeanyhow
crate is used to handle errors ergonomically at binaries and application side.
Basically we can make a table for the usage of these two important crates:
Layer | Use | Why |
---|---|---|
Library | thiserror | Define and expose structured error types |
App/Binary | anyhow | Consume errors and handle/report them easily |
2.9 Functional Features
Closures
- Closures are anonymous functions which you can store in variables or pass as argument to other function.
- Closures can capture variables in the scope which they are defined
Fn
: Immutably borrow variables in environmentFnMut
: Mutably borrow variables in environment. can change environment.FnOnce
: Take ownership of variables in environment. can only be called once.- move keyword can use before | |
- in use case of using thread we can use move keyword for data protection.
Function Pointers
- Fn used for closures. Instead, fn used for function pointers
- Function pointers are similar to closures except they don’t capture variables in their environment.
// function defined by closures (Fn())
fn are_both_true<T, U, V>(f1: T, f2: U, item: &V) -> bool
where T: Fn(&V) -> bool, U: Fn(&V) -> bool {
f1(item) && f2(item)
}
// function defined by function pointers (fn())
fn are_both_true<V>(f1: fn(&V) -> bool, f2: fn(&V) -> bool, item: &V) -> bool {
f1(item) && f2(item)
}
- If we want to use a variable in the closure environment we can not pass it to the function as fn (function pointer). We should use Fn(Closure call). Coercion does not work for this kind of situations.
- Closures can be coerced to function pointer if they are not capturing variable from the environment.
- Function pointers can also be coerced to closures.
Iterator Pattern
- Standard Rust library has an iterator trait looks like this:
trait Iterator {
type Item; // associated type instead of generic
fn next(&mut self) -> Option<Self::Item>;
}
- The ‘next’ method has to be implemented by the structure that wants to use iterator feature.
- type Item: it is an Associated type. It is similar to generic except they are restricted to one concrete type per trait implementation.
- Another important trait in the standard library is IntoIterator trait:
trait IntoIterator {
type Item;
type IntoIter = Iterator;
fn into_iter(self) -> Self::IntoIter;
}
- Usage iterator types - Iterator over Collections (hash map)
//scores is a hash map with type HashMap<String, i32>
for (key: String, value: i32) in scores // -> instead of scores.into_iter()
for (key: &String, value: &i32) in &scores // -> instead of scores.iter()
for (key: &String, value: &mut i32 in &mut scores // instead of scores.iter_mut()
3.1 Concurrency
- Threads have their own stack memory but they share heap memory with other threads in the same process.
- Job of the scheduler is to make sure that each thread gets execution time and the way it does that is through preemptive scheduling. Each threads get a small amount of time to execute. Once that time is over, execution on that thread stops and another thread is scheduled in its place. This process of switching threads is called context switching.
- Preemptive scheduling means that the scheduler can stop executing a thread at any given point. The programmer has no control over this.
- Cooperative scheduling means that programmer can decide when to pause a task to allow other tasks to execute.
- Concurrency:
- When different parts of your program execute independently. This could be done by:
- Time Slicing:
- Execution of these parts is interleaved on a single core.
- Parallel Execution:
- Execution of these parts happens at the same time using multiple cores.
- Time Slicing:
- When different parts of your program execute independently. This could be done by:
- Concurrency Models:
- OS Threads (in Rust)
- A basic concurrency primitive provided by the operating system
- Coroutines
- execute functions concurrently. Hides low level details
- The Actor Model
- Divides concurrent computation into units called actors that could communicate with each other through message parsing.
- Asynchronous Programming (in Rust)
- execute functions concurrently. Exposes some of the low level details.
- Event-driven Programming
- With callbacks, which used to be popular concurrency model in JavaScript
- OS Threads (in Rust)
OS Threads | Async Tasks |
---|---|
Managed by the OS | Managed by language runtime or library |
Preemptive scheduling | Cooperative Scheduling |
Higher performance overhead | Lower performance overhead |
Ideal for small amount of CPU-bound workloads | Ideal for large amount of IO-bound workloads |
Harder to reason about | Easier to reason about |
Creating Threads
use std::thread;
use std::time::Duration;
fn main() {
let s = "Hello Rust World!".to_owned();
// here move keyword used to capture variables from the environment
let handle = thread::spawn(move || {
prinln!("{s}");
for i in 0..10 {
println!("Spawned thread: {i}");
thread::sleep(Duration::from_milis(200));
}
});
for i in 0..10 {
println!("Main thread: {i}");
thread::sleep(Duration::from_milis(200));
}
// Used to make main thread wait until the spawned thread will be executed
handle.join().unwrap();
}
Multi Producer Single Consumer (mpsc)
use std::sync::mpsc;
use std::thread;
fn main() {
let sentences = vec![
"Hello".to_owned(),
"Rust".to_owned(),
"World!".to_owned(),
];
let (tx, rx) = mpsc::channel();
for sentence in sentences {
let tx = tx.clone();
thread::spawn(move || {
let sentence: String = sentence.chars().rev().collect();
tx.send(sentence).unwrap();
});
}
// The receiver of the channel will stop listening for messages
// until all transmitters are dropped.
// Because we are using clones of the transmitter, we have to manually drop
// the transmitter cloned.
drop(tx);
for received in rx {
println!("Got: {}", received);
}
}
Mutexes
#[derive(Debug)]
struct Database {
connections: Vec<i32>,
}
impl Database {
fn new() -> Database {
Database {
connections: Vec::new(),
}
}
fn connect(&mut self, id: i32) {
self.connections.push(id);
}
}
fn main() {
let db = Mutex::new(Database::new());
{
let mut db_lock = db.lock().unwrap();
db_lock.connect(1);
}
}
Mutexes with Arc (Atomic Reference Counter)
use std::{
sync::{Arc, Mutex},
thread
};
#[derive(Debug)]
struct Database {
connections: Vec<i32>,
}
impl Database {
fn new() -> Database {
Database {
connections: Vec::new(),
}
}
fn connect(&mut self, id: i32) {
self.connections.push(id);
}
}
fn main() {
let db = Arc::new(Mutex::new(Database::new()));
let mut handles = vec![];
for i in 0..10 {
let db = db.clone();
let handle = std::thread::spawn(move || {
let mut db_lock = db.lock().unwrap();
db_lock.connect(i);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let db_lock = db.lock().unwrap();
println!("{:?}", db_lock);
}
async & await
- async function is simply a function which returns something that implements the Future trait.
async
/.await
is special syntax which allows us to write functions, closures, and blocks that can pause execution and yield control back to the runtime allowing other code to make progress, and pick back up from where they left off.- One advantage of the
async
/.await
syntax is that it allows us to write asynchronous code, which looks like synchronous code. - In Rust, Futures are lazy, meaning that they won’t do anything unless they are driven to completion by being polled. This allows Futures to be a zero cost abstraction.
- Futures could be driven to completion by either awaiting the future or giving it to an executor.
.await
keyword is only used if the function in which the keyword used is async function.- Futures could be driven to completion in two ways, either calling await on the future or manually polling the future until it’s complete.
Runtime or Executer
- A runtime is responsible polling the top level futures and running them till completion. It’s also responsible for running multiple futures in parallel.
- Rust standard library does not provide an async runtime. Most popular community built runtime is called as “tokio”.
Here is the comparison between std::thread::spawn vs. tokio::spawn
Feature | std::thread::spawn | tokio::spawn |
---|---|---|
Runtime needed | ❌ No | ✅ Yes (Tokio runtime) |
Blocking | ✅ Yes | ❌ No (non-blocking) |
Stack size | Large (OS thread) | Small (uses async stack) |
Use for | CPU-heavy, blocking work | IO-heavy, concurrent async tasks |
Return type | JoinHandle<T> | JoinHandle<T> (async .await ) |
3.2 Macro System
What are macros?
- Also referred to as syntax extensions.
- A way of writing code that writes other code.
- A form of meta programming.
- Compile time process.
Compilation process
You can think of compilation as a big function where the input parameter is your source code, and the return value is an executable. Inside the big function there are smaller functions that analyze and process your code. The first three steps of the compilation process are Lexical
, Syntax
, and Semantic
analyzes as you see below in the diagram.
graph LR; A[Source<br>Code]-->B; B[Lexical<br>Analysis]-->C; C[Syntax<br>Analysis]-->D; D[Semantic<br>Analysis]-->E; E[Intermediate<br>Code<br>Generation]-->F; F[Code<br>Optimization]-->G; G[Target<br>Code<br>Generation]-->H[Binary];
After that intermediate representation of your code is generated, optimized, and finally turned into assembly to produce a binary.
We will focus on first three steps of compilation process to understand macro system better.
Lexical Analysis
- From the perspective of the compiler, your source code starts off as a meaningless text file. The job of the lexical analysis is to turn this meaningless text into a stream of tokens, and then classify the tokens.
- Tokens can be grouped into several categories:
- Identifiers
- Literals
- Keywords
- Symbols
- The stream of tokens are turned into a stream of token trees.
- Almost all tokens are token trees themselves, specifically leaf nodes.
- The only non-leaf nodes are grouping tokes such as parenthesis, brackets, or curly braces.
- Macros in Rust deals with tokens and token trees.
Syntax Analysis
- It turns a stream of token trees into an Abstract Syntax Tree
- Representing the syntax structure of your program.
- The nodes of syntax tree could be literals, function calls, if-else branches, and more.
- Using the AST, programs like linters, test coverage tools, transpilers are implemented.
ℹ️ INFO
In Rust,
macros
are processed or expanded after the AST is constructed, but prior to semantic analysis.
Semantic Analysis
- After all macros are expanded, semantic analysis checks to make sure the AST semantically makes sense.
Rust has two types of macros: Declarative Macros and Procedural Macros.
Declarative Macros
It is similar to match expression.
// lib.rs (lib name macros)
/// macro_rules! name {
/// rule0;
/// rule1;
/// rule2;
/// // ...
/// ruleN;
/// }
///
/// (matcher) => { expansion aka transcriber }
#[macro_export] // used to export macro defined in library
macro_rules! hello {
() => {
println!("Hello");
}
}
//main.rs
use macros::hello;
fn main() {
hello!();
}
Another example including specifiers
//lib.rs
#[macro_export]
macro_rules! map {
// $ [identifier]: [fragment-specifier]
($key: ty, $val: ty) => {{
let map: HashMap<$key, $val> = HashMap::new();
map
}};
// $ (...) sep rep
// rep operators:
// * : zero or more repetitions
// ? : at most one repetition
// + : one or more repetitions
// sep operators must be `=>`, `,` , `;`
($($key: expr => $val: expr),*) => {{
let mut map = HashMap::new();
$(map.insert($key, $val);)*
map
}};
}
//main.rs
use macros::map;
// needed because macro uses HashMap
use std::collections::HashMap;
fn main() {
let new_hash_map = map!(String, i32);
let another_map = map!("test" => 1, "test1" => 2);
}
You can see list of all specifiers in Rust:
Specifier | Matches |
---|---|
block | A block of code, e.g., { ... } |
expr | An expression, e.g., 1 + 2 , foo() , or if x { y } else { z } |
expr_2021 | An expression excluding const blocks and underscore expressions (for backward compatibility) |
ident | An identifier, e.g., foo , bar |
item | An item, such as a function, struct, or module declaration |
lifetime | A lifetime, e.g., 'a |
literal | A literal value, e.g., 42 , "hello" |
meta | A meta item, typically used in attributes, e.g., cfg(...) |
pat | A pattern, e.g., Some(x) , Ok(_) |
pat_param | A pattern without top-level alternation, used in function parameters |
path | A path, e.g., std::io::Result |
stmt | A statement, e.g., let x = 5; |
tt | A single token tree |
ty | A type, e.g., i32 , Vec<T> |
vis | A visibility qualifier, e.g., pub , pub(crate) |
Procedural Macros
- Procedural macros allow us to define a function which takes token stream as an input and returns a token stream output rather than matching against patterns.
- Procedural macros must be defined its own crate with a special crate type
proc-macro
as you see below: proc-macro
crate types cannot export any items other than functions tagged with#[proc_macro]
,#[proc_macro_derive]
, or#[proc_macro_attribute]
[package]
name = "macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
//lib.rs
extern crate proc_macro;
// as an example
use proc_macro::{TokenStream};
There are three type of procedural macros: function like, attribute like, and custom drive.
Function like procedural macros
You can see the example below:
// lib.rs
extern crate proc_macro;
use chrono::Utc;
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro]
pub fn log_info(input: TokenStream) -> TokenStream {
let mut output = "[info] ".to_owned();
for token in input {
let token_string = token.to_string();
match token_string.as_str() {
"[TIME]" => {
let time = Utc::now().time().to_string();
output.push_str(&format!("{} ", time));
}
_ => {
output.push_str(&format!("{} ", token_string));
}
}
}
TokenStream::from(quote! {
println!("{}", #output);
})
}
// main.rs
use macros::log_info;
fn main() {
log_info!([TIME] starting program...);
}
...and the output is:
[info] 14:00:07.586846 starting program . . .
Custom derive procedural macros
It is explained in an example below:
// lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
// Log is the name of the proc drive macro
#[proc_macro_derive(Log)]
pub fn log_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let trait_impl = quote! {
impl Log for #name {
fn info(&self, msg: &str) {
println!("[info] {}: {}", stringify!(#name),msg);
}
fn warn(&self, msg: &str) {
println!("[warn] {}: {}", stringify!(#name),msg);
}
fn error(&self, msg: &str) {
println!("[error] {}: {}", stringify!(#name),msg);
}
}
};
trait_impl.into()
}
// main.rs
use macros::*;
trait Log {
fn info(&self, msg: &str);
fn warn(&self, msg: &str);
fn error(&self, msg: &str);
}
#[derive(Debug, Log)]
struct Database {
url: String,
connections: u32,
}
impl Database {
fn new(url: String) -> Self {
Database {
url,
connections: 0,
}
}
fn connect(&mut self) {
self.info(format!("new connection to {}", self.url).as_str());
self.connections += 1;
if self.connections > 10 {
self.warn(format!("100 or more connections open").as_str());
}
}
}
fn main() {
let mut db = Database::new("localhost::8888".to_owned());
db.connect();
for _ in 0..10 {
db.connect();
}
}
ℹ️ Info
Two important crates
quote
,syn
, anddarling
used to write procedural derive macro.quote: generates Rust code as tokens using macro-like syntax.
syn: parses Rust code into syntax tree (AST)
Because procedural macros receive code as token streams
Need to:
🧩 Parse them (using syn)
✏️ Generate new code (using quote)
darling: crate in Rust is used to make writing procedural macros easier, especially when working with custom attributes.
When you're building a procedural macro (like
#[derive(...)]
), you often need to:Parse attributes like
#[my_attr(foo = "bar", skip)]
Handle lots of repetitive parsing code
🔧 darling does that work for you. It helps you define a Rust struct and automatically maps the attributes to it.
Attribute like procedural macros
It is explained by example below:
// lib.rs
extern crate proc_macro;
use darling::FromMeta;
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{AttributeArgs, FnArg, Ident, ItemFn, Pat, Stmt, parse_macro_input, parse_quote};
#[derive(FromMeta)]
struct MacroArgs {
#[darling(default)]
verbose: bool,
}
#[proc_macro_attribute]
pub fn log_call(args: TokenStream, input: TokenStream) -> TokenStream {
let attr_args = parse_macro_input!(args as AttributeArgs);
let mut input = parse_macro_input!(input as ItemFn);
let attr_args = match MacroArgs::from_list(&attr_args) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(e.write_errors());
}
};
impl_log_call(&attr_args, &mut input)
}
fn impl_log_call(attr_args: &MacroArgs, input: &mut ItemFn) -> TokenStream {
let fn_name = &input.sig.ident;
if attr_args.verbose {
let fn_args = extract_arg_names(input);
let statements = generate_verbose_logs(fn_name, fn_args);
input.block.stmts.splice(0..0, statements);
} else {
input.block.stmts.insert(
0,
parse_quote! {
println!("[Info] calling {}", stringify!(#fn_name));
},
);
}
input.to_token_stream().into()
}
fn extract_arg_names(func: &ItemFn) -> Vec<&Ident> {
func.sig
.inputs
.iter()
.filter_map(|arg| {
if let FnArg::Typed(pat_type) = arg {
if let Pat::Ident(pat) = &(*pat_type.pat) {
return Some(&pat.ident);
}
}
None
})
.collect()
}
fn generate_verbose_logs(fn_name: &Ident, fn_args: Vec<&Ident>) -> Vec<Stmt> {
let mut statements = vec![parse_quote!({
print!("[Info] calling {} | ", stringify!(#fn_name));
})];
for arg in fn_args {
statements.push(parse_quote!({
print!("{} = {:?} ", stringify!(#arg), #arg);
}));
}
statements.push(parse_quote!({
println!();
}));
statements
}
// main.rs
use macros::*;
#[derive(Debug)]
struct Product {
name: String,
price: u32,
}
fn main() {
let laptop = Product {
name: "MacBook Pro".to_owned(),
price: 2000,
};
buy_product(laptop, 20);
}
#[log_call(verbose)]
fn buy_product(product: Product, discount: u32) {
//
}
3.3 Unsafe Rust and FFI
Unsafe Rust
- To write unsafe code in Rust we should use unsafe keyword
- We can do 5 specific things, which we can not do without unsafe keyword, in unsafe block.
- Dereference a raw pointer
- Call an unsafe function
- Implement an unsafe trait
- Access/Modify a mutable static variable
- Access fields of a union.
FFI
- aka Foreign Function Interface.
#[link(name = "adder", kind="static")]
extern "C" {
fn add(a: i32, b:i32) -> i32;
}
fn main() {
let x:i32;
unsafe {
x = add(1, 2);
}
println!("sum: {x}");
}
Overview
Basic Components
1. Packages
They contain one or more crates that provide a set of functionality. Packages allow you to build, test, and share crates.
- Cargo.toml: It describes the package and defines how to build crates.
- Rules
- Must have at least one crate
- At most one library crate
- Any number of binary crates
An example for package : Here in the example bin
folder is used to specify binary crates.
my_package
├── Cargo.lock
├── Cargo.toml
└── src
├── bin
│ ├── another_one.rs
│ └── main.rs
└── lib.rs
Instead of using bin
folder to specify binary crates, we can define them in the Cargo.toml
file with the name and path as follows:
[package]
name = "my_package"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "main"
path = "src/main.rs"
[[bin]]
name = "another_one"
path = "src/another_one.rs"
[dependencies]
With the file structure:
my_package
├── Cargo.lock
├── Cargo.toml
└── src
├── another_one.rs
├── lib.rs
└── main.rs
ℹ️ Info
If we have more than one binary crate, to run one of the binaries we should call it with the name:
cargo run --bin <binary_name>
2. Crates
A tree of modules that produce a library or execute.
3. Modules
- Organize code for readability and reuse
- Control scope and privacy
- Contain items (functions, structs, enums, traits, etc.)
- Explicitly defined (using the mod keyword)
- Not mapped to the filesystem
- Flexibility and straight forward conditional compilation.
ℹ️Info
Rust modules are not mapped into filesystem. So, there is no way to tell a module, which is defined in
lib.rs
or another file, is a module that is submodule of the crate. That is why we should usemod <module_name>
to use the module as submodule of a crate.See the example below:
├── Cargo.lock
├── Cargo.toml
└── src
├── a_submodule.rs
└── lib.rs
//! src/a_submodule.rs
pub fn test_func() {
println!("test function")
}
//! src/lib.rs
mod a_submodule;
use a_submodule::test_func;
//use a_submodule::*;
pub fn main_func() {
test_func();
}
Workspace
Config.toml
Config.toml file for the workspace
[workspace]
# for Rust Edition 2021 and later, use 1 before the Rust Edition 2021
resolver = "2"
# members used to define creates will be used in the project
# members = ["lib-a", "lib-b"] is alternative usage
members = ["libs/*"]
# This section is optional
# If you would like to manage dependencies from the main Config.toml
# You can add them here as follow
[workspace.dependencies]
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
Config.toml file for the specific create in the workspace
[package]
name = "lib-a"
version = "0.1.0"
edition = "2021"
[dependencies]
# If the crate is defined in the main Config.toml file
anyhow = { workspace = true }
rand = "0.8.5"
# dev-dependencies used for testing purpose
[dev-dependencies]
rand_chacha = "0.3.1"
Config.toml file for library specialized for wasm
[package]
name = "lib-wasm"
version = "0.1.0"
edition = "2021"
description = "A wasm-library"
license = "MIT"
repository = "https://github.com/emre-ergun/mdbook-rust.git"
# library type has to be added for wasm build
# type of the library is C dynamic library
[lib]
crate-type = ["cdylib"]
[dependencies]
rand = "0.8.5"
getrandom = { version = "0.2.15", features = ["js"] }
wasm-bindgen = "0.2.100"
# a lib from the same workspace to use it another library
lib-a = { path = "../lib-a/" }
.cargo/config.toml
The .cargo
folder in Rust project is optional but important for custom configuration and overrides. It plays different role than Cargo.toml
or src/
but is still usefull for more advanced Rust setups.
config.toml
This is the most command file in .cargo
folder. You can use it to:
1. Set a custom build target
Useful for cross-compiling
[build]
target = "thumbv7em-none-eabihf"
2. Specify a custom linker
For embedded or non-standard targets
[target.thumbv7em-none-eabihf]
linker = "arm-none-eabi-gcc"
3. Change target directory
Keep build artifacts out of the target/
folder.
[build]
target-dir = "build"
4. Enable or override build scripts
Force features, set environment variables, etc.
[env]
RUSTFLAGS = "-C target-cpu=native"
Example: Embedded Rust Project
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
runner = "probe-rs run"
linker = "arm-none-eabi-ld"
rustflags = [
"-C", "link-arg=-Tlink.x"
]
Binding, Rebinding, and Types
In Rust, creating a binding simply means assigning a value to a name. This binding cannot be changed unless explicitly declared as mutable (mut
).
fn main() {
let x = 10; // binds integer value 10 to identifier `x`
println!("The value of x is {}", x);
// x = 20; // This line would cause a compile error because x is immutable
}
You can allow a binding to be modified by using the keyword mut
:
fn main() {
let mut y = 5; // mutable binding
println!("Initial value of y is {}", y);
y = 15; // changing the value is allowed now
println!("Modified value of y is {}", y);
}
Type | Keyword or syntax | Usage Example |
---|---|---|
Immutable binding | let x = 5; | Can't change later |
Mutable binding | let mut x = 5; | Changeable |
Pattern binding | let (a, b) = (1, 2); | Destructuring tuples |
Reference binding | let r = &val; | Borrowing references |
Shadowing binding | let x = 1; let x = 2; | Redefine existing names |
When defining a function, each parameter has a binding and type. Here inputs
is a binding, Vec<i32>
is a type.
fn test_function(mut inputs: Vec<i32>) {
// do something
}
fn main() {
let inputs = vec![1,2,3,4];
// ^ no mut needed here
test_function(inputs);
// ^ works fine
}
The reason is that mut
we've just introduced appears in so-called binding position.
fn foo_1(items: &[f32]) {
// ^^^^^ ------
// binding type
// (immut.) (immut.)
}
fn foo_2(mut items: &[f32]) {
// ^^^^^^^^^ ------
// binding type
// (mutable) (immut.)
}
fn foo_3(items: &mut [f32]) {
// ^^^^^ ----------
// binding type
// (immut.) (mutable)
}
fn foo_4(mut items: &mut [f32]) {
// ^^^^^^^^^ ----------
// binding type
// (mutable) (mutable)
}
struct Person {
name: String,
eyeball_radius: usize,
}
fn decompose(Person { name, mut eyeball_radius }: Person) {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ------
// binding type
// (partially immutable, partially mutable) (immutable)
}
... and bindings, contrary to types, are local to the function:
fn foo(items: &mut Vec<usize>) {
// When a type is mutable, you can modify the thing being
// referenced:
items.push(1234);
// But if the binding remains immutable, you cannot modify
// *which* thing is referenced:
items = some_another_vector;
// ^ error: cannot assign to immutable argument
}
fn bar(mut items: &Vec<usize>) {
// On the other hand, when a binding is mutable, you can change
// *which* thing is referenced:
items = some_another_vector;
// But if the type remains immutable, you cannot modify the
// thing itself:
items.push(1234);
// ^^^^^ error: cannot borrow `*items` as mutable, as it is
// behind a `&` reference
}
Idiomatic Rust Examples
Combinator
Usage of iter
, map
, collect
together
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled_numbers: Vec<i32> = numbers
.iter()
.map(|number| number * 2)
.collect();
println!("{:?}", numbers);
println!("{:?}", doubled_numbers);
}
iter()
: creates an iterator that allows you to traverse each element of a collection, such as a vector (Vec
). The iterator in the example yields references (&i32) to each element one by one.
map()
: takes each element from the iterator and applies a function (closure) to transform each element into something else.
collect()
: gathers the transformed values from the iterator and converts them back into a collection (such as Vec
).
Usage of zip
and sum
in chain
fn main() {
let vec1 = vec![1, 2, 3, 4, 5];
let vec2 = vec![10, 20, 30, 40, 50];
let output = vec1
.iter()
.zip(&vec2)
.map(|(input1, input2)| input1 * input2)
.sum::<i32>();
let max_value = (output).max(1000);
let min_value = (output).min(1000);
println!("Maximum value: {}", max_value);
println!("Minimum value: {}", min_value);
}
zip()
: pairs elements from two iterators, creating a new iterator of tuples. It stops once one of the iterators runs out of items.
After zip in our example: [(1,10), (2,20), (3,30), (4,40), (5,50)]
sum()
: consumes the iterator and adds all its elements together, giving you a single numeric result. sum::<i32>()
explicitly defines the type of the resulting sum.
ℹ️ Info
Quick note about
max()
andmin()
: max(1000) returns the higher of the two values (output vs. 1000). min(1000) returns the lower of the two values (output vs. 1000).
Usage of fold
in chain
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
let odd_numbers = numbers.iter().fold(Vec::new(), |mut acc, number| {
if number % 2 != 0 {
acc.push(number);
}
acc
});
println!("{:?}", odd_numbers);
let sum = numbers.iter().fold(0, |acc, number| acc + number);
println!("Sum of the numbers: {}", sum);
}
fold()
: allows you to accumulate a value while iterating.
- Initial value:
Vec::new()
is the starting value foracc
. Here it createsacc: Vec<&i32>
acc
is the accumulator which caries the result so fornumber
is the current item of the iterator.
Usage of windows
in chain
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let numbers = numbers
.windows(2)
.map(|numbers| numbers[0] + numbers[1])
.collect::<Vec<i32>>();
println!("Combined array: {:?}", numbers);
}
windows(n)
: creates a sliding window over the vector, giving you overlapping slices of size n
.
In our example windows(2) creates slices like &[1, 2], &[2, 3], &[3, 4], &[4, 5], &[5, 6]
Range based iteration
fn main() {
let output_size = 10;
let numbers: Vec<i32> = (0..output_size).map(|_| 5).collect();
println!("range based array: {:?}", numbers);
}
Here in this example (0..output_size)
creates a range which is iterable and output*size is excluded.
(0..=10)
means 10 is included to the range.
|_|
, also known as toilet closure, is a function which accepts an argument it doesn't care about.
TDD with Rust Libraries
You might've heard that it's useful to name tests based on their preconditions and expectations.
Usually that's true - if we were writing a shop, it could be useful to structure its tests like so:
#[cfg(test)]
mod tests {
use super::*;
mod cart {
use super::*;
mod when_user_adds_a_flower_to_their_cart {
use super::*;
#[test]
fn user_can_see_this_flower_in_their_cart() {
/* ... */
}
#[test]
fn user_can_remove_this_flower_from_their_cart() {
/* ... */
}
mod and_submits_order {
/* ... */
}
mod and_abandons_cart {
/* ... */
}
}
}
}
ℹ️ Info
Creates which will be used only for testing purpose can be added under the
[dev-dependencies]
in the Cargo.toml file of the project.[dev-dependencies]
are not allowed to be used in the library code(only with test configuration code)
[package]
name = "lib-a"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8.5"
[dev-dependencies]
rand_chacha = "0.3.1"
approx = "0.5.1"
use rand::{Rng, RngCore};
use approx::assert_relative_eq;
// ^^^^^^ use of undeclared crate or module "approx"
/*
* Library code here
*/
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use rand_chacha::ChaCha8Rng;
}