C.W.K.
Stream
Lesson 03 of 04 · published

Procedural Macros & derive

~11 min · macros, procedural, derive

Level 0Rust Curious
0 XP0/80 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete

macro_rules! matches patterns. Procedural macros go further: they're Rust functions that take a stream of tokens and return a stream of tokens — arbitrary code transforming code. The most important kind is one you've used constantly: derive.

The three kinds

Derive macros#[derive(Debug, Clone)] — generate trait impls from a struct or enum definition. Attribute macros#[tokio::main], #[test] — transform the item they annotate. Function-like macrossql!(...) — look like macro_rules! calls but run arbitrary Rust to produce the expansion.

You consume far more than you write

Writing a procedural macro is advanced — it lives in its own crate, parses tokens with the syn crate, and generates code with quote. Most Rustaceans never write one. But you use them constantly: every #[derive(...)], every #[tokio::main], every #[derive(Serialize)] is a procedural macro doing compile-time code generation for you.

derive is a procedural macro you use every day. #[derive(Debug, Clone, PartialEq)] isn't built-in syntax — it's a procedural macro that reads your struct and writes the trait impls. Understanding that demystifies the whole attribute-and-derive ecosystem: it's all code generation, triggered by annotations.

The cost-benefit

Procedural macros can eliminate enormous boilerplate — serde derives serialization for a 50-field struct from one line. The cost is compile time (they run a mini-compiler) and opacity (the generated code is invisible). That trade is almost always worth it for the well-tested ecosystem macros; think harder before writing your own.

Code

Procedural macros you invoke with one annotation·rust
// You don't write these — you USE them. Each is a procedural macro:

#[derive(Debug, Clone, PartialEq)] // derive macro: generates trait impls
struct Point { x: i32, y: i32 }

// #[tokio::main]    -> attribute macro: rewrites async main into a runtime
// #[derive(serde::Serialize)] -> serde's derive macro: generates ser/de

fn main() {
    let p = Point { x: 1, y: 2 };
    let q = p.clone();             // Clone — generated by derive
    println!("{p:?} == {q:?}: {}", p == q); // Debug + PartialEq, generated
}

External links

Exercise

Add #[derive(Debug, Clone, PartialEq, Default)] to a struct and use all four behaviors ({:?}, .clone(), ==, Type::default()). For each, state what the derive macro generated on your behalf. How many lines would you have hand-written without derive?
Hint
Each derive writes a full trait impl: Debug's formatter, Clone's field-by-field copy, PartialEq's comparison, Default's constructor. By hand that's dozens of lines; the four-word annotation replaces all of it.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.