过程宏简介
本教程共 78 篇 · 第 75 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:了解 Rust 三类过程宏(derive、属性宏、函数宏)长什么样、怎么用,理解它们以源代码 TokenStream 为输入输出、必须放在独立的 proc-macro 包里,并知道 syn/quote 这套工具链。
上一章的 macro_rules! 是”声明式宏”,靠模式匹配展开。Rust 还有另一大类更强大、更灵活的宏——过程宏(procedural macro)。这一章是概览性的,带你认全三种过程宏,不要求你会手写,但要知道它们能干什么、长啥样。
1-1 过程宏是什么
过程宏的形式像函数:接收一段源代码,经过处理,输出一段全新的源代码。它的输入和输出类型都是 TokenStream(记号流)——也就是 Rust 代码被词法分析后的一串”记号”。
过程宏和声明宏最大的区别有两点:
第一,输入是真正的代码结构,不只是模式匹配。过程宏可以用 syn 把代码解析成 AST(抽象语法树),想改哪改哪,远比 macro_rules! 灵活。
第二,derive 宏不替换原代码,而是追加代码。声明宏是”用展开的代码替换调用处”,而 #[derive(Debug)] 这类宏,是在你写的类型后面”额外加上”一段实现代码,原代码原封不动保留。
1-2 三种过程宏
过程宏一共三种,日常最常见的是第一种:
派生宏(derive)。你见过无数次:#[derive(Debug)]、#[derive(Clone)]。它只能用在结构体、枚举、union 上,作用是根据类型自动生成某 trait 的实现。你只写一个 #[derive(Xxx)],编译器就帮你把整段 impl 补上。
#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
}
类属性宏(attribute-like)。和 derive 类似,但能用在更多地方,包括函数;而且可以带自己的参数。Web 框架里经常见:
#[route(GET, "/")]
fn index() {
// ...
}
这里的 #[route] 就是个属性宏,它根据 GET, "/" 这些参数,给 index 函数生成路由注册相关的代码。
类函数宏(function-like)。调用起来像普通函数,但其实是宏:
let sql = sql!(SELECT * FROM posts WHERE id = 1);
它适合”拿到一段代码、需要解析检查后再生成代码”的场景,比如把 SQL 语句在编译期解析校验。这种事 macro_rules! 很难做,过程宏却游刃有余。
1-3 必须放在独立的包里
写过程宏有个硬规定:定义过程宏的包必须是独立的 proc-macro 包。在它的 Cargo.toml 里要写:
[lib]
proc-macro = true
[dependencies]
syn = "2"
quote = "1"
为什么必须独立?因为过程宏要先被编译成可执行代码,才能去处理你的代码;如果和使用它的代码在同一个包,编译顺序就打架了(Rust 的编译单元是包)。所以规矩是:宏放 xxx_derive 这样的独立包,主包通过依赖引用它。
Note这段限制未来可能会放宽,但目前写过程宏仍要遵守。多数开发者直接用社区现成的过程宏(serde 的
#[derive(Serialize)]、tokio 的#[tokio::main]等),自己动手造的并不多。
1-4 一个 derive 宏长什么样
看个最小示例,感受过程宏的骨架。在 proc-macro 包的 lib.rs 里:
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap(); // 把源代码解析成 AST
impl_hello_macro(&ast) // 生成实现代码
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident; // 拿到类型名,比如 User
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, {}!", stringify!(#name));
}
}
};
gen.into()
}
几个关键点:#[proc_macro_derive(HelloMacro)] 把函数注册成名为 HelloMacro 的派生宏;syn::parse 把输入的 TokenStream 解析成 DeriveInput(AST);quote! 宏用来”写”要生成的 Rust 代码,#name 会被替换成真正的类型名;最后 .into() 转回 TokenStream。syn 负责解析、quote 负责生成,是过程宏的标准搭档。
1-5 过程宏 vs 声明宏怎么选
一句话:能用声明宏解决的,就别上过程宏。过程宏灵活,但代价是可读性差、调试难、还要独立包和 syn/quote 工具链,心智负担明显更重。
经验法则:
- 想要”按模式批量生成代码、调用点和展开代码一一对应”——用
macro_rules!。 - 想要”根据类型结构自动派生 trait 实现”(如序列化、Debug)——用 derive 过程宏,且优先用现成的(serde 等)。
- 想要”编译期解析一段特定领域代码(SQL、路由)“——用类函数宏或属性宏。
Warning宏(无论哪种)会损害代码可读性,维护成本高。社区共识是:宏应该是”按需使用的利器”,不该是日常武器。能写普通函数/泛型解决的,绝不写宏。
1-6 过程宏的调试与生态
过程宏出错时,报错通常指向”展开后的代码”而非你的宏定义,定位难度比声明宏还高。最佳实践同样是 cargo expand 看展开结果,以及给宏生成的代码写单元测试覆盖。因为过程宏要先被编译成独立的可执行包,再去处理你的代码,所以它在大型项目里也会拖慢编译——宏越多,编译越慢。
生态里学习资源很丰富:dtolnay/proc-macro-workshop 是官方推荐的过程宏练手项目;syn 和 quote 的文档值得反复翻,几乎所有过程宏都建立在它们之上。你平时用的 #[derive(Serialize)]、#[tokio::main]、sql! 这些,背后都是过程宏在干活。
需要提醒一点:过程宏虽然强大,但可读性和维护性是它绕不开的代价。社区共识很明确——宏是”按需使用的利器”,不该是日常武器。能写普通函数解决的,绝不写宏;真要写,优先复用社区成熟的过程宏,而不是自己造轮子。
1-7 过程宏与声明宏的能力边界再议
有人问:既然过程宏这么强,能不能完全取代 macro_rules!?理论上能,实际上不值得。macro_rules! 胜在轻量、无需独立包、写法直观,适合”按模式批量生成代码”这类简单活;过程宏胜在能解析 AST、做复杂变换,但代价是独立包、syn/quote 工具链、编译慢。所以二者是互补而非替代关系。
日常百分之九十的宏需求,macro_rules! 或直接使用现成的 derive 宏(如 #[derive(Serialize)])就够。只有当你要”编译期解析一段特定领域代码”或”生成高度依赖类型结构的代码”时,才值得上过程宏。别为了炫技而用过程宏,维护成本会反噬。
1-8 小结
过程宏以 TokenStream 为输入输出,分 derive、属性宏、函数宏三类,必须放在独立的 proc-macro 包里,靠 syn 解析、quote 生成。它比声明宏灵活得多,但也复杂得多,应谨慎使用——多数时候直接用社区现成的 #[derive(...)] 就够了。下一章我们盘点 Rust 生态里那些”人人都在用”的常用 crate。