Rust 学习之自定义派生宏-访问和操作TokenStream(二)

Rust 学习之自定义派生宏-访问和操作TokenStream(二)

要深入理解 TokenStream 及派生宏中的代码生成逻辑,我们可以从 “数据格式”“解析后结构”“多代码块生成” 三个维度展开,结合实例说明细节。 一、TokenStream:代码的 “序列化格式” TokenStream 是 Ru

要深入理解 TokenStream 及派生宏中的代码生成逻辑,我们可以从 “数据格式”“解析后结构”“多代码块生成” 三个维度展开,结合实例说明细节。

一、TokenStream:代码的 “序列化格式”

TokenStream 是 Rust 中代码在编译器与宏之间传递的 “中间载体”,本质是一串有序的 “语法最小单元”(Token)的集合。

1. TokenStream 的组成:TokenTree

TokenStreamTokenTree 组成,TokenTree 是 Rust 语法的最小单元,包含以下几种类型(可理解为 “语法积木”):

  • Ident:标识符(如变量名 user、关键字 struct);

  • Literal:字面量(如 42"hello"3.14);

  • Punct:标点符号(如 :={});

  • Group:分组(如 (...){...}[...],内部包含嵌套的 TokenStream);

  • BinOp:二元运算符(如 +*&&)。

例如,代码 struct User { id: u32 } 对应的 TokenStream 包含的 TokenTree 序列是:

Ident("struct")Ident("User")Group({ ... })(内部包含 Ident("id")Punct(:)Ident("u32"))。

2. TokenStream 的核心作用

  • 输入:编译器将用户写的代码(如结构体定义)转换为 TokenStream,作为派生宏的输入,让宏能 “读取” 用户代码;

  • 输出:派生宏生成的代码也必须转换为 TokenStream 返回,编译器会将其视为用户手写代码进行编译。

它的存在统一了 “宏与编译器” 的交互接口,无论宏生成多少代码,最终都通过 TokenStream 传递。

二、syn::parse_macro_input! 解析后的实例:DeriveInput 的核心方法与字段

syn::parse_macro_input!(input as DeriveInput) 会将输入的 TokenStream 解析为 syn::DeriveInput 结构体 —— 这是 syn 库对 “可被派生的类型”(结构体、枚举、联合体)的结构化表示。

我们可以把 DeriveInput 理解为 “类型的完整档案”,包含类型的所有关键信息,其核心字段如下:

字段 / 方法

类型

作用

示例(针对 struct User { id: u32, name: String }

ident

Ident

类型的名称(结构体 / 枚举名)

ident = Ident("User")

data

Data

类型的具体数据(区分结构体 / 枚举 / 联合体)

data = Data::Struct(Struct { fields: ... })

attrs

Vec<Attribute>

类型上的属性(如 #[derive(Debug)]#[my_attr(key = "value")]

若有 #[to_json(rename = "user")],则包含该属性的解析结果

vis

Visibility

类型的可见性(如 pub、默认私有)

若结构体是 pub struct User,则 vis = Visibility::Public

generics

Generics

类型的泛型参数(如 struct Pair<T> { a: T, b: T } 中的 T

无泛型时为空,有泛型时包含参数列表

如何从 DeriveInput 中提取关键信息?

以最常见的 “结构体” 为例,通过 data 字段可进一步提取字段信息:

use syn::{DeriveInput, Data, Fields, Field, Ident};

// 解析输入得到 DeriveInput
let input: DeriveInput = syn::parse_macro_input!(input as DeriveInput);

// 1. 获取类型名称
let type_name = input.ident; // Ident("User")

// 2. 判断是否为结构体,并提取字段
let fields = match input.data {
    Data::Struct(struct_data) => struct_data.fields, // 结构体的字段集合
    Data::Enum(_) => panic!("不支持枚举"),
    Data::Union(_) => panic!("不支持联合体"),
};

// 3. 处理字段(以“带名称的字段”为例,如 `{ id: u32, name: String }`)
if let Fields::Named(named_fields) = fields {
    // 遍历所有字段,获取字段名和类型
    for field in named_fields.named {
        let field_name = field.ident.unwrap(); // 字段名:Ident("id")、Ident("name")
        let field_type = field.ty; // 字段类型:Type::Path(Path { ident: Ident("u32") }) 等
        println!("字段名:{},类型:{}", field_name, quote::quote!(#field_type));
    }
}
  • field.ident:字段的名称(Option<Ident>,因为元组结构体字段可能没有名称);

  • field.ty:字段的类型(Type 类型,可通过 quote! 转换为代码字符串)。

三、生成多个 trait 或函数时,TokenStream 的处理方式

派生宏可以生成任意数量的代码(多个 trait 实现、多个函数、多个结构体等),只需将所有生成的代码通过 quote! 组合成一个整体,再转换为 TokenStream 即可。

编译器会将返回的 TokenStream 视为 “插入到当前作用域的代码”,多个代码块(如多个 impl 块)可以共存。

示例:同时生成两个 trait 实现和一个辅助函数

假设我们要实现 #[derive(MyMacro)],为结构体同时生成:

  1. Debug trait 的简化实现;

  2. Display trait 的实现;

  3. 一个 to_map 辅助函数(将结构体转换为 HashMap)。

步骤 1:解析输入(提取结构体信息)

use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, Data, Fields, Ident, Type};
use std::collections::HashMap;

#[proc_macro_derive(MyMacro)]
pub fn my_macro(input: TokenStream) -> TokenStream {
    // 解析输入为 DeriveInput
    let input = syn::parse_macro_input!(input as DeriveInput);
    let type_name = input.ident; // 结构体名称(如 User)

    // 提取字段名和类型
    let fields = match input.data {
        Data::Struct(s) => s.fields,
        _ => panic!("只支持结构体"),
    };

    let (field_names, field_types): (Vec<Ident>, Vec<Type>) = if let Fields::Named(named) = fields {
        named.named.iter()
            .map(|f| (f.ident.clone().unwrap(), f.ty.clone()))
            .unzip()
    } else {
        panic!("只支持带名称的字段");
    };

步骤 2:生成多个代码块(用 quote! 组合)

通过 quote! 宏在一个代码块中包含多个 impl 块和函数定义:

    // 生成代码:包含 Debug 实现、Display 实现、to_map 函数
    let generated_code = quote! {
        // 1. 生成 Debug trait 实现
        impl std::fmt::Debug for #type_name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                f.debug_struct(stringify!(#type_name))
                    #(#.field(stringify!(#field_names), &self.#field_names))*
                    .finish()
            }
        }

        // 2. 生成 Display trait 实现
        impl std::fmt::Display for #type_name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{} {{ ", stringify!(#type_name))?;
                #(write!(f, "{}: {}, ", stringify!(#field_names), self.#field_names)?;)*
                write!(f, "}}")
            }
        }

        // 3. 生成 to_map 辅助函数
        impl #type_name {
            pub fn to_map(&self) -> HashMap<String, String> {
                let mut map = HashMap::new();
                #(map.insert(stringify!(#field_names).to_string(), self.#field_names.to_string());)*
                map
            }
        }
    };

    // 将组合后的代码转换为 TokenStream 并返回
    TokenStream::from(generated_code)
}
  • #type_name:嵌入结构体名称(如 User);

  • #field_names:通过 #(...) 语法循环展开字段名列表;

  • 多个 impl 块和函数定义在同一个 quote! 中,最终会被合并为一个 TokenStream

步骤 3:使用宏验证结果

// src/main.rs
use my_macro::MyMacro;
use std::fmt;
use std::collections::HashMap;

#[derive(MyMacro)]
struct User {
    id: u32,
    name: String,
}

fn main() {
    let user = User { id: 1, name: "Alice".into() };

    // 测试 Debug
    println!("Debug: {:?}", user); 
    // 输出:Debug: User { id: 1, name: "Alice" }

    // 测试 Display
    println!("Display: {}", user); 
    // 输出:Display: User { id: 1, name: Alice, }

    // 测试 to_map
    let map = user.to_map();
    println!("to_map: {:?}", map); 
    // 输出:to_map: {"id": "1", "name": "Alice"}
}

关键原理:TokenStream 支持 “代码块拼接”

TokenStream 本质是一个 “容器”,可以容纳任意数量的语法单元。无论是多个 impl 块、函数、结构体定义,只要通过 quote! 组合成一个整体,转换为 TokenStream 后,编译器都会按顺序解析并编译这些代码。

这就像 “把多页文档合并成一个 PDF”——quote! 负责 “合并”,TokenStream 作为 “PDF 文件” 传递给编译器,编译器会逐页(逐代码块)处理。

四、总结

  1. TokenStream:是代码的 “序列化格式”,由 TokenTree(语法最小单元)组成,作为宏与编译器的交互载体;

  2. DeriveInput:解析后的结构化实例,包含类型名称(ident)、数据类型(data)、属性(attrs)等核心信息,通过其字段可提取结构体 / 枚举的详细信息;

  3. 生成多个代码块:通过 quote! 宏将多个 impl 块、函数等组合成一个代码字符串,转换为 TokenStream 即可。编译器会将整个 TokenStream 视为有效代码进行编译,支持任意数量的代码块共存。

掌握这些后,你可以灵活生成各种复杂代码,满足自定义派生宏的需求。

LICENSED UNDER CC BY-NC-SA 4.0
评论