一句话总结
过程宏是"用 Rust 代码生成 Rust 代码"的程序:它接收一段代码的 token 流,运行编写好的逻辑,输出新的 token 流.
天天用的 #[derive(Debug)],#[tokio::main],serde 的派生,背后全是过程宏.
前置回顾
上一篇讲的声明宏是"模式匹配 + 模板替换",能力受限于模式.当需要读取结构体字段,生成 trait 实现等更复杂的代码生成时,就得升级到过程宏.本篇讲清它的工作原理和标准开发套路.
与声明宏的本质区别
声明宏是"模式匹配 + 模板替换",能力受限于模式.过程宏则完全不同,它是一个真正会被编译执行的函数,输入和输出都是代码本身.
| 维度 | 声明宏 macro_rules! |
过程宏 procedural macro |
|---|---|---|
| 编写方式 | 声明匹配规则 | 编写处理 token 的 Rust 函数 |
| 能力 | 模式替换 | 任意逻辑:遍历字段,生成 impl |
| 所在位置 | 普通代码中即可定义 | 必须放在独立的 crate |
| 典型代表 | vec!,println! |
#[derive(...)],serde |
声明宏像是"填空模板":结构早已固定,只能把值填进预留的空格.
过程宏则是雇了一位程序员助手,把一段代码递给他,他能读懂结构,思考逻辑,再亲手写出一段全新的代码交还.
三种过程宏
过程宏按"如何被调用"分为三类,各自解决不同问题.
|
| 类型 | 调用形式 | 典型用途 |
|---|---|---|
| 派生宏 | #[derive(MyTrait)] |
自动实现特征(最常见) |
| 属性宏 | #[my_attr] |
路由注册,改写函数(如 #[tokio::main]) |
| 函数式 | my_macro!(...) |
嵌入式 DSL,编译期校验 |
核心:TokenStream 进,TokenStream 出
所有过程宏的签名都围绕一个类型:TokenStream--它代表一串 Rust token(代码被切分后的最小语法单元).过程宏的工作本质就是一个 TokenStream → TokenStream 的转换.
|
|
过程宏必须独立成 crate
过程宏需要在编译"使用它的代码"之前就被编译成可执行的逻辑,存在先有鸡还是先有蛋的次序问题.Rust 的解法是:过程宏必须放在一个单独的 crate,并在 Cargo.toml 里标注 [lib] proc-macro = true.这个 crate 在编译期作为编译器的"插件"运行.
两大基石:syn 与 quote
直接操作裸 TokenStream 极其痛苦.生态里有两个几乎必用的 crate,把过程宏的开发变得可行.
syn -- 解析器:把 TokenStream 解析成结构化的语法树(AST),让开发者能方便地访问结构体名,字段,字段类型等.
quote -- 代码生成器:用类似模板的语法写出要生成的代码,并产出 TokenStream.用 #变量 把数据插值进模板.
syn 是"读",quote 是"写":syn 像把一篇文章拆解成主谓宾的语法分析器,quote 则像按提纲填词造句的写作工具.
过程宏的典型流程就是 syn 读进来 → 处理 → quote 写出去.
动手看一个派生宏
下面是一个完整的派生宏,为任意结构体自动实现一个打印自身类型名的 Hello 特征.它浓缩了过程宏的标准套路.
|
使用方接下来就能这样写:
|
这正是 #[derive(Debug)] 的真相
标准库的 #[derive(Debug)] 做的事和上例同构,只是更复杂:它用 syn 解析出结构体的每一个字段,再用 quote 生成一个遍历打印所有字段的 fmt 方法.
理解了这个套路,就明白了为何 Debug 能自动派生而 Display 不能--前者有确定的字段遍历规则,后者的"美观格式"无法被程序推断.
什么时候才该写过程宏
过程宏威力巨大,但开发成本高,会拖慢编译,报错也更晦涩.它不是日常工具,而是消除特定重复样板的重武器.
优先级:普通代码 → 声明宏 → 过程宏
遇到重复代码时,按这个顺序逐级升级:能用函数或泛型解决就别碰宏;模式化的语法重复用声明宏;只有当需要读取类型结构,遍历字段,生成 trait 实现这类声明宏做不到的事时,才动用过程宏.
框架作者(如 Web 路由,序列化库)是过程宏的主要编写者;应用开发者更多是使用它们而非编写.
代价提醒
引入过程宏会显著增加项目的编译时间(syn/quote 本身就是较大的依赖),且宏内部的逻辑错误难以调试.在动手前先确认:这段样板真的多到值得一个过程宏?很多时候一个 macro_rules! 或一个泛型函数就够了.
快速回顾
- 过程宏 = 编译期运行的代码生成程序,能力远超声明宏的模式替换
- 三种类型:派生宏(最常见),属性宏,函数式过程宏
- 本质是
TokenStream → TokenStream的转换,必须独立成 proc-macro crate - syn 负责读(解析 AST),quote 负责写(生成代码),是标准组合
#[derive(Debug)]的真相就是遍历字段 + 生成 impl 的派生宏- 升级顺序:普通代码 → 声明宏 → 过程宏,按需而非默认使用
动手练习
- 搭建 proc-macro crate:新建一个
proc-macro = true的 crate,照着例子实现#[derive(Hello)],在另一个 crate 中引用并验证输出. - 扩展字段信息:扩展该派生宏,让生成的方法额外打印结构体的字段数量(提示:从
syn::Data::Struct中取出字段并.len()). - cargo expand 探索:找一个常用的库(如 serde,clap),用
cargo expand查看它的#[derive]实际展开成了什么,对照本篇的套路理解.