Serde 元编程

第 10 章 serde_derive 架构总览:从 AST 到 impl 块

作者 杨艺韬 · 8,093 字 · 发布于 · 更新于

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 行代码,28 个文件:架构全景

先看一眼文件结构:

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 行   属性名常量(Symbol)
    ├── bound.rs                ← 425 行  自动推导泛型 bound
    ├── this.rs                 ← 32 行   "当前类型" 的引用工具
    ├── fragment.rs             ← 74 行   代码片段的组合
    ├── dummy.rs                ← 31 行   dummy const 包装
    ├── pretend.rs              ← 188 行  remote derive 时抑制 dead_code 警告
    └── deprecated.rs           ← 56 行   为带 #[deprecated] 的类型给 impl 加 #[allow(deprecated)]

近 9000 行代码(8969 行)组织成 28 个 .rs 文件。按职责分为三类:

  1. 入口层(lib.rs,127 行):两个 #[proc_macro_derive] 函数。
  2. AST + 属性层(internals/,约 3300 行):从 syn AST 加工出 serde 视角的 AST,解析所有 #[serde(...)] 属性。
  3. 代码生成层(ser.rs、de.rs 及 de/ 目录,约 4700 行):根据加工后的 AST 生成 impl 块。

其余约 800 行是 bound.rs、fragment.rs、pretend.rs 等供代码生成层调用的辅助模块。

这三层的控制流是线性的:入口 → AST 加工 → 代码生成。每一层有明确边界,下一层只看上一层的输出。这种分层让 9000 行代码可读——任何一处都能回答"我在哪一层、干什么"。

10.3 入口层:127 行的 lib.rs

serde_derive/src/lib.rs 只有 127 行,却是两个 derive 宏的入口,整个 Serde 宏系统从这里开始。

// 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):

#[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 完全一致:

  1. parse_macro_input!(input as DeriveInput) — 解析类型定义。
  2. 调用 expand_derive_* 函数(返回 syn::Result<TokenStream>)。
  3. 错误转 compile_error! 代码。
  4. 转回 proc_macro::TokenStream。

127 行里还有一个微妙的细节(lib.rs:95-111):

#[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 号?这是 1.0.225 引入的机制(commit cbe98a98 "Use differently named __private module per patch release"):serde 的 build.rs 用 CARGO_PKG_VERSION_PATCH 生成 pub mod __private228 { pub use crate::private::*; },serde_derive 1.0.228 生成的代码则引用 _serde::__private228::...。两边的 patch 号必须一致——如果依赖树里 serde 和 serde_derive 的版本没对齐(比如 serde 1.0.228 配 serde_derive 1.0.219),serde_derive 1.0.219 生成的 _serde::__private::... 在 serde 1.0.228 里已经不存在,立刻编译报错,而不是撞进 __private 内部 API 悄悄变化导致的莫名其妙错误。

这种工程级细致让内部 API 可以放心演进。__private 不是公开 API,它可以在任何 patch 版本里随意改动——有了这层"版本必须对齐"的硬约束(1.0.225 起),维护者调整内部辅助函数时,不必担心版本错配的 serde_derive 生成的代码悄悄调错。

10.4 数据流:DeriveInput → Container → TokenStream

serde_derive 的核心数据流可以画成一张图:

flowchart LR
    U[用户代码<br>#derive Serialize struct ...] -->|TokenStream| PM[parse_macro_input]
    PM -->|syn::DeriveInput| C[Container::from_ast]
    C -->|ast::Container| E[expand_derive_serialize]
    E -->|syn::Result&lt;TokenStream&gt;| OR[错误 → compile_error]
    E -->|Ok| OS[TokenStream]
    OR --> Out[输出 TokenStream]
    OS --> Out
    Out --> Compiler[rustc 继续编译]
    style PM fill:#e0f0ff
    style C fill:#fff0d0
    style E fill:#ffe0d0
    style Out fill:#e0ffe0

四个阶段:

  1. 解析 (parse_macro_input):token → syn::DeriveInput(纯语法)
  2. AST 加工 (Container::from_ast):syn::DeriveInput → ast::Container(加入 serde 语义)
  3. 代码生成 (expand_derive_*):ast::Container → TokenStream(目标代码)
  4. 错误兜底:syn::Result → 要么是 TokenStream 要么是 compile_error

第 2 步的产出——Container——是 serde_derive 内部最核心的数据结构。整个代码生成都基于它。下一节详细看。

10.5 Container:serde 视角的 AST

serde_derive/src/internals/ast.rs 定义了整个 serde_derive 的"工作对象":

// serde_derive/src/internals/ast.rs:10
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),tuple 字段为 None)。serde 的 Field::member: syn::Member 是一个 enum:

// 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 函数的骨架(简化):

// serde_derive/src/internals/ast.rs:60(简化版,不是源码原文)
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 里的字段:变体自己的 rename_all 优先,
                    // 否则回落到容器级 rename_all_fields
                    for field in &mut variant.fields {
                        field.attrs.rename_by_rules(
                            variant.attrs.rename_all_rules().or(attrs.rename_all_fields_rules()),
                        );
                    }
                }
            }
            Data::Struct(_, fields) => {
                for field in fields {
                    field.attrs.rename_by_rules(attrs.rename_all_rules());
                }
            }
        }

        // 4. 一致性检查
        let mut item = Container { ident: item.ident.clone(), attrs, data, generics: &item.generics, original: item };
        check::check(cx, &mut item, derive);
        Some(item)
    }
}

四步:

  1. 解析 container 级属性(#[serde(rename_all = "..")] 等)
  2. 递归解析 struct 或 enum 的内部结构(struct_from_ast / enum_from_ast)
  3. 应用 rename 规则(把 rename_all 传递到每个字段/变体)
  4. 一致性检查(比如 internally tagged 的 tag 名不能和字段名冲突、#[serde(other)] 不能出现在 untagged enum 上;tag 与 untagged 同时出现这类冲突则在第 1 步解析属性时就报出)

错误处理策略: 所有错误通过 cx: &Ctxt(context)累积,不立刻返回。这样用户一次能看到所有错误——如果同时有三个属性冲突,三个都会报告,而不是"修一个看下一个"。

Ctxt 的实现(serde_derive/src/internals/ctxt.rs):

// serde_derive/src/internals/ctxt.rs:12(省略 new、syn_error 与注释)
#[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。这是一个运行时自检(在宏执行时触发)——防止 serde_derive 的开发者忘记检查错误。

这是 Rust 用 Drop 做"强制调用某方法"的经典模式。第 9 章的 Builder 我们没加这种保护,但生产级的 serde_derive 加了。每一处细节都在保证"错误不会被静默吞掉"。

10.7 代码生成层:ser.rs 的主干

看 expand_derive_serialize 函数的骨架(简化自 serde_derive/src/ser.rs):

pub fn expand_derive_serialize(input: &mut syn::DeriveInput) -> syn::Result<TokenStream> {
    replace_receiver(input);   // 把字段类型里的 Self 替换成具体类型名(internals/receiver.rs)

    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);   // 内部调用 build_generics 推导 bound
    let (impl_generics, ty_generics, where_clause) = params.generics.split_for_impl();
    // serialize_body 先处理 transparent / into,再按 Style 分派到
    // serialize_enum / serialize_struct / serialize_tuple_struct /
    // serialize_newtype_struct / serialize_unit_struct
    let body = Stmts(serialize_body(&cont, &params));

    // 生成 impl 块外壳(remote 类型走另一个分支,生成 inherent fn)
    let impl_block = 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))
}

七个步骤:

  1. 替换 Self(replace_receiver,让字段类型里的 Self 变成具体类型)
  2. 创建 Ctxt 错误上下文、加工 Container AST
  3. 预检查(precondition:#[serde(field_identifier)] / variant_identifier 类型不能 Serialize)
  4. 构造 Parameters(含 bound 推导后的 generics)
  5. 分派到具体生成函数(serialize_body 按 struct/enum 的不同 style 分派)
  6. 生成 impl 外壳(用 quote! 组装)
  7. 包装到 dummy const(dummy::wrap_in_const,下面讲)

dummy const 包装是 serde 生成代码的一个有趣细节。看 serde_derive/src/dummy.rs:

// serde_derive/src/dummy.rs:4
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
        };
    }
}

所有生成的 impl 块被包在 const _: () = { ... }; 里。为什么?

原因 1:隔离命名。 const 块有自己的作用域——里面的 use 和 extern crate 不会泄漏到用户代码。extern crate serde as _serde; 引入 serde 别名而不污染用户 namespace;如果用户写了 #[serde(crate = "...")],这里改成 use #path as _serde;。_serde::__require_serde_not_serde_core!() 是 1.0.220 拆出 serde_core 之后加的守卫宏:serde 里它展开为空,serde_core 里它展开成编译错误——防止用户把 derive 指到只依赖 serde_core 的 crate 上。

原因 2:关闭 lint。 #[allow(...)] 批量关掉一些 lint,避免生成代码触发警告。

原因 3:#[doc(hidden)] 让这段代码不出现在文档里。

这是过程宏里常用的"包裹一层 const 隔离副作用" 模式。你写自己的 derive 宏达到生产级时,会发现这个包装是必要的——生成代码的 use 和 extern crate 总会和用户代码冲突。

10.8 bound.rs:自动推导泛型约束

泛型类型的 derive 有一个棘手问题:

#[derive(Serialize)]
struct Wrapper<T> {
    data: T,
}

生成的 impl 应该是:

impl<T> Serialize for Wrapper<T>
where
    T: Serialize,
{
    // ...
}

注意 T: Serialize 这个 bound——它不在用户代码里,必须由宏推导。

serde_derive/src/bound.rs 的 425 行代码专门处理这件事。核心逻辑:

  1. 遍历所有字段的类型
  2. 对每个出现在类型里的泛型参数,加上 Serialize(或 Deserialize)约束
  3. 合并到原有的 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"展开到源码,serde_derive/src/bound.rs:98 的 with_bound 函数实际上是一个围绕三个集合的 AST 访问器:

// 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 类型系统的细节:

  1. PhantomData 跳过(line 131-137,源码原文注释):

    if 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 实际上从未被序列化。

  2. 单段路径才算命中(line 139):if path.leading_colon.is_none() && path.segments.len() == 1。只有字面的 T 被视为"泛型参数的引用",some::module::T 或 crate::T 不算。意图很明确——带路径的 T 是一个外部类型、不是本 struct 的泛型参数,加 bound 不对。

  3. visit_macro 直接 no-op(line 250,带注释 // Type parameter should not be considered used by a macro path):

    struct 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。除 Infer/Never/Verbatim 什么都不做、Macro 走上面的 no-op 之外,其余每一种都有对应递归——&'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)把编译器非穷举告警作为测试模式下的硬错误——syn 新增 Type 变体时漏处理会立刻被 CI(它用 --cfg exhaustive 跑一遍测试)发现。这是"用类型系统守护自己不退化"的典型范式。

读完这一节你就明白:这 425 行"自动加 bound"的代码之所以"真能对所有正常类型开箱即用",靠的不是什么魔法——靠的是一个三集合跟踪器 + 12 种类型变体的完整递归访问 + 3 条经过严谨理由论证的硬编码特判。PhantomData 和宏这两条特判都有代码注释说明"为什么必须这样"——这也是 dtolnay 代码库的一贯风格:特殊情况不是瑕疵、是文档化的工程智慧。

10.9 Deserialize 比 Serialize 复杂得多

对比 ser.rs 和 de/ 目录的行数:

  • ser.rs:1369 行
  • de.rs + de/:976 + 2383 = 3359 行

Deserialize 代码量是 Serialize 的 2.5 倍。为什么?

原因 1:Visitor 模式的复杂度。第 4 章讲过——反序列化要为每种期望类型生成一个 Visitor(struct 的 Visitor 要同时实现 visit_seq 和 visit_map 两条路径),还要为字段名再生成一个识别用的 Visitor。Serialize 只要按顺序调用 serialize_* 方法。

原因 2:Enum 的四种 tag 模式。Deserialize 要处理 externally、internally、adjacently、untagged 四种。每种 tag 模式在反序列化时流程完全不同——serde_derive 为它们各写了一个实现文件(de/enum_externally.rs 等四个)。Serialize 这边只是写 key 的方式不同,逻辑简单。

原因 3:字段名识别。反序列化要处理输入里的"未知字段"、"重复字段"、"丢失字段"等。字段名 / 变体名的识别(含未知字段)由 serde_derive/src/de/identifier.rs(477 行)生成;重复字段和丢失字段的处理则分别在 de/struct_.rs 和 de.rs 里生成。

原因 4:借用反序列化(第 15 章主题)。&'de str 字段要从 'de 借用中取值,涉及生命周期推导。Serialize 不需要考虑这些。

第 12 章讲 Serialize 代码生成(相对简单);第 13 章讲 Deserialize 代码生成(复杂得多);第 14 章单独讲 enum 的四种 tag 策略(Deserialize 里最复杂的部分)。

10.10 生成代码是什么样的:一个完整例子

为了让前面的抽象具象化,看一个用户代码和展开后的代码对比:

用户代码:

#[derive(Serialize, Deserialize)]
pub struct UserRenamed {
    pub id: u64,
    #[serde(rename = "full_name")]
    pub name: String,
}

展开后的 Serialize 部分(serde 1.0.228 + serde_derive 1.0.228,在 rustc 1.96.0 上用 RUSTC_BOOTSTRAP=1 cargo rustc -- -Zunpretty=expanded 实测,按 rustfmt 风格重新排版;__require_serde_not_serde_core!() 展开为空,原始输出里只剩一个孤立的 ;,这里略去,其余未省略):

#[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)
        }
    }
};

几个观察:

  1. 完全符合第 3 章讲的 Serializer 协议:serialize_struct → 多次 serialize_field → end。
  2. 字段名使用 rename 后的值:"full_name" 而不是 "name"。
  3. 所有 serde 函数调用都用全路径:_serde::ser::SerializeStruct::serialize_field。
  4. 变量都是 __ 前缀:__serde_state、__S、__serializer。
  5. __private 带版本 patch 号变成 __private228(patch=228 对应 1.0.228)——10.3 节讲过的 serde / serde_derive 版本对齐机制。
  6. #[automatically_derived] 标记:告诉 rustc "此 impl 是宏生成的",部分 lint 会放宽。
  7. false as usize + 1 + 1:字段数从 false as usize 起算——这个看似冗余的 false 是 serde_derive 的模板统一化设计(serialize_struct_as_struct 里的 #tag_field_exists as usize:struct 自身带 #[serde(tag = "...")] 时会变成 true,为 tag 字段预留 1 位)。普通 struct 这里永远是 false,被 rustc 优化为常量 0。
  8. 整个代码包在 const _: () = { ... }; 里——10.7 节讲过。

Deserialize 展开要长得多(同样的实测里,-Zunpretty=expanded 原始输出约 165 行)——包括一个 __Field 枚举、一个 __FieldVisitor 识别字段名、一个 __Visitor 组装最终值。第 13 章会带你一行行看这段代码。

10.11 架构的关键设计选择

回顾本章内容,serde_derive 有几个值得学习的架构设计:

1. 双 AST 层(syn::DeriveInput → Container)。把"通用 Rust AST"和"serde 视角 AST"分开,让后续代码生成可以面对一个干净的对象,不用每次都去解析属性。

2. 错误累积模式(Ctxt)。用 RefCell<Option<Vec<syn::Error>>> 收集错误,最后一次性报告。用户体验远好于"第一个错误就退出"。

3. 全路径 hygiene。所有生成的代码都用 _serde::__private228:: / _serde::ser:: 这样的全路径,避免和用户代码冲突。细致的 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 消息格式(那一章对照的是 TypeScript / Python SDK)——如果用 Rust 实现那套请求 / 响应 / 通知类型,给它们打上的 #[derive(Serialize, Deserialize)] 生成的就是本章讲的这些代码。

10.13 "三大文件占 46%、前五占 60%":serde_derive 头部胖、尾巴瘦的实测

把 §10.2 的 28 个文件按行数排序——

排名 文件 行 累计 累计份额
1 serde_derive/src/internals/attr.rs 1818 1818 20.3%
2 ser.rs 1369 3187 35.5%
3 de.rs 976 4163 46.4%
4 serde_derive/src/de/struct_.rs 697 4860 54.2%
5 serde_derive/src/internals/check.rs 477 5337 59.5%
5 serde_derive/src/de/identifier.rs 477 5814 64.8%
7 bound.rs 425 6239 69.6%
8 serde_derive/src/de/enum_adjacently.rs 324 6563 73.2%
9 serde_derive/src/internals/receiver.rs 293 6856 76.4%
10 serde_derive/src/de/tuple.rs 283 7139 79.6%
11~28 其余 18 文件 1830 8969 100%

两条值得记住的物理事实——

  1. 前 3 大文件(attr / ser / de)占 46.4%——再加 4-5 名 serde_derive/src/de/struct_.rs 和 serde_derive/src/internals/check.rs 就过半(59.5%)——这意味着只读这 5 个文件就读懂了 60% 的 serde_derive——是给读者最高效的入门路径
  2. 后 18 个小文件(< 300 行)合计 1830 行——分布在顶层、internals/ 和 de/ 里——其中不少是单一概念的薄实现(respan.rs 16 行重设 span / dummy.rs 31 行包装 const / this.rs 32 行)——这是 Rust 项目里"小文件自洽语义"的范例:像 respan.rs、dummy.rs 这样,一个文件只导出一个函数

"一文件一职责"的纪律有反例吗?——serde_derive/src/internals/attr.rs 1818 行是反例——它塞了 Container / Variant / Field 三种 attr 数据类(第 11 章详细讲)+ 它们的解析逻辑——按理可以拆三文件、但 Serde 选择不拆——因为三种 attr 共享大量解析 helper(get_lit_str / parse_lit_into_path 等)、跨文件复用反而冗长。这是"集中 vs 分散"的工程取舍——对应 LangChain manager.py 2697 / Vite css.ts 3552 / vllm loader.py 1542 的同款选择。

10.14 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, &params) 包装成 Stmts。

这条链说明本章的"双 AST 层"不是抽象设计图,而是每次 derive 都会经过的真实流程:

flowchart LR
  A[syn::DeriveInput] --> B[replace_receiver]
  B --> C[Container::from_ast]
  C --> D[precondition + Ctxt]
  D --> E[Parameters]
  E --> F[deserialize_body]
  F --> G[quote impl Deserialize]

源码里有三个架构点尤其值得注意。

第一,远程 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 归一化 serde_derive/src/de.rs:25-40 后续不再面对原始 syn
remote 分叉 serde_derive/src/de.rs:43-58 impl 形状不同,必须早处理
bound 推导 serde_derive/src/de.rs:190-228 所有分支共享同一套泛型结果
字段 bound 过滤 serde_derive/src/de.rs:230-245 防止 skip / with 字段污染 where clause
数据形态分派 serde_derive/src/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 / serde_derive/src/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 的提前排除(这段只在开启 deserialize_in_place feature 时编译)就是典型例子:有些属性组合从架构上就不适合生成 in-place 更新逻辑,入口层先判掉,后面的 struct_ / tuple 模块就不必每个模板都处理一遍。

读大型过程宏时,可以把这种分层当检查表:入口是否薄,AST 是否归一,属性是否结构化,泛型 bound 是否集中推导,代码生成是否只消费已经算好的决策。serde_derive 在这些点上都给了可复用范式。

这个检查表还有一个实践价值:定位 bug。编译错误指向属性解析,先看 serde_derive/src/internals/attr.rs;where clause 过强,先看 bound.rs 和 build_generics;生成代码形状不对,才进入 ser.rs 或 de/ 子目录。分层清楚,源码阅读就不会从 9000 行里盲搜。

10.15 本章小结

serde_derive 是一个 9000 行的工程,但它的架构只需 4 句话就能概括:

  1. 入口(lib.rs 127 行):#[proc_macro_derive] 接收 DeriveInput。
  2. AST 加工(internals/ 约 3300 行):syn::DeriveInput → serde::Container,解析所有属性。
  3. 代码生成(ser.rs + de.rs + de/ 约 4700 行):根据 Container 生成 impl 块。
  4. 错误兜底: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% 是个反直觉的数字,说明"属性系统的复杂度不亚于 Serialize 一侧的代码生成"——也是 Serde 用户体验好的根本来源。

动手实验

  1. 阅读 lib.rs:打开 serde_derive/src/lib.rs,对照本章 10.3 节逐段读完。127 行,大约需要 30 分钟。
  2. 跟踪一个属性:在 internals/attr.rs 里搜索 rename 关键字,找到它如何被解析成 attr::Field 的一个字段。
  3. 用 cargo expand 观察不同结构的生成结果:
    • 普通 struct
    • 带 #[serde(rename_all)] 的 struct
    • 带泛型的 struct(观察自动推导的 bound)
    • enum 用默认 tag
    • enum 用 #[serde(tag = "type")] 对比它们的差异,思考"为什么要这样生成"。
  4. 画你自己的架构图:读完 lib.rs 和 ast.rs 后,用自己的话画一张"serde_derive 核心数据流图"。

延伸阅读