Macros

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0, the 2024 edition, syn 3.0 and quote 1.0, checked on 2026-10-06. Macros are code that writes code: they run at compile time and expand into ordinary Rust before type checking. Rust has two kinds. Declarative macros (macro_rules!) match token patterns and substitute templates. Procedural macros are Rust functions that receive a token stream and return a new one; they power #[derive(...)], attribute macros such as #[tokio::main], and function-like macros such as sqlx::query!.

flowchart LR src["source text"] --> tok["tokens"] tok --> parse["parser builds the AST;
macro invocations are left as token trees"] parse --> exp{"expansion"} exp -- "macro_rules!: match a rule,
substitute the transcriber" --> ast["new AST fragment"] exp -- "proc macro: run its Rust function
TokenStream in, TokenStream out" --> ast ast --> parse2["parsed and inserted in place;
repeat until no macros remain"] parse2 --> later["name resolution, type checking, borrow checking"]

Why macros

Functions cannot take a variable number of arguments, cannot generate items (structs, impls), cannot inspect the syntax of their arguments, and cannot be evaluated before type checking. Macros can. println! checks its format string at compile time, vec![a, b, c] takes any number of elements, #[derive(Debug)] generates an impl from a struct definition. The cost: macros are harder to write, read and debug, so prefer functions and generics whenever they suffice.

Declarative macros: macro_rules!

A macro_rules! macro is a list of rules, each a matcher and a transcriber. The first rule whose matcher matches the input is expanded. Matchers use metavariables $name:fragment and repetitions $( ... ) sep rep:

/// Builds a HashMap from `key => value` pairs: shelf_map!{"Dune" => 1965, "Emma" => 1815}
macro_rules! shelf_map {
    () => { ::std::collections::HashMap::new() };
    ( $( $key:expr => $value:expr ),+ $(,)? ) => {{     // one or more pairs, optional trailing comma
        let mut map = ::std::collections::HashMap::new();
        $( map.insert($key, $value); )+                   // repeat the statement for each pair
        map
    }};
}

fn main() {
    let years = shelf_map! { "Dune" => 1965, "Emma" => 1815, };
    let empty: std::collections::HashMap<&str, u16> = shelf_map!();
    println!("{:?} {}", years.get("Dune"), empty.len());
}

A macro can be invoked with (), [] or {}; by convention vec![...] uses brackets, statement-like macros use braces, and expression-like ones parentheses. A macro used as a statement or item with () or [] needs a trailing semicolon.

Fragment specifiers

Specifier Matches

expr

an expression; in the 2024 edition this also matches const { ... } blocks and _ (use expr_2021 for the old behaviour)

ident

an identifier or keyword (foo, self), not _

ty

a type: u32, Vec<String>, &'a str

path

a path: std::io::Error, T::Item

pat

a pattern, including top-level | alternatives (2021+); pat_param excludes |

stmt, block, item

a statement without trailing ;, a { ... } block, an item (function, struct, impl, …​)

literal

a literal: 42, "title", -1

lifetime

a lifetime: 'a

vis

a possibly empty visibility: pub, pub(crate), nothing

meta

the contents of an attribute: derive(Debug), doc = "..."

tt

any single token tree: one token, or a group in (), [] or {}

Each specifier has follow-set rules restricting what may come next in the matcher (an expr may only be followed by =>, , or ;), so that future syntax cannot change how existing macros parse. The 2024 edition changed expr to match const blocks and _; cargo fix --edition rewrites affected macros to expr_2021.

Repetitions, recursion and TT munchers

Repetitions use * (zero or more), + (one or more) or ? (zero or one), with an optional separator. Nested repetitions must be transcribed at the same depth they were matched. Macros can call themselves recursively, which allows processing input piece by piece — the TT muncher pattern, which consumes one token tree per step:

/// Defines a fieldless enum plus a `ALL` array and `name()` -- a common code-generation chore.
macro_rules! labelled_enum {
    ($vis:vis enum $name:ident { $( $variant:ident = $label:literal ),* $(,)? }) => {
        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
        $vis enum $name { $( $variant ),* }

        impl $name {
            pub const ALL: &'static [$name] = &[ $( $name::$variant ),* ];
            pub fn label(self) -> &'static str {
                match self { $( $name::$variant => $label ),* }
            }
        }
    };
}

/// Counts token trees with recursion: count!(a b c) == 3.
macro_rules! count {
    () => { 0usize };
    ($head:tt $($tail:tt)*) => { 1usize + count!($($tail)*) };
}

labelled_enum! {
    pub enum Format { Hardcover = "hardcover", Paperback = "paperback", Ebook = "e-book" }
}

fn main() {
    for f in Format::ALL {
        print!("{} ", f.label());
    }
    println!("| {} formats, count = {}", Format::ALL.len(), count!(x y z));
}

Deep recursion hits the recursion_limit (128 by default, raised with #![recursion_limit = "256"]).

Hygiene, $crate and scoping

macro_rules! macros are partly hygienic: local variables introduced inside the macro cannot clash with, or capture, variables at the call site. Items and paths are not hygienic, so exported macros must refer to their own crate’s items with $crate::path (which resolves to the defining crate wherever the macro is used) and to std items with absolute paths (::std::..., ::core::...), as shelf_map! does above.

macro_rules! double {
    ($e:expr) => {{
        let tmp = $e;   // `tmp` is private to the macro: hygiene
        tmp * 2
    }};
}

fn main() {
    let tmp = 10;
    println!("{}", double!(tmp + 1)); // 22: the caller's `tmp` and the macro's `tmp` do not clash
}

Scoping: a macro_rules! macro is usable only after its definition in the source (textual scope), within the module and its children. #[macro_export] places it at the crate root of the defining crate and makes it importable by path (use shelf::shelf_map;) from other crates. Since the 2018 edition, macros from dependencies are imported with use like other items; #[macro_use] extern crate is legacy.

Procedural macros

A procedural macro is a function in a special crate (proc-macro = true in [lib]) that the compiler loads and runs at compile time. There are three kinds:

Kind Declared with Used as

Derive

#[proc_macro_derive(Builder)]

#[derive(Builder)] on a struct, enum or union; adds items

Attribute

#[proc_macro_attribute]

#[route(GET, "/")] on an item; replaces the item

Function-like

#[proc_macro]

sql!(SELECT ...); replaces the invocation

The function receives and returns a proc_macro::TokenStream. In practice everyone uses three crates: syn parses tokens into a Rust syntax tree, quote turns Rust-like templates back into tokens, and proc-macro2 provides token types usable outside the compiler (for tests).

A #[derive(Builder)] example

The shelf workspace has a shelf-macros crate with a derive macro that generates a builder for any struct with named fields:

# shelf-macros/Cargo.toml
[package]
name = "shelf-macros"
version = "0.1.0"
edition = "2024"

[lib]
proc-macro = true

[dependencies]
syn = { version = "3.0.6", features = ["full"] }
quote = "1.0.47"
proc-macro2 = "1.0.107"
// shelf-macros/src/lib.rs
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{Data, DeriveInput, Fields, parse_macro_input};

/// Derives a `FooBuilder` type with one setter per field and a `build` method.
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let builder = format_ident!("{name}Builder");

    let Data::Struct(data) = &input.data else {
        return syn::Error::new_spanned(&input, "Builder can only be derived for structs").to_compile_error().into();
    };
    let Fields::Named(fields) = &data.fields else {
        return syn::Error::new_spanned(&input, "Builder needs named fields").to_compile_error().into();
    };

    let names: Vec<_> = fields.named.iter().map(|f| f.ident.as_ref().expect("named field")).collect();
    let types: Vec<_> = fields.named.iter().map(|f| &f.ty).collect();

    let expanded = quote! {
        #[derive(Default)]
        pub struct #builder {
            #( #names: ::core::option::Option<#types>, )*
        }

        impl #builder {
            #(
                pub fn #names(mut self, value: impl ::core::convert::Into<#types>) -> Self {
                    self.#names = ::core::option::Option::Some(value.into());
                    self
                }
            )*

            pub fn build(self) -> ::core::result::Result<#name, ::std::string::String> {
                ::core::result::Result::Ok(#name {
                    #( #names: self.#names.ok_or(concat!("missing field `", stringify!(#names), "`"))?, )*
                })
            }
        }

        impl #name {
            pub fn builder() -> #builder {
                #builder::default()
            }
        }
    };
    expanded.into()
}
// any crate that depends on shelf-macros
use shelf_macros::Builder;

#[derive(Debug, Builder)]
pub struct SearchQuery {
    text: String,
    limit: u32,
    include_lent: bool,
}

fn main() {
    let q = SearchQuery::builder().text("dune").limit(20u32).include_lent(false).build();
    println!("{q:?}");
    let missing = SearchQuery::builder().text("emma").build();
    println!("{missing:?}"); // Err("missing field `limit`")
}

Notes on the implementation: errors are reported with syn::Error::to_compile_error, which points the user’s compiler error at the offending span instead of panicking; every path in the generated code is absolute (::core::option::Option) so it works whatever the user has imported; and #(...)* in quote! mirrors $(...)* in macro_rules!. Procedural macros can also declare helper attributes (#[proc_macro_derive(Builder, attributes(builder))] enables #[builder(default)] on fields), which is how serde’s #[serde(rename = ...)] works.

cfg_select! (1.95)

cfg_select! chooses between blocks of code by cfg predicate, in order, with _ as the fallback — the standard replacement for the cfg-if crate. It works both at item level and in expression position:

cfg_select! {
    unix => {
        fn data_dir() -> &'static str { "/var/lib/shelf" }
    }
    windows => {
        fn data_dir() -> &'static str { r"C:\ProgramData\shelf" }
    }
    _ => {
        fn data_dir() -> &'static str { "./shelf-data" }
    }
}

fn main() {
    let build = cfg_select! { debug_assertions => "debug build", _ => "release build" };
    println!("{} ({build})", data_dir());
}

Source: std::cfg_select

Rust 1.99 also allows attribute macros on non-inline module declarations (#[my_macro] mod foo;), which previously had to be written as inline modules for the macro to see their contents.

Debugging macros

  • cargo expand (a cargo plugin, cargo install cargo-expand) prints the crate after macro expansion — the single most useful tool for both macro_rules! and procedural macros; rust-analyzer’s Expand macro recursively does the same for one invocation.

  • Errors inside expanded code point at the macro invocation; syn::Error::new_spanned lets proc macros point at the user’s tokens instead.

  • trybuild tests that macros produce the expected compile errors, and macrotest snapshots expansions.

When not to write a macro

Write a function, a generic, a trait or a const fn first. Reach for a macro when you genuinely need one of the things only macros can do: variadic syntax, generating items or impls, compile-time checks of literals, or a small embedded syntax. Procedural macros add a compile-time dependency (syn is not small) and make code harder to navigate; many projects keep them for derives that remove large amounts of boilerplate.

Common errors

Most macro errors have no error code: they are reported while the macro is matched or expanded, before the usual name resolution and type checking that assign codes. The messages below are the ones Rust 1.99 prints; each was reproduced while writing this page.

Error Cause and fix

no rules expected :

the input matches no rule’s matcher; the message points at the first token that no rule accepts. Compare the invocation with the matchers (here "Dune": 1965 against $key:expr => $value:expr), and add a $(,)? if a trailing comma should be accepted

unexpected end of macro invocation

the input ended while a rule still expected tokens, usually a missing argument

recursion limit reached while expanding count!

a recursive macro (a TT muncher) needed more than 128 nested expansions; raise #![recursion_limit = "256"] at the crate root, or rewrite the macro with a repetition instead of recursion (see below)

cannot find macro double in this scope

a macro_rules! macro was used above its definition, or outside the module where it is in textual scope; move the definition up, re-export it with pub(crate) use double;, or use #[macro_export]

E0425 cannot find value tax in this scope

hygiene: the macro body refers to a local variable of the call site, which it cannot see; pass the value in as an argument

E0425 cannot find function normalise in this scope

item paths in an expansion are resolved where the macro is used; write $crate::module::normalise (see the example below)

variable k is still repeating at this depth

a metavariable matched inside $(...)* is used outside a repetition in the transcriber; wrap the use in $(...)* at the same depth

$e:expr is followed by $b:block, which is not allowed for expr fragments

the follow-set rules; separate the two fragments with =>, , or ;

local ambiguity when calling macro m: multiple parsing options

two repetitions ($($a:ident)* $($b:ident)*) can both match the same tokens; put a separator token between them

macros that expand to items must be delimited with braces or followed by a semicolon

an item-producing macro was invoked as labelled!(Hardcover); add a ; or invoke it with braces

E0658 macro is experimental

the pub macro name(...) { ... } syntax (macros 2.0, decl_macro) is nightly-only; use macro_rules! on stable

the proc_macro_derive attribute is only usable with crates of the proc-macro crate type

the crate lacks proc-macro = true in its [lib] section

proc-macro crate types currently cannot export any items other than functions tagged with #[proc_macro], …​

a proc-macro crate exported a type or helper function; move shared code into an ordinary library crate that both the macro crate and its users depend on

cannot find derive macro Builder in this scope

the derive is not imported (use shelf_macros::Builder;) or the crate is not a dependency

cannot find attribute builder in this scope

a field uses a helper attribute the derive did not declare; add it with #[proc_macro_derive(Builder, attributes(builder))]

The two hygiene errors are the ones most often met in practice. A macro_rules! macro that calls a helper function by its bare name works where the helper is in scope and fails everywhere else:

mod catalog {
    pub fn normalise(title: &str) -> String {
        title.trim().to_lowercase()
    }

    macro_rules! norm {
        ($title:expr) => {
            normalise($title) // error[E0425]: cannot find function `normalise` in this scope
        };
    }
    pub(crate) use norm;
}

fn main() {
    println!("{}", catalog::norm!("  DUNE "));
}

$crate always names the crate that defines the macro, so a full path through it resolves from any call site, in this crate or in another one:

mod catalog {
    pub fn normalise(title: &str) -> String {
        title.trim().to_lowercase()
    }

    macro_rules! norm {
        ($title:expr) => {
            $crate::catalog::normalise($title) // resolved from the defining crate's root
        };
    }
    pub(crate) use norm;
}

fn main() {
    println!("{}", catalog::norm!("  DUNE ")); // dune
}

The recursion limit is easiest to avoid by not recursing. Counting with a repetition expands to a single slice whose length is a constant, however many tokens there are:

macro_rules! replace_with_unit {
    ($_t:tt) => {
        ()
    };
}

/// Counts token trees without recursion, so the recursion limit never applies.
macro_rules! count_tts {
    ($($t:tt)*) => {
        <[()]>::len(&[$(replace_with_unit!($t)),*])
    };
}

fn main() {
    const N: usize = count_tts!(Dune Emma Kindred Beloved);
    println!("{N} titles"); // 4 titles
}

Compile-fail tests keep these messages from regressing in a macro crate: trybuild compiles small programs that are expected to fail and compares the compiler output with a stored .stderr file.

Where the books are outdated

The consulted books do not cover cfg_select! (1.95), the 2024 expr fragment change, or syn 3; most macro examples in older material use syn 1 or 2, whose parse_macro_input!/DeriveInput API is close to the version shown here.