本节摘要:过程宏接收语法树(TokenStream)、用代码生成代码,分派生、属性、函数式三类;derive(Debug) 之类的日常魔法都出自它。本节讲三类宏的形态与适用、派生宏的工作流程。读完你对生态里"注解即实现"的库有机制级理解。
| 类别 | 形态 | 用途 | 生态代表 |
|---|---|---|---|
| 派生宏 | 加在类型定义上 | 自动实现 Trait | Debug、Clone、serde 序列化 |
| 属性宏 | 包住一个条目 | 改写或包装代码 | 测试框架的路由注册 |
| 函数式宏 | 像函数调用 | 完全自定义语法 | SQL 与配置内嵌 DSL |
#[derive(Debug, Clone)] // 派生宏:为 Exhibit 自动写实现 struct Exhibit { id: u8 } #[route("/archive")] // 属性宏:把函数登记为处理入口 fn archive_page() -> &'static str { "档案首页" } sql_query!(SELECT * FROM cases); // 函数式宏:自定义语法(示意)
以 derive(Debug) 为例:编译器把 struct Exhibit { id: u8 } 的语法树交给派生宏;宏遍历字段(id 是 u8,u8 实现了 Debug),生成一份匹配的 fmt 实现;生成的代码与手写的完全等价,拼回编译流程。serde 派生同原理,只是生成的序列化代码更精细——字段名、跳过规则、重命名,都从属性参数里读。
过程宏必须放在独立的 proc-macro 类型的 crate 里(清单里声明),它编译成动态库供编译器加载。这是第 7.2 节 crate 形态知识的延伸:proc-macro 是第三种 crate。

过程宏是编译器插件,接收 token 流、返回 token 流。三类入口对应三种挂载点:derive 挂在 struct/enum 上,attribute 挂在任意项上并改写其本体,function-like 当函数调用。
// Cargo.toml —— 过程宏必须是独立 crate // [lib] // proc-macro = true use proc_macro::TokenStream; #[proc_macro_derive(CaseNumber, attributes(case_no))] // 派生宏:自动生成 impl pub fn derive_case(input: TokenStream) -> TokenStream { // input 是被标注的 struct/enum 的 token 流 let _src = input.to_string(); // 最低配:转字符串再拼装 let out = format!(r#" impl CaseNumber for filed {{ fn case_no() -> u32 {{ 173 }} }} "#); out.parse().expect("生成的 token 不合法") } #[proc_macro_attribute] // 属性宏:可吞掉并改写本体 pub fn audited(_attr: TokenStream, item: TokenStream) -> TokenStream { item // 原样放行 = 仅标记 } #[proc_macro] // 函数式:像函数一样调用 pub fn bench_case(input: TokenStream) -> TokenStream { input }
// 使用方 #[derive(CaseNumber)] struct filed; fn main() { println!("{}", filed::case_no()); // 173 —— impl 由宏生成 }
真实工程不手拼字符串,而是 syn 解析成语法树、quote 模板化生成——derive(CaseNumber) 的演示版刻意保留了"字符串进出"的最简形态,先看清插件协议再上工具,出问题时才有能力下钻。
use proc_macro::TokenStream; #[proc_macro_derive(VerdictCount)] pub fn derive_count(input: TokenStream) -> TokenStream { let src = input.to_string(); // 约定:字段名为 u32 的都计入——演示"读结构、生成代码"的最短闭环 let n = src.matches(':').count(); let out = format!( "impl VerdictCount for Any {{ fn count() -> u32 {{ {} }} }}", n.max(0) ); out.parse().unwrap() }
这个演示说明 derive 的本质:编译期反射。#[derive(Debug)] 到 thiserror 的错误类型生成都在同一条流水线上,差别只在语法树分析的深度。排错要点:过程宏 panic 时编译器会把它转成错误输出并指向宏调用点,cargo expand(外部工具)能展开产物直接看生成代码,是过程宏调试的第一证物。
| 维度 | macro_rules | 过程宏 |
|---|---|---|
| 匹配对象 | 有限模式语法 | 任意 token 流 |
| 生成方式 | 模板替换 | 程序构造 |
| 依赖 | 零 | proc-macro2 / syn / quote |
| 适合 | 样板展开、DSL 骨架 | derive、属性改写、复杂代码生成 |
默认先用声明宏,模式语法表达不了意图再升级——过程宏的能力换来的编译成本与调试成本都高一档。
三个判断题收束本章第二节:一,过程宏能在展开时读到字段类型吗——能,syn 解析出的语法树含类型信息,这是它对声明宏的本质优势;二,过程宏 crate 能被同一项目的 bin 直接依赖吗——能,但路径依赖即可,无需发布;三,derive 宏能访问被派生类型的私有字段吗——能,生成代码与类型同模块可见性。三题都指向同一认知:过程宏是编译期的反射程序,能力边界就是"语法树里有什么"。