Macros

Course 3 · Ch 4
Macros
Finally explaining the ! from Course 1 Chapter 1 — and every attribute you've been typing without a full explanation since

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:

macro_rules! square { ($x:expr) => { $x * $x }; } let result = square!(5); // expands, at compile time, to: 5 * 5

$x:expr is a metavariable capturing an expression fragment. A repetition pattern reveals roughly how vec! itself works:

macro_rules! my_vec { ( $($x:expr),* ) => { { let mut v = Vec::new(); $( v.push($x); )* v } }; } let nums = my_vec![1, 2, 3];

Fragment Specifiers

Just enough vocabulary to read real macro_rules! definitions in the wild:

  • expr — an expression
  • ident — an identifier/name
  • ty — a type
  • block — a block of code
  • tt — 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
MechanismPattern-matching on token structureArbitrary Rust code operating on tokens
Where writtenInline, in any crateRequires its own dedicated proc-macro crate
Powersvec!, 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.

Full Circle: Every Attribute You've Been Typing
#[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.
Macros Are Type-Checked Only After Expansion, at the Call Site
A macro can compile fine in isolation but produce a confusing, hard-to-read type error at a completely different location — wherever it's actually called with the wrong kind of input — since type checking only happens after expansion, not at the macro's own definition. This genuinely produces worse error messages than a normal generic function with trait bounds (Course 2 Chapter 3) would give for similar misuse. A real, known downside worth stating honestly, not glossing over.

Coding Challenges

Challenge 1

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 solution
Challenge 2

Write 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 solution
Challenge 3

Explain 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 solution

Chapter 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