Rust Advanced
Async, unsafe, macros, FFI, and performance — the deep end of the language.
Async / Await
01Rust's async fn returns a Future. Futures don't do anything until a runtime polls them — most code uses Tokio.
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = "0.12"
use tokio::time::{sleep, Duration};
async fn fetch(url: &str) -> reqwest::Result<String> {
let body = reqwest::get(url).await?.text().await?;
Ok(body)
}
#[tokio::main]
async fn main() -> reqwest::Result<()> {
// Run two requests concurrently — both start before either awaits
let (a, b) = tokio::join!(
fetch("https://example.com"),
fetch("https://example.org"),
);
println!("a={} b={}", a?.len(), b?.len());
// Spawn a background task
tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
println!("Ran later");
});
sleep(Duration::from_secs(2)).await;
Ok(())
}
Send & Sync
| Marker | Means |
|---|---|
Send | Safe to transfer ownership across threads |
Sync | &T is safe to share across threads |
!Send | e.g. Rc<T>, raw pointers |
!Sync | e.g. RefCell<T>, Cell<T> |
The compiler auto-derives these. If your future captures a Rc across an .await, tokio::spawn won't accept it — use Arc instead.
Unsafe Rust
02unsafe unlocks five superpowers — dereferencing raw pointers, calling unsafe fns, accessing static muts, implementing unsafe traits, and accessing union fields. The borrow checker still runs on everything else inside the block.
fn split_at_mut(v: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = v.len();
assert!(mid <= len);
// The borrow checker can't prove these don't overlap, but we just did.
let ptr = v.as_mut_ptr();
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
Rule of thumb
Encapsulate unsafe in a safe API. The function above is safe to call — callers can't trigger UB no matter what arguments they pass.
Macros
03Declarative macros (macro_rules!) match patterns and expand to code. They run before type-checking and operate on token trees.
macro_rules! hashmap {
( $( $key:expr => $val:expr ),* $(,)? ) => {{
let mut m = ::std::collections::HashMap::new();
$( m.insert($key, $val); )*
m
}};
}
let scores = hashmap! {
"Ada" => 99,
"Linus" => 87,
"Grace" => 95,
};
Procedural Macros
Live in their own crate (proc-macro = true), take a TokenStream, return one. Three flavours — derive (#[derive(Serialize)]), attribute (#[route(GET, "/")]), and function-like (sql!("SELECT…")). The syn + quote crates are the standard toolkit.
FFI — Calling C
04Rust speaks C natively. Declare external functions in an extern "C" block, then call them from unsafe. Use #[repr(C)] on structs that cross the boundary.
use std::os::raw::c_int;
extern "C" {
fn abs(value: c_int) -> c_int;
}
fn main() {
unsafe {
println!("|-3| = {}", abs(-3));
}
}
#[no_mangle]
pub extern "C" fn rust_doubled(x: i32) -> i32 {
x * 2
}
Performance Tuning
05Rust's defaults are fast, but a few habits push performance further.
| Technique | What it does |
|---|---|
| Release builds | cargo build --release turns on optimizations (often 10-100× faster than debug). |
| LTO | lto = "fat" in [profile.release] — cross-crate inlining. |
| codegen-units | Set to 1 in release for max optimization (slower compile). |
| Iterator chains | Compile to the same code as hand-written loops — prefer them, they're clearer. |
Avoid clone() | Borrow with & or &mut first; clone only when you must. |
| Pre-allocate | Vec::with_capacity(n) avoids reallocations when size is known. |
| Inline hints | #[inline] for cross-crate hot functions; usually unnecessary within a crate. |
| Profile, don't guess | Use cargo-flamegraph, perf, tokio-console, criterion for benchmarks. |
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = "symbols"
panic = "abort"