---
title: "24.3 Procedural macros: an overview"
description: "The three kinds of procedural macro behind derive, attributes, and function-like macros, what powers serde and tokio, and why you read them more than write them."
url: "https://learnrust.net/chapter-24/procedural-macros-overview/"
last_updated: "2026-06-13"
---

# 24.3 Procedural macros: an overview

`macro_rules!` (lessons [24.1](https://learnrust.net/chapter-24/declarative-macros/) and [24.2](https://learnrust.net/chapter-24/when-to-write-a-macro/)) 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](https://learnrust.net/chapter-16/deriving-traits/): `#[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.

```rust
#[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](https://learnrust.net/chapter-23/using-tokio/)): 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?

<details class="solution">
<summary>Show solution</summary>

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).

</details>

**Question #2**

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

<details class="solution">
<summary>Show solution</summary>

(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.

</details>

**Question #3**

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

<details class="solution">
<summary>Show solution</summary>

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.

</details>

That completes the macro chapter. The summary and quiz ([24.x](https://learnrust.net/chapter-24/chapter-24-summary-and-quiz/)) 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.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
