24.3Procedural macros: an overview

Last updated June 13, 2026

macro_rules! (lessons 24.1 and 24.2) matches syntax patterns and substitutes code. That's the simpler half of Rust's macro system. The more powerful half is procedural macros, which are actual Rust functions that run at compile time, take your code as input, and produce new code as output, by programming, not pattern-matching. You've been using procedural macros since chapter 10 without knowing it: every #[derive(Debug)] is one. This lesson is a map, not a manual. The goal is to recognize the three kinds when you meet them and understand what they do, because you'll use procedural macros constantly and write them rarely, if ever, for a long time.

What makes them "procedural"

A declarative macro is a fixed set of pattern-to-template rules. A procedural macro is a function that manipulates code: it receives the tokens of your source as data, runs arbitrary Rust logic over them, and emits new tokens. "Procedural" means it generates code by executing a procedure, with loops, conditionals, whatever it needs, rather than by matching patterns. That's strictly more powerful: a procedural macro can inspect the structure of a type (its fields, their types) and generate code tailored to it, which pattern-matching can't do.

The cost of that power is that procedural macros must live in their own dedicated crate and are genuinely involved to write, you work directly with the compiler's representation of code. So this lesson stays at the level of "here's what each kind looks like and does." That's the right depth: knowing how to read them is useful immediately; knowing how to write them is a specialized skill you can learn the day you actually need it.

The three kinds

Procedural macros come in three flavors, distinguished by how you invoke them.

Derive macros generate code from a type, attached with #[derive(...)]. You've used these since lesson 16.5: #[derive(Debug)] reads your struct's fields and generates a Debug implementation that prints them; #[derive(Clone)] generates a clone that clones each field. The macro inspects the type's structure and writes the matching trait impl for you, saving you from hand-writing boilerplate that's mechanical but tedious.

#[derive(Debug, Clone)]   // two derive macros, each generating an impl
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    let q = p.clone();              // Clone's generated code
    println!("{p:?} {q:?}");        // Debug's generated code
}
Point { x: 1, y: 2 } Point { x: 1, y: 2 }

Each derive ran a procedural macro at compile time that examined Point and emitted a trait implementation. You wrote two words and got two full impl blocks.

Attribute macros attach to an item and transform it, written #[name] above a function, struct, or other item. The headline example you've already met is #[tokio::main] (lesson 23.3): it takes your async fn main and rewrites it into an ordinary main that starts the runtime. The attribute macro received your function as code, transformed it, and produced different code. Test frameworks, web routing (#[get("/")]), and many libraries use attribute macros to wrap or rewrite the items you tag.

Function-like macros look like macro_rules! calls, name!(...), but are backed by procedural code, so they can do arbitrary processing of their input. An example is sqlx::query!("SELECT ..."), which at compile time actually parses the SQL string and checks it against your database schema, something no pattern-matching macro could do. They're invoked like a declarative macro but powered by a full procedural function underneath.

You read these far more than you write them

Here's the practical posture, and it's deliberately humble. The crates you'll lean on most provide their power through procedural macros: serde (#[derive(Serialize, Deserialize)]) generates the code to convert your types to and from JSON and other formats, an enormous amount of correct, tedious code from one derive. Tokio gives you #[tokio::main]. Web frameworks give you routing attributes. Test tools give you #[test]-like attributes. Using these is easy: you write the annotation, the macro writes the code. That's the skill that matters, and you already have it.

Writing your own procedural macro is a real project, separate crate, parsing the token stream (usually with helper crates like syn and quote), handling every shape of input, and it's something even experienced Rust programmers do infrequently. The honest advice: learn to read procedural macros and use them fluently now; learn to write one only when you have a concrete need that nothing else solves, and treat that as an advanced, deliberate undertaking. There's no shame in using powerful macros for years without writing one.

Key insight

The macros you rely on most, #[derive(Debug)], #[derive(Serialize)], #[tokio::main], are procedural macros: compile-time functions that read your code's structure and generate tailored code from it. That's why a single #[derive(Serialize)] can replace hundreds of lines of conversion code. You don't need to write procedural macros to benefit enormously from them; the ecosystem's most useful libraries are essentially well-crafted procedural macros, and fluent use is the high-value skill. Reading and applying them well will serve you for years before writing one ever comes up.

Quiz time

Question #1

What's the core difference between a declarative (macro_rules!) macro and a procedural macro?

Show solution

A declarative macro is a fixed set of pattern-to-template rules: it matches the shape of input syntax and substitutes a template. A procedural macro is a compile-time function that receives your code as tokens, runs arbitrary Rust logic over it (loops, conditionals, inspecting structure), and emits new tokens. Procedural macros are more powerful, they can examine a type's fields and generate code tailored to it, but more involved to write (they live in their own crate).

Question #2

Name the three kinds of procedural macro and one example of each.

Show solution

(1) Derive macros, generate code from a type, attached with #[derive(...)], e.g. #[derive(Debug)] or #[derive(Serialize)]. (2) Attribute macros, transform an item they're attached to, e.g. #[tokio::main] rewriting async fn main. (3) Function-like macros, invoked like name!(...) but procedurally powered, e.g. sqlx::query! checking SQL against a schema at compile time.

Question #3

Why does the lesson say you'll read procedural macros far more than you write them?

Show solution

Because the most useful crates expose their power through procedural macros (serde's #[derive(Serialize)], Tokio's #[tokio::main], web-framework routing attributes), and using them is just writing an annotation, the macro generates the code. That's the high-value, easy skill. Writing a procedural macro is a substantial, specialized task (separate crate, parsing token streams with syn/quote) that even experienced Rust programmers do rarely. Fluent use serves you for years before writing one ever becomes necessary.

That completes the macro chapter. The summary and quiz (24.x) consolidate declarative and procedural macros and the judgment of when to use each. After it, chapter 25 takes the course into its most dangerous corner: unsafe Rust and talking to other languages, where, for the first time, you carry the safety guarantees the compiler has carried all along.