Rust 宏开发常用库介绍

Rust 的宏分为两种:

  1. 声明宏 (Declarative Macros):使用 macro_rules! 定义,类似于模式匹配和替换,语法相对固定。
  2. 过程宏 (Procedural Macros):更强大和灵活,可以接收 Rust 代码作为输入 Token Stream,执行任意 Rust 代码进行分析和转换,然后生成新的 Token Stream 作为输出。过程宏又分为三种:自定义派生宏 (#[derive(...)])、属性宏 (#[attribute(...)]) 和函数式宏 (macro_name!(...))。

在开发过程宏时,通常会用到以下几个核心库:

1. proc_macro (Rust 内置库)

用途

这是 Rust 编译器提供的一个内置库,包含了定义过程宏所需的基础类型和功能。它提供了表示 Rust 代码 Token Stream 的 TokenStream 类型,以及用于处理 token 的基本 API,如 Ident (标识符)、Punct (标点)、Literal (字面量) 等。它也提供了 Span 信息,用于将宏生成的代码与原始源代码位置关联,以便编译器报告错误。

使用

在定义过程宏的 crate 的 Cargo.toml 中,需要将 crate 类型设置为 proc-macro

[lib]
proc-macro = true

然后在代码中直接 extern crate proc_macro; 引入即可。过程宏函数的签名通常是 (TokenStream) -> TokenStream(TokenStream, TokenStream) -> TokenStream (对于属性宏)。

// src/lib.rs in a proc-macro crate
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: TokenStream) -> TokenStream {
    // input 是需要应用派生宏的 struct/enum/union 的 TokenStream
    println!("Input: {}", input); // 打印输入 Token Stream (用于调试)

    // 在这里进行 TokenStream 的处理和生成新的 TokenStream
    let output = TokenStream::new(); // 示例:生成空的 TokenStream

    output // 返回生成的 TokenStream
}

注意

proc_macro 中的类型是编译器特定的,不能在非过程宏 crate 中直接使用或测试。

2. proc-macro2

用途

proc-macro2proc_macro 库的一个“克隆”或替代实现,它提供了与 proc_macro 几乎相同的 API,但其类型可以在任何 crate 中使用,而不仅仅是过程宏 crate。它的主要目的是:

  • 允许在非过程宏 crate 中构建和操作 Token Stream:这使得宏辅助库 (helper libraries) 能够独立于过程宏进行开发和测试。
  • 使过程宏可单元测试:由于 proc_macro 类型不能在普通测试函数中使用,proc-macro2 允许你在单元测试中模拟输入 Token Stream,并检查宏生成的输出 Token Stream。

使用

Cargo.toml 中添加依赖:

[dependencies]
proc-macro2 = "1.0" # 使用最新版本

在过程宏代码中,通常会先将输入的 proc_macro::TokenStream 转换为 proc_macro2::TokenStream 进行处理:

// src/lib.rs in a proc-macro crate
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2; // 别名以区分

#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: TokenStream) -> TokenStream {
    let input2: TokenStream2 = input.into(); // 转换为 proc-macro2::TokenStream

    // 使用 proc-macro2::TokenStream 进行处理...
    let output2 = TokenStream2::new(); // 示例

    output2.into() // 转换回 proc_macro::TokenStream 返回
}

在单元测试中:

#[cfg(test)]
mod tests {
    use super::*;
    use proc_macro2::TokenStream as TokenStream2;
    use quote::quote; // 通常与 quote 库一起使用

    #[test]
    fn test_my_derive() {
        // 模拟输入 Token Stream
        let input: TokenStream2 = quote! {
            struct MyStruct {
                field1: i32,
                field2: String,
            }
        };

        // 调用宏函数 (需要将输入/输出转换为 proc_macro::TokenStream)
        let input_pm = input.into();
        let output_pm = my_derive(input_pm);
        let output2: TokenStream2 = output_pm.into();

        // 检查生成的 output2 是否符合预期
        // 可以将其转换为字符串进行比较,或者使用 syn 解析后比较 AST
        let expected_output: TokenStream2 = quote! {
            // 预期生成的代码
        };
        assert_eq!(output2.to_string(), expected_output.to_string());
    }
}

3. syn

用途

syn 是一个强大的 Rust 源代码解析库。过程宏接收的输入是 Token Stream,而直接操作 Token Stream 是非常繁琐的。syn 可以将输入的 Token Stream 解析成结构化的抽象语法树 (AST),让你能够以更方便的方式访问和操作代码的各个部分,如 structenum、函数、表达式、类型等。它提供了丰富的类型来表示 Rust 语法结构的各个节点。

使用

Cargo.toml 中添加依赖:

[dependencies]
syn = { version = "2.0", features = ["full", "derive"] } # 使用最新版本,并启用常用 feature
  • full feature 启用解析所有 Rust 语法结构的功能。
  • derive feature 启用解析派生宏输入所需的功能(如 DeriveInput)。

在过程宏代码中,使用 syn::parse_macro_input! 宏来解析输入的 Token Stream:

extern crate proc_macro;
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
use quote::quote; // 通常与 quote 库一起使用

#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: TokenStream) -> TokenStream {
    // 将输入 TokenStream 解析为 DeriveInput 结构体
    let input_ast = parse_macro_input!(input as DeriveInput);

    // 现在可以通过 input_ast 访问 struct/enum 的名称、字段、泛型等信息
    let name = &input_ast.ident;
    let generics = &input_ast.generics;
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

    // 根据 input_ast 的信息生成新的 Token Stream (通常使用 quote 库)
    let expanded = quote! {
        // 示例:生成一个简单的 impl 块
        impl #impl_generics MyTrait for #name #ty_generics #where_clause {
            // ... 实现 MyTrait 的方法 ...
        }
    };

    expanded.into() // 转换回 proc_macro::TokenStream 返回
}

syn 提供了大量的类型(如 ItemStruct, ItemEnum, FnArg, Type, Expr 等)来表示 Rust 代码的各个部分,以及用于遍历和修改 AST 的 Trait(如 Visit, VisitMut, Fold)。

4. quote

用途

quote 库提供了一个 quote! 宏,用于方便地构建要生成的 Rust 代码的 Token Stream。与手动创建 proc_macro2::TokenStream 中的 Ident, Punct, Literal 等 token 相比,quote! 宏允许你以类似编写实际 Rust 代码的方式来构建 Token Stream,并支持通过 #var 语法方便地插入 syn 解析出的变量或其他 Token Stream 片段。

使用

Cargo.toml 中添加依赖:

[dependencies]
quote = "1.0" # 使用最新版本

在过程宏代码中,结合 syn 解析出的 AST 来使用 quote! 宏生成代码:

extern crate proc_macro;
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
use quote::quote;

#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: TokenStream) -> TokenStream {
    let input_ast = parse_macro_input!(input as DeriveInput);

    let name = &input_ast.ident;
    let generics = &input_ast.generics;
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

    // 使用 quote! 宏构建要生成的代码
    let expanded = quote! {
        impl #impl_generics MyTrait for #name #ty_generics #where_clause {
            fn my_method(&self) {
                println!("Hello from {}!", stringify!(#name)); // stringify! 宏将 token 转换为字符串字面量
            }
        }
    };

    expanded.into() // quote! 宏返回的是 proc_macro2::TokenStream,需要转换为 proc_macro::TokenStream 返回
}

quote! 宏支持 #var 插入变量,#( ... )* 进行重复,这使得根据输入的结构生成重复的代码变得非常方便(例如,为 struct 的每个字段生成代码)。

其他常用辅助库:

  • darling:专门用于简化派生宏中处理属性(Attributes)的库。它可以方便地从 #[derive(...)] 或字段、枚举变体上的属性中提取信息并映射到 Rust 结构体中,减少手动解析属性的工作量。
  • strum / strum_macros:提供了一系列用于处理枚举(Enum)的派生宏,例如自动生成 FromStrDisplayEnumIter 等 Trait 的实现。如果你需要为枚举实现这些常见功能,直接使用 strum 提供的派生宏通常比自己编写过程宏更简单。
  • proc-macro-error / proc-macro-error2:用于在过程宏中报告更友好的编译错误,而不是直接 panic。它们允许你在宏代码中指出错误发生在用户代码的哪个具体位置(通过 Span 信息)。
  • synstructure:另一个用于简化派生宏开发的库,提供了一些方便的 Trait 和宏来处理结构体和枚举的结构。

总结

在 Rust 过程宏开发中,proc_macro 是基础,proc-macro2 使得测试和构建更灵活,syn 负责解析输入的代码结构,而 quote 负责方便地构建输出的代码结构。这四个库是过程宏开发的核心工具。像 darlingstrum 这样的库则是在特定场景下(如处理属性或枚举)提供便利的辅助工具。理解并熟练使用 synquote 是进行复杂过程宏开发的关键。