Appearance
第 10 章 serde_derive 架构总览:从 AST 到 impl 块
10.1 打开地图之前
前面九章是"准备"——Data Model、trait、宏工具链、自己写一个 Builder。现在我们打开一张真实的地图:serde_derive 的源码结构。
serde_derive 是整个 Serde 生态的"机械翻译部门"——它把 #[derive(Serialize, Deserialize)] 翻译成真实的 impl Serialize for T 和 impl Deserialize<'de> for T。这是一个近 9000 行代码的工程,每一行都在回答一个具体问题:
- 如何识别 enum 的四种 tag 模式?
- 如何处理
#[serde(flatten)]把子 struct "摊平" 到父级? - 如何让
&'de str字段零拷贝借用输入? - 如何在泛型类型上自动推导 Serialize bound?
- 如何让错误信息指向用户真实代码?
如果逐行读代码不带地图,你会迷失在细节里。本章的目标是画出这张地图——文件结构、模块职责、数据流、控制流。读完本章你看 serde_derive/src/ 的任何文件都能知道"这个文件在整个架构里的位置"。
后续第 11-14 章会沿着这张地图深入——11 章专讲属性系统(从用户属性到内部表示)、12 章专讲 Serialize 代码生成、13 章专讲 Deserialize 代码生成、14 章专讲 enum 的四种 tag 策略。本章是这个四章的总纲。
本书基于 serde 1.0.228(commit fa7da4a)和 serde_derive 同版本。
10.2 9000 行代码,19 个文件:架构全景
先看一眼文件结构:
serde_derive/
├── Cargo.toml
└── src/
├── lib.rs ← 127 行 入口:两个 #[proc_macro_derive]
├── ser.rs ← 1369 行 Serialize 代码生成
├── de.rs ← 976 行 Deserialize 代码生成(入口)
├── de/ ← Deserialize 的分支实现
│ ├── struct_.rs ← 697 行 struct 反序列化
│ ├── tuple.rs ← 283 行 tuple struct 反序列化
│ ├── unit.rs ← 52 行 unit struct 反序列化
│ ├── enum_.rs ← 96 行 enum 分发入口
│ ├── enum_externally.rs ← 213 行 externally tagged enum
│ ├── enum_internally.rs ← 106 行 internally tagged enum
│ ├── enum_adjacently.rs ← 324 行 adjacently tagged enum
│ ├── enum_untagged.rs ← 135 行 untagged enum
│ └── identifier.rs ← 477 行 字段/变体名识别
├── internals/ ← 共享内部工具
│ ├── mod.rs ← 28 行 模块声明
│ ├── ast.rs ← 225 行 serde 视角的 AST(Container/Variant/Field)
│ ├── attr.rs ← 1818 行 属性解析(所有 #[serde(...)] 语义)
│ ├── case.rs ← 200 行 命名风格转换(snake_case / camelCase 等)
│ ├── check.rs ← 477 行 一致性检查(属性组合的合法性)
│ ├── ctxt.rs ← 67 行 错误累积
│ ├── name.rs ← 113 行 字段/变体名的封装
│ ├── receiver.rs ← 293 行 Self 接收者处理
│ ├── respan.rs ← 16 行 重设 span 的小工具
│ └── symbol.rs ← 71 行 常用 Ident 的常量
├── bound.rs ← 425 行 自动推导泛型 bound
├── this.rs ← 32 行 "当前类型" 的引用工具
├── fragment.rs ← 74 行 代码片段的组合
├── dummy.rs ← 31 行 dummy const 包装
├── pretend.rs ← 188 行 避免 unused_variables 警告
└── deprecated.rs ← 56 行 废弃属性的警告9000 行代码组织成 19 个文件。按职责分为三类:
- 入口层(lib.rs,127 行):两个
#[proc_macro_derive]函数。 - AST + 属性层(internals/,约 3100 行):从 syn AST 加工出 serde 视角的 AST,解析所有
#[serde(...)]属性。 - 代码生成层(ser.rs、de.rs 及 de/ 目录,约 5000 行):根据加工后的 AST 生成 impl 块。
这三层的控制流是线性的:入口 → AST 加工 → 代码生成。每一层有明确边界,下一层只看上一层的输出。这种分层让 9000 行代码可读——任何一处都能回答"我在哪一层、干什么"。
10.3 入口层:127 行的 lib.rs
serde_derive/src/lib.rs 是全书最重要的 127 行代码。整个 Serde 宏系统从这里开始。
rust
// serde_derive/src/lib.rs:69-81
extern crate proc_macro2;
extern crate quote;
extern crate syn;
extern crate proc_macro;
mod internals;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{ToTokens, TokenStreamExt as _};
use syn::parse_macro_input;
use syn::DeriveInput;注意:
extern crate proc_macro2/quote/syn/proc_macro声明外部 crate 依赖。proc_macro2、quote、syn是我们第 6-8 章介绍过的"三件套"。mod internals声明内部辅助模块。use导入常用类型,TokenStreamExt as _用匿名导入(第 8 章讲过)。
两个入口函数(serde_derive/src/lib.rs:113-127):
rust
#[proc_macro_derive(Serialize, attributes(serde))]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(input as DeriveInput);
ser::expand_derive_serialize(&mut input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(Deserialize, attributes(serde))]
pub fn derive_deserialize(input: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(input as DeriveInput);
de::expand_derive_deserialize(&mut input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}这个结构和你第 9 章写的 Builder 完全一致:
parse_macro_input!(input as DeriveInput)— 解析类型定义。- 调用
expand_derive_*函数(返回syn::Result<TokenStream>)。 - 错误转
compile_error!代码。 - 转回
proc_macro::TokenStream。
127 行里还有一个微妙的细节(lib.rs:95-111):
rust
#[allow(non_camel_case_types)]
struct private;
impl private {
fn ident(&self) -> Ident {
Ident::new(
concat!("__private", env!("CARGO_PKG_VERSION_PATCH")),
Span::call_site(),
)
}
}
impl ToTokens for private {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.append(self.ident());
}
}这是 _serde::__private 路径的特殊处理。Serde 生成代码时会调用 _serde::__private::... 下的内部辅助函数(用于字符串转换、错误构造等)。
为什么 __private 后面带版本 patch 号?因为如果两个不同版本的 serde 被链接到同一个 crate(通过不同依赖路径),它们的 __private 模块不能冲突。给每个 patch 版本一个独立的符号名(__private0、__private1 等)能避免冲突。
这种工程级细致是 Serde 长期维护能保持向后兼容的基础。一个 serde_derive 被十年前的 crate 用,也被今天的 crate 用——它们生成的代码可能调用不同 patch 版本的 __private,但彼此不冲突。
10.4 数据流:DeriveInput → Container → TokenStream
serde_derive 的核心数据流可以画成一张图:
四个阶段:
- 解析 (parse_macro_input):token → syn::DeriveInput(纯语法)
- AST 加工 (Container::from_ast):syn::DeriveInput → serde::Container(加入 serde 语义)
- 代码生成 (expand_derive_*):serde::Container → TokenStream(目标代码)
- 错误兜底:
syn::Result→ 要么是 TokenStream 要么是 compile_error
第 2 步的产出——Container——是 serde_derive 内部最核心的数据结构。整个代码生成都基于它。下一节详细看。
10.5 Container:serde 视角的 AST
serde_derive/src/internals/ast.rs 定义了整个 serde_derive 的"工作对象":
rust
// serde_derive/src/internals/ast.rs:9
pub struct Container<'a> {
pub ident: syn::Ident, // 类型名
pub attrs: attr::Container, // serde 专属属性
pub data: Data<'a>, // 类型结构
pub generics: &'a syn::Generics, // 泛型
pub original: &'a syn::DeriveInput, // 原始 syn AST
}
pub enum Data<'a> {
Enum(Vec<Variant<'a>>),
Struct(Style, Vec<Field<'a>>),
}
pub struct Variant<'a> {
pub ident: syn::Ident,
pub attrs: attr::Variant,
pub style: Style,
pub fields: Vec<Field<'a>>,
pub original: &'a syn::Variant,
}
pub struct Field<'a> {
pub member: syn::Member, // 字段的访问方式:ident 或 index
pub attrs: attr::Field,
pub ty: &'a syn::Type,
pub original: &'a syn::Field,
}
pub enum Style {
Struct, // 命名字段
Tuple, // 多个无名字段
Newtype, // 一个无名字段
Unit, // 无字段
}对比 syn 的 AST:
| syn | serde Container | 区别 |
|---|---|---|
DeriveInput | Container | 加了 attr::Container(解析后的 serde 属性) |
Data::Struct/Enum/Union | Data::Struct/Enum | 不要 Union(Serde 不支持) |
Fields::Named/Unnamed/Unit | Style::Struct/Tuple/Newtype/Unit | 把单字段 tuple 单独提为 Newtype |
Variant | Variant(内部) | 加了 attr::Variant |
Field | Field(内部) | 加了 attr::Field + Member(统一命名字段和索引字段的访问方式) |
三个关键加工:
加工 1:属性解析。 attr::Container、attr::Variant、attr::Field 是 serde 专属的属性表示。syn 只能告诉你"这里有个 #[serde(rename = "x")] 属性 token 串";attr::Field 告诉你"这个字段要重命名为 x"——语义已经解析出来。第 11 章专门讲。
加工 2:Newtype 提升。 syn 里 struct Foo(i32) 是 Fields::Unnamed 带 1 个字段。serde 把它提升到独立的 Style::Newtype。为什么?因为 newtype 在序列化时有独特语义——第 2 章讲过,它有自己的 serialize_newtype_struct 原语。把它从 Tuple 里单独出来,后续代码生成分派更自然。
加工 3:Member 统一。 syn 里命名字段用 Option<Ident>(Some(id) 或 None)。serde 的 Field::member: syn::Member 是一个 enum:
rust
// syn::Member 的定义
pub enum Member {
Named(Ident), // struct 字段,如 self.id
Unnamed(Index), // tuple 字段,如 self.0
}统一了"字段怎么访问"的表达。生成 self.#member 时不管是 self.id 还是 self.0 都能正确生成。
10.6 Container::from_ast:AST 加工的主流程
看 Container::from_ast 函数的骨架(简化):
rust
// serde_derive/src/internals/ast.rs:63 (简化版)
impl<'a> Container<'a> {
pub fn from_ast(
cx: &Ctxt,
item: &'a syn::DeriveInput,
derive: Derive,
private: &Ident,
) -> Option<Container<'a>> {
// 1. 解析 container 级别属性(#[serde(rename_all = "..")]、#[serde(tag = "..")] 等)
let attrs = attr::Container::from_ast(cx, item);
// 2. 根据 Data::Struct/Enum/Union 分派
let mut data = match &item.data {
syn::Data::Enum(data) => Data::Enum(enum_from_ast(cx, &data.variants, ..., private)),
syn::Data::Struct(data) => {
let (style, fields) = struct_from_ast(cx, &data.fields, None, ..., private);
Data::Struct(style, fields)
}
syn::Data::Union(_) => {
cx.error_spanned_by(item, "Serde does not support derive for unions");
return None;
}
};
// 3. 应用 rename_all 规则到字段
match &mut data {
Data::Enum(variants) => {
for variant in variants {
variant.attrs.rename_by_rules(attrs.rename_all_rules());
// 递归处理 variant 里的字段
...
}
}
Data::Struct(_, fields) => {
for field in fields {
field.attrs.rename_by_rules(attrs.rename_all_rules());
}
}
}
// 4. 一致性检查
let item = Container { ident: item.ident.clone(), attrs, data, generics: &item.generics, original: item };
check::check(cx, &item, derive);
Some(item)
}
}四步:
- 解析 container 级属性(
#[serde(rename_all = "..")]等) - 递归解析 struct 或 enum 的内部结构(
struct_from_ast/enum_from_ast) - 应用 rename 规则(把
rename_all传递到每个字段/变体) - 一致性检查(比如不能同时指定
#[serde(tag = ..)]和#[serde(untagged)])
错误处理策略: 所有错误通过 cx: &Ctxt(context)累积,不立刻返回。这样用户一次能看到所有错误——如果同时有三个属性冲突,三个都会报告,而不是"修一个看下一个"。
Ctxt 的实现(serde_derive/src/internals/ctxt.rs):
rust
// serde_derive/src/internals/ctxt.rs:12
#[derive(Default)]
pub struct Ctxt {
errors: RefCell<Option<Vec<syn::Error>>>,
}
impl Ctxt {
pub fn error_spanned_by<A: ToTokens, T: Display>(&self, obj: A, msg: T) {
self.errors.borrow_mut().as_mut().unwrap()
.push(syn::Error::new_spanned(obj.into_token_stream(), msg));
}
pub fn check(self) -> syn::Result<()> {
let mut errors = self.errors.borrow_mut().take().unwrap().into_iter();
let Some(mut combined) = errors.next() else {
return Ok(());
};
for rest in errors {
combined.combine(rest);
}
Err(combined)
}
}
impl Drop for Ctxt {
fn drop(&mut self) {
if !thread::panicking() && self.errors.borrow().is_some() {
panic!("forgot to check for errors");
}
}
}注意 Drop 实现——如果 Ctxt 被丢弃但没有调用 check(),会 panic。这是一个编译期 linter——防止开发者忘记检查错误。
这是 Rust 用 Drop 做"强制调用某方法"的经典模式。第 9 章的 Builder 我们没加这种保护,但生产级的 serde_derive 加了。每一处细节都在保证"错误不会被静默吞掉"。
10.7 代码生成层:ser.rs 的主干
看 expand_derive_serialize 函数的骨架(简化自 serde_derive/src/ser.rs):
rust
pub fn expand_derive_serialize(input: &mut syn::DeriveInput) -> syn::Result<TokenStream> {
let ctxt = Ctxt::new();
let cont = match Container::from_ast(&ctxt, input, Derive::Serialize, &private_ident) {
Some(cont) => cont,
None => return Err(ctxt.check().unwrap_err()),
};
precondition(&ctxt, &cont);
ctxt.check()?;
// 根据 data 分派到 struct_body 或 enum_body
let body = Stmts(match cont.data {
Data::Enum(..) => serialize_body_enum(&cont, ¶ms),
Data::Struct(Style::Struct, ..) => serialize_body_struct_struct(&cont, ¶ms),
Data::Struct(Style::Tuple, ..) => serialize_body_struct_tuple(&cont, ¶ms),
Data::Struct(Style::Newtype, ..) => serialize_body_struct_newtype(&cont, ¶ms),
Data::Struct(Style::Unit, ..) => serialize_body_unit_struct(&cont, ¶ms),
});
// 生成 impl 块外壳
let impl_generics = build_generics(&cont);
let (_, ty_generics, where_clause) = cont.generics.split_for_impl();
let ident = &cont.ident;
let impl_block = quote! {
impl #impl_generics _serde::Serialize for #ident #ty_generics #where_clause {
fn serialize<__S>(&self, __serializer: __S)
-> _serde::__private::Result<__S::Ok, __S::Error>
where __S: _serde::Serializer,
{
#body
}
}
};
Ok(dummy::wrap_in_const(impl_block))
}六个步骤:
- 创建 Ctxt 错误上下文
- 加工 Container AST
- 预检查(precondition 一些全局约束)
- 分派到具体生成函数(struct/enum 的不同 style)
- 生成 impl 外壳(用 quote! 组装)
- 包装到 dummy const(dummy::wrap_in_const,下面讲)
dummy const 包装是 serde 生成代码的一个有趣细节。看 serde_derive/src/dummy.rs:
rust
// serde_derive/src/dummy.rs
pub fn wrap_in_const(code: TokenStream) -> TokenStream {
quote! {
#[doc(hidden)]
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
const _: () = {
#[allow(unused_extern_crates, clippy::useless_attribute)]
extern crate serde as _serde;
#code
};
}
}所有生成的 impl 块被包在 const _: () = { ... }; 里。为什么?
原因 1:隔离命名。 const 块有自己的作用域——里面的 use 和 extern crate 不会泄漏到用户代码。extern crate serde as _serde; 引入 serde 别名而不污染用户 namespace。
原因 2:关闭 lint。 #[allow(...)] 批量关掉一些 lint,避免生成代码触发警告。
原因 3:#[doc(hidden)] 让这段代码不出现在文档里。
这是过程宏里常用的**"包裹一层 const 隔离副作用"** 模式。你写自己的 derive 宏达到生产级时,会发现这个包装是必要的——生成代码的 use 和 extern crate 总会和用户代码冲突。
10.8 bound.rs:自动推导泛型约束
泛型类型的 derive 有一个棘手问题:
rust
#[derive(Serialize)]
struct Wrapper<T> {
data: T,
}生成的 impl 应该是:
rust
impl<T: Serialize> Serialize for Wrapper<T> {
// ...
}注意 T: Serialize 这个 bound——它不在用户代码里,必须由宏推导。
serde_derive/src/bound.rs 的 425 行代码专门处理这件事。核心逻辑:
- 遍历所有字段的类型
- 对每个出现在类型里的泛型参数,加上
Serialize(或Deserialize)约束 - 合并到原有的 where 子句
简单情况:字段类型直接是 T,加上 T: Serialize 即可。
复杂情况 1:字段是 Vec<T>——同样加 T: Serialize(因为 Vec<T>: Serialize 要求 T: Serialize)。
复杂情况 2:字段是 PhantomData<T>——不加。因为 PhantomData<T>: Serialize 对任何 T 都成立,加 T: Serialize 会导致用户在用 PhantomData 包不可序列化类型时编译失败。serde_derive 特殊处理 PhantomData。
复杂情况 3:#[serde(bound = "T: MyBound")]——用户显式指定 bound,宏跳过自动推导,用用户的。
这一段代码非常微妙,读起来很吃力。但它让 #[derive(Serialize)] 对绝大多数类型"开箱即用"——用户不需要手动写 trait bound。是 derive 宏"魔法感"的重要来源。
10.8.1 with_bound 的真实骨架:三个集合和一个递归访问器
把上面"步骤 1-3"展开到源码,bound.rs:98 的 with_bound 函数实际上是一个围绕三个集合的 AST 访问器:
rust
// serde_derive/src/bound.rs:104
struct FindTyParams<'ast> {
// 全部泛型参数(比如 struct Foo<A, B, C> 里的 A, B, C)
all_type_params: HashSet<syn::Ident>,
// 实际出现在字段类型里的那部分(比如只有 A 和 B 被用到)
relevant_type_params: HashSet<syn::Ident>,
// 字段类型是"泛型参数的关联类型"(如 T::Item)
associated_type_usage: Vec<&'ast syn::TypePath>,
}三个集合的分工解释了"为什么 Serde 的 bound 推导能对付极其复杂的泛型":
all_type_params - relevant_type_params = 未被使用的泛型。Foo<A, B, C> 里如果只有字段用 A 和 B,C 没被任何字段引用——它不会被加 Serialize 约束。Rust 允许未使用的泛型参数(通过 PhantomData 或其他用法表达),如果宏对所有泛型都加约束,带 C 但不需要 C: Serialize 的场景就会被过约束化。
associated_type_usage 收集 T::Item 这类路径。visit_field(line 120-129)里有一个判断:如果字段类型是 T::SomeAssoc 形式(路径的第一个段是一个已知泛型参数),就记到这里。之后生成的 bound 不是 T: Serialize——而是 T::SomeAssoc: Serialize(需要对关联类型本身加约束,因为 T: Serialize 不能传递给 T::SomeAssoc)。
三处硬编码的例外处理是这套算法能工作的关键,每一处都对应一个 Rust 类型系统的细节:
PhantomData 跳过(line 131-137,源码原文注释):
rustif seg.ident == "PhantomData" { // Hardcoded exception, because PhantomData<T> implements // Serialize and Deserialize whether or not T implements it. return; }PhantomData<T>对任何 T 都实现 Serialize/Deserialize——不应为了它给 T 加Serialize约束。没有这个特判,struct Wrapper<T> { x: u32, _t: PhantomData<T> }会被错误地要求T: Serialize,虽然 T 实际上从未被序列化。单段路径才算命中(line 139):
if path.leading_colon.is_none() && path.segments.len() == 1。只有字面的T被视为"泛型参数的引用",some::module::T或crate::T不算。意图很明确——带路径的T是一个外部类型、不是本 struct 的泛型参数,加 bound 不对。visit_macro直接 no-op(line 250,带注释// Type parameter should not be considered used by a macro path):ruststruct TypeMacro<T> { mac: T!(), // 宏调用结果不可知 marker: PhantomData<T>, }如果宏真的被递归访问,
T!()内部可能出现T——但宏展开之前没办法知道结果是什么。设计选择:不进入宏,让"宏里用 T"变成未命中relevant_type_params。配合 PhantomData 兜底,这种类型仍能 derive 成功。如果T!()展开后真需要T: Serialize,用户必须手写#[serde(bound = "T: Serialize")]——宁可漏加、也不瞎猜。
visit_type 的 12+ 分支大 match(line 152-194)枚举了 syn::Type 的所有变体:Array/BareFn/Group/ImplTrait/Macro/Paren/Path/Ptr/Reference/Slice/TraitObject/Tuple/Infer/Never/Verbatim。对每一种都有对应递归——&'a T 递归访问 T、(T, U) 分别访问两个元素、dyn Trait<X> 访问 bound 里的 X。这 12 个分支就是 Rust 类型语法里所有可能"藏着泛型参数"的位置——少任何一个,某种类型的 derive 都会误推 bound。#![cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))] 这一行(line 154)把编译器非穷举告警作为测试模式下的硬错误——Rust 新增 syn::Type 变体时漏处理会立刻被 CI 发现。这是"用类型系统守护自己不退化"的典型范式。
读完这一节你就明白:这 425 行"自动加 bound"的代码之所以"真能对所有正常类型开箱即用",靠的不是什么魔法——靠的是一个三集合跟踪器 + 12 种类型变体的完整递归访问 + 3 条经过严谨理由论证的硬编码特判。每条特判后面都有对应的代码注释说明"为什么必须这样"——这也是 dtolnay 代码库的一贯风格:特殊情况不是瑕疵、是文档化的工程智慧。
10.9 Deserialize 比 Serialize 复杂一个数量级
对比 ser.rs 和 de/ 目录的行数:
- ser.rs:1369 行
- de.rs + de/:976 + 2730 = 3706 行
Deserialize 代码量是 Serialize 的 2.7 倍。为什么?
原因 1:Visitor 模式的复杂度。第 4 章讲过——反序列化要为每种期望类型写一个 Visitor,实现十几个 visit_* 方法。Serialize 只要调用一个 serialize_* 方法。
原因 2:Enum 的四种 tag 模式。Deserialize 要处理 externally、internally、adjacently、untagged 四种。每种 tag 模式在反序列化时流程完全不同——必须有四个独立的实现文件(de/enum_*.rs)。Serialize 这边只是写 key 的方式不同,逻辑简单。
原因 3:字段名识别。反序列化要处理输入里的"未知字段"、"重复字段"、"丢失字段"等。这些问题在 de/identifier.rs(477 行)里专门处理。
原因 4:借用反序列化(第 15 章主题)。&'de str 字段要从 'de 借用中取值,涉及生命周期推导。Serialize 不需要考虑这些。
第 12 章讲 Serialize 代码生成(相对简单);第 13 章讲 Deserialize 代码生成(复杂得多);第 14 章单独讲 enum 的四种 tag 策略(Deserialize 里最复杂的部分)。
10.10 生成代码是什么样的:一个完整例子
为了让前面的抽象具象化,看一个用户代码和展开后的代码对比:
用户代码:
rust
#[derive(Serialize, Deserialize)]
pub struct UserRenamed {
pub id: u64,
#[serde(rename = "full_name")]
pub name: String,
}cargo expand 真实输出 Serialize 部分(serde 1.0.228 + serde_derive 1.0.228 + rustc 1.89.0,2026-04-20 实测,完整贴出未省略):
rust
#[doc(hidden)]
#[allow(
non_upper_case_globals,
unused_attributes,
unused_qualifications,
clippy::absolute_paths,
)]
const _: () = {
#[allow(unused_extern_crates, clippy::useless_attribute)]
extern crate serde as _serde;
#[automatically_derived]
impl _serde::Serialize for UserRenamed {
fn serialize<__S>(
&self,
__serializer: __S,
) -> _serde::__private228::Result<__S::Ok, __S::Error>
where
__S: _serde::Serializer,
{
let mut __serde_state = _serde::Serializer::serialize_struct(
__serializer,
"UserRenamed",
false as usize + 1 + 1,
)?;
_serde::ser::SerializeStruct::serialize_field(
&mut __serde_state,
"id",
&self.id,
)?;
_serde::ser::SerializeStruct::serialize_field(
&mut __serde_state,
"full_name",
&self.name,
)?;
_serde::ser::SerializeStruct::end(__serde_state)
}
}
};几个观察:
- 完全符合第 3 章讲的 Serializer 协议:
serialize_struct→ 多次serialize_field→end。 - 字段名使用 rename 后的值:
"full_name"而不是"name"。 - 所有 serde 函数调用都用全路径:
_serde::ser::SerializeStruct::serialize_field。 - 变量都是
__前缀:__serde_state、__S、__serializer。 __private带版本 patch 号变成__private228(patch=228 对应 1.0.228)——同一 binary 内多版本共存时的符号隔离机制。#[automatically_derived]标记:告诉 rustc "此 impl 是宏生成的",部分 lint 会放宽。false as usize + 1 + 1:字段数从false as usize起算——这个看似冗余的false是 serde_derive 的模板统一化设计(internally-tagged enum 时会变成true,为 tag 字段预留 1 位)。普通 struct 这里永远是false,被 rustc 优化为常量 0。- 整个代码包在
const _: () = { ... };里——下一节详述。
Deserialize 展开要长得多(约 80 行)——包括一个 FieldVisitor 识别字段名、一个 UserVisitor 组装最终值、一个 field_enum 枚举。第 13 章会带你一行行看这段代码。
10.11 架构的关键设计选择
回顾本章内容,serde_derive 有几个值得学习的架构设计:
1. 双 AST 层(syn::DeriveInput → Container)。把"通用 Rust AST"和"serde 视角 AST"分开,让后续代码生成可以面对一个干净的对象,不用每次都去解析属性。
2. 错误累积模式(Ctxt)。用 RefCell<Vec<Error>> 收集错误,最后一次性报告。用户体验远好于"第一个错误就退出"。
3. 全路径 hygiene。所有生成的代码都用 _serde::std:: 全路径,避免和用户代码冲突。细致的 hygiene 是生产级过程宏的标志。
4. dummy const 包装。用 const _: () = { ... } 隔离副作用,让 extern crate 和 lint 不影响用户代码。
5. 模块划分。Serialize 和 Deserialize 分开文件;Deserialize 按 enum tag 模式分为 4 个独立文件。避免一个文件承担过多职责。
6. 泛型 bound 自动推导(bound.rs)。这是"魔法感"的来源——用户不用手动写 T: Serialize。
这些模式你可以在自己的过程宏里复用。写一个 derive 宏如果想做到工业级,就按这张图组织——即使你的宏小得多,最终架构应该类似。
10.12 和丛书其他书的联系
serde_derive 的设计思想可以和丛书其他书的内容互相印证:
- 丛书卷一《Rust 编译器》第 14 章讲过 Rust 宏在编译管道里的位置——原文:"宏展开发生在 AST 阶段——语法分析之后、名称解析和类型检查之前"。本章的"Container 加工"正是在 AST 层(
syn::DeriveInput层)的工作——从编译器流水线看、expand_derive_serialize被调用时符号还没解析、类型还没检查——这就是为什么 serde_derive 不能依赖任何"已知类型"信息(比如不能问"Vec<T>是否实现了 Serialize"),只能基于字面量类型路径做静态判断。 - **丛书《MCP 协议设计与实现》第 3 章"JSON-RPC 与消息格式"**用的所有
#[derive(Serialize, Deserialize)]生成的就是本章讲的这些代码——读过那一章再回来看本章,能理解"这些生成代码在真实 RPC 协议里是如何被调用的"。
10.12.1 "三大文件占 55%":serde_derive 头部胖、尾巴瘦的实测
把 §10.2 的 19 文件按行数排序——
| 排名 | 文件 | 行 | 累计 | 累计份额 |
|---|---|---|---|---|
| 1 | internals/attr.rs | 1818 | 1818 | 20.3% |
| 2 | ser.rs | 1369 | 3187 | 35.5% |
| 3 | de.rs | 976 | 4163 | 46.4% |
| 4 | de/struct_.rs | 697 | 4860 | 54.2% |
| 5 | internals/check.rs | 477 | 5337 | 59.5% |
| 5 | de/identifier.rs | 477 | 5814 | 64.8% |
| 7 | bound.rs | 425 | 6239 | 69.6% |
| 8 | de/enum_adjacently.rs | 324 | 6563 | 73.2% |
| 9 | internals/receiver.rs | 293 | 6856 | 76.5% |
| 10 | de/tuple.rs | 283 | 7139 | 79.6% |
| 11~19 | 其余 9 文件 | 1830 | 8969 | 100% |
两条值得记住的物理事实——
- 前 3 大文件(attr / ser / de)占 46.4%——再加 4-5 名
de/struct_.rs和internals/check.rs就过半(59.5%)——这意味着只读这 5 个文件就读懂了 60% 的 serde_derive——是给读者最高效的入门路径 - 后 9 个小文件(< 300 行)合计 1830 行——分布在 internals/ 和 de/ 子目录里——它们大多是单一概念的薄实现(
respan.rs16 行重设 span /dummy.rs31 行包装 const /this.rs32 行)——这是 Rust 项目里"小文件自洽语义"的范例:每个文件只有一个pub类型或一个pub函数
"一文件一职责"的纪律有反例吗?——internals/attr.rs 1818 行是反例——它塞了 Container / Variant / Field 三种 attr 数据类(章 11 详细讲)+ 它们的解析逻辑——按理可以拆三文件、但 Serde 选择不拆——因为三种 attr 共享80% 的解析 helper(get_lit_str / parse_lit_into_path 等)、跨文件复用反而冗长。这是"集中 vs 分散"的工程取舍——对应 LangChain manager.py 2697 / Vite css.ts 3552 / vllm loader.py 1542 的同款选择。
10.12.2 Deserialize 入口管线:架构图在 de.rs 里怎样落地
前面从文件规模和模块关系讲 serde_derive 架构,这里把它压到一条真实调用链。serde_derive/src/de.rs:25-40 是 Deserialize 侧总入口:先 replace_receiver(input),再用 Container::from_ast 把 syn::DeriveInput 变成 serde 自己的 Container,随后构造 Parameters,计算带 'de 的 impl generics,最后把 deserialize_body(&cont, ¶ms) 包装成 Stmts。
这条链说明第 10 章的"双 AST 层"不是抽象设计图,而是每次 derive 都会经过的真实流程:
源码里有三个架构点尤其值得注意。
第一,远程 derive 是早期分叉。serde_derive/src/de.rs:43-58 如果发现 remote 属性,就生成 inherent fn deserialize,而不是 impl Deserialize for Type。这不是后期修补,而是在入口阶段直接改变 impl 形状。原因很简单:remote 类型不属于当前 crate,不能为它直接实现外部 trait,只能生成辅助函数。
第二,泛型 bound 不是在 quote 模板里零散拼出来的。serde_derive/src/de.rs:190-228 的 build_generics 先保留用户显式 bound,再按字段和 variant 推导 Deserialize<'de> / Default。serde_derive/src/de.rs:230-245 明确跳过 skip_deserializing、deserialize_with 和显式 bound 的字段,因为这些字段并不是由默认生成逻辑反序列化。这个策略避免了常见宏错误:给不需要的类型参数加上过强 bound,导致用户类型明明可用却编译失败。
第三,主体生成是按数据形态分派。serde_derive/src/de.rs:306-329 的 deserialize_body 先处理 transparent、from、try_from 这些容器级捷径,再按 Data::Enum、Data::Struct(Struct)、Data::Struct(Tuple/Newtype)、Data::Struct(Unit) 分派到不同模块。第 13 章的 struct 生成、第 14 章的 enum tag 生成,都是从这个 match 走出去的。
| 架构职责 | 源码位置 | 为什么放这里 |
|---|---|---|
| AST 归一化 | de.rs:25-40 | 后续不再面对原始 syn |
| remote 分叉 | de.rs:43-58 | impl 形状不同,必须早处理 |
| bound 推导 | de.rs:190-228 | 所有分支共享同一套泛型结果 |
| 字段 bound 过滤 | de.rs:230-245 | 防止 skip / with 字段污染 where clause |
| 数据形态分派 | de.rs:306-329 | struct、tuple、enum 的 visitor 模板完全不同 |
这个入口还解释了第 11 章属性系统为什么那么大。因为 deserialize_body 不会再重新解析 #[serde(...)] 字符串,它只读取 cont.attrs、field.attrs、variant.attrs。属性必须在 Container::from_ast 阶段一次性解析成结构化数据,否则后面的每个生成模块都要重复处理 rename、default、flatten、tag、borrow,代码会迅速散掉。
从工程视角看,serde_derive 的架构不是"入口调很多函数"这么简单,而是把不可变决策尽量前置:哪些字段存在、哪些字段跳过、哪些生命周期要绑定、impl 是 remote 还是普通、数据形态是什么。到了 ser.rs / de/struct_.rs / de/enum_*.rs,代码生成器只需要消费这些决策,而不是再做语义判断。
这条分层还能解释为什么 Deserialize 比 Serialize 更分散。Serialize 的信息流是 &self 到 Serializer,字段能直接按顺序写出;Deserialize 的信息流是输入格式回调 Visitor,生成器必须先准备字段枚举、主 Visitor、缺字段表达式、alias 匹配、borrow lifetime、in-place 分支。serde_derive/src/de.rs:338-347 对 deserialize_in_place 的提前排除就是典型例子:有些属性组合从架构上就不适合生成 in-place 更新逻辑,入口层先判掉,后面的 struct_ / tuple 模块就不必每个模板都处理一遍。
读大型过程宏时,可以把这种分层当检查表:入口是否薄,AST 是否归一,属性是否结构化,泛型 bound 是否集中推导,代码生成是否只消费已经算好的决策。serde_derive 在这些点上都给了可复用范式。
这个检查表还有一个实践价值:定位 bug。编译错误指向属性解析,先看 internals/attr.rs;where clause 过强,先看 bound.rs 和 build_generics;生成代码形状不对,才进入 ser.rs 或 de/ 子目录。分层清楚,源码阅读就不会从 9000 行里盲搜。
这也是 serde_derive 值得反复读的原因:它不是只展示"如何写一个宏",而是展示"宏长大以后如何不失控"。入口薄、数据结构稳、生成模块按语义拆开,这三点比任何单个 quote 模板都更有迁移价值。
把这套方法迁移到自己的宏项目时,不必照抄文件数量,但要照抄责任边界。先把用户输入整理成自己的中间结构,再集中校验,再生成代码。不要让解析、校验和输出模板混在同一个函数里。
边界越早固定,后面的模板越简单,错误信息也越容易指回用户写错的位置,维护成本也越低。
10.13 本章小结
serde_derive 是一个 9000 行的工程,但它的架构只需 4 句话就能概括:
- 入口(lib.rs 127 行):
#[proc_macro_derive]接收DeriveInput。 - AST 加工(internals/ 约 3100 行):syn::DeriveInput → serde::Container,解析所有属性。
- 代码生成(ser.rs + de/ 约 5000 行):根据 Container 生成 impl 块。
- 错误兜底:
Ctxt累积错误,最外层转 compile_error。
三层分离让每层可独立推理——读 internals/ 不用关心代码生成,读 ser.rs 不用关心属性如何解析。这种分层是 serde_derive 能在十年里保持可维护的关键。
internals/attr.rs 1818 行 = 整个 internals/ 55%——下一章的全部主题就是这一个文件——它实现了 serde 全部属性(rename / default / flatten / skip / tag / untagged / with / bound)的解析;attr.rs 比 ser.rs (1369) 还大 33% 是个反直觉的数字,说明"属性系统的复杂度比代码生成本身更高"——也是 Serde 用户体验好的根本来源。
动手实验
- 阅读 lib.rs:打开
serde_derive/src/lib.rs,对照本章 10.3 节逐段读完。127 行,大约需要 30 分钟。 - 跟踪一个属性:在 internals/attr.rs 里搜索
rename关键字,找到它如何被解析成attr::Field的一个字段。 - 用 cargo expand 观察不同结构的生成结果:
- 普通 struct
- 带
#[serde(rename_all)]的 struct - 带泛型的 struct(观察自动推导的 bound)
- enum 用默认 tag
- enum 用
#[serde(tag = "type")]对比它们的差异,思考"为什么要这样生成"。
- 画你自己的架构图:读完 lib.rs 和 ast.rs 后,用自己的话画一张"serde_derive 核心数据流图"。
延伸阅读
- serde 官方的 "Implementing Serialize" 和 "Implementing Deserialize":理解 serde_derive 生成的代码长什么样。
- proc-macro-workshop 的 Debug 题:需要处理 PhantomData、泛型 bound 推导——serde_derive 的简化版,做完对 bound.rs 有更深理解。
- serde_derive 本身的 Cargo.toml:看它的依赖和 feature 配置,理解 proc-macro crate 的工程最佳实践。
- 丛书卷一《Rust 编译器》第 14 章:编译器视角下的宏展开。