When macro_rules! runs out¶
Level: 301 · deep dive
One line: macro_rules! can count its arguments, check a string literal through a const, and word its own errors; what it cannot do on stable Rust is write an identifier that is not in its input — incr_requests from requests — and that is where a function-like procedural macro starts to pay for its extra crate.
The task, done by a procedural macro¶
counters!(requests, errors) should define a struct with a u64 per name, and a method incr_<name> for each:
counters_macro::counters!(requests, errors);
fn main() {
let mut counts = Counters::new();
counts.incr_requests();
counts.incr_requests();
counts.incr_errors();
println!("requests = {}, errors = {}", counts.requests, counts.errors); // requests = 2, errors = 1
}
Verified output of cargo run -q -p counters_app — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
The macro parses a comma-separated list of identifiers and builds one new identifier per name with format_ident!:
//! `counters!(requests, errors)`: a struct with one `u64` per name, and an
//! `incr_<name>` method for each.
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::punctuated::Punctuated;
use syn::{Ident, Token, parse_macro_input};
#[proc_macro]
pub fn counters(input: TokenStream) -> TokenStream {
let names = parse_macro_input!(input with Punctuated::<Ident, Token![,]>::parse_terminated);
let fields: Vec<&Ident> = names.iter().collect();
// The identifier `macro_rules!` cannot write. `format_ident!` gives it the
// span of `name`, the name as the caller typed it.
let methods: Vec<Ident> = fields.iter().map(|name| format_ident!("incr_{}", name)).collect();
quote! {
pub struct Counters {
#(pub #fields: u64,)*
}
impl Counters {
pub fn new() -> Self {
Self { #(#fields: 0,)* }
}
#(pub fn #methods(&mut self) {
self.#fields += 1;
})*
}
}
.into()
}
cargo expand shows what the call became (recorded with the command underneath it, cargo rustc -- -Zunpretty=expanded):
Verified output of cargo rustc -q -p counters_app --profile=check -- -Zunpretty=expanded — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
#![feature(prelude_import)]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
pub struct Counters {
pub requests: u64,
pub errors: u64,
}
impl Counters {
pub fn new() -> Self { Self { requests: 0, errors: 0 } }
pub fn incr_requests(&mut self) { self.requests += 1; }
pub fn incr_errors(&mut self) { self.errors += 1; }
}
fn main() {
let mut counts = Counters::new();
counts.incr_requests();
counts.incr_requests();
counts.incr_errors(); // requests = 2, errors = 1
{
::std::io::_print(format_args!("requests = {0}, errors = {1}\n",
counts.requests, counts.errors));
};
}
Two iterators sit in the same #(…)* repetition, #methods and #fields, and quote! walks them in step. Generating with quote covers repetition and format_ident! in full; Parsing arbitrary tokens covers parse_terminated.
The same task in macro_rules!¶
Everything except the method names translates line for line:
//! `counters!` as a `macro_rules!` macro. The struct and its fields expand
//! fine; the method names do not.
macro_rules! counters {
($($name:ident),* $(,)?) => {
pub struct Counters {
$(pub $name: u64,)*
}
impl Counters {
pub fn new() -> Self {
Self { $($name: 0,)* }
}
$(pub fn incr_$name(&mut self) {
self.$name += 1;
})*
}
};
}
counters!(requests, errors);
fn main() {
let mut counts = Counters::new();
counts.incr_requests();
println!("requests = {}", counts.requests);
}
Verified output of cargo build -q -p rules_pasted, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error: missing parameters for function definition
--> rules_pasted/src/main.rs:15:27
|
15 | $(pub fn incr_$name(&mut self) {
| ^
...
22 | counters!(requests, errors);
| --------------------------- in this macro invocation
|
= note: this error originates in the macro `counters` (in Nightly builds, run with -Z macro-backtrace for more info)
help: add a parameter list
|
15 | $(pub fn incr_()$name(&mut self) {
| ++
error: expected one of `->`, `<`, `where`, or `{`, found identifier `requests`
--> rules_pasted/src/main.rs:15:27
|
10 | impl Counters {
| - while parsing this item list starting here
...
15 | $(pub fn incr_$name(&mut self) {
| ^^^^^ expected one of `->`, `<`, `where`, or `{`
...
18 | }
| - the item list ends here
...
22 | counters!(requests, errors);
| --------------------------- in this macro invocation
|
= note: this error originates in the macro `counters` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0599]: no method named `incr_requests` found for struct `Counters` in the current scope
--> rules_pasted/src/main.rs:26:12
|
6 | pub struct Counters {
| ------------------- method `incr_requests` not found for this struct
...
26 | counts.incr_requests();
| ^^^^^^^^^^^^^ method not found in `Counters`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `rules_pasted` (bin "rules_pasted") due to 3 previous errors
incr_$name is not one identifier with a hole in it. The lexer read incr_ as a complete identifier when the macro was defined, and $name substitutes a second, whole token after it, so the parser sees fn incr_ requests(…): a function called incr_, with something that is not a parameter list after the name. The help is the parser's guess at that reading, not a fix. macro_rules! substitutes tokens; it never joins two into one. The third error follows from the first two: no incr_requests was defined, so main cannot call it.
The feature that does join them exists, and it is not stable:
Verified output of cargo build -q -p rules_concat, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error[E0658]: the `concat` meta-variable expression is unstable
--> rules_concat/src/main.rs:14:23
|
14 | $(pub fn ${concat(incr_, $name)}(&mut self) {
| ^^^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #124225 <https://github.com/rust-lang/rust/issues/124225> for more information
For more information about this error, try `rustc --explain E0658`.
error: could not compile `rules_concat` (bin "rules_concat") due to 1 previous error
Only the method line changed, to pub fn ${concat(incr_, $name)}(&mut self). ${concat(…)} is a metavariable expression, and in 1.98.0 it is behind a feature gate. It replaced the older concat_idents!, which std lists as removed since 1.90.0 (std/src/lib.rs at 1.98.0 ↗).
This is the exact point where macro_rules! stops. On stable, a name that is not already a token in the input can only come from a procedural macro. The crate people reach for is paste ↗, a #[proc_macro] whose documentation shows it used from inside a macro_rules! macro, pasting [<get_ $field>] into one identifier.
What macro_rules! can still do¶
The pull toward a procedural macro often starts earlier than it has to. None of these needs one:
//! Three jobs that sound like they need a procedural macro, done with
//! `macro_rules!` and a `const fn`.
/// Counts its arguments without recursion: one `()` per name, in a slice
/// whose length is a constant.
#[macro_export]
macro_rules! count {
($($name:ident),* $(,)?) => {
<[()]>::len(&[$($crate::count!(@unit $name)),*])
};
(@unit $name:ident) => {
()
};
}
/// Checks a string literal's contents while compiling. The macro cannot look
/// inside the literal, but a `const` it expands to can.
#[macro_export]
macro_rules! hex {
($text:literal) => {{
const _: () = assert!($crate::all_hex($text), "not a hex string");
$text
}};
}
/// An error message of the macro's own, naming the token it refused.
#[macro_export]
macro_rules! method {
(GET) => {
"GET"
};
(POST) => {
"POST"
};
($other:ident) => {
compile_error!(concat!("unknown method `", stringify!($other), "`"))
};
}
pub const fn all_hex(text: &str) -> bool {
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
if !bytes[i].is_ascii_hexdigit() {
return false;
}
i += 1;
}
true
}
use still_rules::{count, hex, method};
const NAMES: usize = count!(requests, errors, retries);
fn main() {
let slots = [0u64; count!(requests, errors)];
println!("{NAMES} names, {} slots", slots.len()); // 3 names, 2 slots
println!("{}", hex!("c0ffee")); // c0ffee
println!("{}", method!(POST)); // POST
}
Verified output of cargo run -q -p still_rules --bin still_rules — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
- Counting.
count!expands to a slice with one()per name and takes its length. No recursion over the list, and the result is a constant:count!(requests, errors)sizes an array. So the claim thatmacro_rules!can only count by recursing is wrong. - Checking a literal's contents. The macro cannot see inside
"c0ffee": to a matcher, aliteralis one token. It can hand the literal to aconst fn, and a failingassert!in aconststops compilation. - An error of its own.
compile_error!takes a message built withconcat!andstringify!, so the message can name the token it refused.
Where they fall short is the underline:
use still_rules::hex;
fn main() {
println!("{}", hex!("coffee"));
}
Verified output of cargo build -q -p still_rules --bin bad_hex, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error[E0080]: evaluation panicked: not a hex string
--> still_rules/src/bin/bad_hex.rs:4:20
|
4 | println!("{}", hex!("coffee"));
| ^^^^^^^^^^^^^^ evaluation of `main::_` failed here
|
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `hex` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0080`.
error: could not compile `still_rules` (bin "bad_hex") due to 1 previous error
use still_rules::method;
fn main() {
println!("{}", method!(PATCH));
println!("{}", method!("GET"));
}
Verified output of cargo build -q -p still_rules --bin bad_method, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error: unknown method `PATCH`
--> still_rules/src/bin/bad_method.rs:4:20
|
4 | println!("{}", method!(PATCH));
| ^^^^^^^^^^^^^^
|
= note: this error originates in the macro `method` (in Nightly builds, run with -Z macro-backtrace for more info)
error: no rules expected `"GET"`
--> still_rules/src/bin/bad_method.rs:5:28
|
5 | println!("{}", method!("GET"));
| ^^^^^ no rules expected this token in macro call
|
note: while trying to match `GET`
--> still_rules/src/lib.rs:29:6
|
29 | (GET) => {
| ^^^
error: could not compile `still_rules` (bin "bad_method") due to 2 previous errors
Both messages the macro wrote underline the whole call, hex!("coffee") and method!(PATCH), never the literal or the identifier inside it. The one precise underline is the compiler's own: when no rule matches, no rules expected points at the token, here "GET" written as a string. A procedural macro puts a syn::Error on any token it likes, which is what A routes! macro does with a {param} its handler does not take.
What the procedural version costs¶
Verified output of cargo tree -p counters_app -p still_rules -e normal — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
counters_app v0.1.0
└── counters_macro v0.1.0 (proc-macro)
├── proc-macro2 v1.0.107
│ └── unicode-ident v1.0.25
├── quote v1.0.47
│ └── proc-macro2 v1.0.107 (*)
└── syn v3.0.6
├── proc-macro2 v1.0.107 (*)
├── quote v1.0.47 (*)
└── unicode-ident v1.0.25
still_rules v0.1.0
still_rules has no dependencies. counters_macro brings syn, quote, proc-macro2 and unicode-ident, all compiled before the first line of counters_app. And it is a crate of its own, of a kind that exports nothing but macros; rules_pasted defines its macro in the same file that calls it.
[package]
name = "counters_macro"
version = "0.1.0"
edition = "2024"
publish = false
[lib]
proc-macro = true
[dependencies]
syn = { version = "3", features = ["full", "extra-traits"] }
quote = "1"
proc-macro2 = "1"
A proc-macro crate is why that [lib] table has to be there, and A function, macro_rules!, or a procedural macro weighs what the dependencies cost.
The decision, for a function-like macro¶
| You need | macro_rules! |
A #[proc_macro] |
|---|---|---|
| Syntax a function cannot take: any number of arguments, names, types | Yes | Yes |
| A count of the arguments | Yes, as a constant: <[()]>::len(&[…]) |
Yes |
A new identifier such as incr_requests |
No on stable: tokens are never joined, ${concat(…)} is unstable |
Yes: format_ident! |
| To reject a string literal by its contents | Yes, through a const fn; the error underlines the whole call |
Yes; the error underlines the literal |
| To turn a literal's contents into code: a pattern, a parameter | No: a matcher cannot see inside a literal | Yes: A routes! macro |
| A message of its own | Yes, compile_error!; underlines the whole call |
Yes, syn::Error; underlines any token |
| To live in the crate that uses it | Yes | No: a crate of its own, proc-macro = true |
| Dependencies to compile first | None | syn, quote, proc-macro2, unicode-ident |
Start with macro_rules!. Move when a row in bold is the one you need, or when the error your users see has to point somewhere more exact than the whole call.
If you are coming from another language¶
- C and C++. The preprocessor can do the one thing on this page
macro_rules!cannot:##pastes two tokens into one, so#define COUNTER(name) static unsigned long name; static void incr_##name(void) { name += 1; }definesincr_requests(checked with Apple clang 21:clang -Eprintsstatic void incr_requests(void)). It cannot look inside a string literal, and it pastes text without knowing what the text is. Rust's split is the other way round:macro_rules!matches structured fragments and will not paste, and pasting is a job for a procedural macro. - Python. Methods named at run time are ordinary:
setattr(Counters, f"incr_{name}", incr)in a loop buildsincr_requestswhile the module runs. The price is that a misspelt call,c.incr_retries(), is anAttributeErrorwhen that line runs (Python 3.14 even suggestsincr_errors). The Rust version builds the method while compiling, so the same typo is a compile error. - Java. No macros. An annotation processor or Lombok builds
getNamefromnameas a string and emits source or bytecode; that is the procedural macro's position, a program generating names, and it runs insidejavacthe way a procedural macro runs insiderustc. - ABAP. (Not machine-checked — CI cannot run ABAP.)
DEFINEmacros substitute placeholders, and a name built at run time is a dynamic call,CALL METHOD (lv_name), checked only when it runs. A method generated with a new name at compile time has no ABAP counterpart outside code generators.
See also¶
- A function,
macro_rules!, or a procedural macro — the same decision for every kind of macro, and what each option costs - Parsing arbitrary tokens — the
syn::parsemachinerycounters!uses one line of - Generating with
quote— repetition andformat_ident! - A
routes!macro — a job that needs the contents of a string literal as code - Macros —
macro_rules!from the caller's side - The Rust Reference: Macros by example ↗ — the fragment specifiers a
macro_rules!matcher accepts
Po polsku¶
macro_rules! potrafi więcej, niż się zwykle mówi. Policzy argumenty bez rekurencji (<[()]>::len(&[…]) daje stałą, którą można wpisać jako długość tablicy), sprawdzi zawartość literału przez const fn i assert! w bloku const, a przez compile_error! zgłosi własny komunikat błędu. Podkreślenie obejmuje wtedy jednak całe wywołanie makra, a nie token, który był zły.
Czego nie zrobi na stabilnym Ruście: nie zbuduje nowego identyfikatora. incr_$name to dwa tokeny, incr_ i podstawione requests, bo makro deklaratywne podstawia całe tokeny i nigdy ich nie skleja. Wyrażenie ${concat(incr_, $name)} robi dokładnie to, ale w 1.98.0 jest niestabilne (E0658), a starsze concat_idents! usunięto w 1.90.0. W tym miejscu opłaca się makro proceduralne funkcyjne: format_ident!("incr_{}", name) z quote. Kosztuje osobny crate i zależności syn, quote, proc-macro2, więc zaczynaj od macro_rules!, a przechodź dopiero wtedy, gdy potrzebujesz nowej nazwy, kodu zbudowanego z treści literału albo błędu wskazującego dokładny token.
Szukaj po polsku: macro_rules! a makra proceduralne · sklejanie identyfikatorów w makrach Rust · rust macro_rules concat identifier · rust macro_metavar_expr_concat · rust paste crate · rust count macro arguments