A function, macro_rules!, or a procedural macro¶
Level: 201 · working knowledge
One line: Reach for a function first, macro_rules! when you need to take syntax or write items, and a procedural macro when the code has to stay where it is declared (#[derive], #[attr]) or a new identifier has to be built. On stable 1.98.0 those last two are the ones macro_rules! cannot do, and a procedural macro costs a crate of its own and a syn build.
One task, three ways¶
The task: list a struct's field names, and give each field a getter, get_x for x. Here it is done by a function, by macro_rules!, and by a derive:
//! One task three ways: list a struct's field names, and give each field a
//! getter.
/// A function cannot see `Point`'s fields. Every name is typed a second time,
/// and nothing checks the copies against the struct.
mod by_function {
pub struct Point {
pub x: i32,
pub y: i32,
}
pub fn field_names() -> &'static [&'static str] {
&["x", "y"]
}
impl Point {
pub fn get_x(&self) -> &i32 {
&self.x
}
pub fn get_y(&self) -> &i32 {
&self.y
}
}
}
/// `macro_rules!` can walk the fields, but only of a struct written inside the
/// call, and only in the shapes its pattern spells out.
macro_rules! with_field_names {
($vis:vis struct $name:ident { $($field_vis:vis $field:ident : $ty:ty),* $(,)? }) => {
$vis struct $name { $($field_vis $field: $ty),* }
impl $name {
pub const FIELD_NAMES: &[&str] = &[$(stringify!($field)),*];
}
};
}
mod by_macro_rules {
with_field_names! {
pub struct Point {
pub x: i32,
pub y: i32,
}
}
}
/// A derive reads the struct where it is declared, and makes `get_x` from `x`.
mod by_derive {
use field_names::FieldNames;
#[derive(FieldNames)]
pub struct Point {
pub x: i32,
pub y: i32,
}
}
fn main() {
let p = by_function::Point { x: 1, y: 2 };
let names = by_function::field_names();
println!("a function {names:?} get_x() + get_y() = {}", p.get_x() + p.get_y());
let p = by_macro_rules::Point { x: 1, y: 2 };
let names = by_macro_rules::Point::FIELD_NAMES;
println!("macro_rules! {names:?} no getters: x + y = {}", p.x + p.y);
let p = by_derive::Point { x: 1, y: 2 };
let names = by_derive::Point::FIELD_NAMES;
println!("a derive {names:?} get_x() + get_y() = {}", p.get_x() + p.get_y());
}
Verified output of cargo run -q -p getters_app — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
a function ["x", "y"] get_x() + get_y() = 3
macro_rules! ["x", "y"] no getters: x + y = 3
a derive ["x", "y"] get_x() + get_y() = 3
All three print the same names, and each got there differently.
- The function
field_names()knows nothing aboutPoint:["x", "y"]and both getters are typed by hand, and renaming a field leaves the list saying"x". A function takes values; it cannot take a type or an identifier, and it cannot declare animpl. macro_rules!does read the fields:$($field:ident : $ty:ty),*walks them, andstringify!($field)turns each into a string. But the struct has to be written inside the call, and the pattern accepts only the shapes it spells out, which here means no attributes on the fields and no generics. And it wrote no getters.- The derive sits on an ordinary struct declaration and builds
get_xfromx. Its source is the proc-macro crate below.
So "only a procedural macro can iterate over a struct's fields" is not quite right. A declarative macro can, if it is handed the struct. What it cannot do on stable is the two things the next section records.
Where macro_rules! stops¶
It cannot build an identifier¶
Making get_x out of get_ and x takes the ${concat(…)} metavariable expression:
//! `macro_rules!` making `get_x` out of `x`. `${concat(…)}` does it, and is
//! not stable in 1.98.0.
macro_rules! getters {
(struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
struct $name { $($field: $ty),* }
impl $name {
$(fn ${concat(get_, $field)}(&self) -> &$ty { &self.$field })*
}
};
}
getters! {
struct Point { x: i32, y: i32 }
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{}", p.get_x() + p.get_y());
}
Verified output of cargo build -q -p concat_ident, 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
--> concat_ident/src/main.rs:9:19
|
9 | $(fn ${concat(get_, $field)}(&self) -> &$ty { &self.$field })*
| ^^^^^^^^^^^^^^^^^^^^^^
|
= 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 `concat_ident` (bin "concat_ident") due to 1 previous error
When macro_rules! runs out goes through the rest of what a declarative macro cannot compute.
It cannot be a derive or an attribute¶
A macro_rules! macro can have derive() and attr() rules, which would let it sit on a struct the way a derive does:
//! `macro_rules!` used as a derive and as an attribute, so the struct stays
//! where it is declared. `derive()` and `attr()` rules do that, and neither is
//! stable in 1.98.0.
macro_rules! FieldNames {
derive() (struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
impl $name {
const FIELD_NAMES: &[&str] = &[$(stringify!($field)),*];
}
};
}
macro_rules! with_field_names {
attr() (struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
struct $name { $($field: $ty),* }
impl $name {
const FIELD_NAMES: &[&str] = &[$(stringify!($field)),*];
}
};
}
#[derive(FieldNames)]
struct Point {
x: i32,
y: i32,
}
#[with_field_names]
struct Size {
width: u32,
height: u32,
}
fn main() {
let (p, s) = (Point { x: 1, y: 2 }, Size { width: 3, height: 4 });
println!("{:?} {}", Point::FIELD_NAMES, p.x + p.y);
println!("{:?} {}", Size::FIELD_NAMES, s.width * s.height);
}
Verified output of cargo build -q -p rules_derive, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error[E0658]: `macro_rules!` derives are unstable
--> rules_derive/src/main.rs:5:1
|
5 | / macro_rules! FieldNames {
6 | | derive() (struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
7 | | impl $name {
8 | | const FIELD_NAMES: &[&str] = &[$(stringify!($field)),*];
9 | | }
10 | | };
11 | | }
| |_^
|
= note: see issue #143549 <https://github.com/rust-lang/rust/issues/143549> for more information
error[E0658]: `macro_rules!` attributes are unstable
--> rules_derive/src/main.rs:13:1
|
13 | / macro_rules! with_field_names {
14 | | attr() (struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
15 | | struct $name { $($field: $ty),* }
... |
20 | | };
21 | | }
| |_^
|
= note: see issue #143547 <https://github.com/rust-lang/rust/issues/143547> for more information
For more information about this error, try `rustc --explain E0658`.
error: could not compile `rules_derive` (bin "rules_derive") due to 2 previous errors
Both errors are E0658 with a tracking issue, so both features exist and are unstable. This page's answer is about stable Rust 1.98.0; if either stabilises, the line between the two kinds of macro moves with it.
The procedural macro that did it¶
//! `#[derive(FieldNames)]`: a list of a struct's field names, and a getter for
//! each field. The getter's name, `get_x`, is built from the field's, which is
//! the step `macro_rules!` cannot take on stable Rust.
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{Data, DeriveInput, Error, Fields, parse_macro_input};
#[proc_macro_derive(FieldNames)]
pub fn derive_field_names(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let Data::Struct(data) = &input.data else {
return Error::new_spanned(&input.ident, "FieldNames needs a struct")
.to_compile_error()
.into();
};
let Fields::Named(fields) = &data.fields else {
return Error::new_spanned(&input.ident, "FieldNames needs named fields")
.to_compile_error()
.into();
};
let name = &input.ident;
let fields: Vec<_> = fields.named.iter().map(|f| (f.ident.as_ref().unwrap(), &f.ty)).collect();
let names = fields.iter().map(|(field, _)| field.to_string());
let getters = fields.iter().map(|(field, ty)| {
let getter = format_ident!("get_{field}");
quote! { pub fn #getter(&self) -> &#ty { &self.#field } }
});
quote! {
impl #name {
pub const FIELD_NAMES: &[&str] = &[#(#names),*];
#(#getters)*
}
}
.into()
}
parse_macro_input! turns the tokens into a DeriveInput, the let … else lines reject anything but a struct with named fields, format_ident! builds get_x, and quote! writes the impl. Parsing with syn and Generating with quote take those steps one at a time. The derive ignores generics to stay short; Generics, lifetimes and where is what a real one adds.
What it costs¶
A crate of its own¶
proc-macro = true in its manifest, and a dependency from every crate that uses it. A proc-macro crate is about why. The function and the macro_rules! above live in main.rs.
Four crates in every clean build¶
Verified output of cargo tree -p getters_app — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
getters_app v0.1.0
└── field_names 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
The filter in cargo_runs.toml removes the folder Cargo prints after each path dependency, since that is an absolute path on the machine that ran it. (*) marks a crate whose dependencies are already shown higher up. The derive brings in proc-macro2, quote, syn and unicode-ident, and none of them ends up in the program: they are compiled for the compiler to run.
How long they take depends on the machine, so the numbers below are one machine's real runs, not an answer key. Each is a cargo build of the program crate in an empty target directory. "Without the derive" is the same main.rs with the by_derive module and its line in main deleted, in a package with no dependencies.
without the derive, three clean builds
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.74s
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.50s
with the derive, five clean builds
Finished `dev` profile [unoptimized + debuginfo] target(s) in 10.17s
Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.83s
Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.72s
Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.80s
Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.30s
with the derive, after touch app/src/main.rs
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
cargo build --timings on the third of those put syn alone at 3.87 s. The cost is paid once per clean target directory: a fresh clone, a CI run without a cache, a cargo clean. Editing the program afterwards does not recompile syn. Compile times covers the rest of a build's seconds.
Debugging through an expansion¶
A bug in a function is in the code you read. A bug in what a macro generated is in code nobody wrote down, and the compiler points at the macro call instead: the refusal on Expanding thiserror marks #[derive(Error)] with "in this derive macro expansion". cargo expand is how you see the code the error is about, and that page reads one expansion line by line.
The decision¶
| A function | macro_rules! |
A procedural macro | |
|---|---|---|---|
| Lives in | any module | any module | its own proc-macro = true crate |
| Takes | values | tokens that match a pattern you wrote | any tokens |
Can declare a struct, an impl or a fn |
no | yes | yes |
| Reads a struct's fields | no | only of a struct written inside the call | yes, from #[derive] on the declaration |
Can be #[derive(…)] or an attribute |
no | not on stable 1.98.0 (E0658 above) | yes |
Builds get_x from x |
no | not on stable 1.98.0 (E0658 above) | yes, format_ident! |
| Adds to a clean build | nothing | nothing | syn, quote, proc-macro2, unicode-ident |
Read it from the left and stop at the first column that does the job. The chapter's function-like pages go further into the middle column's limits — Why println! is a macro and A routes! macro check a string's contents at compile time — and the attribute pages into the right one, where a macro reads a function's signature: Parse, tweak, re-emit.
If you are coming from another language¶
- C and C++. The same ladder exists: a function, then a
#define, then a code generator run by the build.macro_rules!is the#definerung made safer: it matches token patterns rather than substituting text, and a captured$e:exprtravels inside an invisible group, so$e * 2cannot rebind1 + 1the way a bare#defineargument can (Tokens and token streams prints that group). Names are where C is ahead:##pastesget_andxintoget_x, and stable Rust'smacro_rules!has no equivalent. A procedural macro is the code-generator rung, a program that runs during the build, but it receives parsed tokens and runs inside the compiler instead of writing files for it to read. - Python. Python needs only the first rung, because a function can inspect a class at runtime:
dataclasses.fields(Point)lists a dataclass's fields, andsetattr(cls, "get_x", …)makes the getter. Rust has no runtime reflection, so the same work happens at compile time, in a macro. What changes is when you learn about a mistake: a typo in the generated code is a compile error, not anAttributeErroron the first call. - Java. The first rung is a method, and reflection (
Point.class.getDeclaredFields()) does at runtime what the derive does here. The compile-time route is an annotation processor, which, like a procedural macro, is a separately compiled program the compiler loads, and which also costs a build dependency. Java has nothing in the middle: there is nomacro_rules!rung, and arecordis the language building accessors in, the job this page's derive does. - ABAP. (Not machine-checked — CI cannot run ABAP.) A method first;
DEFINE … END-OF-DEFINITIONfor textual repetition, with no way to read a structure's components; and for that, the runtime type services, where acl_abap_structdescrlists a structure's components withget_components( ). There is no rung where your own code runs inside the ABAP compiler, so what a derive does before the program runs, RTTS does while it runs.
See also¶
- Expanding
thiserror— the previous page: the code one real derive writes - A proc-macro crate — the next page: the crate the right-hand column needs
- Three kinds of procedural macro — derive, function-like and attribute
- When
macro_rules!runs out — the same question for a function-like macro specifically - Macros — what the
!means at a call site - Compile times — where the rest of a build's seconds go
- The Rust Reference: Macros by example ↗
Po polsku¶
Wybór idzie od najprostszego narzędzia. Funkcja przyjmuje wartości; nie przyjmie typu ani identyfikatora i nie zadeklaruje impl. Makro deklaratywne (macro_rules!) dopasowuje wzorce tokenów, więc może przejść po polach struktury — ale tylko takiej, którą wpisano wewnątrz wywołania. Na stabilnym Ruscie 1.98.0 nie potrafi dwóch rzeczy: sklejić nowego identyfikatora (get_x z x; ${concat(…)} daje E0658) i działać jako #[derive] albo atrybut (reguły derive() i attr() też dają E0658). Makro proceduralne potrafi oba, ale kosztuje: osobny crate z proc-macro = true oraz zależności syn, quote, proc-macro2 i unicode-ident, które przy czystej kompilacji (clean build) na tej maszynie dodały około dziewięciu sekund — raz, a nie przy każdej zmianie programu.
Szukaj po polsku: makro deklaratywne czy proceduralne · macro_rules! a proc_macro · rust macro_rules vs proc macro · rust concat idents macro_rules · rust syn compile time