目录
- 宏的定位与分类
- 声明宏 macro_rules!
- 匹配规则与 token 流
- 过程宏三大类
- syn + quote 操作 AST
- derive 宏实战
- 属性宏与函数宏
- 宏调试与陷阱
- 宏在生态中的经典应用
- 速查表与最佳实践
1. 宏的定位与分类
Rust 宏是在编译期生成代码的元编程工具,与 C 宏(纯文本替换)不同,Rust 宏在 token 层操作且遵守作用域规则。
// 最简单的宏:类似 C 的替代,但类型安全
macro_rules! say {
($x:expr) => { println!("{}", $x); };
}
fn main() {
say!("hello");
let n = 42;
say!(n * 2);
}
| 类型 | 语法 | 用途 |
|---|---|---|
| 声明宏 | macro_rules! | 模式匹配生成代码,日常 80% |
| 派生宏 | #[derive(X)] | 为结构体/枚举生成 trait 实现 |
| 属性宏 | #[route("/x")] | 给项添加属性,axum/serde 大量使用 |
| 函数宏 | format! | 类似函数调用,处理 token |
2. 声明宏 macro_rules!
声明宏的核心是 模式匹配 + 代码替换。
macro_rules! vec2 {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$( temp_vec.push($x); )* // 重复展开
temp_vec
}
};
}
fn main() {
let v = vec2![1, 2, 3, 4];
assert_eq!(v, vec![1, 2, 3, 4]);
}
2.1 元变量类型
| 指示符 | 匹配 |
|---|---|
:expr | 表达式 |
:ident | 标识符(变量名、函数名) |
:ty | 类型 |
:pat | 模式 |
:tt | token 树(任意) |
:stmt | 语句 |
:block | 代码块 |
:lifetime | 生命周期 |
2.2 重复语法
$( ... ),* # 0 次以上
$( ... ),+ # 1 次以上
$( ... )? # 0 或 1 次
3. 匹配规则与 token 流
3.1 带分隔符的重复
macro_rules! count_comma {
( $($x:expr),* ) => { /* 逗号分隔 */ };
( $($x:expr);+ ) => { /* 分号分隔 */ };
}
// 通用:记录 n 个表达式
macro_rules! n_args {
() => { 0 };
($a:expr) => { 1 };
($a:expr, $($rest:expr),*) => { 1 + n_args!($($rest),*) };
}
3.2 递归宏
// 经典:计算到 0 为止
macro_rules! count_down {
() => { println!("0"); };
($n:expr) => {
println!("{}", $n);
count_down!($n - 1); // 注意:表达式递归要小心求值顺序
};
}
递归陷阱:宏按
tt层展开,递归调用会改变 token 语义(如$n - 1在宏内是字面值再展开)。生产代码尽量少用深递归。
4. 过程宏三大类
过程宏是编译器暴露给用户的函数:输入 token 流,输出 token 流。
// Cargo.toml(需要单独 proc-macro crate)
[lib]
proc-macro = true
4.1 派生宏
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 解析结构体,生成 impl HelloMacro
input
}
// 使用
#[derive(HelloMacro)]
struct Pancakes;
4.2 属性宏
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
// attr = #[route("/x")] 里的 "/x"
// item = 函数定义
item
}
4.3 函数宏
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
// 解析 SQL,生成类型安全的查询代码
input
}
// 使用
let user = sql!(SELECT * FROM users WHERE id = $1);
5. syn + quote 操作 AST
真正的过程宏开发依赖 syn(解析 Rust 语法树)与 quote(生成代码)。
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyDebug)]
pub fn my_debug(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let expanded = quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "自定义 Debug: {}", stringify!(#name))
}
}
};
expanded.into()
}
5.1 遍历字段
use syn::{Data, Fields};
let data = &input.data;
if let Data::Struct(data) = data {
if let Fields::Named(fields) = &data.fields {
for field in &fields.named {
let ident = &field.ident;
let ty = &field.ty;
// 生成: self.field 访问
let _ = quote! { self.#ident: #ty };
}
}
}
6. derive 宏实战
以「自动生成配置读取」的派生宏为例:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(FromEnv, attributes(env))]
pub fn from_env(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let mut set_fields = Vec::new();
if let Data::Struct(s) = &input.data {
if let Fields::Named(fields) = &s.fields {
for field in &fields.named {
let ident = &field.ident;
// 读取 #[env("NAME")] 属性,否则用字段名大写
let env_name = field
.attrs
.iter()
.find_map(|a| {
if a.path().is_ident("env") {
a.parse_args::<syn::LitStr>().ok().map(|s| s.value())
} else { None }
})
.unwrap_or_else(|| ident.as_ref().unwrap().to_uppercase());
set_fields.push(quote! {
#ident: std::env::var(#env_name)
.unwrap_or_default()
.parse()
.unwrap_or_default(),
});
}
}
}
let expanded = quote! {
impl #name {
pub fn from_env() -> Self {
Self { #(#set_fields)* }
}
}
};
expanded.into()
}
使用:
#[derive(FromEnv)]
struct Config {
#[env("HOST")]
host: String,
port: u16,
}
fn main() {
std::env::set_var("HOST", "0.0.0.0");
std::env::set_var("PORT", "8080");
let cfg = Config::from_env();
println!("{}:{}", cfg.host, cfg.port);
}
7. 属性宏与函数宏
7.1 属性宏:记录函数耗时
#[proc_macro_attribute]
pub fn timed(attr: TokenStream, item: TokenStream) -> TokenStream {
let func = parse_macro_input!(item as syn::ItemFn);
let name = &func.sig.ident;
let block = &func.block;
let expanded = quote! {
#func
// 或生成包装函数
fn #name() {
let start = std::time::Instant::now();
#block
println!("{} 耗时 {:?}", stringify!(#name), start.elapsed());
}
};
expanded.into()
}
7.2 函数宏:简化初始化
#[proc_macro]
pub fn build(input: TokenStream) -> TokenStream {
// 把结构体字面量拆成 builder 链
input
}
注意:属性宏/函数宏输出的函数签名变化会破坏调用点,生产上通常保留原函数并新增辅助代码,或用
#[proc_macro]包装调用。
8. 宏调试与陷阱
8.1 调试工具
# cargo expand:看宏展开后的真实代码
cargo install cargo-expand
cargo expand
# 依赖默认 nightly 特性
8.2 常见陷阱
| 陷阱 | 规避 |
|---|---|
| 卫生性问题(变量泄漏) | 用 $crate 前缀引用自己 crate 的项 |
| 多次求值副作用 | 宏展开里 $x 可能求值多次 |
| token 顺序错误 | 用 quote 模板而非字符串拼接 |
宏内 ? 无法用 | 声明宏里不能用 ?,需 match |
| 递归深度限制 | 简化递归或改用函数 |
// $crate 用法:确保宏引用的辅助函数解析到自己 crate
macro_rules! try_ok {
($e:expr) => {{
#[allow(unused_mut)]
let mut __x = $e;
match __x { Ok(v) => v, Err(e) => return Err(e.into()) }
}};
}
8.3 编译错误定位
rustc --pretty expanded # 旧方式
cargo expand --target-dir /tmp/exp # 不污染原 target
9. 宏在生态中的经典应用
| crate | 宏 | 作用 |
|---|---|---|
| serde | #[derive(Serialize, Deserialize)] | 自动序列化 |
| clap | #[derive(Parser)] | 参数解析代码生成 |
| axum | #[derive(IntoResponse)] / 路由宏 | HTTP 处理 |
| tokio | #[tokio::main] | 运行时包装 |
| sqlx | query!() / sqlx::query! | 编译期 SQL 校验 |
| thiserror | #[derive(Error)] | 错误类型生成 |
| diesel | table!() | schema 生成 |
宏的边界判断:
| 该用宏 | 不该用宏 |
|---|---|
| 需要编译期检查(SQL、HTML) | 简单的循环/重复 |
| 无法用泛型/trait 表达 | 可用泛型解决 |
| 跨类型模式重复 | 普通函数可抽象 |
| DSL 领域 | 运行时反射即可 |
10. 速查表与最佳实践
| 任务 | 工具 |
|---|---|
| 简单代码生成 | macro_rules! |
| 为类型实现 trait | #[derive] + syn/quote |
| 装饰函数/类型 | #[proc_macro_attribute] |
| DSL 解析 | #[proc_macro] |
| 解析 Rust 代码 | syn::parse_macro_input! |
| 生成代码 | quote! |
| 查看展开 | cargo expand |
最佳实践:
- 宏代码要小而聚焦,宁可用函数。
- 过程宏必须纯函数(输入决定输出),不要做副作用。
- 给宏写详尽的文档注释和示例(宏难以调试)。
- 用
compile_error!在宏内给清晰错误信息。 - 优先选择「函数 + trait + 泛型」的组合,宏是最后手段。
一句话记忆:声明宏处理 80% 日常(匹配 + 展开),过程宏用 syn 解析 + quote 生成处理剩余 20% 高级场景;调试靠
cargo expand,设计坚持「能用泛型别用宏」。
延伸阅读
- https://plumephp.com/rust-core-concepts/ — trait 与泛型基础
- https://plumephp.com/rust-toolchain-guide/ — cargo-expand 工具链
- https://plumephp.com/rust-web-frameworks/ — axum 路由宏实践
- https://plumephp.com/rust-error-handling-testing/ — thiserror derive 宏
- [[python]] — Python 元编程(描述符/元类)对比
- [[cpp]] — C++ 模板元编程对比
宏是 Rust 表达力的终极体现:当普通函数和泛型不够时,宏让你在编译期生成安全、高效、类型化的代码。它是区分「会用 Rust」和「精通 Rust」的分水岭之一。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。