Appearance
第 12 章 Serialize 代码生成:结构体与枚举
12.1 从设计意图到真实代码
第 10 章画了 serde_derive 的整体架构,第 11 章拆解了属性系统。现在我们走到"生成 impl Serialize 块"这一步——这是整个过程的终点,前面所有工作(解析 AST、解析属性、检查一致性)都是为了这一刻做准备。
本章的主角是 serde_derive/src/ser.rs——1369 行代码、21 个 serialize_* 分派函数(grep -c "^fn serialize_" serde_derive-1.0.228/src/ser.rs 得到的精确计数)。它的任务:接收一个已经处理好的 Container,输出一段 impl Serialize for T 的 TokenStream。
核心模式很简单:根据 Container::data 分派到不同的生成函数。每个函数按照第 3 章讲的 Serializer 协议调用对应方法,组合成最终 impl 块。
但实际情况复杂得多,因为要处理:
- 四种 struct 形态:Struct(命名)、Tuple、Newtype、Unit
- enum 的五种子情况:Unit variant、Newtype variant、Tuple variant、Struct variant,外加四种 tag 模式的组合
- 十几种影响生成代码的属性:rename、skip、skip_serializing_if、flatten、transparent、serialize_with、remote...
这使得 ser.rs 虽然"只是模板代码",也成了千余行的工程。本章带你走一遍主要分支,理解每一处的生成策略。不把全部代码讲完(那样会变成枯燥的代码朗读)——只聚焦最有代表性的几个分派点。
12.2 expand_derive_serialize 的全貌
rust
// serde_derive/src/ser.rs:13 (精简)
pub fn expand_derive_serialize(input: &mut syn::DeriveInput) -> syn::Result<TokenStream> {
replace_receiver(input); // 处理 Self 引用
let ctxt = Ctxt::new();
let Some(cont) = Container::from_ast(&ctxt, input, Derive::Serialize, &private.ident()) else {
return Err(ctxt.check().unwrap_err());
};
precondition(&ctxt, &cont);
ctxt.check()?;
let ident = &cont.ident;
let params = Parameters::new(&cont);
let (impl_generics, ty_generics, where_clause) = params.generics.split_for_impl();
let body = Stmts(serialize_body(&cont, ¶ms));
let impl_block = if let Some(remote) = cont.attrs.remote() {
// remote 模式的 impl:生成独立函数而不是 trait 实现
quote! {
impl #impl_generics #ident #ty_generics #where_clause {
pub fn serialize<__S>(__self: &#remote #ty_generics, __serializer: __S)
-> _serde::#private::Result<__S::Ok, __S::Error>
where __S: _serde::Serializer,
{
#body
}
}
}
} else {
// 普通 impl Serialize
quote! {
#[automatically_derived]
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(cont.attrs.custom_serde_path(), impl_block))
}八步:
replace_receiver:把类型定义里的Self替换成具体类型名(处理一个 Rust 的边缘情况,见下文)。Container::from_ast:加工 AST(第 10 章讲过)。precondition:一些早期检查(如#[serde(field_identifier)]不能用于 Serialize)。ctxt.check()?:把累积的错误转换成 Result 返回。Parameters::new:封装生成代码需要的参数(self 的变量名、generics 等)。serialize_body:核心分派——根据cont.data生成方法体 Fragment。- 选择 impl 外壳:
remote模式(生成自由函数)vs 普通模式(实现 trait)。 - dummy const 包装(第 10 章讲过)。
Parameters 是一个很重要的辅助类型:
rust
struct Parameters {
self_var: Ident, // "self" 或 "__self"(remote 模式)
this_type: TokenStream, // 类型引用(带 generics)
this_value: TokenStream,
generics: syn::Generics, // 加了 Serialize bound 的 generics
is_remote: bool,
is_packed: bool,
}把"生成代码时需要的上下文"打包——下层函数只需要 &Parameters 就能访问所有必要信息。
12.3 serialize_body:五路分派
serialize_body 函数是代码生成的入口(serde_derive/src/ser.rs:171):
rust
fn serialize_body(cont: &Container, params: &Parameters) -> Fragment {
if cont.attrs.transparent() {
serialize_transparent(cont, params)
} else if let Some(type_into) = cont.attrs.type_into() {
serialize_into(params, type_into)
} else {
match &cont.data {
Data::Enum(variants) => serialize_enum(params, variants, &cont.attrs),
Data::Struct(Style::Struct, fields) => serialize_struct(params, fields, &cont.attrs),
Data::Struct(Style::Tuple, fields) => serialize_tuple_struct(params, fields, &cont.attrs),
Data::Struct(Style::Newtype, fields) => serialize_newtype_struct(params, &fields[0], &cont.attrs),
Data::Struct(Style::Unit, _) => serialize_unit_struct(&cont.attrs),
}
}
}五个分支:
transparent属性:直接转发字段的 serializeinto属性:先 clone 转成目标类型,再序列化- Enum:走 enum 专门流程(下一节详述)
- Struct 四种 style:分别处理
Fragment 是什么? 看 serde_derive/src/fragment.rs(74 行):
rust
pub enum Fragment {
Expr(TokenStream), // 单个表达式
Block(TokenStream), // 一段语句块
}Fragment 是"代码片段"的抽象——它要么是一个表达式(42、foo()),要么是一段语句块({ let x = 1; x })。这个区分重要,因为上下文可能要求表达式(赋值右侧)或语句(单独一行)。
quote_expr! / quote_block! 这两个宏分别构造 Expr 和 Block Fragment。最终 Stmts(fragment) 把 Fragment 包装成可以在 impl fn serialize {} 里放的语句。
12.4 最简单的:unit struct
rust
// serde_derive/src/ser.rs:223
fn serialize_unit_struct(cattrs: &attr::Container) -> Fragment {
let type_name = cattrs.name().serialize_name();
quote_expr! {
_serde::Serializer::serialize_unit_struct(__serializer, #type_name)
}
}用户写:
rust
#[derive(Serialize)]
struct Empty;生成:
rust
_serde::Serializer::serialize_unit_struct(__serializer, "Empty")一个方法调用搞定——因为 unit struct 没有数据。这是本章最简单的例子。
注意 cattrs.name().serialize_name()——属性系统的输出被用来决定类型名。如果用户写了 #[serde(rename = "OtherName")],生成的代码里就是 "OtherName" 而不是 "Empty"。属性系统和代码生成在这里自然衔接。
为什么 unit struct 还要传 type_name?Serializer 协议设计时的权衡——严格说 unit struct 没有任何数据、理论上 serializer.serialize_unit() 就够了(serialize_unit 是 Serializer trait 上另一个方法、完全没字符串参数)。Serde 却专门留了 serialize_unit_struct(serializer, name) 的独立方法——多出一个 name 参数。原因是某些格式需要区分"unit 值"和"具名 unit 类型"——bincode 两者没差别(都 0 字节)、JSON 两者都是 null、但 XML / YAML 的一些 schema、debug 输出、自描述格式(比如 ron)会把 struct Foo; 编成 Foo 这个 token、而把裸 unit () 编成别的。传 name 是给这些格式保留区分能力——Serde 的哲学是 "让格式自己决定"、数据模型层总是把信息提供给下游、哪怕 JSON 这种主流格式会忽略它。
这是 Serde 设计纪律的一次体现——"宁可多传、不能少传"。serialize_unit_struct 内部实现在默认情况下会 self.serialize_unit()——多传的 name 被"转发到上级丢弃"。上游付一行代码的代价、换来下游任意格式的自由度。
12.5 Newtype struct:透明转发
rust
// serde_derive/src/ser.rs:231
fn serialize_newtype_struct(
params: &Parameters,
field: &Field,
cattrs: &attr::Container,
) -> Fragment {
let type_name = cattrs.name().serialize_name();
let mut field_expr = get_member(
params,
field,
&Member::Unnamed(Index { index: 0, span: Span::call_site() }),
);
if let Some(path) = field.attrs.serialize_with() {
field_expr = wrap_serialize_field_with(params, field.ty, path, &field_expr);
}
let span = field.original.span();
let func = quote_spanned!(span=> _serde::Serializer::serialize_newtype_struct);
quote_expr! {
#func(__serializer, #type_name, #field_expr)
}
}生成代码:
rust
_serde::Serializer::serialize_newtype_struct(__serializer, "Millimeters", &self.0)三个细节:
1. get_member 根据 Parameters::self_var 和字段的 member 生成字段访问表达式(&self.0 或 &__self.0——remote 模式时是后者)。封装这个细节让生成代码能在 remote 和非 remote 两种模式无缝切换。
2. serialize_with 属性处理:如果字段上有 #[serde(serialize_with = "my_fn")],wrap_serialize_field_with 会包装字段表达式成 &SerializeWith { field: &self.0, ... }——其中 SerializeWith 是一个临时 struct,它的 Serialize 实现调用用户指定的 my_fn。第 16 章会深入讲这个技巧。
3. quote_spanned! 用 field.original.span():让生成的 serialize_newtype_struct 调用的 span 指向用户代码的原字段——如果类型不实现 Serialize,错误指向用户代码的字段行,不是宏调用行。
这就是第 8 章讲的 span 管理的实战应用。每一处用户代码引用都用 quote_spanned!。
span 错位的典型症状——假如 serde_derive 用 quote! 代替 quote_spanned!、生成的 serialize_newtype_struct(__serializer, "Millimeters", &self.0) 调用的 span 就会落到宏调用点(即用户的 #[derive(Serialize)] 那一行)。当 self.0 的类型不实现 Serialize 时、rustc 抛出的"doesn't implement Serialize"错误箭头就会指向 #[derive(Serialize)] 这一行而不是实际的字段声明行。用户看到"整个 derive 宏在报错"、却不知道是哪个字段出问题——span 错位 = 用户体验崩坏。serde_derive 为此在所有字段级代码生成处强制使用 field.original.span()(field.original 是 &syn::Field、即用户源码里那一行的原始语法树节点、span 精确到字段名位置)。这个"spanned 到用户字段"的约定是整个 serde 生态错误提示质量的根基——而它的代价仅仅是多写一个 quote_spanned! 而非 quote!。
12.6 Struct:状态机模式
rust
// serde_derive/src/ser.rs:296 (简化)
fn serialize_struct(params: &Parameters, fields: &[Field], cattrs: &attr::Container) -> Fragment {
assert!(fields.len() as u64 <= u64::from(u32::MAX));
if cattrs.has_flatten() {
serialize_struct_as_map(params, fields, cattrs)
} else {
serialize_struct_as_struct(params, fields, cattrs)
}
}两种分派:
- 如果有
#[serde(flatten)]字段,走 map 模式(因为 flatten 会引入未知字段数量) - 否则走 struct 模式(字段数量编译期已知)
看 serialize_struct_as_struct(精简版):
rust
// serde_derive/src/ser.rs:328
fn serialize_struct_as_struct(
params: &Parameters,
fields: &[Field],
cattrs: &attr::Container,
) -> Fragment {
let serialize_fields = serialize_struct_visitor(fields, params, false, &StructTrait::SerializeStruct);
let type_name = cattrs.name().serialize_name();
let tag_field = serialize_struct_tag_field(cattrs, &StructTrait::SerializeStruct);
let tag_field_exists = !tag_field.is_empty();
let mut serialized_fields = fields
.iter()
.filter(|&field| !field.attrs.skip_serializing())
.peekable();
// 编译期计算字段数(常量)
let let_mut = mut_if(serialized_fields.peek().is_some() || tag_field_exists);
let len = serialized_fields
.map(|field| match field.attrs.skip_serializing_if() {
None => quote!(1),
Some(path) => {
let field_expr = get_member(params, field, &field.member);
quote!(if #path(#field_expr) { 0 } else { 1 })
}
})
.fold(
quote!(#tag_field_exists as usize),
|sum, expr| quote!(#sum + #expr),
);
quote_block! {
let #let_mut __serde_state = _serde::Serializer::serialize_struct(__serializer, #type_name, #len)?;
#tag_field
#(#serialize_fields)*
_serde::ser::SerializeStruct::end(__serde_state)
}
}核心生成代码的形状:
rust
let mut __serde_state = _serde::Serializer::serialize_struct(__serializer, "User", FIELD_COUNT)?;
/* 对每个字段:*/
_serde::ser::SerializeStruct::serialize_field(&mut __serde_state, "FIELD_NAME", &self.FIELD)?;
/* ... */
_serde::ser::SerializeStruct::end(__serde_state)注意字段计数的精妙处理:
- 如果字段没有
skip_serializing_if,贡献 1。 - 如果有
skip_serializing_if = "fn",贡献if fn(&self.field) { 0 } else { 1 }——运行时决定。 - 所有字段的贡献求和得到最终字段数——一半编译期常量一半运行时判断。
这种"字段数编译期尽可能静态、必要时动态"的设计来自 Serializer trait 的 serialize_struct(self, name, len: usize) 签名——len 是 usize 必须的。
为什么 len 要是精确值而不是上界?——这个设计约束不是 Rust 语言层面的要求、而是 Serde 数据模型的显式承诺。某些格式(bincode / CBOR / MessagePack)在 struct 的二进制编码里前置字段数、解码侧按这个数值预分配缓冲、然后精确读取对应个数的 serialize_field。len 多报一个——解码器会在缺失字段处卡住;少报一个——多出的字段直接损坏下游数据。所以 serde_derive 不能偷懒用"字段总数"、必须精确减去 skip_serializing_if 为真的字段数——这就是上文那个 fold 表达式的由来。这也是为什么 skip_serializing_if 和 skip_field 要同时存在——有些格式不需要"运行时跳过字段但要通知 Serializer"(JSON 这种自描述格式),有些格式需要(bincode 这种按位置编码的格式)。SerializeStruct::skip_field 在 JSON 里是 no-op、在 bincode 里会写一个"该位置无值"的占位符——同一份生成代码兼容两类格式。
注意 skip_serializing(无条件跳过)和 skip_serializing_if(条件跳过)的区别:
skip_serializing:在 filter 里排除,不出现在字段迭代中。skip_serializing_if:仍然迭代,但加 if 判断;即使跳过,也要调skip_field(某些格式需要知道"有这个字段但被略过")。
第 11 章讲的属性系统在这里全部落地为生成代码的 if 分支、字段过滤等。
12.7 Struct 字段迭代:serialize_struct_visitor
rust
// serde_derive/src/ser.rs:1105 (简化)
fn serialize_struct_visitor(
fields: &[Field],
params: &Parameters,
is_enum: bool,
struct_trait: &StructTrait,
) -> Vec<TokenStream> {
fields
.iter()
.filter(|&field| !field.attrs.skip_serializing())
.map(|field| {
let member = &field.member;
let mut field_expr = if is_enum {
let id = field_i(&field.member); // 元组字段用 id
quote!(#id)
} else {
let self_var = ¶ms.self_var;
quote!(&#self_var.#member)
};
let key_expr = field.attrs.name().serialize_name();
// 如果有 serialize_with,包装成 SerializeWith
if let Some(path) = field.attrs.serialize_with() {
field_expr = wrap_serialize_field_with(params, field.ty, path, &field_expr);
}
let span = field.original.span();
let ser_if_expr = if let Some(path) = field.attrs.skip_serializing_if() {
quote!(if !#path(#field_expr))
} else {
quote!()
};
let serialize_call = struct_trait.serialize_field(span, key_expr, field_expr);
let skip_call = struct_trait.skip_field(span, key_expr);
if ser_if_expr.is_empty() {
quote!(#serialize_call?;)
} else {
quote! {
#ser_if_expr { #serialize_call?; } else { #skip_call?; }
}
}
})
.collect()
}每个字段生成的代码形状(最复杂情况):
rust
if !is_skipped(&self.field) {
_serde::ser::SerializeStruct::serialize_field(
&mut __serde_state, "serialize_name", &self.field)?;
} else {
_serde::ser::SerializeStruct::skip_field(&mut __serde_state, "serialize_name")?;
}这是整个 Serialize 生成的"内循环"——每个字段变成一段这样的代码。
StructTrait 抽象:
rust
enum StructTrait {
SerializeStruct, // 普通 struct
SerializeStructVariant, // struct variant of enum
SerializeMap, // flatten 模式
}不同 trait 的 serialize_field 和 skip_field 方法路径不同。StructTrait 把它们抽象成统一接口——struct_trait.serialize_field(span, key, value) 根据具体变体生成对应 trait 的方法调用。
这个抽象让 serialize_struct_visitor 能被 3 种场景复用:普通 struct、enum 的 struct variant、flatten 模式。代码复用换来可读性——同一个"迭代字段并生成 serialize_field 调用"的逻辑,只有调用的 trait 路径不同。
12.8 Tuple struct:按 index 访问
rust
// serde_derive/src/ser.rs:257 (简化)
fn serialize_tuple_struct(
params: &Parameters,
fields: &[Field],
cattrs: &attr::Container,
) -> Fragment {
let serialize_stmts = serialize_tuple_struct_visitor(
fields, params, false, &TupleTrait::SerializeTupleStruct,
);
let type_name = cattrs.name().serialize_name();
let mut serialized_fields = fields
.iter()
.filter(|&field| !field.attrs.skip_serializing())
.peekable();
let let_mut = mut_if(serialized_fields.peek().is_some());
let len = serialized_fields
.map(|field| match field.attrs.skip_serializing_if() {
None => quote!(1),
Some(path) => {
let index = field_i(&field.member);
quote!(if #path(&#index) { 0 } else { 1 })
}
})
.fold(quote!(0), |sum, expr| quote!(#sum + #expr));
quote_block! {
let #let_mut __serde_state = _serde::Serializer::serialize_tuple_struct(
__serializer, #type_name, #len)?;
#(#serialize_stmts)*
_serde::ser::SerializeTupleStruct::end(__serde_state)
}
}生成代码:
rust
let mut __serde_state = _serde::Serializer::serialize_tuple_struct(
__serializer, "Point", 2)?;
_serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.0)?;
_serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.1)?;
_serde::ser::SerializeTupleStruct::end(__serde_state)和普通 struct 几乎一样——只是字段访问从 self.name 变成 self.0,调用的 trait 从 SerializeStruct 变成 SerializeTupleStruct,没有字段名(serialize_field 只传 value 不传 key)。
12.9 Enum:最复杂的分派
Enum 的序列化是 ser.rs 里最复杂的部分,约 600 行。核心入口(serde_derive/src/ser.rs:395):
rust
fn serialize_enum(params: &Parameters, variants: &[Variant], cattrs: &attr::Container) -> Fragment {
let self_var = ¶ms.self_var;
let arms: Vec<_> = variants
.iter()
.enumerate()
.map(|(variant_index, variant)| {
serialize_variant(params, variant, variant_index, cattrs)
})
.collect();
quote_expr! {
match *#self_var {
#(#arms)*
}
}
}生成一个 match self 表达式,每个变体一个 arm。核心逻辑在 serialize_variant:
rust
// serde_derive/src/ser.rs:421 (简化)
fn serialize_variant(
params: &Parameters,
variant: &Variant,
variant_index: usize,
cattrs: &attr::Container,
) -> TokenStream {
let this_value = ¶ms.this_value;
let variant_ident = &variant.ident;
if variant.attrs.skip_serializing() {
// 跳过——生成 panic
...
} else {
let body = match cattrs.tag() {
attr::TagType::External => serialize_externally_tagged_variant(...),
attr::TagType::Internal { tag } => serialize_internally_tagged_variant(..., tag),
attr::TagType::Adjacent { tag, content } => serialize_adjacently_tagged_variant(..., tag, content),
attr::TagType::None => serialize_untagged_variant(...),
};
// match arm 骨架
match variant.style {
Style::Unit => quote! { #this_value::#variant_ident => { #body } },
Style::Newtype => quote! { #this_value::#variant_ident(ref __field0) => { #body } },
Style::Tuple => {
let field_names: Vec<_> = (0..variant.fields.len())
.map(|i| Ident::new(&format!("__field{}", i), Span::call_site()))
.collect();
quote! { #this_value::#variant_ident(#(ref #field_names),*) => { #body } }
}
Style::Struct => {
let members = variant.fields.iter().map(|f| &f.member);
let names: Vec<_> = ...;
quote! { #this_value::#variant_ident { #(#members: ref #names),* } => { #body } }
}
}
}
}两个分派:
- 按 tag 模式分派(第 14 章专讲):External/Internal/Adjacent/Untagged——4 种不同的 tag 策略。
- 按 variant style 分派:Unit/Newtype/Tuple/Struct——4 种变体形态。
4 × 4 = 16 种组合。每种组合的生成代码都不一样。这就是 ser.rs 行数膨胀的直接来源。
但 16 种并非每种都合法——internals/check.rs 在 Container::from_ast 收尾前会拦掉非法组合。例如 #[serde(tag = "type")] 的 internally tagged enum 不允许 Tuple variant(多个值塞不进一个 map 的"tag + content 同层"里)——生成代码真走到那个分支时用 unreachable!("checked in serde_derive_internals") 兜底(见 §12.10 引用的 serde_derive_internals match arm)。把"不可达"明确写出来是 Rust 的习惯——编译器能据此做代码生成优化、同时给未来维护者留下"这里曾经思考过"的痕迹。serde 的 derive 代码里 unreachable! 出现了十余次、全部对应 check.rs 已经拦截过的路径——是"编译期失败 + 运行时兜底"的双重保险。
下一章(第 13 章 Deserialize 代码生成)的 enum 部分更复杂——因为反序列化时还要根据 tag 识别走哪个变体。再下一章(第 14 章)专门剖析 4 种 tag 模式,让你彻底理解"为什么 enum 这么难"。
12.10 externally tagged variant:默认模式
最简单的 tag 模式——外部标签。对 enum E { V(u8) }:
rust
enum E { V(u8) }
let value = E::V(42);
// 序列化成 JSON: {"V": 42}看 serialize_externally_tagged_variant 的 Unit 分支(serde_derive/src/ser.rs:502,精简):
rust
fn serialize_externally_tagged_variant(...) -> TokenStream {
let variant_name = variant.attrs.name().serialize_name();
match variant.style {
Style::Unit => {
quote_expr! {
_serde::Serializer::serialize_unit_variant(
__serializer, #type_name, #variant_index as u32, #variant_name)
}
}
Style::Newtype => {
...
quote_expr! {
_serde::Serializer::serialize_newtype_variant(
__serializer, #type_name, #variant_index as u32, #variant_name, &__field0)
}
}
Style::Tuple => { /* 用 serialize_tuple_variant + 多次 serialize_field + end */ }
Style::Struct => { /* 用 serialize_struct_variant + 多次 serialize_field + end */ }
}
}生成代码(对 E::V(42)):
rust
match *self {
E::V(ref __field0) => {
_serde::Serializer::serialize_newtype_variant(
__serializer, "E", 0u32, "V", &__field0)
}
}直接调 Serializer 的 serialize_newtype_variant——把 enum 名、变体索引、变体名、值传给它,格式自己决定 JSON 编码。
四种 tag 模式的生成策略差异(第 14 章详解):
- External:调 Serializer 的
serialize_xxx_variant方法(上面例子)。 - Internal:手动在 map 里插一个 tag 键值对。
- Adjacent:生成一个带
tag和content两个字段的 map。 - Untagged:直接调字段的 Serialize,完全丢弃 tag 信息。
从 serialize_internally_tagged_variant 的源码能直接看出"手动插 tag"是什么意思(ser.rs:576-642)——对 Style::Unit 变体,生成代码精简如下:
rust
let mut __struct = _serde::Serializer::serialize_struct(__serializer, #type_name, 1)?;
_serde::ser::SerializeStruct::serialize_field(&mut __struct, #tag, #variant_name)?;
_serde::ser::SerializeStruct::end(__struct)一个有 0 个数据字段的 variant,却调用 serialize_struct 开出一个 1 字段的结构体——因为要把 tag 塞进去。这正是"internally tagged"这个名字的字面意思:tag 不再是 map 外的独立键(那是 external),而是和 data 混在同一层 map 里。对 Newtype variant,则调 __private::ser::serialize_tagged_newtype 这个隐藏在 serde 正式 crate 里的帮手函数——它会先反省要序列化的值"是不是 map/struct 形态"、如果是就把 tag 插进去;如果不是(比如 newtype 包了个 u32),internally tagged 压根没法表示、运行时会报错(这也是第 11 章 check.rs 里要拦截"internally tagged enum 的 newtype variant 只能包 map-like 类型"的原因——一部分错误放到编译期无法判断、只能 runtime 兜底)。
Adjacent 模式的不同之处在 serialize_adjacently_tagged_variant(ser.rs:642-766)里——它固定生成"2 字段 struct":第一个字段是 tag(变体名字符串),第二个字段是 content(不管什么 Style 的数据都塞成一个值)。所以对 Struct variant,生成代码会嵌套一层 serialize_struct——外层 2 字段(tag + content),内层 N 字段(真实字段)。生成的总代码量是四种 tag 模式里最大的——100 余行生成代码对应用户 1 行 enum E { V { x: u8 } } 的 Adjacent 变体。
12.11 辅助工具:SerializeWith 包装
一个值得细讲的模式——#[serde(serialize_with = "my_fn")] 如何处理。
用户代码:
rust
#[derive(Serialize)]
struct User {
#[serde(serialize_with = "serialize_timestamp")]
created_at: DateTime<Utc>,
}
fn serialize_timestamp<S: Serializer>(t: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error> {
s.serialize_i64(t.timestamp())
}期望行为:生成的代码应该调用 serialize_timestamp(&self.created_at, serializer) 而不是 self.created_at.serialize(serializer)。
挑战:serialize_struct 的 serialize_field 方法要求第二个参数必须实现 Serialize trait——不能直接传一个函数调用结果。
serde 的解决方案:生成一个临时 struct SerializeWith,它 wrap 字段值,然后给这个 wrapper 实现 Serialize:
rust
// 生成代码(精简)
struct SerializeWith<'__a> {
values: (&'__a DateTime<Utc>,),
phantom: PhantomData<User>,
}
impl<'__a> _serde::Serialize for SerializeWith<'__a> {
fn serialize<__S>(&self, __s: __S) -> Result<...> where __S: Serializer {
serialize_timestamp(self.values.0, __s)
}
}
// 然后在字段位置:
SerializeStruct::serialize_field(
&mut state,
"created_at",
&SerializeWith {
values: (&self.created_at,),
phantom: PhantomData,
},
)?;用临时类型 wrap + 为它实现 Serialize——这是一个非常 Rust 风格的技巧。函数指针不是 Serialize,但包装它们的临时类型可以成为 Serialize。
源码实现(serde_derive/src/ser.rs 的 wrap_serialize_field_with 函数、ser.rs:1171-1207)生成上面这段代码。这个模式也用于 deserialize_with(第 13 章)、with(第 16 章)。
为什么要走这么绕的一圈?——因为 Rust 的 trait system 不允许"在 generic struct 方法里直接传函数指针替代 trait 实现"。SerializeStruct::serialize_field<V: Serialize>(&mut self, key, value: &V) 的 value 形参有 Serialize 约束、函数指针 fn(&T, S) -> Result<...> 不满足这个约束。所以必须先造一个满足约束的载体类型(临时 SerializeWith struct)、再让这个载体的 Serialize 实现转发到用户函数。ser.rs 里这个技巧出现了 3 次——wrap_serialize_field_with(字段级)、wrap_serialize_variant_with(variant 级)、wrap_serialize_with(底层通用),分别对应 #[serde(serialize_with)] 能挂载的三种位置。
和 Rust 标准库的对称——std::fmt::Display 也有类似模式:write!(f, "{:>10}", value) 不能直接格式化任意函数、但可以用 format_args! 构造一个实现了 Display 的临时结构。"给中间值挂 trait 实现"是 Rust 生态的通用 workaround——Serde 把它用到极致、serde_derive 的 6 大 wrap_* 函数(含 Deserialize 侧)共占 ser.rs + de.rs 约 200 行代码。
一个常被问到的细节——SerializeWith 持有 phantom: PhantomData<User> 的用意是什么?为了让临时 wrapper 带上用户类型的 generic 参数——如果用户的 struct 是 User<T>、那 wrapper 也得是 SerializeWith<'a, T>、PhantomData 让 generic 参数"出现"在 wrapper 的定义里却不占用运行时空间(PhantomData<T> 是零字节 ZST)。这是 Rust 标准的"把 generic 参数持有住但不实际存储"套路——在 Serde 生成代码里用得非常频繁。
12.12 transparent 模式
#[serde(transparent)] 应用在单字段 struct 上:
rust
#[derive(Serialize)]
#[serde(transparent)]
pub struct UserId(u64);生成代码(见第 12.3 节的 serialize_transparent):
rust
_serde::Serialize::serialize(&self.0, __serializer)就是这么简单——完全忽略 struct 的存在,直接序列化字段。
**为什么要这个属性?**在第 2 章讲过,newtype 原语默认就是透明的(serialize_newtype_struct 的默认实现是 value.serialize(self))。但某些格式可能覆写默认行为(比如给 newtype 加一个类型标签)。#[serde(transparent)] 绕过所有格式的 newtype 处理,强制透明——在你想要的就是"这个 struct 不存在"的时候用。
12.13 remote 模式:外部类型的 Serialize
这是 ser.rs 的一个独特分支。用户代码:
rust
#[derive(Serialize, Deserialize)]
#[serde(remote = "std::time::Duration")]
struct DurationDef {
secs: u64,
nanos: u32,
}用户定义了一个和 std::time::Duration 结构相同的 struct,告诉 serde "请为 std::time::Duration 生成 Serialize 实现——但用我这个 DurationDef 作为描述"。
为什么要这样? 因为用户不能给不属于自己 crate 的类型(比如标准库的 Duration)实现 trait(Rust 的 orphan rule)。remote 模式是一个 workaround——让用户通过代理 struct 描述外部类型,serde 生成自由函数而不是 trait 实现。
生成代码(摘自第 12.2 节):
rust
impl DurationDef {
pub fn serialize<__S>(__self: &std::time::Duration, __serializer: __S) -> Result<...>
where __S: Serializer,
{
// 和普通 Serialize 同样的逻辑,但用 __self 代替 self
...
}
}用户如何使用?通过 #[serde(with = "DurationDef")]:
rust
#[derive(Serialize)]
struct MyStruct {
#[serde(with = "DurationDef")]
duration: std::time::Duration,
}第 16 章会详细讨论 remote 和 with 的配合使用——这是 serde 最巧妙的 workaround 之一。
remote 模式在 ser.rs 里的具体分叉点在 §12.2 已经展示——expand_derive_serialize 最后的 if let Some(remote) = cont.attrs.remote() 这个分支。两种外壳的唯一区别是——
- 普通模式:
impl Serialize for #ident { fn serialize(&self, ...) { #body } }——方法签名按 trait 固定、self是&#ident - remote 模式:
impl #ident { pub fn serialize<__S>(__self: &#remote, ...) { #body } }——自由函数、第一参数叫__self、类型是远端类型而非自身
#body 共用同一份——这是最精彩的设计:serialize_body 生成的代码里对 self 的引用走 params.self_var、而 self_var 在 Parameters::new 里根据 is_remote 选择 "self" 或 "__self"。代码生成不知道自己在生成普通还是 remote 版本——只通过换一个标识符字符串就复用了整个生成管线。这种**"正交切换"**的架构——通过在参数对象里藏一个 bool、让下层数千行代码对两种模式无感知——是 Serde 源码里反复出现的范式、也是让 ser.rs 能在 1369 行内装下 21 个分派函数的核心原因。
orphan rule 的历史补丁——Rust 1.0 时代 serde 甚至没有 remote 模式、用户必须 fork 标准库类型或者等第三方 crate 出代理实现。serde 1.0(2017)加入 #[serde(remote)] 后、std 里的 Duration / SystemTime / IpAddr 等类型都能通过用户 crate 里的 derive 支持 Serde——这是整个 Rust 生态能形成"所有非本 crate 类型都能序列化"共识的技术前提。第 16 章会给 remote + with 写一个完整样板 crate、顺便讨论当一个 crate 既想支持 serde 又不想强依赖时的"可选 remote 代理"模式。
12.13.1 dummy::wrap_in_const:生成代码外层的 32 行"卫生间"
每一个 #[derive(Serialize)] 产出的代码都要经过最后一步 dummy::wrap_in_const。这个函数只有 32 行(serde_derive/src/dummy.rs),但它决定了生成代码对用户 crate 的影响边界:
rust
pub fn wrap_in_const(serde_path: Option<&syn::Path>, code: TokenStream) -> TokenStream {
let use_serde = match serde_path {
Some(path) => quote! {
use #path as _serde;
},
None => quote! {
#[allow(unused_extern_crates, clippy::useless_attribute)]
extern crate serde as _serde;
},
};
quote! {
#[doc(hidden)]
#[allow(
non_upper_case_globals,
unused_attributes,
unused_qualifications,
clippy::absolute_paths,
)]
const _: () = {
#use_serde
_serde::__require_serde_not_serde_core!();
#code
};
}
}四层设计各司其职:
1. const _: () = { ... }; 匿名常量是宏卫生的关键容器。把整个生成代码塞进一个匿名 const 表达式:
_serde别名只在 const 内部可见——不会污染用户模块的命名空间。用户如果自己写了mod _serde { ... }不会和生成代码冲突。- 内部声明的 impl 项仍然通过 trait system 注册到用户类型上——trait impl 有"全局锚定"特性,不受 const 表达式作用域限制。这是 Rust "impl 和表达式作用域解耦" 的巧妙使用。
- **
const _(下划线名)**让同一文件可以有多个 wrap_in_const 产物而不冲突——#[derive(Serialize)] struct A;和struct B;会产生两个const _: () = { ... };,匿名 const 不需要唯一命名。
2. Serde 路径的两条分叉(line 5-13):
rust
// 用户配置了 #[serde(crate = "my_serde")]
use my_serde as _serde;
// 默认路径
#[allow(unused_extern_crates, clippy::useless_attribute)]
extern crate serde as _serde;Rust 2018+ 不需要 extern crate 就能用 crate 依赖、但 #[derive(Serialize)] 不知道用户 crate 运行在哪个 edition。统一生成 extern crate serde 在所有 edition 都工作——#[allow(unused_extern_crates)] 抑制 2018+ 的 "无用 extern" 警告。clippy::useless_attribute 进一步抑制 "这个 allow 本身没用" 的 clippy lint——一层 allow 套一层 allow,针对的是不同编译器/clippy 版本的不同 lint。这种针对工具演化的层层防御在生成代码里必要、在手写代码里会被视为过度。
3. _serde::__require_serde_not_serde_core!(); 安全闸门(line 26):
这是一个隐藏的宏,serde crate 里定义。如果用户尝试让 #[derive(Serialize)] 生成的代码只依赖 serde_core(更轻量的 no-std 子集)、这个宏会在编译时报错。原因:serde_core 不包含 derive 依赖的全部 glue code——但 derive 不知道这一点、总会生成完整代码。闸门在最外层强制检查"运行环境是完整 serde、不是简化 serde_core"。
4. 四个 #[allow] 抑制不同的 lint 噪声:
non_upper_case_globals——匿名 const 名为_,其他常量名可能违反大写全局命名规则unused_attributes——用户结构体上可能有 serde 不消费的其他属性(如#[cfg(...)]在 derive 展开时的处理)unused_qualifications——生成代码大量使用_serde::Serializer这种全限定名、即使_serde::Serializer已在作用域内也用全名(确保 hygiene),这触发unused_qualificationslintclippy::absolute_paths——clippy 的"偏好相对路径"规则、同理被禁掉
#[doc(hidden)] 是锦上添花——这个 const 在 rustdoc 里不会出现,用户文档干净。
合起来看,wrap_in_const 是一个**"卫生间"**:所有 serde_derive 对用户代码的影响都被圈在这 32 行定义的 const 作用域里、外部只看到类型上多了一个 impl Serialize 这个"结果"。这种严格的隔离让 serde_derive 能和任意用户代码共存——用户用什么奇葩的 crate name、什么 lint 等级、什么 Rust edition,都不会让 derive 生成的代码出问题。呼应第 7 章 Ctxt 的 Drop 契约、第 8 章 quote! 的 fast-path 优化——都是 Serde 生态"每个细节都认真"的工程神经的一次体现。
12.14 实战:追踪一个真实例子的完整生成
把本章内容整合,追踪一个真实类型的完整 Serialize 生成:
用户代码:
rust
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiResponse<T: Serialize> {
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
pub timestamp: u64,
}processes(12 步,从 lib.rs 开始):
parse_macro_input!→DeriveInputContainer::from_ast→Container:- attrs: rename_all=CamelCase
- data: Struct(Style::Struct, 3 个 Field)
- 字段的 rename 按 rename_all 转换:status→"status"(无变化)、data→"data"(无变化)、timestamp→"timestamp"(无变化)
- 字段 data 的属性 skip_serializing_if = Path("Option::is_none")
Parameters::new:处理 generics,加T: Serializeboundserialize_body→ 分派到serialize_structserialize_struct没有 flatten,走serialize_struct_as_struct- 计算字段数:
1 + (if Option::is_none(&self.data) { 0 } else { 1 }) + 1 serialize_struct_visitor生成 3 个 serialize_field 语句- 第二个字段带 if 判断
- 组装成
serialize_struct+ 3 个 serialize_field +end - 外层 impl 模板:
impl<T: Serialize> Serialize for ApiResponse<T> - 用
dummy::wrap_in_const包装 - 返回 TokenStream
最终生成代码(基于 serde 1.0.228 + serde_derive 1.0.228 的真实 cargo expand 输出,已精简外层注释,但代码结构一字未改):
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<T: Serialize> _serde::Serialize for ApiResponse<T>
where
T: _serde::Serialize,
{
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,
"ApiResponse",
false as usize + 1 + if Option::is_none(&self.data) { 0 } else { 1 } + 1,
)?;
_serde::ser::SerializeStruct::serialize_field(
&mut __serde_state,
"status",
&self.status,
)?;
if !Option::is_none(&self.data) {
_serde::ser::SerializeStruct::serialize_field(
&mut __serde_state,
"data",
&self.data,
)?;
} else {
_serde::ser::SerializeStruct::skip_field(&mut __serde_state, "data")?;
}
_serde::ser::SerializeStruct::serialize_field(
&mut __serde_state,
"timestamp",
&self.timestamp,
)?;
_serde::ser::SerializeStruct::end(__serde_state)
}
}
};几处生产级细节值得注意(常被教程忽略,但真实输出就长这样):
_serde::__private228:不是__private,而是带版本 patch 号(228 对应 serde 1.0.228)。第 10.3 节讨论过这个符号隔离技巧——同一 crate 里可能存在多个 serde 版本,不同 patch 号的__private避免冲突。false as usize + 1 + ... + 1:字段数从false as usize(即 0)加起来。看似奇怪的写法,是为了让生成逻辑统一——如果是 internally-tagged enum 变体,这里的false会变成true(多加 1 给 tag 字段)。同一套模板处理所有情况。clippy::absolute_paths:2024 起加入的 allow,让全路径引用不触发 lint 警告。
短短 3 个字段的用户代码,生成了 40+ 行代码。每一行都精准对应本章讲过的某一处逻辑——属性解析决定字段名、skip_serializing_if 决定 if 判断、状态机模式决定 serialize_struct/serialize_field/end 结构。版本:本 cargo expand 输出由 serde 1.0.228 + serde_derive 1.0.228 + rustc 1.89.0 产生(2026-04-20 实测)。
从这个 40 行代码里能反向验证本章讲的设计——
1. 无 panic! 无 unwrap()——生成代码里没有任何 runtime 崩溃点。所有错误都通过 ? 向上传播到 fn serialize 的 Result 返回值。即使用户的类型实现 Serialize 时故意 panic、也是用户的 trait impl 里发生、不是 derive 生成的 glue code 里发生。derive 生成的代码是透明胶水、不引入新的失败模式——这个纪律在 ser.rs 的 1369 行里一次未破。
2. 字段数的 false as usize + 1 + if ... + 1——fold 起点 false as usize 在这个例子里被折叠成 0 + 1 + if ... + 1、rustc 常量传播后会优化成 2 + if ... { 0 } else { 1 }。但源码生成不做优化——把 fold 的原始骨架完整交给 rustc、让编译器做后续化简。过程宏时代的代码生成器不应该做编译器该做的事——这是 serde 和一些"过度聪明"的 macro crate 的重要分野。
3. 外层的 const _: () = { ... }; 包装——用户代码里完全看不到 _serde、_serde::ser::SerializeStruct、__serde_state 这些符号。整个 derive 的影响被严格圈在一个匿名 const 表达式内——§12.13.1 详解过的"卫生间"设计在这里具体兑现。
4. T: _serde::Serialize 追加 bound——用户原本只写了 T: Serialize(裸 Serialize,不带 _serde::)、但生成代码里额外追加了 where T: _serde::Serialize——这是 bound.rs 干的活(用户可能在 crate 里根本没 use serde::Serialize、derive 生成的代码只信得过 _serde::Serialize、所以 bound 条款必须用全限定名)。这是"生成代码不能依赖用户 scope"原则的又一次体现——整个 derive 输出对用户源文件的 use 列表、作用域别名完全免疫。
12.15 serde_derive crate 的全貌:9 个顶层文件 + internals 子模块
读到这里你已经掌握了 ser.rs(1369 行、占全 crate 21%)的分派骨架。把视角抬高一层、看整个 serde_derive/src/。wc -l serde_derive-1.0.228/src/**/*.rs 给出如下账单——顶层 9 个 .rs 文件 3281 行 + internals/ 子目录 8 个文件 3294 行,合计 6575 行、每个文件的职责泾渭分明:
| 文件 | 行 | 角色 |
|---|---|---|
ser.rs | 1369 | 本章主角——Serialize 的 21 个 serialize_* 分派函数 |
de.rs | 973 | 下一章主角——Deserialize 代码生成;比 ser.rs 短是因为 Visitor 模式让分派更均匀、没有 4×4 枚举展开 |
bound.rs | 410 | trait bound 推断——7 个 pub fn(without_defaults、with_where_predicates、with_where_predicates_from_fields、with_where_predicates_from_variants、with_bound、with_self_bound、with_lifetime_bound),从字段类型反推 where T: Serialize 子句、处理 #[serde(bound = "...")] 覆盖 |
pretend.rs | 188 | 假装使用字段以抑制 dead_code 警告——Serde 的一个独特发明、见 §10 |
lib.rs | 127 | #[proc_macro_derive] 入口、3 个 derive(Serialize / Deserialize / Serialize + Deserialize 两用) |
fragment.rs | 74 | Fragment 类型 + quote_expr! / quote_block! 两个包装宏(见本章 §12.3) |
deprecated.rs | 56 | 旧版本兼容的 deprecated proc macro 警告 |
this.rs | 32 | this::this_type() / this::this_value()——两个小工具生成 Self / self 的 TokenStream |
dummy.rs | 31 | wrap_in_const——§12.13.1 专门分析的"卫生间"(文件 31 行,其中函数体 32 行含展开的 quote 模板) |
三点读完全表才看得出的设计——
bound.rs居然比pretend.rs还大 2 倍——说明 trait bound 推断远比想象复杂——字段里可能有Option<Vec<HashMap<K, V>>>、每层都要展开判断、还要处理用户写#[serde(bound(serialize = "..."))]全覆盖 vs 部分覆盖——这是用户最常踩坑的地方ser.rs+de.rs= 2342 行、占 71%——serde_derive 本质上是两个代码生成器塞在一个 crate 里、公用bound / fragment / this / dummy四个小工具——要学过程宏的读者、直接切成两块读最划算- 同样一个过程宏 crate、
lib.rs只有 127 行——说明 Serde 的设计纪律性极强——入口文件不塞业务逻辑、只负责TokenStream → AST → dispatch → TokenStream的壳子——这是很多"过程宏越写越乱"项目值得学的范式
再往下钻一层、internals/ 子模块是 Serialize 和 Deserialize 共享的基础设施,按职责切成 8 个文件——把它一次算清楚,后面 ser.rs / de.rs 里所有"调属性"、"取名字"、"走一致性检查"的引用你都能秒懂:
internals/ 文件 | 行 | 角色 |
|---|---|---|
attr.rs | 1831 | 属性解析总仓——第 11 章主角;Container::from_ast / Variant::from_ast / Field::from_ast 三个大状态机把 #[serde(...)] 解析成结构化字段。56% 的 internals 代码在这 |
check.rs | 477 | 一致性检查——跨属性冲突(flatten + skip 不许,tag + content 不许,field_identifier 只许用于 Deserialize),失败用 ctxt.error_spanned_by 记到错误账本 |
receiver.rs | 293 | replace_receiver——递归 visit_mut 整个 AST、把所有 Self 替换成具体类型、处理边缘情况如 impl<T> Foo<T> where Self::Assoc: Bar。ser.rs:expand_derive_serialize 第一步调用 |
ast.rs | 218 | AST 中间表示——Container / Data{Enum,Struct} / Variant / Field / Style{Struct,Tuple,Newtype,Unit};本章 §12.3 的 5 路分派就是对 Style 的穷举 |
case.rs | 200 | rename_all 规则——CamelCase / snake_case / SCREAMING_SNAKE_CASE / kebab-case 等 9 种转换的字符串处理 |
name.rs | 113 | Name { serialize: String, deserialize: String }——field/variant/container 的两个独立名字槽,让 #[serde(rename(serialize = "foo", deserialize = "bar"))] 双向分叉 |
symbol.rs | 71 | 字符串常量表——SERDE = Symbol("serde")、RENAME = Symbol("rename")……集中拼写避免 typo(用 == 对 syn::Path 做匹配用的) |
ctxt.rs | 68 | Ctxt 错误账本 + Drop 卫兵——第 7 章专门讲过、check() 必须消费错误否则 Drop 里 panic。expand_derive_serialize 第 4 步的 ctxt.check()? 就是消费 |
mod.rs | 28 | 模块门面、pub use 列表 |
attr.rs 1831 行一个文件占 internals 55% 的体量——读到这个数字就该明白:第 11 章之所以敢单独拎一章讲属性系统、并不是"水章数",而是因为属性系统本身就是 serde_derive 工程量最重的一块。ser.rs 每调一次 cattrs.name().serialize_name() 或 field.attrs.skip_serializing_if()——背后都是 attr.rs 里几十行状态机的凝结。把代码生成和属性解析切开讲、是读懂源码的前提。
12.15.1 Struct 三形态展开前后对比表
把本章 §12.4、§12.5、§12.6、§12.8 讲的四种 Style 的生成结果摆在一起——同一个字段数、同一个字段类型、只因 Style 不同生成的代码就分四种骨架:
| 用户写的 | Style | Serializer 调用序列 | 字段位置 | 生成代码行数(示例 1 字段) |
|---|---|---|---|---|
struct Unit; | Style::Unit | serialize_unit_struct("Unit") | —— | 1 |
struct New(T); | Style::Newtype | serialize_newtype_struct("New", &self.0) | 直传 | 1 |
struct Tup(T); | Style::Tuple | serialize_tuple_struct(...) → serialize_field(&self.0) → end | &self.0(按 index) | 3 |
struct S { f: T } | Style::Struct | serialize_struct(...) → serialize_field("f", &self.f) → end | &self.f(按名) | 3 |
几个容易混的点、下面逐一现场讲清楚、避免日后踩坑:
1. Newtype 和 Tuple 的边界在哪? internals/ast.rs 的 Style 枚举把只有一个无名字段的 tuple struct 单独列为 Newtype——不是 Tuple(fields.len()==1)。这是因为 Serializer 协议的 serialize_newtype_struct 方法专门为它设计了"透明包装"的默认实现(default: value.serialize(self)),让 newtype 在大多数格式里不引入额外层级。Tuple 则走 serialize_tuple_struct——多数格式会产出数组 [v]、[v1, v2]。这个区分是用户经常踩坑的地方:struct Millis(u64) 在 JSON 里就是 123,但 struct Point(u64, u64) 在 JSON 里是 [1, 2]——只差一个逗号,语义完全不同。
2. Unit struct 有字段数吗? 没有——serialize_unit_struct(&serializer, "Empty") 的签名里就没有 len。但 Serializer 的实现仍然可以输出非空内容:JSON 输出 null、bincode 输出 0 字节、postcard 甚至完全跳过。"Unit 没字段"是语义、不是"输出一定是空"。
3. Tuple/Struct 的 len 参数——serialize_struct(serializer, name, len) 的 len 是编译期常量(无 skip_serializing_if)或带条件的 if 表达式(有 skip_serializing_if)。Serializer 可以信任这个 len(比如预分配数组容量)、也可以忽略(比如 JSON 不需要预分配)。本章 §12.6 的 serialize_struct_as_struct 代码里的 .fold(quote!(#tag_field_exists as usize), ...) 就是在做这个编译期 + 运行时混合计算——fold 起点是 false as usize(即 0),如果是 internally-tagged enum 的 struct variant,这里会变成 true as usize(多加 1 给 tag 字段)。同一份 fold 代码、通过 tag_field_exists 这个 bool 统一两种场景——是 ser.rs 精简的典型手法。
12.15.2 本章 ↔ ch11(derive-macro)↔ ch13(deserialize-codegen)三章对照
按代码路径把三章接起来——读完本章、下一章就没有"新的宏观结构要学"、只有对称的 Deserialize 的细节。
ch11(derive-macro 架构)给出的入口——第 11 章讲 serde_derive/src/lib.rs(127 行)的三个 #[proc_macro_derive] 函数:derive_serialize、derive_deserialize、derive_serialize_and_deserialize。每个函数都走同一个三段式骨架——
proc_macro TokenStream
→ syn::parse_macro_input! ← 第 8 章讲的 syn
→ expand_derive_serialize(&mut) ← 本章讲的入口
→ dummy::wrap_in_const ← 本章 §12.13.1 讲的卫生间
→ TokenStream本章讲的是骨架第二步里的 expand_derive_serialize——再展开成 Container::from_ast → Parameters::new → serialize_body → 套 impl 模板的五步。第 11 章给 Container::from_ast 内部的属性解析分配了 1831 行(internals/attr.rs);本章给 serialize_body 及其 21 个分派函数分配了 1369 行(ser.rs)。两章合起来是 serde_derive 的 Serialize 侧全景。
ch13(deserialize-codegen)的镜像关系——下一章讲的是 expand_derive_deserialize → deserialize_body → 21 个(其实 de.rs 有 25 个顶层 fn,但专门做分派的 deserialize_* 也是同一量级)分派函数——结构完全对称、但内部模型换成了 Visitor:
| 本章(Serialize) | 下一章(Deserialize) | 对称性说明 |
|---|---|---|
serialize_body 5 路分派(Unit/Newtype/Tuple/Struct/Enum) | deserialize_body 同样 5 路分派 | Style 穷举共享 |
serialize_struct_visitor 生成 SerializeStruct::serialize_field 链 | deserialize_seq / deserialize_map 生成 Visitor::visit_seq/visit_map | 输出线性 vs 输入驱动 |
serialize_externally_tagged_variant 4 tag × 4 style = 16 分支 | deserialize_externally_tagged_enum 同样 16 分支 + "field identifier" 枚举 | 反序列化还要识别 tag |
SerializeWith<'a> 临时 wrapper(§12.11) | DeserializeSeed / PhantomData 反向 wrapper | 核心手法都是"给中间类型挂 trait 实现" |
编译期 + 运行时混合 len | 编译期字段索引枚举 + 运行时分派 visitor | 两侧都是 Rust 常量化极致 |
可以先把本章读透、再翻 ch13——你会发现 ch13 的 900 余行源码大半能在心里补出结构**、只需关注 Visitor 特有的"未知字段如何处理"、"借用还是拥有"、"Deserializer 不信任输入"三个新维度。这是 Serde 源码的对称美——一旦抓住 Serialize 的骨架、Deserialize 就是对镜像的走查。
12.15.3 一张图记住 ser.rs 的 21 个分派函数
纸面上 21 个函数名看着杂、按依赖层级画出来只有 4 层——
text
layer 1 入口
serialize_body ← 唯一公开分派
├→ serialize_transparent # cattrs.transparent() = true
├→ serialize_into # cattrs.type_into() = Some(_)
└→ match Data
├→ serialize_unit_struct
├→ serialize_newtype_struct
├→ serialize_tuple_struct ──┐
├→ serialize_struct ──┼─→ layer 2
└→ serialize_enum ──┘
layer 2 struct/enum 主干
serialize_tuple_struct ──→ serialize_tuple_struct_visitor
serialize_struct ──→ { serialize_struct_as_struct ──→ serialize_struct_visitor
| serialize_struct_as_map ──→ serialize_struct_visitor (flatten) }
serialize_struct_tag_field ← internally-tagged struct 用
serialize_enum ──→ serialize_variant
layer 3 variant 分派(tag × style 交叉)
serialize_variant ──match cattrs.tag()──┐
├→ serialize_externally_tagged_variant
├→ serialize_internally_tagged_variant
├→ serialize_adjacently_tagged_variant
└→ serialize_untagged_variant
这四个函数内部再 match variant.style,重用:
├→ serialize_tuple_variant # Style::Tuple
├→ serialize_struct_variant # Style::Struct,不含 flatten
└→ serialize_struct_variant_with_flatten # Style::Struct,含 flatten
layer 4 叶子工具(重用最多)
serialize_tuple_struct_visitor ← 被 tuple_struct + tuple_variant 共用
serialize_struct_visitor ← 被 struct + struct_variant + flatten map 共用
wrap_serialize_field_with ← serialize_with 属性生成临时 struct
wrap_serialize_variant_with ← variant 级 serialize_with
wrap_serialize_with ← 通用底层包装看清这 4 层的意义——所有看似 16 种 enum 组合的生成代码、最终都落在最下层 2-3 个复用函数里。serialize_struct_visitor 被 3 个高层函数(普通 struct、struct variant、flatten map)共用——所以本章 §12.7 讲的"过滤 skip + 生成 if 分支 + 调 serialize_field / skip_field"这套模式掌握一次、能识别出 ser.rs 一半以上的生成代码。
12.16 本章小结
Serialize 代码生成的核心是"按 Container::data 的结构分派,每种分支对应一种 Serializer 协议调用模式":
| Container::data | 分派到 | Serializer 调用 |
|---|---|---|
| Struct(Struct, fields) | serialize_struct_as_struct | serialize_struct → N× serialize_field → end |
| Struct(Tuple, fields) | serialize_tuple_struct | serialize_tuple_struct → N× serialize_field → end |
| Struct(Newtype, fields) | serialize_newtype_struct | serialize_newtype_struct(field) |
| Struct(Unit, _) | serialize_unit_struct | serialize_unit_struct |
| Enum(variants) | serialize_enum → match + serialize_variant | 4 tag × 4 style = 16 分支 |
| +transparent | serialize_transparent | 字段.serialize(s) |
| +into | serialize_into | clone().into().serialize(s) |
属性系统在这里全部落地——rename 影响字段名字符串、skip_serializing_if 添加 if 分支、serialize_with 触发 SerializeWith 包装、flatten 改走 map 模式、transparent 完全透明转发……
Span 管理 通过 quote_spanned! 和 field.original.span() 让错误信息精准——这是 Serde 用户体验的基石。
把本章的知识打个包——读到这里、你面对一个真实的 serde_derive 输出应该能做到:
- 立即定位:看到
serialize_struct(__serializer, "X", len)?;形态、知道是Style::Struct走serialize_struct_as_struct(§12.6);看到serialize_newtype_variant知道是 externally tagged enum 的 Newtype variant(§12.10)。 - 反推属性:看到
if Option::is_none(&self.data) { 0 } else { 1 }知道字段有#[serde(skip_serializing_if = "Option::is_none")];看到生成的临时SerializeWith<'a>知道字段有#[serde(serialize_with = "...")](§12.11)。 - 诊断错误:
impl Serialize for X报 "doesn't implement Serialize" 错在字段行而不是 derive 行——说明quote_spanned!正确工作(§12.5);报 "internally tagged enum variant must be a struct or map"——说明__private::ser::serialize_tagged_newtype的 runtime 检查触发了(§12.10)。 - 预测生成量:用户 1 行
#[derive(Serialize)] enum E { ... }生成 M 行——M 约等于变体数 × (3 + 字段数 × 4),tagged 模式再额外× 2。几百行生成代码对应几十行用户代码是 Serde 的常态、不是"代码膨胀"——是精准编译期展开的必然产物。
动手实验
- cargo expand 观察:写一个带 3 种字段(普通、
skip_serializing_if、serialize_with)的 struct,用cargo expand看完整生成代码。对照本章 12.14 节的例子。 - 追踪 enum 的 externally tagged:写一个有 4 种 style 变体(Unit、Newtype、Tuple、Struct)的 enum,用 cargo expand 观察 4 个 match arm 的差异。
- 触发 remote 模式:写一个
#[serde(remote = "chrono::DateTime<Utc>")]代理 struct,看生成的代码如何变成自由函数。 - 理解 Fragment:读
serde_derive/src/fragment.rs(74 行),看quote_expr!和quote_block!两个宏如何实现。
延伸阅读
- serde_derive/src/ser.rs 完整源码:1369 行,读过本章再读这份代码会流畅很多。
- Serde "Implementing Serialize":官方从用户视角讲"如何手写 Serialize"——看完你能明白宏生成的代码为什么长那样。
- proc-macro-workshop Debug 题:生成一个 Debug 实现,结构和 Serialize 生成非常类似。