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:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shThis will install rust in $HOME/.cargo/bin or %USERPROFILE%\.cargo\bin.
Uninstall, update or validate the installation
# Uninstall Rustrustup self uninstall
# Update Rustrustup update
# Validate installationrustc --versioncargo --versionrustdoc --versionToolchain #
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
fn main(){ println! ("Hello World");}rustc main.rs./mainrustdoc #
The HTML documentation generator from doc comments (/// for items and //! for modules/crates) into ./doc/ directory.
rustdoc src/lib.rsUsing rustdoc directly
/// Adds two numbers and returns the result.////// Returns `a + b` as an `i32`.pub fn add(a: i32, b: i32) -> i32 { a + b}rustdoc src/lib.rscargo #
Build system and package manager. Used to create projects, resolve dependencies, compile and execute Rust.
new #
Create a new cargo project.
cargo new my_project # To create a binary project (default)cargo new my_lib --lib # To create a library projectProject 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.
cargo checkbuild #
Compile a project with all its dependencies. (invokes rustc with flags)
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>]:
| Profile | Triggered by | Optimization | Debug info |
|---|---|---|---|
dev | cargo build | None | Yes (rust-gdb, rust-lldb, debug_assert) |
test | cargo build --profile test | None | Yes, plus include test attributes |
release | cargo build --release | Full (opt-level=3) | No |
bench | cargo build --profile bench | Full (opt-level=3) | No, but include bench attributes |
run #
Build and then execute the resulting binary (invokes rustc with flags and executes).
cargo run # Build + run (default dev build profile)Run with other build 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).
cargo doc --open # Build docs and open in browsercargo doc --no-deps # Only generate your crate docs, skip dependenciescrates #
Dependencies are defined in Cargo.toml and fetched from crates.io.
cargo install dependency_name # Install crate globally into ~/.cargo/bincargo install --list # Display globally installed cratescargo uninstall dependency_name # Uninstall crate globallycargo add dependency_name # Fetch and add latest version to projectcargo update -p dependency_name # Update crate to latest compatible vercargo remove dependency_name # Remove crate from projectCommonly used dependency commands
cargo update # Update all dependencies within semver constraintscargo clean # Remove all build artifacts and dependency buildscargo add dependency_name@1.0 # Add specific crate versioncargo add dependency_name --features derive # Add with feature flagscargo add --dev dependency_name # Add crate as a dev dependencycargo add --build dependency_namecargo tree # Full dependency tree with versionscargo tree -i dependency_name # Reverse: who pulls in dependency_name?cargo tree --duplicates # Find multiple versions of the same cratecargo search dependency_name # Search for specific cratecargo 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:
[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, 2024authors = ["Name <me@example.com>"] # Author name and emaildescription = "A short blurb" # Required for publishing to crates.iolicense = "MIT" # SPDX expressionrepository = "https://github.com/user/repo" # Git repositoryreadme = "README.md" # Readme filekeywords = ["cli", "parser"] # Max 5, for crates.io searchcategories = ["command-line-utilities"] # creates.io package categoryrust-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 #
[dependencies]is whatsrc/imports. Ships in the final binary or library. e.g. serde, tokio, clap, anyhow.[dev-dependencies]is whattests/,benches/,examples/, and#[cfg(test)]blocks use. Stripped from the published crate. e.g. criterion, proptest, mockall, assert_cmd.[build-dependencies]is whatbuild.rsuses to do code generation, native compilation, or linker setup before your crate is built. Never touched bysrc/. 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-prefixeddep_name = "0" # >=0.0.0, <1.0.0dep_name = "1" # >=1.0.0, <2.0.0dep_name = "~1" # >=1.0.0, <2.0.0dep_name = "1.*" # >=1.0.0, <2.0.0dep_name = "1.2" # >=1.2.0, <2.0.0dep_name = "1.2.3" # >=1.2.3, <2.0.0# For 0.x minor instead of major bumb is breakingdep_name = "0.0" # >=0.0.0, <0.1.0dep_name = "0.5" # >=0.5.0, <0.6.0dep_name = "0.5.3" # >=0.5.3, <0.6.0dep_name = "~1.2" # >=1.2.0, <1.3.0dep_name = "1.2.*" # >=1.2.0, <1.3.0# For 0.0.x patch instead of minor bumb is breakingdep_name = "0.0.0" # >=0.0.0, <0.0.1dep_name = "0.0.5" # >=0.0.5, <0.0.6dep_name = "~1.2.3" # >=1.2.3, <1.2.4# Wildcards are best avoideddep_name = "*" # Any version - blocked by crates.io# Comparisons are more loosedep_name = ">=1.2.3" # At least 1.2.3, no upper bounddep_name = ">1.2.3" # Greater than 1.2.3, no upper bounddep_name = "<1.2.3" # Less than 1.2.3, no lower bounddep_name = "<=1.2.3" # At most 1.2.3, no lower bounddep_name = "=1.2.3" # Exactly 1.2.3, disables updatesdep_name = ">=1.2, <1.5" # 1.2.0 to 1.4.xdep_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 lowerdep_name = "1.0.0-beta.2" # Opt into 1.0.0-beta linedep_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 optionsdep_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 pathdep_name = { version = "1", git = "https://github.com/usr/repo", branch = "main"} # Fetched from git repo branchdep_name = { version = "1", git = "https://github.com/usr/repo", rev = "abc123"} # Fetched from git repo commitdep_name = { version = "1", optional = true} # Fetched async only when enabled by a featureFeature #
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_dependencyasync = ["dep:async_dependency"] # If async is enabled it pulls async_dependencyfull = ["json, async"] # If full is enabled it enables json and asynccargo build --no-default-featurescargo build --features asynccargo build --features jsoncargo build --features fullcargo build --all-featuresProfile #
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 optimizationlto = "fat" # Link-time optimizations - off, thin, fatcodegen-units = 1 # Lower equals better optimizations and slower compilestrip = "symbols" # Strip debug symbolspanic = "abort" # Smaller binary with no unwindingTarget #
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.rspath = "src/bin/cli.rs"[[example]] # Build demo from examples/demo.rsname = "demo"[[bench]] # Build bench from benches/perf.rsname = "perf"harness = false # Also disable, nightly built-in bench harnessWorkspace #
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 onceserde = "1.0"tokio = { version = "1.0", features = ["full"] }Then each member crate just inherits:
[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.
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.
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.
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.
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).
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 to . Unsigned go from to . Signed are stored using two’s complement representation.
| Bit Width | Signed | Unsigned | Signed Range | Unsigned Range |
|---|---|---|---|---|
| 8 | i8 | u8 | −128 to 127 | 0 to 255 |
| 16 | i16 | u16 | −32,768 to 32,767 | 0 to 65,535 |
| 32 (default) | i32 | u32 | −2,147,483,648 to 2,147,483,647 | 0 to 4,294,967,295 |
| 64 | i64 | u64 | −(2^63) to 2^63 − 1 | 0 to 2^64 − 1 |
| 128 | i128 | u128 | −(2^127) to 2^127 − 1 | 0 to 2^128 − 1 |
| arch-dependent | isize | usize | matches 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.).
fn main() { let max_u8 = u8::MAX; let wrapped = max_u8.wrapping_add(1); println!("Wrapped: {}", wrapped); // Outputs Wrapped: 0}| Method | Behavior | Return Type | Example: u8::MAX.X_add(1) | When to Use | Wrapper Notation |
|---|---|---|---|---|---|
wrapping_* | Wraps around via two’s complement (Default) | T (the wrapped value) | 0 | For 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> | None | Overflow 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 wrapping | T (clamped value) | 255 | For progress bars, audio volume, percentages, sensor readings | Saturating(255u8) + Saturating(1) = Saturating(255u8) |
Scalar #
| Type | Annotation | Size | Range / Valid Values | Default | Signed? | Key Notes |
|---|---|---|---|---|---|---|
| Integer | i8-i128,u8-u128,isize,usize | 1–16 bytes (arch isize/usize) | See integer table above | i32 | i=yes, u=no | Two’s complement. |
| Floating-point | f32, f64 | 4 or 8 bytes | IEEE-754 single (≈7 decimal digits) / double (≈15–17 decimal digits) precision | f64 | always signed | f64 is roughly the same speed as f32 on modern CPUs. |
| Boolean | bool | 1 byte | true or false | — | n/a | Used in conditionals and logic. |
| Character | char | 4 bytes | Unicode scalar: U+0000–U+D7FF and U+E000–U+10FFFF | — | n/a | Single quotes: 'A', '🦀', '\u\{2A\}'. |
Compound #
| Type | Syntax | Type Annotation | Length | Element Access | Element Types | Memory | Key Notes |
|---|---|---|---|---|---|---|---|
| Tuple | (v1, v2, ...) | (T1, T2, ...) | Fixed at declaration | Period + index: tup.0 | Mixed types allowed | Stack | Empty () 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 time | Brackets: arr[0] | All elements share one type | Stack (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).
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:
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.
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 ;.
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.
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.
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.
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 (_).
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.
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.
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.
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:.
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:
- f32 is the default for floats = false, it is f64
- Doing
let a = 5;the data type of x is i8 = false, it is i32 - Floats are compound data types = false, it is scalar
- Statements do not produce a result = true, statements evaluate to ().
- 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"));