Macros
Course 1 Chapter 1 flagged println!/vec!/format! as macros — the ! signals it — without explaining how they actually work. This chapter finally explains: what a macro really is, how to write a declarative macro, and a brief, honest look at procedural macros.
What a Macro Actually Is
A macro operates on code itself, not values, and runs entirely at compile time — taking Rust syntax as input and producing Rust syntax as output, which is then compiled normally. A function operates on values, at runtime. This is exactly why macros can do things a function fundamentally cannot: vec![1, 2, 3] expands into code that creates a Vec and pushes each element — a function couldn't accept a variable number of arguments the way vec! does, since a function's signature is fixed.
Declarative Macros with macro_rules!
Pattern-matching on the structure of the tokens passed in — similar in spirit to Course 1 Chapter 6's match, but matching code patterns instead of values:
$x:expr is a metavariable capturing an expression fragment. A repetition pattern reveals roughly how vec! itself works:
Fragment Specifiers
Just enough vocabulary to read real macro_rules! definitions in the wild:
expr— an expressionident— an identifier/namety— a typeblock— a block of codett— a single token tree, the most flexible, general catch-all
Procedural Macros: A Brief Look
Declarative macros match and substitute token patterns. Procedural macros are more powerful — actual Rust functions taking a TokenStream as input and producing a TokenStream as output, giving full programmatic control. Three kinds:
- Derive macros —
#[derive(Debug)],#[derive(Clone)]— generate trait implementations automatically - Attribute-like macros —
#[tokio::main]from Chapter 3, now fully explained - Function-like macros — look like
macro_rules!macros, but with arbitrary Rust code power (e.g.sqlx::query!)
Writing a procedural macro requires its own dedicated crate (proc-macro = true in Cargo.toml) — a genuinely advanced topic. This chapter's goal is recognizing and understanding them when used, not writing one from scratch.
| Declarative (macro_rules!) | Procedural | |
|---|---|---|
| Mechanism | Pattern-matching on token structure | Arbitrary Rust code operating on tokens |
| Where written | Inline, in any crate | Requires its own dedicated proc-macro crate |
| Powers | vec!, println!, custom function-like macros | #[derive(...)], #[tokio::main], sqlx::query! |
Macro vs. Function
Operates on code at compile time, not values at runtime — enabling variable-argument syntax a function can't.
macro_rules!
Pattern-matches token structure using metavariables like $x:expr.
Fragment Specifiers
expr, ident, ty, block, tt — the vocabulary for reading real macro definitions.
Procedural Macros
Derive, attribute-like, and function-like — full TokenStream-to-TokenStream Rust code.
#[tokio::main] (Chapter 3), #[test] (Course 2, Chapter 7), and #[derive(Debug)] are all procedural macros. This chapter retroactively explains attribute syntax that's appeared throughout the entire course without full explanation — the same kind of payoff Course 1 Chapter 5's Struct::new() gave for String::from.
Coding Challenges
Write a declarative macro max_of_two! that takes two expressions and expands to code returning whichever is larger, following this chapter's square! macro as a template.
📄 View solutionWrite a declarative macro print_all! that accepts a variable number of expressions (using the $($x:expr),* repetition pattern from this chapter's my_vec! example) and prints each one on its own line.
📄 View solutionExplain why #[derive(Debug)] is classified as a procedural macro rather than a declarative one, referencing what it actually needs to do (inspecting a specific struct's field names and types) that macro_rules!'s pattern-matching approach couldn't express.
📄 View solutionChapter 4 Quick Reference
- A macro operates on code at compile time; a function operates on values at runtime
- macro_rules! name { (pattern) => { expansion }; } — declarative, pattern-matching macros
- $x:expr — a metavariable; common specifiers: expr, ident, ty, block, tt
- $(...),* — repetition, the mechanism behind vec!'s variable-argument syntax
- Procedural macros — derive (#[derive(Debug)]), attribute-like (#[tokio::main]), function-like (sqlx::query!)
- Procedural macros require their own dedicated proc-macro crate; declarative macros don't
- Macro misuse produces a type error at the call site, after expansion — often less clear than a generic function's trait-bound error
- Next chapter: Performance & Memory — zero-cost abstractions, stack vs. heap, profiling tools, contrasting Rust's manual-but-safe model against Go's GC