我们可以把 “自定义派生宏” 理解成 “自己动手造一台专属的代码自动生成机”—— 内置派生宏(如 Debug
、Clone
)是工厂现成的 “通用打印机”,只能打标准格式的代码;而自定义派生宏是你为特定需求造的 “定制打印机”,比如专门给结构体生成 “转 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
方法。
二、自定义派生宏的 “造机三步”
造这台 “打印机”,本质是解决三个问题:
“读”:看懂用户给的结构体(有哪些字段、叫什么名、是什么类型);
“算”:根据结构体信息,生成对应的
to_json
代码模板;“调”:允许用户加自定义参数(比如字段名在 JSON 里要改名叫
user_id
,而不是id
)。
而 Rust 生态的三个核心库,正好对应这三个步骤的 “工具”。
三、核心库:造 “打印机” 的三大工具
就像造物理打印机需要 “扫描仪”“喷头”“参数面板”,造自定义派生宏也需要三个核心库:
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
会做这样的事:
拿到字段列表
["id", "name", "is_vip"]
;按模板生成
format!
字符串里的键值对("id":{}
,"name":"{}"
等);拼出完整的
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
的作用是:
帮你解析用户加的自定义参数(
rename_id = "user_id"
);把参数转换成代码里能用的变量(比如
let rename_id = "user_id"
);你在生成代码时,就可以根据这个参数调整逻辑(把
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
:参数面板,让用户能加自定义设置,让打印机更灵活。
这三个库配合,就能让你从 “手动写重复代码” 变成 “造机器自动生成代码”—— 本质是把 “重复劳动” 自动化,而且更不容易出错。