Compare

Lang Compare

Rust

Systems

Overview

Systems programming language focused on safety, speed, and concurrency. Guarantees memory safety without a garbage collector.

Resources

Popular learning and reference links:

Installation & Getting Started

Install via rustup, the official Rust toolchain installer. It manages Rust versions, components, and targets.

# Install Rust via rustup (recommended)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# macOS (Homebrew — not recommended, use rustup)
brew install rust

# Verify installation
rustc --version
cargo --version

# Update Rust
rustup update

# Install a specific toolchain
rustup install nightly
rustup default stable
# No built-in REPL — but evcxr is a popular Rust REPL
cargo install evcxr_repl
evcxr

# Rust Playground — try in the browser
# https://play.rust-lang.org/

# Run a file directly
rustc hello.rs && ./hello

# Run via Cargo (preferred)
cargo run

# Quick script with cargo-script
cargo install cargo-script
cargo script hello.rs

Project Scaffolding

Cargo creates the full project structure with one command.

# Create a new binary project
cargo new my-project
cd my-project

# Create a library project
cargo new my-lib --lib

# Project structure created:
# my-project/
#   Cargo.toml
#   src/
#     main.rs  (or lib.rs)

# Run the project
cargo run

# Build for release
cargo build --release

# Run tests
cargo test

Package Management

Cargo is Rust’s built-in package manager and build tool. Packages are called “crates”.

# Add a dependency
cargo add serde

# Add with features
cargo add serde --features derive

# Add a dev dependency
cargo add --dev tokio-test

# Remove a dependency
cargo remove serde

# Update dependencies
cargo update

# Search for crates
cargo search json

Tooling & Formatter/Linter

Rust ships with rustfmt and clippy as official tools. The compiler itself is an excellent linter.

# rustfmt — official formatter
rustfmt src/main.rs
cargo fmt              # Format entire project
cargo fmt -- --check   # Check without modifying

# Clippy — official linter (hundreds of lints)
cargo clippy
cargo clippy -- -W clippy::pedantic  # Stricter lints
cargo clippy --fix     # Auto-fix suggestions

# cargo check — fast type/borrow checking (no codegen)
cargo check
# rustfmt.toml
max_width = 100
edition = "2021"
use_field_init_shorthand = true
# Clippy config in Cargo.toml
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
needless_pass_by_value = "allow"
# rust-analyzer — language server for all editors
# Provides inline errors, completions, refactoring,
# hover docs, and inlay type hints

Build & Compile Model

Ahead-of-time compiled to native binary via LLVM. Rust produces highly optimized machine code with zero-cost abstractions.

# Debug build (fast compile, slow runtime)
cargo build
./target/debug/myapp

# Release build (slow compile, fast runtime)
cargo build --release
./target/release/myapp

# Compile + run
cargo run
cargo run --release

# Cross-compilation
rustup target add x86_64-unknown-linux-musl
cargo build --target x86_64-unknown-linux-musl --release

# Check without building (fastest feedback)
cargo check

# Output: native binary, or .rlib/.so/.a for libraries

Execution model:

  • Source → HIR → MIR (borrow checking) → LLVM IR → Machine code
  • No runtime, no GC — zero-cost abstractions
  • Compile times are slower than Go/C due to deep optimization + borrow checker
  • Incremental compilation and sccache help with rebuild speed
  • Can target WASM: cargo build --target wasm32-unknown-unknown

Libraries & Frameworks

Rust’s ecosystem is centered on crates.io. Strong focus on performance, safety, and systems programming.

Web Frameworks - Actix Web, Axum, Rocket, Warp, Poem

Async Runtime - Tokio, async-std, smol

Serialization - serde, serde_json, bincode, postcard

Databases - SQLx, Diesel, SeaORM, rusqlite

CLI - clap, structopt, dialoguer, indicatif, ratatui (TUI)

Error Handling - anyhow, thiserror, eyre, miette

HTTP Clients - reqwest, hyper, ureq

Logging - tracing, log, env_logger

Concurrency - rayon, crossbeam, tokio, parking_lot

Crypto - ring, rustls, rust-crypto

WASM - wasm-bindgen, wasm-pack, Leptos, Yew, Dioxus

Embedded - embedded-hal, embassy, probe-rs

Testing

Rust has built-in testing support — no external framework needed. Tests live alongside code or in a tests/ directory.

// Unit tests — in the same file as the code
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow() {
        let _: u8 = 255u8 + 1; // panics in debug mode
    }
}
// Integration tests — in tests/ directory
// tests/integration_test.rs
use my_crate::add;

#[test]
fn it_adds() {
    assert_eq!(add(2, 3), 5);
}
# Run tests
cargo test              # All tests
cargo test test_add     # Specific test
cargo test -- --nocapture  # Show println! output
cargo test --doc        # Doc tests only
cargo bench             # Benchmarks (nightly)

Debugging

Use LLDB or GDB for debugging. The compiler provides excellent error messages. dbg!() macro for quick inspection.

// dbg! macro — print expression + value + file:line
let x = 42;
dbg!(x);        // [src/main.rs:3] x = 42
dbg!(x * 2);    // [src/main.rs:4] x * 2 = 84

// println! for basic output
println!("value: {:?}", my_struct);   // Debug format
println!("value: {:#?}", my_struct);  // Pretty-print

// eprintln! — print to stderr
eprintln!("error: {}", msg);

// tracing — structured logging
use tracing::{info, warn, error, debug, instrument};

#[instrument]
fn process(id: u32) {
    info!(id, "processing");
    debug!(?some_value, "details");
    error!(%err, "failed");
}
# LLDB (default on macOS, works on Linux)
lldb target/debug/myapp
# b main.rs:42    (breakpoint)
# r                (run)
# n                (next)
# s                (step into)
# p variable       (print)
# bt               (backtrace)

# GDB
gdb target/debug/myapp
# Same commands but GDB syntax

# VS Code — CodeLLDB extension (recommended)
# launch.json:
{
  "type": "lldb",
  "request": "launch",
  "program": "${workspaceFolder}/target/debug/myapp",
  "args": [],
  "cwd": "${workspaceFolder}"
}
# Set breakpoints, inspect variables, view call stack

# Compiler errors are the first line of debugging
# Rust's error messages include:
# - Exact location, suggested fixes
# - Borrow checker explanations
# cargo clippy for additional warnings

Variables

Variables are immutable by default. Use mut for mutability.

// Immutable (default)
let name = "Alice";
let age = 30;

// Mutable
let mut count = 0;
count += 1;

// Type annotation
let x: i32 = 42;
let pi: f64 = 3.14;

// Shadowing (re-declare with same name)
let x = 5;
let x = x + 1; // x is now 6
let x = "now a string"; // different type

// Constants (must be typed, compile-time)
const MAX_SIZE: usize = 100;

Types

Statically typed with powerful enums, structs, traits, and generics.

// Primitives
let s: &str = "hello";
let n: i32 = 42;
let f: f64 = 3.14;
let b: bool = true;
let c: char = 'a';

// Structs
struct User {
    name: String,
    age: u32,
    email: Option<String>,
}

// Enums (algebraic data types)
enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
}

// Vectors and HashMaps
let nums: Vec<i32> = vec![1, 2, 3];
let mut map = HashMap::new();
map.insert("key", "value");

// Option and Result
let maybe: Option<i32> = Some(42);
let result: Result<i32, String> = Ok(42);

Data Structures

Rich standard library: Vec, HashMap, HashSet, BTreeMap, VecDeque, LinkedList. All generic and ownership-aware.

// Vec — growable array (most common)
let mut v = vec![1, 2, 3];
v.push(4);
v.pop();                // Some(4)
v[0];                   // 1
v.len();                // 3
v.contains(&2);         // true
let slice = &v[1..3];   // &[2, 3]

// HashMap
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("alice", 95);
map.insert("bob", 87);
map.get("alice");       // Some(&95)
map.contains_key("bob");
map.entry("charlie").or_insert(0);

// HashSet
use std::collections::HashSet;
let mut set: HashSet<i32> = [1, 2, 3].into();
set.insert(4);
set.contains(&2);      // true
let intersection = &set & &other_set;

// Iterators (lazy, chainable)
let sum: i32 = v.iter().filter(|&&x| x > 1).sum();
let doubled: Vec<_> = v.iter().map(|x| x * 2).collect();
// Tuples
let pair: (String, i32) = ("age".into(), 30);
let (key, value) = pair;

// BTreeMap — sorted keys
use std::collections::BTreeMap;
let mut bt = BTreeMap::new();
bt.insert(3, "c");
bt.insert(1, "a");
// Iteration is in key order

// VecDeque — double-ended queue
use std::collections::VecDeque;
let mut dq = VecDeque::from([1, 2, 3]);
dq.push_front(0);
dq.push_back(4);

Functions

Functions use fn. The last expression is the return value (no semicolon).

// Basic function
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

// Multiple parameters
fn add(a: i32, b: i32) -> i32 {
    a + b
}

// Closures
let add = |a: i32, b: i32| -> i32 { a + b };
let double = |x| x * 2;

// Generic function
fn first<T>(items: &[T]) -> Option<&T> {
    items.first()
}

// Methods (impl block)
impl User {
    fn new(name: String, age: u32) -> Self {
        Self { name, age, email: None }
    }

    fn display(&self) -> String {
        format!("{} ({})", self.name, self.age)
    }
}

Conditionals

if/else is an expression. match is powerful pattern matching.

// If / else (is an expression)
let label = if x > 0 {
    "positive"
} else if x == 0 {
    "zero"
} else {
    "negative"
};

// Match (exhaustive pattern matching)
match shape {
    Shape::Circle(r) => std::f64::consts::PI * r * r,
    Shape::Rectangle(w, h) => w * h,
}

// Match with guards
match age {
    0..=12 => "child",
    13..=17 => "teen",
    18..=64 => "adult",
    _ => "senior",
}

// If let (single pattern)
if let Some(value) = maybe_value {
    println!("Got: {}", value);
}

// Let-else
let Some(x) = optional else {
    return;
};

Loops

for, while, and loop (infinite). Iterators are idiomatic.

// For loop with range
for i in 0..5 {
    println!("{}", i);
}

// Iterate over a collection
let items = vec!["a", "b", "c"];
for item in &items {
    println!("{}", item);
}

// While loop
let mut n = 0;
while n < 3 {
    n += 1;
}

// Infinite loop with break
loop {
    break;
}

// Loop with return value
let result = loop {
    if condition {
        break 42;
    }
};

// Iterator methods
let squares: Vec<i32> = (0..10).map(|x| x * x).collect();
let sum: i32 = vec![1, 2, 3].iter().sum();

Generics & Type System

Powerful generics with trait bounds. Monomorphized at compile time (zero-cost). Supports associated types, lifetimes, and const generics.

// Generic function
fn identity<T>(value: T) -> T { value }

// Trait bounds — constrain type parameters
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut max = &list[0];
    for item in &list[1..] {
        if item > max { max = item; }
    }
    max
}

// Multiple bounds
fn print_debug<T: std::fmt::Debug + Clone>(item: T) {
    println!("{:?}", item.clone());
}

// Where clause (cleaner for complex bounds)
fn process<T, U>(t: T, u: U) -> String
where
    T: std::fmt::Display + Clone,
    U: std::fmt::Debug,
{
    format!("{} {:?}", t, u)
}
// Generic structs
struct Wrapper<T> {
    value: T,
}

impl<T: std::fmt::Display> Wrapper<T> {
    fn show(&self) { println!("{}", self.value); }
}

// Associated types (in traits)
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

// Lifetime generics
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Const generics
fn create_array<const N: usize>() -> [i32; N] {
    [0; N]
}
let arr = create_array::<5>(); // [0, 0, 0, 0, 0]

// Trait objects (dynamic dispatch)
fn print_all(items: &[&dyn std::fmt::Display]) {
    for item in items { println!("{}", item); }
}

Inheritance & Composition

Rust has no class inheritance. Uses traits for shared behavior, generics for polymorphism, and composition via struct fields.

// Traits (shared behavior)
trait Speak {
    fn speak(&self) -> String;
}

struct Dog { name: String }
impl Speak for Dog {
    fn speak(&self) -> String {
        format!("{} barks", self.name)
    }
}

// Default implementations
trait Greet {
    fn name(&self) -> &str;
    fn greet(&self) -> String {
        format!("Hello, {}!", self.name())
    }
}

// Trait inheritance (supertraits)
trait Animal: Speak + std::fmt::Display {}

// Composition via fields
struct PetOwner {
    pet: Dog,
    name: String,
}

// Generic bounds (static dispatch)
fn announce(speaker: &impl Speak) {
    println!("{}", speaker.speak());
}

// Trait objects (dynamic dispatch)
fn announce_dyn(speaker: &dyn Speak) {
    println!("{}", speaker.speak());
}

// Derive macros (auto-implement traits)
#[derive(Debug, Clone, PartialEq)]
struct Point { x: f64, y: f64 }

Functional Patterns

Iterators, closures, and Option/Result combinators are core to idiomatic Rust. Zero-cost abstractions make functional style performant.

// Closures
let double = |x: i32| x * 2;
let add = |a: i32, b: i32| a + b;

// Iterator combinators
let nums = vec![1, 2, 3, 4, 5];
let squared: Vec<i32> = nums.iter().map(|x| x * x).collect();
let evens: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();
let sum: i32 = nums.iter().sum();

// Chaining
let result: i32 = (0..100)
    .filter(|x| x % 2 == 0)
    .map(|x| x * x)
    .take(10)
    .sum();

// Option combinators
let name: Option<String> = Some("Alice".into());
let upper = name.map(|n| n.to_uppercase());
let len = name.and_then(|n| if n.is_empty() { None } else { Some(n.len()) });
let fallback = name.unwrap_or("default".into());

// Result combinators
let parsed: Result<i32, _> = "42".parse();
let doubled = parsed.map(|n| n * 2);
let chained = parsed.and_then(|n| if n > 0 { Ok(n) } else { Err("negative") });

// Higher-order functions
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(x) }

Concurrency

Fearless concurrency — the borrow checker prevents data races at compile time. Supports threads, async/await, and message passing.

// Threads
use std::thread;

let handle = thread::spawn(|| {
    println!("Hello from thread!");
});
handle.join().unwrap();

// Channels (mpsc — multiple producer, single consumer)
use std::sync::mpsc;

let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    tx.send("hello").unwrap();
});
println!("{}", rx.recv().unwrap());

// Shared state with Mutex + Arc
use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
    let counter = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        *counter.lock().unwrap() += 1;
    }));
}
for h in handles { h.join().unwrap(); }
// async/await (with Tokio runtime)
use tokio;

#[tokio::main]
async fn main() {
    let (a, b) = tokio::join!(
        fetch_data("url_a"),
        fetch_data("url_b"),
    );

    // Spawn concurrent tasks
    tokio::spawn(async { do_work().await });
}

// Rayon — data parallelism
use rayon::prelude::*;
let sum: i32 = (0..1000).into_par_iter().sum();

Modules & Imports

Rust uses a module tree rooted at lib.rs or main.rs. Modules can be inline or in separate files. Crates are the unit of compilation and distribution.

// Declare a module (looks for math.rs or math/mod.rs)
mod math;

// Use items from a module
use crate::math::add;
use crate::math::{add, subtract};

// Use with alias
use std::collections::HashMap as Map;

// Glob import (discouraged)
use std::io::prelude::*;

// Re-export
pub use crate::math::add;

// Nested paths
use std::{
    fs::File,
    io::{self, Read, Write},
};

// External crate (from Cargo.toml)
use serde::{Serialize, Deserialize};
use tokio::runtime::Runtime;

// Inline module
mod inner {
    pub fn helper() -> i32 { 42 }
}

// Visibility modifiers
pub fn public() {}           // public
pub(crate) fn crate_only() {} // crate-visible
fn private() {}               // module-private
pub(super) fn parent_only() {} // parent module

Error Handling

No exceptions. Uses Result<T, E> and Option<T> types with the ? operator.

// Result type
fn read_file(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?; // ? propagates error
    Ok(content)
}

// Handling Results
match result {
    Ok(value) => println!("Got: {}", value),
    Err(e) => eprintln!("Error: {}", e),
}

// unwrap (panics on error — use sparingly)
let value = result.unwrap();
let value = result.unwrap_or(default);
let value = result.unwrap_or_else(|e| handle(e));

// Custom error types
#[derive(Debug)]
enum AppError {
    NotFound(String),
    ParseError(std::num::ParseIntError),
}

impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self {
        AppError::ParseError(e)
    }
}

Memory Management

Ownership and borrowing — no GC. Memory is freed deterministically when the owner goes out of scope. The borrow checker enforces safety at compile time.

// Ownership — each value has exactly one owner
let s1 = String::from("hello");
let s2 = s1;        // s1 is MOVED to s2
// println!("{}", s1); // ERROR: s1 is no longer valid

// Clone — explicit deep copy
let s3 = s2.clone();

// Borrowing — references without ownership
fn print_len(s: &String) {  // Immutable borrow
    println!("{}", s.len());
}

fn append(s: &mut String) {  // Mutable borrow
    s.push_str(" world");
}

// Rules:
// 1. Many immutable borrows OR one mutable borrow
// 2. References must always be valid (no dangling)
// Drop trait — deterministic cleanup (like RAII)
struct FileHandle { fd: i32 }

impl Drop for FileHandle {
    fn drop(&mut self) {
        close_fd(self.fd);  // Runs when value goes out of scope
    }
}

{
    let f = FileHandle { fd: 42 };
    // ... use f ...
} // f.drop() called here automatically

// Smart pointers
use std::rc::Rc;        // Reference counted (single-thread)
use std::sync::Arc;     // Atomic reference counted (multi-thread)
use std::cell::RefCell; // Interior mutability

let shared = Rc::new(vec![1, 2, 3]);
let clone = Rc::clone(&shared); // Cheap (increments count)

// Box — heap allocation
let boxed: Box<dyn Trait> = Box::new(MyStruct {});

// No GC pauses, no runtime overhead
// Memory bugs caught at compile time, not runtime

Performance Profiling

Use cargo bench for benchmarks, perf/flamegraph for CPU profiling, and DHAT/Valgrind for memory.

// Criterion.rs — statistical benchmarking
// Cargo.toml: criterion = { version = "0.5", features = ["html_reports"] }
use criterion::{criterion_group, criterion_main, Criterion};

fn bench_sort(c: &mut Criterion) {
    c.bench_function("sort 1000", |b| {
        b.iter(|| {
            let mut v: Vec<i32> = (0..1000).rev().collect();
            v.sort();
        })
    });
}

criterion_group!(benches, bench_sort);
criterion_main!(benches);
# Built-in benchmarks (nightly only)
cargo bench

# Criterion benchmarks (stable Rust)
cargo bench  # with criterion in Cargo.toml

# Flame graphs
cargo install flamegraph
cargo flamegraph          # Generates flamegraph.svg

# perf (Linux)
perf record --call-graph dwarf ./target/release/myapp
perf report

# Valgrind / Cachegrind
valgrind --tool=callgrind ./target/release/myapp
# Visualize with kcachegrind

# DHAT — heap profiler
valgrind --tool=dhat ./target/release/myapp

# cargo-instruments (macOS)
cargo install cargo-instruments
cargo instruments -t "CPU Profiler"

# Compile time profiling
cargo build --timings    # HTML report of compile times

Interop

Excellent C FFI (zero overhead). Rust can call C and be called from C. wasm-bindgen for JavaScript interop.

// Call C from Rust
extern "C" {
    fn sqrt(x: f64) -> f64;
    fn printf(fmt: *const i8, ...) -> i32;
}

fn main() {
    unsafe {
        let result = sqrt(16.0);
        println!("{}", result); // 4.0
    }
}

// Expose Rust to C
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}
// Build: cargo build --release (produces .so/.dylib/.dll)

// bindgen — auto-generate Rust bindings from C headers
// build.rs:
// bindgen::Builder::default()
//     .header("wrapper.h")
//     .generate()
// wasm-bindgen — Rust ↔ JavaScript
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

// Called from JS:
// import { greet } from './pkg/mylib.js'
// greet("World")

// PyO3 — Rust ↔ Python
use pyo3::prelude::*;

#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> String {
    (a + b).to_string()
}

// Neon — Rust ↔ Node.js
// uniffi — cross-language bindings (Kotlin, Swift, Python)

Packaging & Distribution

Publish libraries to crates.io. Distribute binaries via cargo install, GitHub releases, or package managers.

# Publish a crate to crates.io
cargo login <token>
cargo publish

# Cargo.toml metadata
[package]
name = "my-crate"
version = "1.0.0"
edition = "2021"
description = "My awesome library"
license = "MIT"
repository = "https://github.com/user/my-crate"

# Install a binary crate
cargo install ripgrep

# Build for release
cargo build --release
# Output: target/release/myapp
# Cross-compilation
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
# Produces fully static binary

# cargo-dist — automated release pipeline
cargo install cargo-dist
cargo dist init
cargo dist build
# Creates GitHub releases with binaries for all platforms

# cargo-binstall — binary installation
cargo binstall ripgrep  # Downloads pre-built binary

# Docker (tiny images)
FROM rust:1.78 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
COPY --from=builder /app/target/release/myapp /usr/local/bin/
CMD ["myapp"]

# WASM distribution
wasm-pack build --target web
# Publish to npm: wasm-pack publish