Rust 学习之自定义派生宏

Rust 学习之自定义派生宏

我们可以把 “自定义派生宏” 理解成 “自己动手造一台专属的代码自动生成机”—— 内置派生宏(如 Debug、Clone)是工厂现成的 “通用打印机”,只能打标准格式的代码;而自定义派生宏是你为特定需求造的 “定制打印机”,比如专门给结构体生成 “转 JSON 代码”“数据库映射代码” 的机器。 下

我们可以把 “自定义派生宏” 理解成 “自己动手造一台专属的代码自动生成机”—— 内置派生宏(如 DebugClone)是工厂现成的 “通用打印机”,只能打标准格式的代码;而自定义派生宏是你为特定需求造的 “定制打印机”,比如专门给结构体生成 “转 JSON 代码”“数据库映射代码” 的机器。

下面用 “造一台‘结构体转 JSON’的定制打印机” 为例,一步步讲清楚自定义派生宏的逻辑,以及所需库的作用。

一、先懂需求:为什么要造 “定制打印机”?

假设你经常需要把结构体转成 JSON 字符串,比如:

// 你定义的结构体
struct User {
    id: u32,
    name: String,
    is_vip: bool,
}

// 你想要自动生成的代码(不用手动写)
impl User {
    fn to_json(&self) -> String {
        format!(
            r#"{{"id":{},"name":"{}","is_vip":{}}}"#,
            self.id, self.name, self.is_vip
        )
    }
}

如果每个结构体都手动写 to_json 方法,字段多了会累死。这时候你就需要造一台 “结构体转 JSON” 的定制打印机 —— 也就是 #[derive(ToJson)] 自定义派生宏,只要给结构体加一句这个指令,就能自动生成 to_json 方法。

二、自定义派生宏的 “造机三步”

造这台 “打印机”,本质是解决三个问题:

  1. “读”:看懂用户给的结构体(有哪些字段、叫什么名、是什么类型);

  2. “算”:根据结构体信息,生成对应的 to_json 代码模板;

  3. “调”:允许用户加自定义参数(比如字段名在 JSON 里要改名叫 user_id,而不是 id)。

而 Rust 生态的三个核心库,正好对应这三个步骤的 “工具”。

三、核心库:造 “打印机” 的三大工具

就像造物理打印机需要 “扫描仪”“喷头”“参数面板”,造自定义派生宏也需要三个核心库:

库名

类比工具

核心作用(“打印机” 场景)

syn

扫描仪

把用户写的结构体 “扫” 成机器能懂的 “零件图”

quote

喷头

把 “零件图” 拼成实际的 Rust 代码(打印输出)

darling

参数面板

让用户给打印机加自定义设置(如字段重命名)

1. 第一步:用 syn 做 “扫描仪”—— 看懂结构体

用户写的结构体(如 User)在编译器眼里是一堆 “语法树碎片”(比如 “这是个结构体”“有个字段叫 id,类型是 u32”)。人类能看懂,但代码没法直接用。

syn 的作用就是把这些 “碎片” 扫成一份清晰的 “零件图”(Rust 结构体类型)。比如:

  • struct User { id: u32, name: String } 后,syn 会生成一个包含以下信息的结构体:

    • 类型:结构体(不是枚举);

    • 名称:User

    • 字段列表:[{名字: "id", 类型: "u32"}, {名字: "name", 类型: "String"}]

形象比喻:就像扫描仪把一张手写的 “快递单模板”(用户的结构体)扫成电脑里的 “表格文件”(syn 生成的语法树结构体),方便后续处理。

2. 第二步:用 quote 做 “喷头”—— 生成代码

有了 “零件图”(syn 解析后的结构体信息),下一步就是按 “转 JSON” 的规则生成代码。

quote 的作用是 “按模板填空”:你写一个代码模板(比如 fn to_json(&self) -> String { ... }),然后把 syn 扫出来的字段名、类型填进去,最后生成可编译的 Rust 代码。

比如针对 User 结构体,quote 会做这样的事:

  1. 拿到字段列表 ["id", "name", "is_vip"]

  2. 按模板生成 format! 字符串里的键值对("id":{}, "name":"{}" 等);

  3. 拼出完整的 to_json 方法代码。

形象比喻:就像打印机的喷头,根据 “表格文件”(syn 的结果)里的内容,按 “快递单格式”(代码模板)打印出一张完整的快递单(生成的代码)。

3. 第三步:用 darling 做 “参数面板”—— 支持自定义

如果用户想给 “打印机” 加自定义设置(比如 “把结构体的 id 字段在 JSON 里改名叫 user_id”),就需要 darling

比如用户这样写:

#[derive(ToJson)]
#[to_json(rename_id = "user_id")]  // 自定义参数:id 重命名为 user_id
struct User {
    id: u32,
    name: String,
}

darling 的作用是:

  1. 帮你解析用户加的自定义参数(rename_id = "user_id");

  2. 把参数转换成代码里能用的变量(比如 let rename_id = "user_id");

  3. 你在生成代码时,就可以根据这个参数调整逻辑(把 id 换成 user_id)。

形象比喻:就像打印机的 “参数面板”,用户可以设置 “打印份数”“是否双面”,darling 就是帮你读取这些设置,让打印机按用户需求工作。

四、完整 “造机流程”:从需求到使用

以造 #[derive(ToJson)] 宏为例,完整流程就像这样:

1. 明确需求(要打印什么)

“我要一台打印机,只要给结构体加 #[derive(ToJson)],就能自动生成 to_json 方法,把结构体转成 JSON 字符串;还支持用户给字段重命名。”

2. 准备工具(三大库)

Cargo.toml 里装上 “扫描仪”“喷头”“参数面板”:

[lib]
proc-macro = true  # 声明这是“造打印机的工厂”

[dependencies]
syn = { version = "2.0", features = ["full"] }  # 扫描仪
quote = "1.0"      # 喷头
darling = "0.20"   # 参数面板

3. 造打印机(写宏逻辑)

(1)用 darling 定义 “参数面板”(解析用户设置)

先告诉 darling:“用户可能会加 rename_id 这个参数,默认值是空。”

use darling::FromMeta;

// 定义用户能加的自定义参数
#[derive(Debug, FromMeta)]
struct ToJsonArgs {
    #[darling(default)]  // 没设置的话,默认是 None
    rename_id: Option<String>,
}

(2)用 syn 扫结构体(读零件图)

告诉 syn:“把用户写的结构体扫成我们能懂的格式。”

use proc_macro::TokenStream;
use syn::{DeriveInput, Fields};

// 宏的入口(相当于打印机的电源开关)
#[proc_macro_derive(ToJson, attributes(to_json))]
pub fn derive_to_json(input: TokenStream) -> TokenStream {
    // 1. 用 syn 扫用户的结构体,得到“零件图”
    let input = syn::parse_macro_input!(input as DeriveInput);
    let struct_name = input.ident;  // 结构体名(比如 User)
    let fields = match input.data {
        syn::Data::Struct(s) => s.fields,  // 结构体的字段列表
        _ => panic!("只能给结构体用 ToJson 宏!"),
    };

    // 2. 用 darling 读用户的自定义参数(比如 rename_id)
    let args = ToJsonArgs::from_attrs(&input.attrs)
        .expect("参数格式错了!比如 #[to_json(rename_id = \"user_id\")]");

(3)用 quote 拼代码(打印输出)

根据 “零件图” 和 “用户设置”,拼出 to_json 方法:

use quote::quote;

    // 3. 生成 JSON 里的键值对(比如 "id": self.id, "name": self.name )
    let json_fields = match fields {
        Fields::Named(named) => named.named.iter().map(|field| {
            let field_name = field.ident.as_ref().unwrap();  // 字段名(比如 id)
            // 处理重命名:如果用户设置了 rename_id,就把 id 换成 user_id
            let json_key = if field_name == "id" && args.rename_id.is_some() {
                args.rename_id.as_ref().unwrap()
            } else {
                &field_name.to_string()
            };
            // 拼出 "json_key": self.field_name
            quote! {
                #json_key: self.#field_name
            }
        }),
        _ => panic!("只支持带字段名的结构体(比如 struct A { x: i32 })!"),
    };

    // 4. 用 quote 拼完整的 to_json 方法代码
    let expanded = quote! {
        impl #struct_name {
            pub fn to_json(&self) -> String {
                format!(
                    "{{ {} }}",
                    // 把所有键值对用逗号连接(比如 "id":1, "name":"Alice")
                    vec![#(#json_fields),*]
                        .into_iter()
                        .map(|(k, v)| format!("\"{}\":{}", k, v))
                        .collect::<Vec<_>>()
                        .join(", ")
                )
            }
        }
    };

    // 5. 输出生成的代码(打印机吐出成品)
    TokenStream::from(expanded)
}

4. 用打印机(测试宏)

用户现在可以直接用你造的 #[derive(ToJson)] 了:

// 用自定义宏
#[derive(ToJson)]
#[to_json(rename_id = "user_id")]  // 加自定义参数
struct User {
    id: u32,
    name: String,
    is_vip: bool,
}

fn main() {
    let user = User { id: 1, name: "Alice".into(), is_vip: true };
    println!("{}", user.to_json());
    // 输出:{"user_id":1, "name":"Alice", "is_vip":true}
}

五、总结:自定义派生宏就是 “定制代码打印机”

  • 自定义派生宏:你为特定需求造的 “代码打印机”,解决内置宏满足不了的场景(如转 JSON、数据库映射);

  • syn:扫描仪,把用户的结构体扫成 “零件图”,让代码能看懂输入;

  • quote:喷头,把 “零件图” 按模板拼成代码,完成输出;

  • darling:参数面板,让用户能加自定义设置,让打印机更灵活。

这三个库配合,就能让你从 “手动写重复代码” 变成 “造机器自动生成代码”—— 本质是把 “重复劳动” 自动化,而且更不容易出错。

LICENSED UNDER CC BY-NC-SA 4.0
评论