Macros
You have used macros since the first lesson: println!, format!, vec!,
assert_eq!, and the #[derive(...)] attributes on your structs. A macro is
code that writes code. The compiler expands each macro call into ordinary Rust
before it type-checks the program, so a macro can do things a function can't:
take any number of arguments, accept syntax that isn't a value (a type, a
name, a =>), and generate whole functions, impl blocks and types.
This lesson shows how to write your own declarative macros with
macro_rules!, and what the other kind, procedural macros, are and how
one is built.
A first macro
macro_rules! defines a macro. Its body is a list of rules; each rule has a
matcher on the left of => and a transcriber on the right:
macro_rules! shout {
($text:expr) => {
println!("{}!", $text.to_uppercase())
};
}
fn main() {
shout!("hello");
let name = "ferris";
shout!(name);
}HELLO!
FERRIS!The matcher ($text:expr) says: expect one expression, and call it $text.
The transcriber is the code that replaces the call, with $text substituted.
So shout!("hello") becomes println!("{}!", "hello".to_uppercase()), and
only then does the compiler check types. A macro works on syntax, not on
values.
A call can use (), [] or {}: shout!("hi"), shout!["hi"] and
shout! { "hi" } are the same. By convention you pick the one that reads
best, like vec![...] for a list.
Expressions stay whole
A $x:expr fragment is substituted as one unit, so operator precedence can't
break it apart the way it can with text-replacing macros in C:
macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(2 + 3));
}25It is still substituted twice, though. If the expression has a side effect, the side effect happens twice:
macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
let mut calls = 0;
let mut next_reading = || {
calls += 1;
4
};
println!("{}", square!(next_reading()));
println!("called {calls} times");
}16
called 2 timesThe fix is to evaluate the argument once into a variable. A transcriber
wrapped in double braces, {{ ... }}, expands to a block expression, so it
can hold let statements and still produce a value:
macro_rules! square {
($x:expr) => {{
let value = $x;
value * value
}};
}
fn main() {
let value = 3;
println!("{}", square!(value + 1));
}16The caller has a value too, and the two don't clash. Macros are
hygienic: a variable the macro declares lives in the macro's own naming
context, so it can't capture or overwrite the caller's value. (The outer
braces are the rule's delimiters; the inner pair is the block in the
generated code.)
Fragment specifiers
expr is one of several fragment specifiers. Each tells the macro what
kind of syntax to match:
| Specifier | Matches | Example |
|---|---|---|
expr | an expression | 2 + 3, f(x) |
ident | a name | total, Book |
ty | a type | Vec<u32> |
literal | a literal value | 42, "hi" |
pat | a pattern | Some(x), _ |
block | a block | { a + b } |
path | a path | std::io::Read |
stmt | a statement | let x = 1 |
item | an item: a function, struct, impl, ... | fn f() {} |
tt | a single token tree, anything in brackets | +, (a, b) |
Pick the narrowest one that fits. It gives callers better errors, and the compiler knows how the fragment can be used in the expansion.
Several rules
A macro can have several rules. The compiler tries them from top to bottom
and uses the first one that matches. Anything in a matcher that is not a
$ fragment has to appear literally in the call, which lets a macro accept
small bits of custom syntax:
macro_rules! seconds {
($n:literal h) => {
$n * 3600
};
($n:literal min) => {
$n * 60
};
($n:literal s) => {
$n
};
}
fn main() {
let total = seconds!(1 h) + seconds!(30 min) + seconds!(15 s);
println!("{total} seconds");
}5415 secondsh, min and s are not variables: they are tokens the caller has to
write. A call that matches no rule is a compile error at the call site.
Repetition
The reason vec![1, 2, 3] can take any number of elements is repetition. In a
matcher, $( ... ),* matches the part in parentheses zero or more times,
separated by commas. In the transcriber, $( ... )* repeats its contents once
for each match:
use std::collections::HashMap;
macro_rules! hash_map {
($($key:expr => $value:expr),* $(,)?) => {{
let mut map = HashMap::new();
$(
map.insert($key, $value);
)*
map
}};
}
fn main() {
let stock = hash_map! {
"apples" => 12,
"pears" => 0,
"plums" => 7,
};
println!("{} kinds", stock.len());
println!("apples: {}", stock["apples"]);
}3 kinds
apples: 12The call has three key => value pairs, so the expansion has three
map.insert(...) lines. $key and $value are used inside the same
$( )* in the transcriber, so they are repeated in step. $(,)? matches an
optional trailing comma, a small courtesy that lets callers put each entry on
its own line.
| Operator | Means |
|---|---|
$( ... )* | zero or more times |
$( ... )+ | one or more times |
$( ... )? | zero or one time |
The separator goes between the ) and the operator: , in $( ... ),*,
; in $( ... );*, or nothing for no separator.
Recursion
A macro can call itself, which is how you handle a list one element at a time. This one finds the largest of any number of values:
macro_rules! max_of {
($x:expr) => {
$x
};
($x:expr, $($rest:expr),+) => {{
let first = $x;
let rest = max_of!($($rest),+);
if first > rest { first } else { rest }
}};
}
fn main() {
println!("{}", max_of!(3, 9, 4));
println!("{}", max_of!(2.5, -1.0));
println!("{}", max_of!("pear", "apple", "fig"));
}9
2.5
pearThe second rule takes the first value, and hands the rest ($($rest),+) back
to max_of!, until one value is left and the first rule ends the recursion.
Because the expansion is plain code, it works for any type that supports >:
integers, floats and strings alike.
Generating items
Macros can expand to items, not only expressions: functions, structs, enums
and impl blocks. That is where they save the most typing. Here an ident
fragment names an enum and its variants, and the macro writes the enum along
with a method and a constant that would otherwise have to be kept in sync by
hand:
macro_rules! named_enum {
($name:ident { $($variant:ident),* $(,)? }) => {
#[derive(Clone, Copy)]
enum $name {
$($variant),*
}
impl $name {
const ALL: &[$name] = &[$($name::$variant),*];
fn name(self) -> &'static str {
match self {
$($name::$variant => stringify!($variant)),*
}
}
}
};
}
named_enum! { Planet { Mercury, Venus, Earth, Mars } }
fn main() {
for planet in Planet::ALL {
println!("{}", planet.name());
}
}Mercury
Venus
Earth
Mars$variant is used twice in the transcriber: once to declare the variants and
once in the match, and each use repeats over the same list. stringify!
turns tokens into a string literal at compile time, so Mercury becomes
"Mercury". Adding a planet is now one word in one place.
Generating the same impl for several types works the same way, with a ty
fragment for the type. It is the main reason macros appear in the standard
library's own source: most of the arithmetic traits for the dozen integer
types are written once, in a macro.
Where a macro can be used
Macros are defined textually: a macro_rules! macro can only be used below
its definition in the file (or in a module declared after it). This is
different from functions, which can be called before they are defined.
To use a macro from another crate, the defining crate marks it
#[macro_export]. That puts it at the root of the crate, and other crates
import it like anything else:
// in a library crate called `units`
#[macro_export]
macro_rules! seconds {
($n:literal min) => {
$n * 60
};
}
// in a crate that depends on `units`:
// use units::seconds;
// let timeout = seconds!(5 min);When a macro doesn't expand to what you expect, cargo expand (installed with
cargo install cargo-expand) prints your code with every macro expanded.
Procedural macros
Declarative macros match patterns. Procedural macros are Rust functions
that run at compile time: they receive the tokens of your code as a
TokenStream, and return a new TokenStream that the compiler uses instead.
There are three kinds:
| Kind | Looks like | Examples |
|---|---|---|
| Derive | #[derive(Serialize)] | serde's Serialize, thiserror |
| Attribute | #[route(GET, "/")] on an item | #[tokio::main], #[async_trait] |
| Function-like | sql!(SELECT ...) | sqlx::query!, html! |
A derive macro only adds code after the item it is on. An attribute macro
replaces the item with whatever it returns. A function-like macro is called
like a macro_rules! macro but can run any code to produce its output.
A procedural macro has to live in its own crate, marked proc-macro = true,
because the compiler builds it first and runs it while compiling the crates
that use it. Here is a small derive macro, TypeName, that gives a struct a
type_name() function. The project has the macro crate inside it:
invoices/
โโโ Cargo.toml
โโโ src/main.rs
โโโ type_name_derive/
โโโ Cargo.toml
โโโ src/lib.rsThe macro crate's Cargo.toml turns on proc-macro and pulls in the two
crates almost every procedural macro uses: syn parses the token stream into
a syntax tree, and quote turns Rust-like code back into tokens:
[package]
name = "type_name_derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
quote = "1"
syn = "2"The macro itself, in type_name_derive/src/lib.rs:
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
#[proc_macro_derive(TypeName)]
pub fn derive_type_name(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let label = name.to_string();
let expanded = quote! {
impl #name {
pub fn type_name() -> &'static str {
#label
}
}
};
expanded.into()
}#[proc_macro_derive(TypeName)] registers the function as the derive for
TypeName. parse_macro_input! parses the tokens of the struct into a
DeriveInput, which has the item's name (ident), its generics, and its
data: the fields of a struct or the variants of an enum, which a real derive
usually walks over. Inside quote!, #name inserts a variable, as an
identifier here, and #label inserts a String as a string literal.
The main crate depends on the macro crate by path, in its Cargo.toml:
[dependencies]
type_name_derive = { path = "type_name_derive" }and derives it like any built-in trait:
use type_name_derive::TypeName;
#[derive(TypeName)]
struct Invoice {
total_cents: u64,
}
fn main() {
let invoice = Invoice { total_cents: 1999 };
println!("{}: {} cents", Invoice::type_name(), invoice.total_cents);
}Invoice: 1999 centsProcedural macros are a large topic. The
syn docs and the Rust Reference's chapter on
procedural macros
go further.
When to write a macro
Macros are harder to read than functions, their errors point at generated code, and editors understand them less well. Reach for a function or generics first. A macro is the right tool when:
- the call needs a variable number of arguments, like
vec!orhash_map!; - the input is syntax, not a value: a type, a name, a small custom notation;
- the same items (
implblocks, functions, enums) would otherwise be written out many times with only names or types changing.
Common mistakes
A call that matches no rule
macro_rules! seconds {
($n:literal min) => {
$n * 60
};
($n:literal s) => {
$n
};
}
fn main() {
let timeout = seconds!(2 hours);
println!("{timeout}");
}The compiler tried every rule and none of them accepts hours. Either call
the macro with syntax one of its rules expects, or add a rule for it.
Using a macro above its definition
fn main() {
let total = double!(21);
println!("{total}");
}
macro_rules! double {
($x:expr) => {
$x * 2
};
}Unlike functions, macro_rules! macros exist only from their definition
downwards. Move the macro above the code that uses it, or into a module
declared before that code.
An expr followed by a word
macro_rules! between {
($value:expr in $low:expr, $high:expr) => {
$low <= $value && $value <= $high
};
}
fn main() {
println!("{}", between!(5 in 1, 10));
}An expression can go on for more tokens than it seems (5 in ... might be
the start of a longer expression in a future Rust), so the compiler only
allows =>, , or ; right after an expr fragment. Use one of those
(between!(5, 1, 10) or between!(5 => 1, 10)), or match a narrower
fragment such as literal or ident when that is all you need.
A repeated variable outside its repetition
macro_rules! sum {
($($n:expr),*) => {
0 + $n
};
}
fn main() {
println!("{}", sum!(1, 2, 3));
}$n was matched inside $( ... ),*, so it stands for a whole list, and it can
only be used inside a $( ... ) repetition in the transcriber too:
0 $(+ $n)* expands to 0 + 1 + 2 + 3.
Summary
- A macro is expanded into ordinary Rust before type checking. It can take any number of arguments and any syntax, and generate items.
macro_rules!holds rules of(matcher) => { transcriber }, tried top to bottom.$name:fragmentcaptures syntax:expr,ident,ty,literal,ttand others.- An
exprargument is substituted as a whole but evaluated each time it appears. Bind it once withletinside a{{ ... }}block. Macro variables are hygienic and don't clash with the caller's. $( ... ),*repeats in the matcher and the transcriber;+means one or more,?optional. Recursion handles a list one element at a time.macro_rules!macros are usable below their definition;#[macro_export]makes one available to other crates.- Procedural macros (derive, attribute, function-like) are compile-time
functions from
TokenStreamtoTokenStream, in aproc-macro = truecrate, usually built withsynandquote. - Prefer functions and generics; write a macro when they can't express it.
Practise what you read
Challenges that use the ideas from this lesson.