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!.
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"]
Source: The Book: Macros
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());
}
Source: Rust by Example: macro_rules!
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 |
|---|---|
|
an expression; in the 2024 edition this also matches |
|
an identifier or keyword ( |
|
a type: |
|
a path: |
|
a pattern, including top-level |
|
a statement without trailing |
|
a literal: |
|
a lifetime: |
|
a possibly empty visibility: |
|
the contents of an attribute: |
|
any single token tree: one token, or a group in |
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));
}
Source: The Rust Reference: Repetitions
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
}
Source: The Rust Reference: Hygiene
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 |
|
|
Attribute |
|
|
Function-like |
|
|
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()
}
Source: syn documentation and quote documentation
// 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`")
}
Source: syn documentation and quote documentation
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 bothmacro_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_spannedlets proc macros point at the user’s tokens instead. -
trybuildtests that macros produce the expected compile errors, andmacrotestsnapshots 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 |
unexpected end of macro invocation |
the input ended while a rule still expected tokens, usually a missing argument |
recursion limit reached while expanding |
a recursive macro (a TT muncher) needed more than 128 nested
expansions; raise |
cannot find macro |
a |
E0425 cannot find value |
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 |
item paths in
an expansion are resolved where the macro is used; write |
variable |
a metavariable matched inside |
|
the follow-set rules; separate the two
fragments with |
local ambiguity when calling macro |
two repetitions ( |
macros that expand to items must be delimited with braces or followed by a semicolon |
an item-producing macro was
invoked as |
E0658 |
the |
the |
the crate lacks
|
|
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 |
the derive is not imported ( |
cannot find attribute |
a field uses a helper attribute the derive did not declare; add it with
|
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 "));
}
Source: Error E0425 and The Rust Reference: Hygiene
$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
}
Source: The Little Book of Rust Macros: Counting (community)
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.
References
-
Rust by Example: macro_rules! (designators, overload, repeat, DRY, DSL, variadics)
-
The Little Book of Rust Macros (community)