Procedural & Derive Macros

Reviewed & published by Brayan K

Macros let Rust generate code at compile time. Procedural macros take this furthest — running real Rust over your source's tokens to power tools like serde's derives and web-framework routing attributes.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll meet the three kinds of proc macros, set up a proc-macro crate, and write a working derive macro with syn and quote — contrasting them with declarative macro_rules!.

What You'll Learn in This Lesson

1️⃣ Declarative Macros First (macro_rules!)

Before the procedural kind, know the simpler macro_rules! macros: they match patterns and substitute code, need no extra crate, and are strongly hygienic. Reach for these first.

// First, the SIMPLER kind: declarative macros with macro_rules!.
// They match patterns and substitute code. No separate crate needed.
macro_rules! my_vec {
    // Match zero or more comma-separated expressions.
    ( $( $x:expr ),* $(,)? ) => {{
        let mut v = Vec::new();
        $( v.push($x); )*   // repeat the push for each matched $x
        v
    }};
}

fn main() {
    let nums: Vec<i32> = my_vec![1, 2, 3];
    println!("{nums:?}");
}

The $( ... )* repetition expands once per matched expression — pure pattern substitution, no arbitrary code.

2️⃣ A Procedural-Macro Crate

Procedural macros are compiler plugins, so they live in their own crate marked proc-macro = true. That crate depends on syn (to parse tokens) and quote (to generate them). There are three kinds: derive, attribute-like, and function-like.

# A procedural macro lives in its OWN crate, flagged proc-macro = true.
# Cargo.toml of the macro crate:

[package]
name = "hello_derive"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true   # this is what makes it a proc-macro crate

[dependencies]
syn = "2"     # parse a TokenStream into a syntax tree
quote = "1"   # build a TokenStream from a Rust-like template

A proc-macro crate exports macros, not ordinary runtime items, which is why libraries often pair a macro crate with a regular one that re-exports it.

3️⃣ Writing a Derive Macro

A derive macro takes a TokenStream, parses it with syn into a syntax tree, builds new code with quote!, and returns the result. The flow is always parse, then generate, then return.

// A simple derive macro: #[derive(HelloWorld)] adds a hello() method.
// This lives in the proc-macro crate's lib.rs.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloWorld)]
pub fn hello_world(input: TokenStream) -> TokenStream {
    // 1. PARSE the incoming tokens into a syntax tree with syn.
    let ast = parse_macro_input!(input as DeriveInput);
    let name = &ast.ident; // the type's name, e.g. Pancakes

    // 2. GENERATE new code with quote!. #name interpolates the ident.
    let expanded = quote! {
        impl #name {
            fn hello() {
                println!("Hello from {}!", stringify!(#name));
            }
        }
    };

    // 3. RETURN the generated tokens for the compiler to inject.
    expanded.into()
}

// In another crate you would then write:
//   #[derive(HelloWorld)]
//   struct Pancakes;
//   fn main() { Pancakes::hello(); }   // -> Hello from Pancakes!

Inside quote!, #name splices in the parsed type's identifier — the engine behind real-world derives like serde's.

Mini-Challenge: A max2! Macro

Write a declarative macro_rules! that expands to the larger of two expressions.

// 🎯 MINI-CHALLENGE: a declarative macro for the max of two values.
//
// 1. Write  macro_rules! max2  that takes two expressions $a and $b.
// 2. Expand to a block that returns the larger using an if expression.
// 3. Call  max2!(3, 9)  in main and print the result.
//
// Expected output:  9

macro_rules! max2 {
    // your pattern here
}

fn main() {
    // println!("{}", max2!(3, 9));
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Macros

GoalRust Syntax
Declarative macromacro_rules! name { () => {} }
Mark proc-macro crate[lib] proc-macro = true
Derive macro#[proc_macro_derive(Name)]
Attribute macro#[proc_macro_attribute]
Parse tokensparse_macro_input!(input as DeriveInput)
Generate tokensquote! { impl #name { } }

🎉 Lesson Complete!

Practice quiz

How many kinds of procedural macros does Rust have?

  • Three
  • Five
  • One
  • Two

Answer: Three. There are three: derive macros, attribute-like macros, and function-like macros.

Which macro kind powers #[derive(Debug)]-style code generation?

  • An attribute macro
  • A function-like macro
  • A derive macro
  • A declarative macro

Answer: A derive macro. Derive macros generate code from a #[derive(...)] applied to a struct or enum.

Where must procedural macros be defined?

  • In a build.rs file
  • In a special crate with proc-macro = true
  • In any module
  • Inside main.rs only

Answer: In a special crate with proc-macro = true. Proc macros live in their own crate marked proc-macro = true in Cargo.toml.

What type do procedural macro functions take and return?

  • Vec<u8>
  • syn::File
  • String
  • proc_macro::TokenStream

Answer: proc_macro::TokenStream. Proc macros operate on proc_macro::TokenStream values: the tokens in, the generated tokens out.

What is the syn crate used for?

  • Parsing a TokenStream into a syntax tree
  • Sending HTTP requests
  • Running benchmarks
  • Formatting output

Answer: Parsing a TokenStream into a syntax tree. syn parses raw tokens into a structured Rust syntax tree you can inspect.

What does the quote crate provide?

  • A test runner
  • A quote! macro to turn Rust-like templates back into tokens
  • Async tasks
  • A logging facade

Answer: A quote! macro to turn Rust-like templates back into tokens. quote! lets you write Rust code as a template and produce the corresponding TokenStream.

Which attribute marks a function as a derive macro implementation?

  • #[macro_export]
  • #[proc_macro_attribute]
  • #[proc_macro]
  • #[proc_macro_derive(Name)]

Answer: #[proc_macro_derive(Name)]. A derive macro function is annotated with #[proc_macro_derive(Name)].

What does macro hygiene help prevent?

  • Memory leaks
  • Data races
  • Slow compilation
  • Accidental capture or collision of identifiers from the macro and call site

Answer: Accidental capture or collision of identifiers from the macro and call site. Hygiene keeps identifiers introduced by a macro from clashing with names at the call site.

How do declarative macros differ from procedural ones?

  • Declarative macros need a separate crate
  • Proc macros can't generate code
  • macro_rules! matches patterns; proc macros run arbitrary Rust on tokens
  • They are identical

Answer: macro_rules! matches patterns; proc macros run arbitrary Rust on tokens. macro_rules! uses pattern-based substitution, while proc macros execute real Rust code over TokenStreams.

Which kind would you use for a custom #[route("/path")] attribute on a function?

  • A derive macro
  • An attribute-like macro
  • A declarative macro
  • A function-like macro

Answer: An attribute-like macro. Attribute-like macros define new outer attributes such as #[route(...)] that transform the item they annotate.

Continue this course

Frequently asked questions

What are the three kinds of procedural macros?

Procedural macros come in three flavours, each triggered differently. Derive macros respond to #[derive(MyTrait)] on a struct or enum and generate additional items, typically trait implementations — this is how #[derive(Debug)] and serde's derives work. Attribute-like macros define brand-new attributes such as #[route("/")] that wrap or transform the item they are placed on, common in web frameworks. Function-like macros look like a function call ending in an exclamation mark, such as sql!(...), and operate on the tokens inside their parentheses. All three are 'procedural' because, unlike pattern-matching macro_rules!, they run arbitrary Rust code at compile time to produce their output.

Why do procedural macros need their own crate?

A procedural macro is essentially a compiler plugin: it runs during compilation of the crate that uses it, so it must be compiled first, separately, into a form the compiler can load. Rust enforces this by requiring proc macros to live in a dedicated crate whose Cargo.toml sets [lib] proc-macro = true. That crate can export proc macros but, by design, cannot also export ordinary runtime items for the same consumers, which is why ecosystems often pair a macro crate (like serde_derive) with a normal crate (serde) that re-exports the macros. Your application then depends on the regular crate and gets the macros transparently.

How do TokenStream, syn, and quote fit together?

The compiler hands your macro a proc_macro::TokenStream — a flat sequence of Rust tokens — and expects a TokenStream back. Working with raw tokens directly is painful, so two crates do the heavy lifting. The syn crate parses an incoming TokenStream into a rich syntax tree (for example a DeriveInput describing a struct, its name, and its fields) that you can inspect and pattern-match. The quote crate goes the other way: its quote! macro lets you write the code you want to generate as a Rust-like template, interpolating values with #name, and produces the corresponding TokenStream. The typical pipeline is parse with syn, build with quote, and return .into().

What is hygiene, and how do proc macros compare to macro_rules!?

Hygiene is the property that identifiers a macro introduces do not accidentally collide with, or capture, names at the call site, and vice versa — so a temporary variable your macro creates will not clash with one the user already has. Declarative macros (macro_rules!) are strongly hygienic and are defined inline with pattern-and-substitution rules; they are perfect for concise, repetitive code and need no extra crate. Procedural macros are far more powerful because they can run arbitrary logic over a full syntax tree, but they require a separate crate, the syn/quote tooling, and more care around hygiene and error reporting. The guidance is to reach for macro_rules! first and graduate to a proc macro only when you genuinely need to inspect or transform the structure of the code.