Getting started
Hello world
fn main() {
println!("Hello, world!");
println!("x = {}", 42); // Formatted
println!("{:?}", vec![1, 2]); // Debug format
}
Variables
let x = 5; // Immutable
let mut y = 10; // Mutable
y = 15; // Ok
const MAX: u32 = 100; // Constant
static GLOBAL: &str = "hi"; // Static
Data types
// Integers
let a: i32 = -42; // Signed 32-bit
let b: u64 = 100; // Unsigned 64-bit
// Floats
let c: f64 = 3.14;
// Boolean
let d: bool = true;
// Character
let e: char = 'z';
// String
let s: &str = "hello"; // String slice
let s: String = String::from("hello");
Functions
fn add(x: i32, y: i32) -> i32 {
x + y // No semicolon = return
}
fn nothing() {
println!("No return");
}
// Multiple returns with tuple
fn swap(x: i32, y: i32) -> (i32, i32) {
(y, x)
}
Control flow
If expressions
let number = 6;
if number % 2 == 0 {
println!("even");
} else {
println!("odd");
}
// If as expression
let result = if number > 5 { "big" } else { "small" };
Loops
// Infinite loop
loop {
println!("forever");
break; // Exit loop
}
// While loop
while number < 10 {
number += 1;
}
// For loop
for i in 0..5 { // Range 0 to 4
println!("{}", i);
}
for item in &vec![1, 2, 3] {
println!("{}", item);
}
Match expressions
let number = 3;
match number {
1 => println!("one"),
2 | 3 => println!("two or three"),
4..=6 => println!("four through six"),
_ => println!("something else"), // Catch-all
}
// Match as expression
let description = match number {
1 => "one",
_ => "other",
};
if let / while let
// if let - pattern matching shorthand
let some_value = Some(3);
if let Some(x) = some_value {
println!("Got: {}", x);
} else {
println!("No value");
}
// More concise than:
// match some_value {
// Some(x) => println!("Got: {}", x),
// None => println!("No value"),
// }
// while let - loop with pattern matching
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}
// Process values until None
let mut optional = Some(0);
while let Some(i) = optional {
if i > 5 {
optional = None;
} else {
optional = Some(i + 1);
}
}
Ownership & Borrowing
Ownership rules
let s1 = String::from("hello");
let s2 = s1; // s1 moved to s2
// println!("{}", s1); // ⚠️ Error: s1 no longer valid
let s3 = s2.clone(); // Deep copy
println!("{} {}", s2, s3); // Both valid
Ownership rules:
- Each value has one owner
- Owner goes out of scope → value dropped
- Assignment moves ownership (except Copy types)
References & borrowing
let s = String::from("hello");
// Immutable borrow
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2); // Ok: multiple immutable refs
// Mutable borrow
let mut s = String::from("hello");
let r = &mut s;
r.push_str(" world");
// ⚠️ Can't have other refs while mutable ref exists
Slices
let s = String::from("hello world");
let hello: &str = &s[0..5]; // Slice
let world: &str = &s[6..11];
// Array slices
let a = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3]; // [2, 3]
Structs
Defining structs
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
// Tuple struct
struct Color(i32, i32, i32);
// Unit struct
struct AlwaysEqual;
Using structs
let user = User {
email: String::from("test@example.com"),
username: String::from("user123"),
active: true,
sign_in_count: 1,
};
// Access fields
println!("{}", user.email);
// Mutable struct
let mut user2 = User { ..user }; // Struct update syntax
user2.email = String::from("new@example.com");
Methods
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Method (takes &self)
fn area(&self) -> u32 {
self.width * self.height
}
// Associated function (no self)
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}
// Usage
let rect = Rectangle { width: 30, height: 50 };
println!("{}", rect.area());
let sq = Rectangle::square(20);
Enums
Defining enums
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Option enum
// Built-in enum for nullable values
enum Option<T> {
Some(T),
None,
}
let some_number = Some(5);
let absent_number: Option<i32> = None;
// Using Option
match some_number {
Some(i) => println!("Got: {}", i),
None => println!("No value"),
}
// Unwrap shortcuts
let x = some_number.unwrap_or(0); // 5
let y = absent_number.unwrap_or(0); // 0
Result enum
// Built-in enum for error handling
enum Result<T, E> {
Ok(T),
Err(E),
}
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
// Usage
match divide(10, 2) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
Collections
Vectors
// Create vector
let v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3]; // Macro
// Add elements
let mut v = Vec::new();
v.push(5);
v.push(6);
// Access elements
let third = &v[2]; // Panics if out of bounds
let third = v.get(2); // Returns Option<&T>
// Iterate
for i in &v {
println!("{}", i);
}
// Mutable iteration
for i in &mut v {
*i += 50;
}
Strings
// Create string
let mut s = String::new();
let s = String::from("hello");
let s = "hello".to_string();
// Append
s.push_str(" world");
s.push('!');
// Concatenate
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 moved
let s4 = format!("{}{}", s2, s3);
// Slicing ⚠️ Use carefully with UTF-8
let hello = &s[0..5];
// Iteration
for c in "नमस्ते".chars() {
println!("{}", c);
}
Hash maps
use std::collections::HashMap;
// Create
let mut scores = HashMap::new();
// Insert
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 50);
// Access
let team = String::from("Blue");
let score = scores.get(&team); // Returns Option<&V>
// Iterate
for (key, value) in &scores {
println!("{}: {}", key, value);
}
// Update
scores.insert(String::from("Blue"), 25); // Overwrites
// Insert if absent
scores.entry(String::from("Yellow")).or_insert(50);
// Update based on old value
let count = scores.entry(String::from("Blue")).or_insert(0);
*count += 1;
Error handling
Panic
// Unrecoverable errors
panic!("crash and burn");
// Access out of bounds causes panic
let v = vec![1, 2, 3];
v[99]; // Panic!
Result type
use std::fs::File;
use std::io::ErrorKind;
// Explicit match
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Error creating: {:?}", e),
},
other_error => panic!("Error opening: {:?}", other_error),
},
};
// Shortcuts
let f = File::open("hello.txt").unwrap(); // Panic on error
let f = File::open("hello.txt").expect("Failed to open");
Propagating errors
use std::io;
use std::fs::File;
use std::io::Read;
fn read_username() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?; // ? operator
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// ? operator shortcuts
fn read_username_short() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
Traits
Defining traits
trait Summary {
fn summarize(&self) -> String;
// Default implementation
fn summarize_default(&self) -> String {
String::from("(Read more...)")
}
}
// Implement trait
struct Article {
title: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, self.content)
}
}
Trait bounds
// Trait as parameter
fn notify(item: &impl Summary) {
println!("{}", item.summarize());
}
// Trait bound syntax
fn notify<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
// Multiple traits
fn notify<T: Summary + Display>(item: &T) {}
// Where clause for readability
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// ...
}
Returning traits
// Return impl Trait
fn returns_summarizable() -> impl Summary {
Article {
title: String::from("Title"),
content: String::from("Content"),
}
}
// ⚠️ Can only return single type
// This won't work if returning different types:
// fn returns_summarizable(switch: bool) -> impl Summary {
// if switch { Article {...} } else { Tweet {...} }
// }
Trait objects
// Dynamic dispatch with dyn Trait
trait Draw {
fn draw(&self);
}
struct Circle {
radius: f64,
}
struct Square {
side: f64,
}
impl Draw for Circle {
fn draw(&self) {
println!("Drawing circle");
}
}
impl Draw for Square {
fn draw(&self) {
println!("Drawing square");
}
}
// Box<dyn Trait> for heterogeneous collections
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Square { side: 10.0 }),
];
for shape in shapes {
shape.draw(); // Dynamic dispatch
}
// Static vs dynamic dispatch
fn static_dispatch(item: &impl Draw) { // Monomorphization
item.draw();
}
fn dynamic_dispatch(item: &dyn Draw) { // Runtime vtable lookup
item.draw();
}
Static dispatch (impl Trait):
- Faster (no vtable)
- Generates code for each type
- Larger binary size
Dynamic dispatch (dyn Trait):
- Runtime flexibility
- Single function body
- Smaller binary, slight overhead
From/Into traits
// Implementing From<T>
struct Number {
value: i32,
}
impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}
// Into is automatically implemented
let num: Number = 5.into();
let num = Number::from(5);
// Error conversion pattern
use std::fs::File;
use std::io;
#[derive(Debug)]
enum MyError {
Io(io::Error),
Parse(std::num::ParseIntError),
}
impl From<io::Error> for MyError {
fn from(error: io::Error) -> Self {
MyError::Io(error)
}
}
impl From<std::num::ParseIntError> for MyError {
fn from(error: std::num::ParseIntError) -> Self {
MyError::Parse(error)
}
}
// Use with ? operator
fn read_and_parse() -> Result<i32, MyError> {
let mut file = File::open("number.txt")?; // Auto-converts io::Error
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let num: i32 = contents.trim().parse()?; // Auto-converts ParseIntError
Ok(num)
}
Display and Debug traits
use std::fmt;
struct Point {
x: i32,
y: i32,
}
// Debug trait - for developers
#[derive(Debug)]
struct DebugPoint {
x: i32,
y: i32,
}
// Display trait - for users
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
// Custom Debug implementation
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
// Usage
let p = Point { x: 10, y: 20 };
println!("{}", p); // Display: (10, 20)
println!("{:?}", p); // Debug: Point { x: 10, y: 20 }
println!("{:#?}", p); // Pretty Debug (multiline)
let dp = DebugPoint { x: 5, y: 15 };
println!("{:?}", dp); // Derived Debug
Generics
Generic functions
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
// Multiple type parameters
fn mix<T, U>(x: T, y: U) -> (T, U) {
(x, y)
}
Generic structs
struct Point<T> {
x: T,
y: T,
}
// Multiple types
struct Point<T, U> {
x: T,
y: U,
}
// Methods on generic types
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// Methods for specific types
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Generic enums
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Smart pointers
Box<T>
// Heap allocation
let b = Box::new(5);
println!("b = {}", b);
// Recursive types (unknown size at compile time)
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
let list = Cons(1,
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
// Large data on heap
struct BigStruct {
data: [u8; 10000],
}
let big = Box::new(BigStruct { data: [0; 10000] });
Use when:
- Type has unknown size at compile time
- Large data to avoid stack overflow
- Transfer ownership without copying
Rc<T>
use std::rc::Rc;
// Reference counting for shared ownership
let a = Rc::new(5);
let b = Rc::clone(&a); // Increment count
let c = Rc::clone(&a);
println!("count = {}", Rc::strong_count(&a)); // 3
// Shared ownership of graph/tree
enum List {
Cons(i32, Rc<List>),
Nil,
}
use List::{Cons, Nil};
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
// Drop decrements count
drop(a);
println!("count after drop = {}", Rc::strong_count(&b)); // 2
Use when:
- Multiple owners needed
- Single-threaded only (use Arc for threads)
- Immutable shared data
RefCell<T>
use std::cell::RefCell;
// Interior mutability - mutate through immutable reference
let data = RefCell::new(5);
*data.borrow_mut() += 1; // Mutable borrow
println!("{}", data.borrow()); // Immutable borrow
// Borrow checking at runtime
let mut_borrow = data.borrow_mut();
// let another = data.borrow_mut(); // ⚠️ Panic at runtime!
// Multiple owners with mutable data
use std::rc::Rc;
let value = Rc::new(RefCell::new(5));
let a = Rc::clone(&value);
let b = Rc::clone(&value);
*a.borrow_mut() += 10;
*b.borrow_mut() += 20;
println!("{:?}", value); // 35
Use when:
- Need mutability through shared reference
- Borrow rules checked at runtime
- Single-threaded (use Mutex for threads)
Cow<T>
use std::borrow::Cow;
// Clone-on-write - avoid cloning until mutation
fn process(input: &str) -> Cow<str> {
if input.contains("REPLACE") {
Cow::Owned(input.replace("REPLACE", "FIXED"))
} else {
Cow::Borrowed(input) // No clone!
}
}
let s1 = "Hello world";
let result1 = process(s1); // Borrowed
let s2 = "REPLACE this";
let result2 = process(s2); // Owned
// Works with any cloneable + borrowable type
fn abs_all(input: &mut Cow<[i32]>) {
for i in 0..input.len() {
let v = input[i];
if v < 0 {
input.to_mut()[i] = -v; // Clone on first mutation
}
}
}
Use when:
- Might not need to modify data
- Avoid unnecessary clones
- Working with borrowed or owned data
Decision table
| Pointer | Ownership | Mutability | Thread-safe | Use case |
|---|---|---|---|---|
Box<T> |
Single | Mutable | No | Heap allocation, recursion |
Rc<T> |
Shared | Immutable | No | Shared ownership (single-threaded) |
Arc<T> |
Shared | Immutable | Yes | Shared ownership (multi-threaded) |
RefCell<T> |
Single | Interior | No | Runtime borrow checking |
Mutex<T> |
Shared | Interior | Yes | Thread-safe interior mutability |
Cow<T> |
Either | Clone-on-write | No | Avoid unnecessary clones |
Lifetimes
Lifetime annotations
// Generic lifetime 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
// Usage
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("{}", result);
}
// ⚠️ result can't outlive string2
Lifetime in structs
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
}
Lifetime elision
// These are equivalent:
fn first_word(s: &str) -> &str {}
fn first_word<'a>(s: &'a str) -> &'a str {}
// Compiler infers lifetimes based on rules
Cargo
Project commands
cargo new myproject # Create new project
cargo init # Initialize in current dir
cargo build # Build debug
cargo build --release # Build optimized
cargo run # Build and run
cargo check # Check without building
cargo test # Run tests
cargo doc --open # Generate and open docs
Dependencies
# Cargo.toml
[package]
name = "myproject"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1.0"
serde_json = "1.0"
rand = "0.8.5"
[dev-dependencies]
criterion = "0.5"
cargo add serde # Add dependency
cargo update # Update dependencies
cargo clean # Remove build artifacts
Workspaces
# Cargo.toml (workspace root)
[workspace]
members = [
"crate1",
"crate2",
]
cargo build --workspace # Build all crates
cargo test --workspace # Test all crates
Modules and crates
Module basics
// Define module
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {} // Private
}
mod serving {
fn take_order() {}
fn serve_order() {}
}
}
// Use module
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
File structure
// src/lib.rs or src/main.rs
mod garden; // Looks for src/garden.rs or src/garden/mod.rs
// src/garden.rs
pub mod vegetables; // Looks for src/garden/vegetables.rs
// src/garden/vegetables.rs
#[derive(Debug)]
pub struct Asparagus {}
src/
├── lib.rs
├── garden.rs
└── garden/
└── vegetables.rs
Old style (still works):
src/
├── lib.rs
├── garden/
│ ├── mod.rs
│ └── vegetables.rs
Visibility modifiers
mod outer {
pub fn public_fn() {}
fn private_fn() {}
pub mod inner {
pub fn inner_public() {}
pub(crate) fn crate_visible() {} // Visible in crate
pub(super) fn parent_visible() {} // Visible to parent
pub(in crate::outer) fn outer_visible() {} // Visible in outer
}
}
// Usage
outer::public_fn(); // Ok
outer::inner::inner_public(); // Ok
outer::inner::crate_visible(); // Ok (same crate)
// outer::private_fn(); // Error
use and re-exporting
// Import
use std::collections::HashMap;
use std::io::{self, Write}; // Import module and trait
use std::fmt::Result;
use std::io::Result as IoResult; // Rename
// Glob import (use sparingly)
use std::collections::*;
// Re-export with pub use
pub use crate::front_of_house::hosting;
// External crates
use rand::Rng;
use serde::{Serialize, Deserialize};
Idiomatic patterns:
// For functions: import module
use std::collections::HashMap;
let map = HashMap::new();
// For types/traits: import directly
use std::fmt::Display;
fn print(item: impl Display) {}
Testing
Unit tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
fn exploration() {
assert!(true);
}
#[test]
#[should_panic]
fn panic_test() {
panic!("This should panic");
}
#[test]
fn result_test() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two != four"))
}
}
}
Assertions
assert!(condition); // Panic if false
assert_eq!(left, right); // Panic if not equal
assert_ne!(left, right); // Panic if equal
// With custom message
assert!(condition, "Error: {}", msg);
assert_eq!(a, b, "Values differ: {} vs {}", a, b);
Running tests
cargo test # Run all tests
cargo test test_name # Run specific test
cargo test -- --nocapture # Show println! output
cargo test -- --ignored # Run ignored tests
Concurrency
Threads
use std::thread;
use std::time::Duration;
// Spawn thread
let handle = thread::spawn(|| {
for i in 1..10 {
println!("spawned thread: {}", i);
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap(); // Wait for thread to finish
// Move closure
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("vector: {:?}", v);
});
Channels
use std::sync::mpsc;
use std::thread;
// Create channel
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
// Multiple producers
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
tx.send(String::from("hi from tx")).unwrap();
});
thread::spawn(move || {
tx1.send(String::from("hi from tx1")).unwrap();
});
Shared state
use std::sync::{Arc, Mutex};
use std::thread;
// Arc = Atomic Reference Counter
// Mutex = Mutual Exclusion
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
Async/Await
Async functions
// Async function returns a Future
async fn fetch_data() -> String {
String::from("data")
}
// .await to execute Future
async fn process() {
let data = fetch_data().await;
println!("{}", data);
}
// Async block
let future = async {
let result = fetch_data().await;
result
};
Tokio setup
// Cargo.toml
// [dependencies]
// tokio = { version = "1", features = ["full"] }
// Main function with tokio runtime
#[tokio::main]
async fn main() {
println!("Hello from async!");
let result = fetch_data().await;
println!("{}", result);
}
// Alternative: manual runtime
fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
fetch_data().await;
});
}
Spawning tasks
use tokio::task;
#[tokio::main]
async fn main() {
// Spawn concurrent task
let handle = task::spawn(async {
// Runs on tokio thread pool
expensive_computation().await
});
// Do other work
do_something_else().await;
// Wait for task
let result = handle.await.unwrap();
println!("Result: {}", result);
}
// Multiple tasks
async fn fetch_multiple() {
let task1 = task::spawn(fetch_user(1));
let task2 = task::spawn(fetch_user(2));
let (user1, user2) = tokio::join!(task1, task2);
}
Async examples
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
// Async TCP server
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, _) = listener.accept().await?;
// Spawn task per connection
tokio::spawn(async move {
let mut buf = [0; 1024];
loop {
match socket.read(&mut buf).await {
Ok(0) => return, // Connection closed
Ok(n) => {
if socket.write_all(&buf[0..n]).await.is_err() {
return;
}
}
Err(_) => return,
}
}
});
}
}
// Async HTTP request
use reqwest;
async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
let body = reqwest::get(url)
.await?
.text()
.await?;
Ok(body)
}
// Timeout
use tokio::time::{timeout, Duration};
async fn with_timeout() {
let result = timeout(
Duration::from_secs(5),
fetch_data()
).await;
match result {
Ok(data) => println!("Got: {:?}", data),
Err(_) => println!("Timeout!"),
}
}
Macros
Common macros
println!("Hello"); // Print with newline
print!("Hello"); // Print without newline
eprintln!("Error"); // Print to stderr
format!("x = {}", x); // Format to String
vec![1, 2, 3]; // Create vector
panic!("Error!"); // Panic with message
assert!(condition); // Assert condition
assert_eq!(a, b); // Assert equality
debug_assert!(condition); // Assert in debug only
Declarative macros
// Define macro
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
// With arguments
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("Function {:?} called", stringify!($func_name));
}
};
}
// Multiple patterns
macro_rules! my_vec {
() => {
Vec::new()
};
($elem:expr; $n:expr) => {
vec![$elem; $n]
};
}
Derive macros
#[derive(Debug)] // Auto-implement Debug
#[derive(Clone)] // Auto-implement Clone
#[derive(PartialEq)] // Auto-implement PartialEq
#[derive(Eq)] // Auto-implement Eq
// Multiple derives
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
Attributes
Common attributes
// Derive traits
#[derive(Debug, Clone, PartialEq)]
struct MyStruct {}
// Allow/warn/deny lints
#[allow(dead_code)]
#[warn(missing_docs)]
#[deny(unsafe_code)]
// Testing
#[test]
fn my_test() {}
#[cfg(test)]
mod tests {}
// Conditional compilation
#[cfg(target_os = "linux")]
fn linux_only() {}
#[cfg(not(target_os = "windows"))]
fn not_windows() {}
#[cfg(any(unix, target_os = "windows"))]
fn multi_platform() {}
#[cfg(all(unix, target_arch = "x86_64"))]
fn specific_platform() {}
Feature flags
// Cargo.toml
// [features]
// default = ["std"]
// std = []
// experimental = []
#[cfg(feature = "std")]
use std::collections::HashMap;
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap;
#[cfg(feature = "experimental")]
pub mod experimental {
pub fn new_feature() {}
}
Documentation attributes
/// Document the following item
///
/// # Examples
///
/// ```
/// let x = example();
/// ```
pub fn example() {}
//! Document the enclosing item (module/crate)
#[doc = "Alternative doc syntax"]
pub fn other() {}
// Doc tests
/// ```
/// assert_eq!(add(2, 2), 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Hide from docs
#[doc(hidden)]
pub fn internal_api() {}
Other attributes
// Inline suggestions
#[inline]
fn small_function() {}
#[inline(always)]
fn always_inline() {}
// Must use return value
#[must_use]
fn important() -> i32 { 42 }
// Deprecated
#[deprecated]
fn old_api() {}
#[deprecated(since = "1.5.0", note = "Use new_api instead")]
fn old_api_detailed() {}
// Non-exhaustive (for library evolution)
#[non_exhaustive]
pub enum ApiError {
NotFound,
Timeout,
}
#[non_exhaustive]
pub struct Config {
pub timeout: u64,
}
Common patterns
Iterator patterns
let v = vec![1, 2, 3, 4, 5];
// Map
let v2: Vec<_> = v.iter().map(|x| x + 1).collect();
// Filter
let v3: Vec<_> = v.iter().filter(|x| *x % 2 == 0).collect();
// Fold (reduce)
let sum: i32 = v.iter().fold(0, |acc, x| acc + x);
// Chain operations
let result: Vec<_> = v.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.collect();
// Find
let first_even = v.iter().find(|x| *x % 2 == 0);
// Any/All
let has_even = v.iter().any(|x| x % 2 == 0);
let all_positive = v.iter().all(|x| *x > 0);
Closure patterns
// Closure types
let add_one = |x| x + 1; // Type inferred
let add_one = |x: i32| -> i32 { x + 1 };
// Capture environment
let x = 4;
let equal_to_x = |z| z == x; // Captures x
// Move ownership
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x; // Takes ownership
// Function parameters
fn apply<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(x)
}
Builder pattern
struct Circle {
x: f64,
y: f64,
radius: f64,
}
struct CircleBuilder {
x: f64,
y: f64,
radius: f64,
}
impl CircleBuilder {
fn new() -> CircleBuilder {
CircleBuilder {
x: 0.0,
y: 0.0,
radius: 1.0,
}
}
fn x(&mut self, x: f64) -> &mut Self {
self.x = x;
self
}
fn y(&mut self, y: f64) -> &mut Self {
self.y = y;
self
}
fn radius(&mut self, radius: f64) -> &mut Self {
self.radius = radius;
self
}
fn build(&self) -> Circle {
Circle {
x: self.x,
y: self.y,
radius: self.radius,
}
}
}
// Usage
let c = CircleBuilder::new()
.x(10.0)
.y(20.0)
.radius(5.0)
.build();
File I/O
Basic file operations
use std::fs;
use std::io;
// Read entire file to string
let contents = fs::read_to_string("file.txt")?;
// Read entire file to bytes
let bytes = fs::read("file.bin")?;
// Write string to file (overwrites)
fs::write("output.txt", "Hello, world!")?;
// Write bytes to file
fs::write("output.bin", &[0, 1, 2, 3])?;
// Check if file exists
if fs::metadata("file.txt").is_ok() {
println!("File exists");
}
// Create directory
fs::create_dir("my_dir")?;
fs::create_dir_all("path/to/nested/dir")?;
// Remove file
fs::remove_file("file.txt")?;
File handle
use std::fs::File;
use std::io::{Read, Write};
// Open for reading
let mut file = File::open("input.txt")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
// Create or truncate
let mut file = File::create("output.txt")?;
file.write_all(b"Hello, world!")?;
// Append
use std::fs::OpenOptions;
let mut file = OpenOptions::new()
.append(true)
.open("log.txt")?;
file.write_all(b"New log entry\n")?;
// Read and write
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open("data.txt")?;
Buffered I/O
use std::fs::File;
use std::io::{BufReader, BufRead, BufWriter, Write};
// Buffered reading (efficient for large files)
let file = File::open("large.txt")?;
let reader = BufReader::new(file);
// Read line by line
for line in reader.lines() {
let line = line?;
println!("{}", line);
}
// Buffered writing
let file = File::create("output.txt")?;
let mut writer = BufWriter::new(file);
writer.write_all(b"Line 1\n")?;
writer.write_all(b"Line 2\n")?;
writer.flush()?; // Ensure data is written
// Read specific lines
let file = File::open("file.txt")?;
let reader = BufReader::new(file);
let lines: Vec<_> = reader
.lines()
.take(5) // First 5 lines
.collect();
Path operations
use std::path::Path;
use std::fs;
let path = Path::new("dir/file.txt");
// Path info
println!("Exists: {}", path.exists());
println!("Is file: {}", path.is_file());
println!("Is dir: {}", path.is_dir());
println!("File name: {:?}", path.file_name());
println!("Extension: {:?}", path.extension());
println!("Parent: {:?}", path.parent());
// Build paths
use std::path::PathBuf;
let mut path = PathBuf::from("/tmp");
path.push("dir");
path.push("file.txt");
// Result: /tmp/dir/file.txt
// Canonicalize (resolve symlinks, relative paths)
let canonical = fs::canonicalize("./file.txt")?;
Serde
JSON serialization
// Cargo.toml
// [dependencies]
// serde = { version = "1.0", features = ["derive"] }
// serde_json = "1.0"
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Person {
name: String,
age: u32,
email: String,
}
// Serialize to JSON
let person = Person {
name: "Alice".to_string(),
age: 30,
email: "alice@example.com".to_string(),
};
let json = serde_json::to_string(&person)?;
println!("{}", json);
// {"name":"Alice","age":30,"email":"alice@example.com"}
// Pretty print
let json = serde_json::to_string_pretty(&person)?;
// Deserialize from JSON
let json_str = r#"{"name":"Bob","age":25,"email":"bob@example.com"}"#;
let person: Person = serde_json::from_str(json_str)?;
println!("{:?}", person);
// From file
let file = File::open("data.json")?;
let person: Person = serde_json::from_reader(file)?;
Field attributes
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct User {
// Rename field in JSON
#[serde(rename = "userName")]
username: String,
// Skip field
#[serde(skip)]
password_hash: String,
// Skip when serializing
#[serde(skip_serializing)]
internal_id: u64,
// Skip when deserializing
#[serde(skip_deserializing)]
computed: String,
// Default value if missing
#[serde(default)]
active: bool,
// Custom default function
#[serde(default = "default_role")]
role: String,
// Flatten nested struct
#[serde(flatten)]
metadata: Metadata,
}
fn default_role() -> String {
"user".to_string()
}
#[derive(Serialize, Deserialize)]
struct Metadata {
created_at: String,
updated_at: String,
}
// Option fields
#[derive(Serialize, Deserialize)]
struct OptionalFields {
// Skip if None
#[serde(skip_serializing_if = "Option::is_none")]
middle_name: Option<String>,
}
Container attributes
// Rename all fields
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Product {
product_name: String, // -> "productName"
unit_price: f64, // -> "unitPrice"
}
// Tag for enums
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Message {
Text { content: String },
Image { url: String },
}
// {"type":"Text","content":"hello"}
// Untagged enum (by structure)
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Value {
Number(i32),
Text(String),
}
// Deny unknown fields
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Strict {
field1: String,
}
FFI (Foreign Function Interface)
Calling C from Rust
use libc::size_t;
#[link(name = "snappy")]
unsafe extern "C" {
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}
fn main() {
let x = unsafe { snappy_max_compressed_length(100) };
println!("max compressed length: {}", x);
}
Add to Cargo.toml: libc = "0.2"
Exporting Rust functions
#[no_mangle]
pub extern "C" fn hello_from_rust() {
println!("Hello from Rust!");
}
// With safety wrapper
#[no_mangle]
pub unsafe extern "C" fn process_data(ptr: *const u8, len: usize) -> i32 {
if ptr.is_null() {
return -1;
}
let slice = std::slice::from_raw_parts(ptr, len);
// Process data
slice.len() as i32
}
Bindgen basics
// build.rs
fn main() {
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.generate()
.expect("Unable to generate bindings");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings");
}
// In lib.rs
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
Add to Cargo.toml: bindgen = "0.69" (build-dependencies)
C types mapping
| C Type | Rust Type |
|---|---|
int |
c_int |
unsigned int |
c_uint |
char |
c_char |
void* |
*mut c_void |
const char* |
*const c_char |
size_t |
usize |
use std::ffi::{c_int, c_char, c_void, CStr, CString};
// String conversion
let c_str = CString::new("hello").unwrap();
let ptr: *const c_char = c_str.as_ptr();
// From C string
unsafe {
let s = CStr::from_ptr(ptr).to_str().unwrap();
}
Unsafe Rust
Unsafe superpowers
unsafe {
// 1. Dereference raw pointers
let x = 5;
let r = &raw const x;
let val = *r;
// 2. Call unsafe functions
dangerous();
// 3. Access/modify mutable statics
COUNTER += 1;
// 4. Implement unsafe traits
// (requires unsafe impl)
// 5. Access union fields
// my_union.field
}
unsafe fn dangerous() {}
static mut COUNTER: u32 = 0;
Raw pointers
let mut num = 5;
let r1 = &raw const num; // *const i32
let r2 = &raw mut num; // *mut i32
unsafe {
println!("r1: {}", *r1);
*r2 = 10;
}
// From references
let r: *const i32 = #
let r: *mut i32 = &mut num;
// Null pointer
let null: *const i32 = std::ptr::null();
let null_mut: *mut i32 = std::ptr::null_mut();
// Check null
if !null.is_null() {
unsafe { println!("{}", *null); }
}
Safety invariants
- No data races
- No dereferencing null/dangling pointers
- References always point to valid data
- No uninitialized memory reads
- Uphold lifetime requirements
Always wrap unsafe code in safe abstractions.
Unsafe traits
// Marker traits for thread safety
unsafe trait MyUnsafeTrait {
fn do_something(&self);
}
unsafe impl MyUnsafeTrait for i32 {
fn do_something(&self) {
println!("{}", self);
}
}
// Example: Send and Sync
// - Send: safe to transfer between threads
// - Sync: safe to share references between threads
// Most types implement these automatically
Unions
#[repr(C)]
union MyUnion {
i: i32,
f: f32,
}
fn main() {
let u = MyUnion { i: 42 };
// Accessing union fields is unsafe
unsafe {
println!("As int: {}", u.i);
println!("As float: {}", u.f); // Reinterprets bits
}
}
Common Crates
Rayon (parallelism)
use rayon::prelude::*;
// Parallel iterator
let sum: i32 = vec![1, 2, 3, 4, 5]
.par_iter()
.map(|&x| x * 2)
.sum();
// Parallel sorting
let mut v = vec![5, 2, 1, 4, 3];
v.par_sort_unstable();
// Custom parallel tasks
rayon::join(
|| heavy_task_1(),
|| heavy_task_2()
);
// Parallel chunks
data.par_chunks(100)
.for_each(|chunk| process(chunk));
Add: rayon = "1.10"
Clap (CLI parsing)
use clap::Parser;
#[derive(Parser)]
#[command(version, about)]
struct Args {
/// Name of the person
#[arg(short, long)]
name: String,
/// Number of times to greet
#[arg(short, long, default_value_t = 1)]
count: u8,
/// Enable verbose output
#[arg(long)]
verbose: bool,
}
fn main() {
let args = Args::parse();
for _ in 0..args.count {
println!("Hello {}!", args.name);
}
}
Add: clap = { version = "4", features = ["derive"] }
thiserror (library errors)
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error at line {line}: {msg}")]
Parse { line: usize, msg: String },
#[error("Invalid value: {0}")]
Invalid(String),
}
fn read_config() -> Result<Config, MyError> {
let content = std::fs::read_to_string("config.txt")?;
// ...
}
Add: thiserror = "1.0"
anyhow (application errors)
use anyhow::{Context, Result, bail, anyhow};
fn read_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.context("Failed to read config file")?;
let config: Config = serde_json::from_str(&content)
.context("Failed to parse config")?;
if config.name.is_empty() {
bail!("Config name cannot be empty");
}
Ok(config)
}
fn main() -> Result<()> {
let config = read_config("config.json")?;
Ok(())
}
Add: anyhow = "1.0"
Reqwest (HTTP client)
use reqwest;
use std::collections::HashMap;
// Async GET
let resp = reqwest::get("https://api.example.com/data")
.await?
.json::<HashMap<String, String>>()
.await?;
// Async POST with JSON
let client = reqwest::Client::new();
let resp = client
.post("https://api.example.com/users")
.json(&user)
.header("Authorization", "Bearer token")
.send()
.await?;
// Blocking (sync) client
let body = reqwest::blocking::get("https://example.com")?
.text()?;
Add: reqwest = { version = "0.12", features = ["json"] }
Tracing (structured logging)
use tracing::{info, warn, error, debug, instrument, span, Level};
#[instrument]
fn process_request(id: u64) {
info!(id, "Processing request");
if id == 0 {
warn!("Invalid ID");
return;
}
debug!(result = 42, "Computation complete");
}
// Setup subscriber
use tracing_subscriber;
fn main() {
tracing_subscriber::fmt::init();
let span = span!(Level::INFO, "my_span");
let _guard = span.enter();
process_request(1);
}
Add: tracing = "0.1", tracing-subscriber = "0.3"
Memory Layout
repr attributes
// C-compatible layout
#[repr(C)]
struct CStruct {
a: u8, // offset 0
b: u32, // offset 4 (padded)
c: u16, // offset 8
} // size: 12 bytes
// Transparent wrapper (same ABI as inner type)
#[repr(transparent)]
struct Wrapper(u32);
// Remove padding (use carefully!)
#[repr(packed)]
struct Packed {
a: u8,
b: u32, // No alignment padding
}
// Can't take references to packed fields!
let p = Packed { a: 1, b: 2 };
let val = p.b; // OK: copy the value
// let r = &p.b; // ERROR: unaligned reference
// Specific alignment
#[repr(align(16))]
struct Aligned {
data: [u8; 4],
}
Size and alignment
use std::mem::{size_of, align_of, size_of_val};
// Primitives
assert_eq!(size_of::<u8>(), 1);
assert_eq!(size_of::<u64>(), 8);
assert_eq!(align_of::<u32>(), 4);
// Zero-sized types (ZST)
struct Empty;
assert_eq!(size_of::<Empty>(), 0);
struct Unit;
assert_eq!(size_of::<Unit>(), 0);
// PhantomData
use std::marker::PhantomData;
struct Marker<T>(PhantomData<T>);
assert_eq!(size_of::<Marker<String>>(), 0);
// Runtime size
let v = vec![1, 2, 3];
println!("Vec header: {}", size_of_val(&v)); // 24
println!("Vec data: {}", v.len() * size_of::<i32>());
Enum layout
use std::mem::size_of;
// Simple enum
enum Simple { A, B, C }
assert_eq!(size_of::<Simple>(), 1); // Just discriminant
// Enum with data
enum Message {
Quit,
Move { x: i32, y: i32 },
Text(String),
}
// Size = discriminant + largest variant + padding
// Niche optimization
assert_eq!(size_of::<Option<&i32>>(), 8); // Same as &i32!
// None uses null pointer niche
// NonZero for niche
use std::num::NonZeroU32;
assert_eq!(size_of::<Option<NonZeroU32>>(), 4);
DST and fat pointers
use std::mem::size_of;
// Thin pointers (sized types)
assert_eq!(size_of::<&i32>(), 8); // Just address
assert_eq!(size_of::<Box<i32>>(), 8);
// Fat pointers (unsized types)
assert_eq!(size_of::<&str>(), 16); // ptr + len
assert_eq!(size_of::<&[i32]>(), 16); // ptr + len
assert_eq!(size_of::<&dyn Clone>(), 16); // ptr + vtable
// Box to unsized
let boxed: Box<dyn std::fmt::Debug> = Box::new(42);
assert_eq!(size_of_val(&boxed), 16); // Fat pointer
Performance Patterns
Zero-cost abstractions
// Iterator chains compile to same code as manual loops
let sum: i32 = (1..100)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.sum();
// Equivalent to optimized:
// let mut sum = 0;
// for x in 1..100 {
// if x % 2 == 0 {
// sum += x * x;
// }
// }
// Generics are monomorphized
fn process<T: Clone>(item: T) -> T {
item.clone()
}
// Generates specialized code for each T
Optimization attributes
#[inline]
fn small_func() { }
#[inline(always)]
fn must_inline() { }
#[inline(never)]
fn no_inline() { }
#[cold]
fn error_path() {
// Hint: rarely called
}
#[cfg(not(debug_assertions))]
fn release_only() { }
// Target-specific optimization
#[cfg(target_arch = "x86_64")]
fn use_simd() { }
Common pitfalls
// BAD: Unnecessary clones
for item in collection.clone() { }
// GOOD: Iterate by reference
for item in &collection { }
// BAD: Allocations in hot loop
for _ in 0..1000 {
let v = vec![1, 2, 3];
}
// GOOD: Reuse allocation
let mut v = Vec::with_capacity(3);
for _ in 0..1000 {
v.clear();
v.extend_from_slice(&[1, 2, 3]);
}
// BAD: String concatenation in loop
let mut s = String::new();
for item in items {
s = s + &item.to_string(); // Reallocates each time
}
// GOOD: Use push_str or format!
let mut s = String::with_capacity(100);
for item in items {
s.push_str(&item.to_string());
}
Profiling tools
# Flamegraph
cargo install flamegraph
cargo flamegraph --bin myapp
# Linux perf
perf record -g ./target/release/myapp
perf report
# macOS Instruments
cargo install cargo-instruments
cargo instruments -t time
# Memory profiling
cargo install cargo-bloat
cargo bloat --release --crates
# Benchmarking
cargo bench # Requires criterion or test harness
Benchmarking with Criterion
// benches/my_bench.rs
use criterion::{criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 | 1 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(20)));
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
Add: criterion = { version = "0.5", features = ["html_reports"] }
Procedural Macros
Proc macro setup
# Cargo.toml for proc-macro crate
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
Proc macros must be in a separate crate.
Derive macro
// my_macro/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let builder_name = quote::format_ident!("{}Builder", name);
let expanded = quote! {
impl #name {
pub fn builder() -> #builder_name {
#builder_name::default()
}
}
#[derive(Default)]
pub struct #builder_name {
// Builder fields...
}
};
TokenStream::from(expanded)
}
Attribute macro
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn log_calls(
_attr: TokenStream,
item: TokenStream
) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let name = &input.sig.ident;
let block = &input.block;
let sig = &input.sig;
let expanded = quote! {
#sig {
println!("Entering: {}", stringify!(#name));
let result = { #block };
println!("Exiting: {}", stringify!(#name));
result
}
};
TokenStream::from(expanded)
}
// Usage:
// #[log_calls]
// fn my_function() { }
Function-like macro
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr};
#[proc_macro]
pub fn make_answer(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as LitStr);
let value = input.value();
let expanded = quote! {
fn answer() -> &'static str {
#value
}
};
TokenStream::from(expanded)
}
// Usage: make_answer!("42");
// Generates: fn answer() -> &'static str { "42" }
Helper attributes
#[proc_macro_derive(MyTrait, attributes(my_attr))]
pub fn derive_my_trait(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Access helper attributes on fields
// #[my_attr(skip)]
// field: Type,
// Parse attributes from input.attrs
// and field.attrs
TokenStream::new()
}
Embedded Rust
no_std basics
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
#[entry]
fn main() -> ! {
// Your embedded code here
loop {
// Main loop
}
}
#![no_std] disables the standard library, #![no_main] disables the standard entry point.
Panic handler
#![no_std]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
// Options:
// - Loop forever
// - Reset device
// - Log error and halt
loop {}
}
// Or use a crate
// use panic_halt as _; // Halt
// use panic_reset as _; // Reset
// use panic_semihosting; // Debug output
Using alloc
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use alloc::string::String;
use alloc::boxed::Box;
// Must provide allocator
use embedded_alloc::LlffHeap as Heap;
#[global_allocator]
static HEAP: Heap = Heap::empty();
fn init_heap() {
use core::mem::MaybeUninit;
const HEAP_SIZE: usize = 1024;
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] =
[MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
}
Hardware abstraction
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use stm32f4xx_hal::{pac, prelude::*};
#[entry]
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
// Configure clocks
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.sysclk(48.MHz()).freeze();
// Configure GPIO
let gpioa = dp.GPIOA.split();
let mut led = gpioa.pa5.into_push_pull_output();
loop {
led.toggle();
cortex_m::asm::delay(8_000_000);
}
}
Embedded targets
# Add target
rustup target add thumbv7em-none-eabihf # Cortex-M4F/M7F
rustup target add thumbv7m-none-eabi # Cortex-M3
rustup target add thumbv6m-none-eabi # Cortex-M0/M0+
rustup target add riscv32imac-unknown-none-elf # RISC-V
# Build
cargo build --target thumbv7em-none-eabihf
# .cargo/config.toml
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
runner = "probe-run --chip STM32F411CEUx"
WebAssembly
wasm-bindgen basics
use wasm_bindgen::prelude::*;
// Export function to JavaScript
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Import from JavaScript
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn show_alert() {
alert("Hello from Rust!");
}
#[wasm_bindgen(start)]
pub fn main() {
log("WASM module initialized");
}
Add: wasm-bindgen = "0.2"
wasm-pack workflow
# Install
cargo install wasm-pack
# Build for bundlers (webpack, etc.)
wasm-pack build --target bundler
# Build for web (ES modules)
wasm-pack build --target web
# Build for Node.js
wasm-pack build --target nodejs
# Test in browser
wasm-pack test --headless --firefox
# Publish to npm
wasm-pack publish
web-sys DOM access
use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, Window};
#[wasm_bindgen]
pub fn manipulate_dom() -> Result<(), JsValue> {
let window: Window = web_sys::window()
.ok_or("no window")?;
let document: Document = window.document()
.ok_or("no document")?;
let element: Element = document
.create_element("p")?;
element.set_text_content(Some("Hello from Rust!"));
document.body()
.ok_or("no body")?
.append_child(&element)?;
Ok(())
}
Add: web-sys = { version = "0.3", features = ["Window", "Document", "Element"] }
JS interop
use wasm_bindgen::prelude::*;
use js_sys::{Array, Object, Reflect};
// Accept JS values
#[wasm_bindgen]
pub fn process_array(arr: &Array) -> u32 {
arr.length()
}
// Return JS objects
#[wasm_bindgen]
pub fn create_object() -> Object {
let obj = Object::new();
Reflect::set(&obj, &"name".into(), &"Rust".into()).unwrap();
obj
}
// Async JavaScript
#[wasm_bindgen]
pub async fn fetch_data(url: &str) -> Result<JsValue, JsValue> {
let window = web_sys::window().unwrap();
let resp = wasm_bindgen_futures::JsFuture::from(
window.fetch_with_str(url)
).await?;
Ok(resp)
}
Add: js-sys = "0.3", wasm-bindgen-futures = "0.4"
Cargo Advanced
Workspaces
# Root Cargo.toml
[workspace]
members = ["crate1", "crate2", "crates/*"]
resolver = "2"
[workspace.package]
version = "1.0.0"
edition = "2021"
authors = ["Your Name"]
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# In member Cargo.toml
[package]
name = "crate1"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true
cargo build --workspace
cargo test --workspace
cargo test -p crate1 # Single package
Build scripts
// build.rs
fn main() {
// Rerun conditions
println!("cargo::rerun-if-changed=build.rs");
println!("cargo::rerun-if-changed=src/data.json");
println!("cargo::rerun-if-env-changed=MY_VAR");
// Link native library
println!("cargo::rustc-link-lib=mylib");
println!("cargo::rustc-link-search=/usr/local/lib");
// Set cfg flags
println!("cargo::rustc-cfg=feature=\"custom\"");
// Set environment for code
println!("cargo::rustc-env=GIT_HASH={}", get_git_hash());
// Warning message
println!("cargo::warning=Custom build warning");
}
fn get_git_hash() -> String {
std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "unknown".to_string())
}
Target-specific dependencies
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
nix = "0.27"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
# Cross-compilation
[target.x86_64-unknown-linux-gnu]
linker = "x86_64-linux-gnu-gcc"
[target.aarch64-apple-darwin]
rustflags = ["-C", "target-cpu=apple-m1"]
Cargo features
[features]
default = ["std"]
std = []
alloc = []
serde = ["dep:serde"]
full = ["std", "serde", "async"]
async = ["dep:tokio"]
[dependencies]
serde = { version = "1.0", optional = true }
tokio = { version = "1", optional = true }
#[cfg(feature = "std")]
use std::collections::HashMap;
#[cfg(all(feature = "alloc", not(feature = "std")))]
extern crate alloc;
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
Release profiles
[profile.dev]
opt-level = 0
debug = true
overflow-checks = true
[profile.release]
opt-level = 3
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
panic = "abort" # Smaller binary
strip = true # Remove symbols
[profile.bench]
inherits = "release"
debug = true # For profiling
# Custom profile
[profile.release-debug]
inherits = "release"
debug = true
Common Pitfalls
Integer overflow
// In release mode, overflow wraps silently
let x: u8 = 255;
let y = x + 1; // y = 0 in release, panics in debug
// Explicit handling
let y = x.checked_add(1); // Returns Option
let y = x.saturating_add(1); // Clamps to max (255)
let y = x.wrapping_add(1); // Wraps to 0
let (y, overflow) = x.overflowing_add(1); // Returns both
// Use larger types for intermediate calculations
let result: u64 = (a as u64) * (b as u64);
Floating point equality
// BAD: Direct comparison
let x = 0.1 + 0.2;
if x == 0.3 { } // Often false!
// GOOD: Epsilon comparison
const EPSILON: f64 = 1e-10;
if (x - 0.3).abs() < EPSILON { }
// For relative comparison
fn approx_eq(a: f64, b: f64, epsilon: f64) -> bool {
(a - b).abs() <= epsilon * a.abs().max(b.abs())
}
// Consider using fixed-point or decimal crates
// for financial calculations
Mutex poisoning
use std::sync::Mutex;
let mutex = Mutex::new(0);
let result = mutex.lock();
match result {
Ok(guard) => {
// Use guard
},
Err(poisoned) => {
// Another thread panicked while holding lock
// Can still recover the data
let guard = poisoned.into_inner();
// But consider if data is valid
}
}
// Alternative: always recover
let guard = mutex.lock().unwrap_or_else(|e| e.into_inner());
Arc vs Rc confusion
use std::rc::Rc;
use std::sync::Arc;
// Rc: Single-threaded only
let rc = Rc::new(5);
// thread::spawn(move || println!("{}", rc)); // ERROR!
// Arc: Thread-safe
let arc = Arc::new(5);
let arc_clone = Arc::clone(&arc);
std::thread::spawn(move || {
println!("{}", arc_clone); // OK
});
// Common pattern: Arc<Mutex<T>>
use std::sync::Mutex;
let shared = Arc::new(Mutex::new(Vec::new()));
Deadlocks
use std::sync::{Mutex, MutexGuard};
// BAD: Lock ordering deadlock
fn deadlock_risk() {
let a = Mutex::new(1);
let b = Mutex::new(2);
// Thread 1: locks a, then b
// Thread 2: locks b, then a
// -> Deadlock!
}
// GOOD: Consistent lock ordering
fn safe() {
let a = Mutex::new(1);
let b = Mutex::new(2);
// Always lock in same order (a before b)
let guard_a = a.lock().unwrap();
let guard_b = b.lock().unwrap();
}
// GOOD: Lock, copy, release
fn also_safe() {
let a = Mutex::new(1);
let val = *a.lock().unwrap(); // Lock released immediately
}
String vs str confusion
// &str: Borrowed string slice (view into data)
// String: Owned, growable string
fn takes_str(s: &str) { }
fn takes_string(s: String) { }
let owned = String::from("hello");
let borrowed: &str = "hello";
takes_str(&owned); // &String coerces to &str
takes_str(borrowed); // Already &str
takes_string(owned); // Moves ownership
takes_string(borrowed.to_string()); // Creates new String
// Common pattern: Accept &str for flexibility
fn flexible(s: impl AsRef<str>) {
let s: &str = s.as_ref();
}
Gotchas
Common mistakes
// ⚠️ Can't borrow as mutable while immutable borrow exists
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // Error!
// ✅ Fix: End immutable borrows first
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // r1 last use
let r2 = &mut s; // Ok: r1 no longer used
// ⚠️ Dangling reference
fn dangle() -> &String { // Error: missing lifetime
let s = String::from("hello");
&s // s dropped here
}
// ✅ Fix: Return owned value
fn no_dangle() -> String {
String::from("hello")
}
// ⚠️ Moving out of borrowed content
let v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // Error: v is borrowed
// ✅ Fix: Use indexing after modification
let mut v = vec![1, 2, 3];
v.push(4);
let first = &v[0];
Performance tips
// Use &str instead of String when possible
fn process(s: &str) {} // Accepts both &str and &String
// Avoid unnecessary clones
let s1 = String::from("hello");
let s2 = s1.clone(); // Expensive
let s3 = &s1; // Cheap
// Use iterators instead of for loops
// Slower
let mut sum = 0;
for i in 0..n {
sum += i;
}
// Faster (zero-cost abstraction)
let sum: i32 = (0..n).sum();
// Preallocate capacity
let mut v = Vec::with_capacity(1000); // Avoid reallocations
Type annotations
// Sometimes needed when compiler can't infer
let v: Vec<i32> = Vec::new();
let v = Vec::<i32>::new();
// Turbofish syntax
let numbers = "1,2,3";
let nums: Vec<i32> = numbers
.split(',')
.map(|s| s.parse().unwrap()) // Error: type unclear
.collect();
// Fix with turbofish
let nums: Vec<i32> = numbers
.split(',')
.map(|s| s.parse::<i32>().unwrap())
.collect();
Also see
- The Rust Programming Language Book (doc.rust-lang.org)
- Rust by Example (doc.rust-lang.org)
- The Cargo Book (doc.rust-lang.org)
- Rust Standard Library (doc.rust-lang.org)
- Rustonomicon (doc.rust-lang.org) - Advanced unsafe Rust
- Embedded Rust Book (docs.rust-embedded.org) - Embedded development
- Rust and WebAssembly (rustwasm.github.io) - WASM guide
- Rust Playground (play.rust-lang.org)
- docs.rs (docs.rs) - Crate documentation
- crates.io (crates.io)