首页 / Rust 入门教程 / 过程宏简介

Rust 入门教程

过程宏简介

本教程共 78 篇 · 第 75 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

RustRust 入门教程过程宏deriveproc-macrosynquote

本节目标:了解 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() 转回 TokenStreamsyn 负责解析、quote 负责生成,是过程宏的标准搭档。

1-5 过程宏 vs 声明宏怎么选

一句话:能用声明宏解决的,就别上过程宏。过程宏灵活,但代价是可读性差、调试难、还要独立包和 syn/quote 工具链,心智负担明显更重。

经验法则:

  • 想要”按模式批量生成代码、调用点和展开代码一一对应”——用 macro_rules!
  • 想要”根据类型结构自动派生 trait 实现”(如序列化、Debug)——用 derive 过程宏,且优先用现成的(serde 等)。
  • 想要”编译期解析一段特定领域代码(SQL、路由)“——用类函数宏或属性宏。
Warning

宏(无论哪种)会损害代码可读性,维护成本高。社区共识是:宏应该是”按需使用的利器”,不该是日常武器。能写普通函数/泛型解决的,绝不写宏。

1-6 过程宏的调试与生态

过程宏出错时,报错通常指向”展开后的代码”而非你的宏定义,定位难度比声明宏还高。最佳实践同样是 cargo expand 看展开结果,以及给宏生成的代码写单元测试覆盖。因为过程宏要先被编译成独立的可执行包,再去处理你的代码,所以它在大型项目里也会拖慢编译——宏越多,编译越慢。

生态里学习资源很丰富:dtolnay/proc-macro-workshop 是官方推荐的过程宏练手项目;synquote 的文档值得反复翻,几乎所有过程宏都建立在它们之上。你平时用的 #[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。