Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

ToolDescription
rustcThe Rust compiler. It compiles .rs source code into binary executables or libraries.
cargoThe Rust package manager and build tool. It handles project creation, building, dependency management, and running tests.
rustupThe Rust toolchain installer and version manager. It lets you install and manage multiple Rust versions and components.

Project & Dependency Tools

ToolDescription
crates.ioThe official package registry for Rust libraries. It hosts reusable packages called "crates".
Cargo.tomlThe manifest file for a Rust project. It lists metadata, dependencies, and configuration for cargo.

Testing & Formatting Tools

ToolDescription
cargo testRuns unit tests or integration tests for your Rust project.
cargo fmtAutomatically formats your code using Rust’s official style guide.
cargo clippyA linter that provides suggestions to improve code style and catch common mistakes.

Documentation

ToolDescription
rustdocGenerates HTML documentation from your Rust code using doc comments (///).
cargo docBuilds documentation for your project and its dependencies using rustdoc.

Nightly & Advanced Tools

ToolDescription
cargo benchRuns benchmark tests to measure performance. Available on nightly.
cargo expandExpands macros to show generated code. Useful for debugging procedural macros.
miriAn interpreter for Rust programs to detect undefined behavior. Works on nightly.

Toolchain Channels

ChannelDescription
stableMost reliable, updated every 6 weeks.
betaNext release candidate for stable. Used for testing.
nightlyUpdated 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

ConstantStatic
SCREAMING_SNAKE_CASESCREAMING_SNAKE_CASE
Explicit type annotationExplicit type annotation
Can not be mutatedCan be marked as mutated
Value of it will be inlined at compile timeIt 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

CategoryOperatorsDescriptionExample
Arithmetic+, -, *, /, %Basic math operations5 + 3, 10 % 4
Comparison==, !=, <, >, <=, >=Compare values, return boola == b, x < y
Logical&&, !Boolean logic operationstrue && false, !is_valid
Bitwise&, ^, <<, >>Bit-level operations on integersx & y, 1 << 3
Assignment=, +=, -=, *=, /=, %=, etc.Assign and update valuescount += 1, x = 42
Reference&Borrowing (create reference)let r = &value;
Dereference*Follow a reference to access valuelet x = *ptr;
Range.., ..=Range expressions (exclusive/inclusive)for i in 0..5 {}, 1..=3
Error Propagation?Return early if Result::Err or Option::Nonelet v = might_fail()?;
CastingasType conversionlet x = y as f64;

1.3 Functions, Control Flow, Comments

ℹ️ Rust coding rules suggestions:

Item TypeCase StyleExample
Project Namesnake_case or kebab-casemy_project or my-project
Variablessnake_casemy_variable
Functionssnake_casecalculate_sum()
StructsPascalCaseMyStruct
EnumsPascalCaseMyEnum
Enum VariantsPascalCaseSomeVariant
ConstantsSCREAMING_SNAKE_CASEMAX_LIMIT
StaticsSCREAMING_SNAKE_CASEMAX_LIMIT
Modulessnake_casemy_module
Cratessnake_casemy_crate
TraitsPascalCaseDisplay
Type AliasesPascalCaseMyType
Lifetimeslowercase 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

  1. Basic generic function:

    fn identity<T>(value: T) -> T {
        value
    }
  2. Generic with constraints (trait bounds)

    fn print_value<T: std::fmt::Display>(value: T) {
        println!("{}", value);
    }
  3. Multiple generic parameters

     fn pair<T, U>(a: T, b: U) {
        // do something
    }
  4. With return type and trait bounds

    fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
        a + b
    }
  5. 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

SyntaxDescription
if / else if / elseBranches execution based on a condition
matchPattern 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

SyntaxDescription
loopInfinite loop (exit with break)
whileLoop while a condition is true
forLoop 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

SyntaxDescription
breakExit the loop
continueSkip the rest of current loop iteration
break 'labelBreak 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 TypeSyntaxPurpose
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

  1. 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
    }
  2. 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,
    }
  3. 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
    }
  4. 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

  1. Each value in Rust has a variable that is called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

Which Problems Ownership solves?

  1. Memory/Resource Leaks
  2. Double free
  3. 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

  1. At any given time, you can have either one mutable reference or any number of immutable reference.
  2. References must always be valid.

Which problems borrowing solves

  1. Data races.
  2. 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:

  1. Immutable borrowing of self: It borrows self as immutable
  2. Mutable borrowing of self: It borrows self as mutable. So, we can change the value of the attributes.
  3. Self owned: It moves ownership to the member function.
  4. 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:

  1. It should cover all branches.
  2. _ can be used to cover rest of branches.
  3. 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 as Rc::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 operations
    • std::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:

  1. The config crate makes it very ergonomic to load and parse configuration files (like YAML, TOML, JSON, etc.)
  2. thiserror crate is used at the library side to define custom error type
  3. anyhow 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:

LayerUseWhy
LibrarythiserrorDefine and expose structured error types
App/BinaryanyhowConsume 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 environment
  • FnMut: 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.
  • 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 ThreadsAsync Tasks
Managed by the OSManaged by language runtime or library
Preemptive schedulingCooperative Scheduling
Higher performance overheadLower performance overhead
Ideal for small amount of CPU-bound workloadsIdeal for large amount of IO-bound workloads
Harder to reason aboutEasier 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

Featurestd::thread::spawntokio::spawn
Runtime needed❌ No✅ Yes (Tokio runtime)
Blocking✅ Yes❌ No (non-blocking)
Stack sizeLarge (OS thread)Small (uses async stack)
Use forCPU-heavy, blocking workIO-heavy, concurrent async tasks
Return typeJoinHandle<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:

SpecifierMatches
blockA block of code, e.g., { ... }
exprAn expression, e.g., 1 + 2, foo(), or if x { y } else { z }
expr_2021An expression excluding const blocks and underscore expressions (for backward compatibility)
identAn identifier, e.g., foo, bar
itemAn item, such as a function, struct, or module declaration
lifetimeA lifetime, e.g., 'a
literalA literal value, e.g., 42, "hello"
metaA meta item, typically used in attributes, e.g., cfg(...)
patA pattern, e.g., Some(x), Ok(_)
pat_paramA pattern without top-level alternation, used in function parameters
pathA path, e.g., std::io::Result
stmtA statement, e.g., let x = 5;
ttA single token tree
tyA type, e.g., i32, Vec<T>
visA 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, and darling 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.
    1. Dereference a raw pointer
    2. Call an unsafe function
    3. Implement an unsafe trait
    4. Access/Modify a mutable static variable
    5. 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 use mod <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);
}
TypeKeyword or syntaxUsage Example
Immutable bindinglet x = 5;Can't change later
Mutable bindinglet mut x = 5;Changeable
Pattern bindinglet (a, b) = (1, 2);Destructuring tuples
Reference bindinglet r = &val;Borrowing references
Shadowing bindinglet 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() and min(): 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.

  1. Initial value: Vec::new() is the starting value for acc. Here it creates acc: Vec<&i32>
  2. acc is the accumulator which caries the result so for
  3. number 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;
}