12. Macros — Code That Writes Code 🟡
# 12. 宏:生成代码的代码 🟡
What you’ll learn:
本章将学到什么:
- Declarative macros (
macro_rules!) with pattern matching and repetition
如何使用macro_rules!编写带模式匹配和重复规则的声明式宏- When macros are the right tool vs generics/traits
什么时候该用宏,什么时候该用泛型或 trait- Procedural macros: derive, attribute, and function-like
过程宏的三种形态:derive、attribute 和函数式宏- Writing a custom derive macro with
synandquote
如何借助syn与quote编写自定义 derive 宏
Declarative Macros (macro_rules!)
声明式宏 macro_rules!
Macros match patterns on syntax and expand to code at compile time:
宏会对语法模式做匹配,并在编译期把它们展开成代码:
#![allow(unused)]
fn main() {
// A simple macro that creates a HashMap
macro_rules! hashmap {
// Match: key => value pairs separated by commas
( $( $key:expr => $value:expr ),* $(,)? ) => {
{
let mut map = std::collections::HashMap::new();
$( map.insert($key, $value); )*
map
}
};
}
let scores = hashmap! {
"Alice" => 95,
"Bob" => 87,
"Carol" => 92,
};
// Expands to:
// let mut map = HashMap::new();
// map.insert("Alice", 95);
// map.insert("Bob", 87);
// map.insert("Carol", 92);
// map
}
Macro fragment types:
宏片段类型:
| Fragment 片段 | Matches 匹配内容 | Example 示例 |
|---|---|---|
$x:expr | Any expression 任意表达式 | 42, a + b, foo() |
$x:ty | A type 一个类型 | i32, Vec<String> |
$x:ident | An identifier 一个标识符 | my_var, Config |
$x:pat | A pattern 一个模式 | Some(x), _ |
$x:stmt | A statement 一条语句 | let x = 5; |
$x:tt | A single token tree 单个 token tree | Anything (most flexible) 几乎什么都行,最灵活 |
$x:literal | A literal value 字面量 | 42, "hello", true |
Repetition: $( ... ),* means “zero or more, comma-separated”
重复规则:$( ... ),* 的意思是“零个或多个,用逗号分隔”。
#![allow(unused)]
fn main() {
// Generate test functions automatically
macro_rules! test_cases {
( $( $name:ident: $input:expr => $expected:expr ),* $(,)? ) => {
$(
#[test]
fn $name() {
assert_eq!(process($input), $expected);
}
)*
};
}
test_cases! {
test_empty: "" => "",
test_hello: "hello" => "HELLO",
test_trim: " spaces " => "SPACES",
}
// Generates three separate #[test] functions
}
When (Not) to Use Macros
什么时候该用宏,什么时候别用
Use macros when:
下面这些情况适合用宏:
- Reducing boilerplate that traits/generics can’t handle (variadic arguments, DRY test generation)
想消除 trait、泛型搞不定的样板代码,例如可变参数、批量生成测试 - Creating DSLs (
html!,sql!,vec!)
要构造 DSL,比如html!、sql!、vec! - Conditional code generation (
cfg!,compile_error!)
需要按条件生成代码,例如cfg!、compile_error!
Don’t use macros when:
下面这些情况最好别用宏:
- A function or generic would work (macros are harder to debug, autocomplete doesn’t help)
函数或泛型就能搞定,因为宏更难调试,自动补全也帮不上太多忙 - You need type checking inside the macro (macros operate on tokens, not types)
宏内部需要类型检查,因为宏操作的是 token,不是类型系统 - The pattern is used once or twice (not worth the abstraction cost)
模式只出现一两次,抽象成本反而更高
#![allow(unused)]
fn main() {
// ❌ Unnecessary macro — a function works fine:
macro_rules! double {
($x:expr) => { $x * 2 };
}
// ✅ Just use a function:
fn double(x: i32) -> i32 { x * 2 }
// ✅ Good macro use — variadic, can't be a function:
macro_rules! println {
($($arg:tt)*) => { /* format string + args */ };
}
}
The usual rule is simple: prefer functions and traits until syntax itself becomes the problem. Macros shine when the call-site shape matters more than the runtime behavior.
经验规则很简单:先优先考虑函数和 trait,只有当“调用语法本身”成了问题时,再把宏搬出来。宏真正擅长的是塑造调用形式,而不是替代普通逻辑封装。
Procedural Macros Overview
过程宏总览
Procedural macros are Rust functions that transform token streams. They require a separate crate with proc-macro = true:
过程宏本质上是“接收 token stream、再吐回 token stream”的 Rust 函数。它们必须放在单独的 crate 里,并开启 proc-macro = true:
#![allow(unused)]
fn main() {
// Three types of proc macros:
// 1. Derive macros — #[derive(MyTrait)]
// Generate trait implementations from struct definitions
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Config {
name: String,
port: u16,
}
// 2. Attribute macros — #[my_attribute]
// Transform the annotated item
#[route(GET, "/api/users")]
async fn list_users() -> Json<Vec<User>> { /* ... */ }
// 3. Function-like macros — my_macro!(...)
// Custom syntax
let query = sql!(SELECT * FROM users WHERE id = ?);
}
Derive Macros in Practice
Derive 宏在实战中的样子
The most common proc macro type. Here’s how #[derive(Debug)] works conceptually:
最常见的过程宏就是 derive。下面用概念化的方式看看 #[derive(Debug)] 干了什么:
#![allow(unused)]
fn main() {
// Input (your struct):
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
// The derive macro generates:
impl std::fmt::Debug for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
}
Commonly used derive macros:
常见 derive 宏:
| Derive | Crate | What It Generates 生成内容 |
|---|---|---|
Debug | std | fmt::Debug impl (debug printing)调试打印实现 |
Clone, Copy | std | Value duplication 值复制能力 |
PartialEq, Eq | std | Equality comparison 相等性比较 |
Hash | std | Hashing for HashMap keys 为 HashMap 键提供哈希能力 |
Serialize, Deserialize | serde | JSON/YAML/etc. encoding JSON、YAML 等序列化能力 |
Error | thiserror | std::error::Error + Display |
Parser | clap | CLI argument parsing 命令行参数解析 |
Builder | derive_builder | Builder pattern Builder 模式 |
Practical advice: Use derive macros liberally — they remove a lot of error-prone boilerplate. Writing custom proc macros is an advanced topic, so it usually makes sense to rely on mature libraries such as
serde,thiserror, andclapbefore inventing your own.
实战建议:derive 宏可以放心多用,它们能消掉大量容易写错的样板代码。至于自定义过程宏,那就属于进阶内容了;在自己造轮子之前,通常先把serde、thiserror、clap这些成熟库吃透更划算。
Macro Hygiene and $crate
宏卫生与 $crate
Hygiene means identifiers created inside a macro do not accidentally collide with names in the caller’s scope. Rust’s macro_rules! is partially hygienic:
宏卫生 指的是:宏内部生成的标识符,别莫名其妙和调用方作用域里的名字撞在一起。Rust 的 macro_rules! 属于“部分卫生”:
macro_rules! make_var {
() => {
let x = 42; // This 'x' is in the MACRO's scope
};
}
fn main() {
let x = 10;
make_var!(); // Creates a different 'x' (hygienic)
println!("{x}"); // Prints 10, not 42 — macro's x doesn't leak
}
$crate: When writing macros in a library, use $crate to refer to your own crate. It resolves correctly regardless of how downstream users rename the dependency:$crate:在库里写宏时,要用 $crate 引用当前 crate。这样无论下游用户怎么给依赖改名,它都能解析正确:
#![allow(unused)]
fn main() {
// In my_diagnostics crate:
pub fn log_result(msg: &str) {
println!("[diag] {msg}");
}
#[macro_export]
macro_rules! diag_log {
($($arg:tt)*) => {
// ✅ $crate always resolves to my_diagnostics, even if the user
// renamed the crate in their Cargo.toml
$crate::log_result(&format!($($arg)*))
};
}
// ❌ Without $crate:
// my_diagnostics::log_result(...) ← breaks if user writes:
// [dependencies]
// diag = { package = "my_diagnostics", version = "1" }
}
Rule: Always use
$crate::inside#[macro_export]macros. Never hard-code your crate name there.
规则:凡是#[macro_export]导出的宏,内部引用本 crate 时一律写$crate::,别把 crate 名字硬编码进去。
Recursive Macros and tt Munching
递归宏与 tt munching
Recursive macros can process input one token tree at a time; this technique is often called tt munching:
递归宏可以一次吃掉一部分 token tree,再继续递归处理剩下的输入。这套技巧通常就叫 tt munching:
// Count the number of expressions passed to the macro
macro_rules! count {
// Base case: no tokens left
() => { 0usize };
// Recursive case: consume one expression, count the rest
($head:expr $(, $tail:expr)* $(,)?) => {
1usize + count!($($tail),*)
};
}
fn main() {
let n = count!("a", "b", "c", "d");
assert_eq!(n, 4);
// Works at compile time too:
const N: usize = count!(1, 2, 3);
assert_eq!(N, 3);
}
#![allow(unused)]
fn main() {
// Build a heterogeneous tuple from a list of expressions:
macro_rules! tuple_from {
// Base: single element
($single:expr $(,)?) => { ($single,) };
// Recursive: first element + rest
($head:expr, $($tail:expr),+ $(,)?) => {
($head, tuple_from!($($tail),+))
};
}
let t = tuple_from!(1, "hello", 3.14, true);
// Expands to: (1, ("hello", (3.14, (true,))))
}
Fragment specifier subtleties:
片段说明符里的细节坑:
| Fragment 片段 | Gotcha 注意点 |
|---|---|
$x:expr | Greedily parses — 1 + 2 is ONE expression, not three tokens会贪婪匹配, 1 + 2 会被当成一个表达式,而不是三个 token |
$x:ty | Greedily parses — Vec<String> is one type; can’t be followed by + or <同样会贪婪匹配, Vec<String> 算一个完整类型,后面不能随便再接 + 或 < |
$x:tt | Matches exactly ONE token tree — most flexible, least checked 精确匹配一个 token tree,最灵活,但约束也最少 |
$x:ident | Only plain identifiers — not paths like std::io只匹配纯标识符,像 std::io 这种路径不算 |
$x:pat | In Rust 2021, matches A | B patterns; use $x:pat_param for single patterns在 Rust 2021 里会匹配 A | B 这种模式;如果只想要单个模式,改用 $x:pat_param |
When to use
tt: Reach forttwhen tokens need to be forwarded to another macro without being constrained by the parser.$($args:tt)*is the classic “accept anything” pattern used by macros such asprintln!,format!, andvec!.
什么时候该用tt:当 token 需要原封不动转交给另一个宏,而又不想提前被解析器限制时,就该上tt。$($args:tt)*就是那种经典的“来啥都接”写法,println!、format!、vec!都常这么干。
Writing a Derive Macro with syn and quote
用 syn 与 quote 写一个 derive 宏
Derive macros live in a separate crate (proc-macro = true) and usually follow a pipeline of parsing with syn and generating code with quote:
derive 宏必须放在单独的 proc-macro crate 里,典型流程是:先用 syn 解析,再用 quote 生成代码:
# my_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
#![allow(unused)]
fn main() {
// my_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
/// Derive macro that generates a `describe()` method
/// returning the struct name and field names.
#[proc_macro_derive(Describe)]
pub fn derive_describe(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let name_str = name.to_string();
// Extract field names (only for structs with named fields)
let fields = match &input.data {
syn::Data::Struct(data) => {
data.fields.iter()
.filter_map(|f| f.ident.as_ref())
.map(|id| id.to_string())
.collect::<Vec<_>>()
}
_ => vec![],
};
let field_list = fields.join(", ");
let expanded = quote! {
impl #name {
pub fn describe() -> String {
format!("{} {{ {} }}", #name_str, #field_list)
}
}
};
TokenStream::from(expanded)
}
}
// In the application crate:
use my_derive::Describe;
#[derive(Describe)]
struct SensorReading {
sensor_id: u16,
value: f64,
timestamp: u64,
}
fn main() {
println!("{}", SensorReading::describe());
// "SensorReading { sensor_id, value, timestamp }"
}
The workflow: TokenStream → syn::parse → inspect/transform → quote! → TokenStream back to the compiler.
工作流:TokenStream 原始 token → syn::parse 解析成 AST → 检查或变换 → quote! 重新生成 token → 再交回编译器。
| Crate | Role 角色 | Key types 关键类型 |
|---|---|---|
proc-macro | Compiler interface 编译器接口 | TokenStream |
syn | Parse Rust source into AST 把 Rust 源码解析成 AST | DeriveInput, ItemFn, Type |
quote | Generate Rust tokens from templates 从模板生成 Rust token | quote!{}, #variable interpolation |
proc-macro2 | Bridge between syn/quote and proc-macro 在 syn、quote 与 proc-macro 之间做桥接 | TokenStream, Span |
Practical tip: Before writing a custom derive, read the source of a simple crate such as
thiserrororderive_more. Also keepcargo expandhandy — it shows the exact expansion result and saves a huge amount of guessing.
实战提示:在真正自己写 derive 宏之前,先看看thiserror、derive_more这类相对简单的实现源码。再配上cargo expand一起用,能直接看到宏展开结果,省掉一大堆瞎猜。
Key Takeaways — Macros
本章要点 — 宏
macro_rules!for straightforward code generation; proc macros (syn+quote) for more complex transforms
简单代码生成适合macro_rules!;复杂变换则交给过程宏加syn、quote- Prefer generics and traits when they solve the problem cleanly — macros are harder to debug and maintain
如果泛型和 trait 已经能优雅解决问题,就优先用它们;宏的调试和维护成本更高$cratekeeps exported macros robust, andttmunching is the core recursive trick$crate能让导出宏更稳,ttmunching 则是递归宏的核心技巧
See also: Ch 2 — Traits for when traits and generics beat macros. Ch 14 — Testing for testing code generated by macros.
延伸阅读: 想判断 trait、泛型何时比宏更合适,可以看 第 2 章:Trait;想看宏生成代码怎么测,可以看 第 14 章:测试。
flowchart LR
A["Source code<br/>源代码"] --> B["macro_rules!<br/>pattern matching<br/>模式匹配"]
A --> C["#[derive(MyMacro)]<br/>proc macro<br/>过程宏"]
B --> D["Token expansion<br/>Token 展开"]
C --> E["syn: parse AST<br/>解析 AST"]
E --> F["Transform<br/>变换"]
F --> G["quote!: generate tokens<br/>生成 token"]
G --> D
D --> H["Compiled code<br/>编译后的代码"]
style A fill:#e8f4f8,stroke:#2980b9,color:#000
style B fill:#d4efdf,stroke:#27ae60,color:#000
style C fill:#fdebd0,stroke:#e67e22,color:#000
style D fill:#fef9e7,stroke:#f1c40f,color:#000
style E fill:#fdebd0,stroke:#e67e22,color:#000
style F fill:#fdebd0,stroke:#e67e22,color:#000
style G fill:#fdebd0,stroke:#e67e22,color:#000
style H fill:#d4efdf,stroke:#27ae60,color:#000
Exercise: Declarative Macro — map! ★ (~15 min)
练习:声明式宏 map! ★(约 15 分钟)
Write a map! macro that creates a HashMap from key-value pairs:
写一个 map! 宏,用键值对创建 HashMap:
let m = map! {
"host" => "localhost",
"port" => "8080",
};
assert_eq!(m.get("host"), Some(&"localhost"));
Requirements: support trailing comma and empty invocation map!{}.
要求:支持结尾逗号,并支持空调用 map!{}。
🔑 Solution
🔑 参考答案
macro_rules! map {
() => { std::collections::HashMap::new() };
( $( $key:expr => $val:expr ),+ $(,)? ) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($key, $val); )+
m
}};
}
fn main() {
let config = map! {
"host" => "localhost",
"port" => "8080",
"timeout" => "30",
};
assert_eq!(config.len(), 3);
assert_eq!(config["host"], "localhost");
let empty: std::collections::HashMap<String, String> = map!();
assert!(empty.is_empty());
let scores = map! { 1 => 100, 2 => 200 };
assert_eq!(scores[&1], 100);
}