Level 3 · Advanced

Rust Advanced

Async, unsafe, macros, FFI, and performance — the deep end of the language.

Async/Await Tokio Send/Sync Pin Unsafe Macros FFI Performance

Async / Await

01

Rust's async fn returns a Future. Futures don't do anything until a runtime polls them — most code uses Tokio.

Cargo.toml
[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

MarkerMeans
SendSafe to transfer ownership across threads
Sync&T is safe to share across threads
!Sende.g. Rc<T>, raw pointers
!Synce.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

02

unsafe 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

03

Declarative 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

04

Rust 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));
    }
}
Exposing Rust to C
#[no_mangle]
pub extern "C" fn rust_doubled(x: i32) -> i32 {
    x * 2
}

Performance Tuning

05

Rust's defaults are fast, but a few habits push performance further.

TechniqueWhat it does
Release buildscargo build --release turns on optimizations (often 10-100× faster than debug).
LTOlto = "fat" in [profile.release] — cross-crate inlining.
codegen-unitsSet to 1 in release for max optimization (slower compile).
Iterator chainsCompile 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-allocateVec::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 guessUse cargo-flamegraph, perf, tokio-console, criterion for benchmarks.
Cargo.toml — aggressive release profile
[profile.release]
opt-level     = 3
lto           = "fat"
codegen-units = 1
strip         = "symbols"
panic         = "abort"

You made it

That's the full Rust track. Build something — a CLI, a TCP server with Tokio, a game with Bevy, a parser with nom. Then come back to Beginner or Mid for any topics you want to revisit.

Articles Tags Products