Rust 宏系统与元编程:声明宏、过程宏与 derive 实战

Rust 宏系统完整指南:声明宏(macro_rules!)的匹配规则、过程宏三大类(派生/属性/函数宏)、syn + quote 抽象语法树操作、derive 宏实战、常见宏陷阱与调试技巧。

目录

  1. 宏的定位与分类
  2. 声明宏 macro_rules!
  3. 匹配规则与 token 流
  4. 过程宏三大类
  5. syn + quote 操作 AST
  6. derive 宏实战
  7. 属性宏与函数宏
  8. 宏调试与陷阱
  9. 宏在生态中的经典应用
  10. 速查表与最佳实践

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模式
:tttoken 树(任意)
: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]运行时包装
sqlxquery!() / sqlx::query!编译期 SQL 校验
thiserror#[derive(Error)]错误类型生成
dieseltable!()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

最佳实践:

  1. 宏代码要小而聚焦,宁可用函数。
  2. 过程宏必须纯函数(输入决定输出),不要做副作用。
  3. 给宏写详尽的文档注释和示例(宏难以调试)。
  4. 用 compile_error! 在宏内给清晰错误信息。
  5. 优先选择「函数 + 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」的分水岭之一。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「rust」更多文章

  1. Rust 嵌入式开发与 FFI 互操作:no_std、embedded-hal 与 C 接口
  2. Rust 学习路线与资源导航:从 The Book 到生产级实战
  3. Rust 工具链精讲:Cargo 高级特性、交叉编译与质量门禁