Rust

Rust programming language

Start

Install

For Windows go to Rust install page and download the installer. For Unix systems install by downloading the rustup utility:

Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

This will install rust in $HOME/.cargo/bin or %USERPROFILE%\.cargo\bin.

💡Uninstall, update or validate the installation
Terminal window
# Uninstall Rust
rustup self uninstall
# Update Rust
rustup update
# Validate installation
rustc --version
cargo --version
rustdoc --version

Toolchain

The installation tool rustup installs itself and three core tools: rustc, rustdoc and cargo. Rust is designed to be built and executed using cargo (it wraps the other two).

rustc

The compiler, it takes .rs files and produces native executables.

📌Using rustc directly
main.rs
fn main(){
println! ("Hello World");
}
Compile Rust
rustc main.rs
./main

rustdoc

The HTML documentation generator from doc comments (/// for items and //! for modules/crates) into ./doc/ directory.

Generate docs
rustdoc src/lib.rs
📌Using rustdoc directly
lib.rs
/// Adds two numbers and returns the result.
///
/// Returns `a + b` as an `i32`.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Generate docs
rustdoc src/lib.rs

cargo

Build system and package manager. Used to create projects, resolve dependencies, compile and execute Rust.

new

Create a new cargo project.

New project
cargo new my_project # To create a binary project (default)
cargo new my_lib --lib # To create a library project
💡Project structure generated by cargo new
my_project/
├── Cargo.toml # Manifest: name, version, dependencies
├── cargo.lock # Locked dependency versions for build
├── src/
│ └── main.rs # Entry point (lib.rs for libraries)
└── target/ # Build artifacts (generated on build)

check

Check if the project code will compile.

Check project code
cargo check

build

Compile a project with all its dependencies. (invokes rustc with flags)

Build
cargo build # Debug build → target/debug/
cargo build --release # Optimized → target/release/

Resolves Cargo.toml crates and updates the Cargo.lock file, then invokes rustc with a set of flags placing the output in target/<profile>.

💡Build profiles

Cargo has four built-in profiles, each configurable in Cargo.toml under [profile.<name>]:

ProfileTriggered byOptimizationDebug info
devcargo buildNoneYes (rust-gdb, rust-lldb, debug_assert)
testcargo build --profile testNoneYes, plus include test attributes
releasecargo build --releaseFull (opt-level=3)No
benchcargo build --profile benchFull (opt-level=3)No, but include bench attributes

run

Build and then execute the resulting binary (invokes rustc with flags and executes).

Run
cargo run # Build + run (default dev build profile)
💡Run with other build profiles
Run with release, test and bench profiles
cargo run --release # Build + run (release profile)
cargo test # Build + run with test profile (runs all tests)
cargo bench # Build + run with bench profile (runs all benchmarks)

Test and bench execute the functions with #[test] and #[bench] attributes

doc

Create the HTML documentation (invokes rustdoc).

Generate docs
cargo doc --open # Build docs and open in browser
cargo doc --no-deps # Only generate your crate docs, skip dependencies

crates

Dependencies are defined in Cargo.toml and fetched from crates.io.

Add, update, remove dependency
cargo install dependency_name # Install crate globally into ~/.cargo/bin
cargo install --list # Display globally installed crates
cargo uninstall dependency_name # Uninstall crate globally
cargo add dependency_name # Fetch and add latest version to project
cargo update -p dependency_name # Update crate to latest compatible ver
cargo remove dependency_name # Remove crate from project
💡Commonly used dependency commands
Terminal window
cargo update # Update all dependencies within semver constraints
cargo clean # Remove all build artifacts and dependency builds
cargo add dependency_name@1.0 # Add specific crate version
cargo add dependency_name --features derive # Add with feature flags
cargo add --dev dependency_name # Add crate as a dev dependency
cargo add --build dependency_name
cargo tree # Full dependency tree with versions
cargo tree -i dependency_name # Reverse: who pulls in dependency_name?
cargo tree --duplicates # Find multiple versions of the same crate
cargo search dependency_name # Search for specific crate
cargo outdated # Check for outdated crates (requires cargo-outdated)
cargo audit # Audit crates against CVEs (requires cargo-audit)

Cargo.toml

The manifest declares package metadata, dependencies, targets, features and build configuration.

Metadata

The mimimum required fields are name, version and edition:

Cargo.toml
[package]
name = "my_project"
version = "0.1.0"
edition = "2024"
💡Common metadata fields
[package]
name = "my_project" # crates.io name (required, must be unique)
version = "0.1.0" # semver (required)
edition = "2024" # Rust edition: 2015, 2018, 2021, 2024
authors = ["Name <me@example.com>"] # Author name and email
description = "A short blurb" # Required for publishing to crates.io
license = "MIT" # SPDX expression
repository = "https://github.com/user/repo" # Git repository
readme = "README.md" # Readme file
keywords = ["cli", "parser"] # Max 5, for crates.io search
categories = ["command-line-utilities"] # creates.io package category
rust-version = "1.75" # Minimum Supported Rust Version (MSRV)

Dependencies

Dependencies must have a context and a semver requirement. They can be fetched from crates.io, a git repo or a local path. They can have specific feature flags enabled.

Context

  1. [dependencies] is what src/ imports. Ships in the final binary or library. e.g. serde, tokio, clap, anyhow.
  2. [dev-dependencies] is what tests/, benches/, examples/, and #[cfg(test)] blocks use. Stripped from the published crate. e.g. criterion, proptest, mockall, assert_cmd.
  3. [build-dependencies] is what build.rs uses to do code generation, native compilation, or linker setup before your crate is built. Never touched by src/. e.g. cc, bindgen, prost-build.
[dependencies] # Used by src and release artifacts.
dependency_name = "1.0"
[dev-dependencies] # Used only by src for test, bench and example artifacts.
dependency_name = "1.0"
[build-dependencies] # Not used by artifacts, used by buils.rs at compile time.
dependency_name = "1.0"

Semver

All dependencies require a semver requirement string. Cargo updates dependencies only within the range specified by the semver string, by default jumps between major versions 1.x -> 2.x have to be added explicitly with cargo add.

# Plain semver string are implicitly caret-prefixed
dep_name = "0" # >=0.0.0, <1.0.0
dep_name = "1" # >=1.0.0, <2.0.0
dep_name = "~1" # >=1.0.0, <2.0.0
dep_name = "1.*" # >=1.0.0, <2.0.0
dep_name = "1.2" # >=1.2.0, <2.0.0
dep_name = "1.2.3" # >=1.2.3, <2.0.0
# For 0.x minor instead of major bumb is breaking
dep_name = "0.0" # >=0.0.0, <0.1.0
dep_name = "0.5" # >=0.5.0, <0.6.0
dep_name = "0.5.3" # >=0.5.3, <0.6.0
dep_name = "~1.2" # >=1.2.0, <1.3.0
dep_name = "1.2.*" # >=1.2.0, <1.3.0
# For 0.0.x patch instead of minor bumb is breaking
dep_name = "0.0.0" # >=0.0.0, <0.0.1
dep_name = "0.0.5" # >=0.0.5, <0.0.6
dep_name = "~1.2.3" # >=1.2.3, <1.2.4
# Wildcards are best avoided
dep_name = "*" # Any version - blocked by crates.io
# Comparisons are more loose
dep_name = ">=1.2.3" # At least 1.2.3, no upper bound
dep_name = ">1.2.3" # Greater than 1.2.3, no upper bound
dep_name = "<1.2.3" # Less than 1.2.3, no lower bound
dep_name = "<=1.2.3" # At most 1.2.3, no lower bound
dep_name = "=1.2.3" # Exactly 1.2.3, disables updates
dep_name = ">=1.2, <1.5" # 1.2.0 to 1.4.x
dep_name = ">1.0, <=1.4.5" # 0.x.x to 1.4.5
# Pre-releases are outside the normal range, this means that "<1"
# does not include 0.0.1-beta even though technically it is lower
dep_name = "1.0.0-beta.2" # Opt into 1.0.0-beta line
dep_name = ">=1.0.0-alpha" # Opt into 1.0.0 pre-releases and stable forward
# Inline table explicit form
# This notation is used to add features, optional, path, git and other options
dep_name = { version = "1", features = ["full"], optional=true }

Source

We can specify the source from which we fetch the crates.

[dependencies]
dep_name = "1" # Fetched from crates.io (default)
dep_name = { version = "1", path = "../my_utils"} # Fetched from local path
dep_name = { version = "1", git = "https://github.com/usr/repo", branch = "main"} # Fetched from git repo branch
dep_name = { version = "1", git = "https://github.com/usr/repo", rev = "abc123"} # Fetched from git repo commit
dep_name = { version = "1", optional = true} # Fetched async only when enabled by a feature

Feature

Feature are opt-in compile flags that toggle extra code paths or pull optional dependencies.

[dependencies]
json_dependency = { version = "1", optional = true }
async_dependency = { version = "1", optional = true }
[features]
default = ["json"] # Always enabled (unless --no-default-features is set)
json = ["dep:json_dependency"] # If json is enabled it pulls json_dependency
async = ["dep:async_dependency"] # If async is enabled it pulls async_dependency
full = ["json, async"] # If full is enabled it enables json and async
Terminal window
cargo build --no-default-features
cargo build --features async
cargo build --features json
cargo build --features full
cargo build --all-features

Profile

You can override one of the compiler build profiles or create a custom one. This is mainly used to fine tune the release artifacts build process.

[profile.release]
opt-level = 3 # 0 to 3 levels of optimization
lto = "fat" # Link-time optimizations - off, thin, fat
codegen-units = 1 # Lower equals better optimizations and slower compile
strip = "symbols" # Strip debug symbols
panic = "abort" # Smaller binary with no unwinding

Target

The target is auto-detected by cargo but you can declare it explicitly. Note the double brackets since toml will treat it as an array of tables (a crate can produce multiple artifacts).

[[bin]]
name = "cli" # Build as cli artifact from cli.rs
path = "src/bin/cli.rs"
[[example]] # Build demo from examples/demo.rs
name = "demo"
[[bench]] # Build bench from benches/perf.rs
name = "perf"
harness = false # Also disable, nightly built-in bench harness

Workspace

Some projects contain multiple crates, in this case the root cargo.toml can act as a workspace root file so that all members share a single cargo.lock file and target/ directory. This improves build speeds and the project structure.

[workspace]
members = ["app", "core", "utils"]
resolver = "2" # Default in edition 2021+
[workspace.dependencies] # Shared dependencies, defined once
serde = "1.0"
tokio = { version = "1.0", features = ["full"] }

Then each member crate just inherits:

app/Cargo.toml
[dependencies]
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true }

Fundamentals

Variables

Variables are named storage locations that map a symbolic name to a value in memory, the name is replaced with the memory location at compile time. Declared with let, immutable by default; opt into mutability with mut.

main.rs - variables
fn main(){
let x = 2;
println! ("x = {}", x);
let mut y = 2;
y = 3;
println! ("y = {}", y);
}

Shadowing

Rebinding a variable name with a new let shadows the previous binding. Unlike mut, shadowing creates a fresh binding and therefore can change the type.

main.rs - shadowing
rustfn main() {
let x = "5"; // &str
let x = x.parse::<i32>().unwrap(); // Shadowing the string into i32
let x = x + 1; // i32, value = 6
println!("x = {}", x);
}

Constants

Use const to declare a compile-time constant. Constants must be annotated, their values must be a constant expression and they conventionally use SCREAMING_SNAKE_CASE. They are inlined when used, meaning they don’t necessarily have a fixed memory address.

main.rs - constants
rustconst MAX_USERS: u32 = 100_000;

Use static to declare a value with a fixed memory address. They are usually used to define the global state of the program, creating a mutable static requires an unsafe memory access point.

main.rs - statics
ruststatic APP_NAME: &str = "my-app";

Data Types

Rust is statically typed, data types must be known at compile time. The type can be inferred or explicitly annotated with a colon. Variables can be declared without a value as long as they are annotated (however they must be initialized before being read).

Data Types
fn main() {
let _int: i32 = 42;
println! ("Int = {}", _int);
let _float: f64 = 3.141;
let _boolean: bool = true;
let _character: char = 'R';
let _tuple: (i32, f64, char, bool) = (42, 3.141, 'R', true);
let _unit_tuple: () = ();
let _array: [i32; 5] = [1, 2, 3, 4, 5];
}

Data types can be broadly subdivided into scalar and compound data types.

Integers

Signed go from (2n1)-(2^{n-1}) to 2n112^{n-1}-1. Unsigned go from 00 to 2n12^n-1. Signed are stored using two’s complement representation.

Bit WidthSignedUnsignedSigned RangeUnsigned Range
8i8u8−128 to 1270 to 255
16i16u16−32,768 to 32,7670 to 65,535
32 (default)i32u32−2,147,483,648 to 2,147,483,6470 to 4,294,967,295
64i64u64−(2^63) to 2^63 − 10 to 2^64 − 1
128i128u128−(2^127) to 2^127 − 10 to 2^128 − 1
arch-dependentisizeusizematches pointer width (32 or 64 bit)pointer width

The primary use for isize/usize is indexing collections. Literals accept _ as a visual separator and type suffixes: Hex as 0xff, octal as 0o77, binary as 0b11110000 or byte as b'A' (u8 only). Dev and test artifacts include an overflow panic check, however release artifacts wrap overflows via two’s complement.

Overflow

In debug (dev, test) normal arithmetic operators + - * / will panic when overflown but silently wrap in release (releease, bench). If you expect overflow to happen you can explicitly handle it by calling an overflow method and the intended arithmetic operator (_add, _sub, _mul, _div, _neg, _rem, _pow, _shl, _shr, etc.).

main.rs
fn main() {
let max_u8 = u8::MAX;
let wrapped = max_u8.wrapping_add(1);
println!("Wrapped: {}", wrapped); // Outputs Wrapped: 0
}
MethodBehaviorReturn TypeExample: u8::MAX.X_add(1)When to UseWrapper Notation
wrapping_*Wraps around via two’s complement (Default)T (the wrapped value)0For cyclic operations, hashes, ring buffers, low-level bit manipulation, crypto.Wrapping(255u8) + Wrapping(1) = Wrapping(0)
checked_*Returns None if it would overflow; otherwise Some(result)Option<T>NoneOverflow is an error condition that must be handled. For input validation or math.Checked(255u8) + Checked(1) = None
overflowing_*Returns the wrapped result including a flag indicating whether overflow occurred(T, bool)(0, true)For bignum arithmetic, carry-chain logic, or cases where you want to continue.Overflowing(255u8) + Overflowing(1) = Overflowing(0, true)
saturating_*Clamps at the type’s MIN or MAX instead of wrappingT (clamped value)255For progress bars, audio volume, percentages, sensor readingsSaturating(255u8) + Saturating(1) = Saturating(255u8)

Scalar

TypeAnnotationSizeRange / Valid ValuesDefaultSigned?Key Notes
Integeri8-i128,u8-u128,isize,usize1–16 bytes (arch isize/usize)See integer table abovei32i=yes, u=noTwo’s complement.
Floating-pointf32, f644 or 8 bytesIEEE-754 single (≈7 decimal digits) / double (≈15–17 decimal digits) precisionf64always signedf64 is roughly the same speed as f32 on modern CPUs.
Booleanbool1 bytetrue or falsen/aUsed in conditionals and logic.
Characterchar4 bytesUnicode scalar: U+0000U+D7FF and U+E000U+10FFFFn/aSingle quotes: 'A', '🦀', '\u\{2A\}'.

Compound

TypeSyntaxType AnnotationLengthElement AccessElement TypesMemoryKey Notes
Tuple(v1, v2, ...)(T1, T2, ...)Fixed at declarationPeriod + index: tup.0Mixed types allowedStackEmpty () is the implicit return for no value expressions. Mutable ones can change values but not their type or size.
Array[v1, v2, ...] or [v; N] (repeat)[T; N]Fixed at compile timeBrackets: arr[0]All elements share one typeStack (stored sequentially)Rust panics in both compile and runtime if try to access an element out of bounds. Use Vec<T> for growable heap-allocated collection.

Casting

Rust never coerces between types implicitly, type casting is usually done explicitly with as. This defaults to truncating or wrapping on narrowing conversions (e.g i32 -> i8).

main.rs
fn main() {
let a: i32 = 5;
let b: f64 = 2.5;
let sum = a as f64 + b; // 7.5
println!("{}", sum);
}

For safe, fallible conversions use TryFrom and TryInto, for lossless conversions use From and Into.

Comments

Single-line comments use double forward slash while multi-line comments use a comment block:

main.rs
fn main() {
// This is a single-line comment
/* This is a multi-line
comment
*/
}

Functions

Functions organize code into reusable units. main() is the entry point of every Rust program. Declared with fn, followed by name, parameters with explicit types, optional return type, and a body. The convention is to use snake_case for the function name and parameters.

main.rs - functions
fn function_name(arg1:type, arg2:type,...) -> ReturnType{
// Body1
}

Statement, Expression

A function’s body consists of statements and or expressions. Statements perform an action and return a unit touple (meaning no value), they end with a ;. Expressions evaluate to a value they return, they have no trailing ;.

main.rs - statements and expressions
fn main() {
let x = 5; // statement does not return a value (simply binds to 5)
let y = { x + 1 }; // the expression (x+1) does evaluates to a value (6)
println!("{}", y);
}

Return Single

You can annotate a function’s return type with -> and return a value explicitly with return or implicitly by ending the function’s body with an expression.

main.rs - return a value
fn sum(x: i16, y: i16) -> i16 {
x + y // no `;` so it implicitly returns this expression's value
}
fn abs(x: i32) -> i32 {
if x < 0 { return -x; } // early explicit return
x
}
fn main() {
println!("{}", sum(2, 3)); // 5
println!("{}", abs(-7)); // 7
}

Return Multiple

To return multiple values at the same time we can use a tuple and then destructure it at the call site.

main.rs - return multiple values
fn min_max(a: i32, b: i32) -> (i32, i32) {
if a < b { (a, b) } else { (b, a) }
}
fn main() {
let (lo, hi) = min_max(7, 3);
println!("lo={}, hi={}", lo, hi); // lo=3, hi=7
}

Control Flow

Control flow tools allow us to execute code based on certain conditions or execute repeating code.

If - Else

We use if to conditionally execute code based on an explicit boolean condition (Rust does not coerce integers, options or similar into truthy-falsy).

For mutually exclusive blocks we can use if - else expressions and for multiple non-overlapping conditions we can use if - else if expressions. if expression branches that implicitly return a value of the same data type can be assigned into a variable.

main.rs - conditionals
fn main() {
let a = 10;
if a == 10 {
println!("a is 10");
} else {
println!("a is not 10");
}
let grade = 67;
let letter = if grade > 90 { 'A' }
else if grade > 80 { 'B' }
else if grade > 70 { 'C' }
else if grade > 60 { 'D' }
else { 'F' };
println!("Grade: {}", letter);
}

Match

match is an alternative to if - else if expression branches that compare a value against a pattern and return a value, match branches must be exhaustive meaning that they cover every possible value. The pattern can be a literal (0), an OR pipe pattern (1|2), a range (3..=9) or any (_).

main.rs - conditionals
fn main() {
let n = 3;
let label = match n {
0 => "zero",
1 | 2 => "small",
3..=9 => "medium",
_ => "large",
};
println!("{}", label);
}

Loops

We can repeatidly execute a block of code with a loop (infinite), while (condition-driven) or a for (iterator-driven) loop method.

Loop

Runs until it encounters a break, else it runs indefinitely. You can pass a value to break in order to return it from the loop.

main.rs - loop
fn main() {
let mut a = 5;
let final_a = loop {
a -= 1;
if a == 2 { break a; }
};
println!("Loop result = {}", final_a); // 2
}

While

Runs until a condition goes from true to false.

main.rs - while
fn main() {
let mut b = 5;
while b > 2 {
b -= 1;
println!("b = {}", b); // 5,4,3
}
}

For

Runs until it finishes iterating over the elements of a collection. A for loop can be considered a while loop with a counter but it is optimized for this use case.

main.rs - for
fn main() {
for i in 0..3 { println!("{}", i); } // 0, 1, 2
for i in 0..=3 { println!("{}", i); } // 0, 1, 2, 3
let values = [1, 2, 3, 4];
for v in &values {
println!("value = {}", v); // 1,2,3,4
}
}

Break, Continue, Labels

To exit the innermost loop use break. To skip the next iteration use continue. To target a specific loop within nested loops you can add a label to it 'loop_name:.

break, continue, labels
fn main() {
'outer: for i in 0..5 {
for j in 0..5 {
if i * j > 6 { break 'outer; }
println!("{}, {}", i, j);
}
}
}

Questions

Does this compile? If not, why and how to correct?

fn main() {
let a=2;
let b=3.0;
println!("Sum of a and b = {}", a+b);
}
Answer

No since a is inferred as i32 and b as f64. Fix with an annotation let a:f64=2.0; or a cast let sum=a as f64+b;.

You have an array of 4 floats, implement find_mean()

fn main() {
let ar = [2.5, 3.0, 4.5, 2.0];
println!("Mean of ar = {}", find_mean(ar));
}
Answer
fn find_mean(ar: [f64; 4]) -> f64 {
let mut sum = 0.0;
for v in ar.iter() { sum += v; }
sum / ar.len() as f64
}

Write down the output of this program

fn main() {
let mut a:i8 = 125;
a = a+3;
println!("a={}", a);
}
Answer

i8 overflows so it depends on the build profile of the artifact. On debug it panics but on release it wraps via two’s complement (-128).

State if true or false:

  1. f32 is the default for floats = false, it is f64
  2. Doing let a = 5; the data type of x is i8 = false, it is i32
  3. Floats are compound data types = false, it is scalar
  4. Statements do not produce a result = true, statements evaluate to ().
  5. What is shadowing = rebinding an existing name, creates a fresh bind

Ownership

Structs

Packages

Errors

Generics

Files

Text

Concurrency

Input-Output

Terminals

Signals

Databases

Networks

Unsafe

Foreign

Embedded

Web

Rhai

Egui

GUI libary for rust that runs natively or on the web, also available as a libary on many game engines. It is a very simple, easy to use GUI library. It uses the Eframe framework meaning it supports Linux, Mac, Windows, Android and Web.

ui.heading("My egui Application");
ui.horizontal(|ui| {
ui.label("Your name: ");
ui.text_edit_singleline(&mut name);
});
ui.add(egui::Slider::new(&mut age, 0..=120).text("age"));
if ui.button("Increment").clicked() {
age += 1;
}
ui.label(format!("Hello '{name}', age {age}"));
ui.image(egui::include_image!("ferris.png"));

https://docs.rs/egui.