21. Macros — 宏编程
Rust 的宏系统是编译期的元编程工具,能在类型检查之前对代码进行变换。与 C/C++ 的文本替换宏不同,Rust 宏操作的是 token 树,支持模式匹配、保持词法卫生(hygiene),并且与语言的其他部分深度集成。本章从 macro_rules! 声明式宏入手,延伸到过程宏的基本概念。
宏基础
宏在编译早期被展开——每个宏调用被替换为生成的 Rust 代码,然后才进行类型检查和代码生成。宏调用总是以 ! 结尾,在代码中非常显眼。
macro_rules! 通过模式匹配定义宏:
macro_rules! my_macro {
(pattern1) => { template1 };
(pattern2) => { template2 };
// ...
}以 assert_eq! 的简化实现为例:
macro_rules! assert_eq {
($left:expr, $right:expr) => {{
match (&$left, &$right) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
panic!(
"assertion failed: `(left == right)` \
(left: `{:?}`, right: `{:?}`)",
left_val, right_val
);
}
}
}
}};
}注意几个关键细节:使用引用 & 避免移动值;将表达式结果绑定到变量避免重复求值。
片段类型(Fragment Types)
宏模式中的片段指示符告诉 Rust 应该匹配什么类型的语法构造:
| 片段类型 | 匹配内容 | 可后续的 token |
|---|---|---|
expr | 表达式 2 + 2, x.len() | => , ; |
ident | 标识符 x, my_var | 任意 |
ty | 类型 String, Vec<u8> | => , ; | { [ : > as where |
pat | 模式 Some(x), _ | => , = | if in |
path | 路径 std::collections::HashMap | => , ; | { [ : > as where |
stmt | 语句(不含尾部分号) | => , ; |
block | 代码块 { ... } | 任意 |
item | 条目 struct, fn, mod 等 | 任意 |
literal | 字面量 42, "hello", true | 任意 |
lifetime | 生命周期 'a, 'static | 任意 |
vis | 可见性 pub, pub(crate) | 任意 |
tt | 单个 token 树(最灵活) | 任意 |
meta | 属性内容 inline, derive(Copy) | 任意 |
tt(token tree)是最灵活的片段类型——匹配一个正确配对的括号组 (...)、[...] 或 {...},或单个 token。构建 DSL 风格的宏时最常用。
重复(Repetition)
重复语法让宏处理可变数量的输入:
| 模式 | 含义 |
|---|---|
$( ... )* | 匹配 0 次或多次,无分隔符 |
$( ... ),* | 匹配 0 次或多次,逗号分隔 |
$( ... );* | 匹配 0 次或多次,分号分隔 |
$( ... )+ | 匹配 1 次或多次 |
$( ... )? | 匹配 0 次或 1 次 |
vec! 宏的典型实现展示了重复的用法:
macro_rules! vec {
($elem:expr; $n:expr) => {
std::vec::from_elem($elem, $n)
};
($($x:expr),* $(,)?) => {
<[_]>::into_vec(Box::new([$($x),*]))
};
}规则 1 匹配 vec![0; 10] 形式;规则 2 匹配元素列表 vec![1, 2, 3]。模板中的 $( $x ),* 会对每个匹配到的元素重复生成代码。
支持尾部逗号的标准技巧是添加一条额外的规则:
($($x:expr),+ ,) => {
vec![$($x),*] // 递归调用自身,去掉尾部逗号
};卫生性(Hygiene)
Rust 宏是卫生的——宏内部定义的局部变量不会与调用处的变量冲突:
macro_rules! create_counter {
() => {
let count = 0; // 被"染色",不会与外部 count 冲突
};
}
let count = 42;
create_counter!();
println!("{}", count); // 打印 42,不是 0可以把卫生性想象为染色:宏展开的代码被染上一种"颜色",不同颜色的同名变量实际上有不同的名字。
卫生性的重要例外:它对类型名、函数名、模块名、常量名无效。如果宏内部使用了 HashMap 而调用处没有导入,就会编译失败。解决方案是使用 $crate 前缀:
macro_rules! json {
(null) => { $crate::Json::Null };
// ...
}$crate 展开为定义该宏的 crate 的根路径,确保无论在哪里调用,类型引用都是正确的。
宏的导出与导入
- 模块中的宏自动在其子模块中可见
- 使用
#[macro_use]将宏从子模块"逆向"导出到父模块 - 使用
#[macro_export]标记的宏变为pub,可通过路径引用:
#[macro_export]
macro_rules! my_public_macro {
// ...
}
// 使用者
use my_crate::my_public_macro;
my_public_macro!();内建宏
Rust 提供了若干编译器内置的实用宏:
| 宏 | 功能 |
|---|---|
file!() | 当前文件名(&str) |
line!() | 当前行号(u32) |
column!() | 当前列号(u32) |
stringify!(...) | 将 token 序列转换为字符串字面量 |
concat!(a, b, ...) | 连接字符串字面量 |
cfg!(condition) | 编译期条件判断(返回 bool) |
env!("VAR") | 编译期读取环境变量 |
option_env!("VAR") | 编译期读取环境变量(可能不存在) |
include!("file") | 包含 Rust 源文件 |
include_str!("file") | 包含文本文件为 &'static str |
include_bytes!("file") | 包含二进制文件为 &'static [u8] |
todo!() | 标记未完成代码 |
unimplemented!() | 标记未实现功能 |
matches!(expr, pattern) | 模式匹配测试(返回 bool) |
调试宏
调试宏展开结果有三种主要方法:
1. cargo expand(推荐)
cargo install cargo-expand
cargo expand # 展开当前 crate 的所有宏
cargo expand --lib my_module # 展开特定模块2. trace_macros!(nightly)
#![feature(trace_macros)]
trace_macros!(true);
let v = vec![1, 2, 3]; // 编译器打印展开过程
trace_macros!(false);3. log_syntax!(nightly)
#![feature(log_syntax)]
log_syntax!("current token: ", $some_token);设计宏的实用建议
避免歧义规则
让每个规则的起始 token 不同,或者把更具体的规则放在前面:
macro_rules! message {
// 具体规则在前
(error: $msg:expr) => { eprintln!("ERROR: {}", $msg) };
(warn: $msg:expr) => { eprintln!("WARN: {}", $msg) };
// 通用规则在后
($msg:expr) => { println!("{}", $msg) };
}使用 trait 简化宏
宏不擅长区分类型。当需要处理多种类型转换时,用 From trait 辅助:
// 宏只做一件事:委托给 From trait
macro_rules! json_value {
($val:literal) => {
Json::from($val) // 依赖 From<bool>, From<i32>, From<&str> 等实现
};
}递归宏
宏可以递归调用自身,这是处理嵌套结构的关键技术:
macro_rules! json {
(null) => { Json::Null };
([$($elem:tt),*]) => {
Json::Array(vec![$(json!($elem)),*])
};
({$($key:tt : $val:tt),*}) => {
Json::Object(/* ... */)
};
($other:tt) => { Json::from($other) };
}默认递归深度上限为 64,可通过 #![recursion_limit = "256"] 调高。
过程宏(Procedural Macros)
过程宏是作为 Rust 函数实现的宏,比 macro_rules! 更强大也更复杂。三种形式:
derive 宏
最常用的过程宏,为类型自动生成 trait 实现:
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}属性宏
创建自定义属性,可应用于模块、函数、结构体等:
#[my_attribute]
fn my_function() { }函数式宏
像 macro_rules! 宏一样调用,但实现为函数:
my_proc_macro!(some input tokens);过程宏需要定义在单独的 proc-macro 类型 crate 中,使用 proc_macro crate 提供的 API 操作 token 流。常用辅助 crate 包括 syn(解析)和 quote(生成代码)。
宏的替代方案
当宏变得过于复杂时,考虑这些替代方案:
- 泛型+trait:很多可以用宏实现的功能也可以用泛型和 trait 更简洁地实现
- 构建脚本(build.rs):在编译前生成 Rust 代码,使用
include!引入 - 函数/闭包:如果不需要操作语法,普通函数通常是更好的选择
小结
macro_rules!通过模式匹配定义宏,操作 token 树而非文本,天生避免了 C 宏常见的括号地狱和多次求值问题。- 片段类型(
expr,tt,ident等)精确控制宏匹配的内容;tt是最灵活的万能类型。 - 重复语法(
$(...)*和$(...)+)处理可变数量的输入,是vec!、println!等常用宏的基础。 - 卫生性防止宏内部变量与调用处冲突,但类型名等全局名称不受保护——使用
$crate确保路径正确。 - 过程宏提供了比
macro_rules!更强的元编程能力,以 Rust 函数的形式操作 token 流。 - 宏是一把双刃剑——功能强大但可能让代码难以阅读和调试。遵循"先用函数,不行再用宏"的原则。