Rust in Everything
基于 serde 1.0.229

派生宏的流水线

主线位置:序列化主线和反序列化主线的起点。两条主线里「类型」那一半的代码(impl Serialize 里的 serialize_struct / serialize_field,impl Deserialize 里的 __Field / __Visitor)都是派生宏在编译期写出来的。本页只讲流水线本身:输入怎么变成内部 AST、错误怎么报、生成的 impl 外面包了什么;函数体里写了什么在生成的 Serialize 和生成的 Deserialize。

flowchart TB
  ts["TokenStream"] --> di["syn::DeriveInput"]
  di --> recv[replace_receiver]
  recv --> ast["Container::from_ast"]
  ast --> attrs["attr.rs 读属性"]
  attrs --> chk["check.rs 交叉检查"]
  chk --> pre[precondition]
  pre --> ctx{"Ctxt::check"}
  ctx -->|有错误| ce["compile_error!"]
  ctx -->|没有| body["Fragment 函数体"]
  body --> implblock["impl 块"]
  implblock --> wrap["wrap_in_const"]

代码都在 serde_derive/src/ 里:入口 lib.rs,两个方向各一个 ser.rs / de.rs,internals/ 是两边共用的「解析 + 检查」(它也单独发布成 serde_derive_internals,见源码地图),剩下 dummy.rs、fragment.rs、pretend.rs、deprecated.rs、this.rs、bound.rs 是生成代码用的小工具。实验在 M2 Max、macOS、rustc 1.99.0 上跑,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154"。

入口:两个 proc_macro_derive

lib.rs 里只有两个公开函数:

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

三件事:

  • attributes(serde) 向 rustc 登记 #[serde(...)] 是这两个派生宏的惰性辅助属性:rustc 不去解析它,原样交给宏。没有派生宏的类型上写 #[serde(...)] 就是一个不认识的属性(src/bin/no_derive.rs,Post 只 #[derive(Debug)]):

    error: cannot find attribute `serde` in this scope
     --> src/bin/no_derive.rs:3:5
      |
    3 |   #[serde(rename = "t")]
      |     ^^^^^
      |
      = note: `serde` is in scope, but it is a crate, not an attribute
    
  • parse_macro_input! 把 token 解析成 syn::DeriveInput(名字、泛型、属性、struct / enum / union 的数据)。之后的工作都在 expand_derive_* 里,它返回 syn::Result<TokenStream>。

  • 出错时 syn::Error::into_compile_error 把错误变成带 span 的 compile_error! 调用,作为宏的输出交还 rustc。所以属性写错时,rustc 报的是 serde_derive 自己写的文字,位置指向写错的那个属性;这时宏不输出任何 impl。

同一个文件里还有 struct private:它在 quote! 里展开成 __private229,生成代码里的 _serde::__private229::... 都由它拼出,为什么带补丁号见源码地图。

expand_derive_serialize:一条直线

expand_derive_serialize 的开头是整条流水线的骨架:

pub fn expand_derive_serialize(input: &mut syn::DeriveInput) -> syn::Result<TokenStream> {
    replace_receiver(input);

    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, &params));
    let allow_deprecated = allow_deprecated(input);
    // ...
    Ok(dummy::wrap_in_const(
        cont.attrs.custom_serde_path(),
        impl_block,
    ))
}

expand_derive_deserialize 一模一样,只是把 Derive::Serialize 换成 Derive::Deserialize,泛型换成带 'de 的那一套(generics_with_de_lifetime),非 remote 时多生成一个 deserialize_in_place(只在 deserialize_in_place feature 下有内容)。

按顺序:

步骤做什么讲它的地方
replace_receiver把字段类型里的 Self 换成具体类型名,生成代码里的 Self 会指向别的东西(比如 __Visitor)泛型与 bound
Container::from_astsyn::DeriveInput → serde 自己的 AST,顺带解析全部 #[serde] 属性、跑 check.rs下一节;属性在属性
precondition只和一个方向有关的检查下文
ctxt.check()?有任何错误就在这里返回下文
Parameters::new决定 self 还是 __self、this_type、加了 bound 的泛型泛型与 bound
serialize_body按 struct / enum、属性生成函数体生成的 Serialize
wrap_in_const外面包一层 const _: () = { ... };下文

两个 precondition 都很短。序列化一侧(ser.rs)拒绝 #[serde(field_identifier)] / #[serde(variant_identifier)] 的 enum,报 field identifiers cannot be serialized:这两个属性把 enum 变成「字段名 / 变体名」的标识符,只用来反序列化。反序列化一侧(de.rs)拒绝最后一个字段是切片的动态大小 struct(cannot deserialize a dynamically sized struct),以及要借用输入时已经有一个叫 'de 的生命周期参数(cannot deserialize when there is a lifetime parameter called 'de),因为生成的 impl 要自己引入 'de。

internals::ast:serde 自己的 AST

ast.rs 定义了四个类型,结构和 syn 的对应,只是每一层都多了一个解析好的 attrs:

pub struct Container<'a> {
    /// The struct or enum name (without generics).
    pub ident: syn::Ident,
    /// Attributes on the structure, parsed for Serde.
    pub attrs: attr::Container,
    /// The contents of the struct or enum.
    pub data: Data<'a>,
    /// Any generics on the struct or enum.
    pub generics: &'a syn::Generics,
    /// Original input.
    pub original: &'a syn::DeriveInput,
}

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,
    pub attrs: attr::Field,
    pub ty: &'a syn::Type,
    pub original: &'a syn::Field,
}

和 syn 相比有三处取舍:

  • Style 把 struct 和变体统一起来。 Style 有 Struct(具名字段)、Tuple、Newtype(恰好一个无名字段)、Unit 四种。Newtype 在 syn 里只是「一个元素的 Unnamed」,serde 单独拿出来,因为数据模型里 newtype struct / newtype variant 是独立的类型(serialize_newtype_struct),生成代码也完全不同。struct 是 Data::Struct(Style, fields),enum 的每个 Variant 也带一个 Style,所以「按字段生成代码」的函数两边都能用。
  • member: syn::Member。 具名字段是 Named(ident),元组字段是 Unnamed(index),生成代码统一写 self.#member,不用区分 self.title 和 self.0。
  • 借用,不复制。 ty、generics、original 都是指回 DeriveInput 的引用,生命周期 'a 就是输入的生命周期。只有 ident 和解析出来的 attrs 是自己的。original 留着给报错定位(error_spanned_by(cont.original, ...))。

syn::Data::Union 没有对应的变体:Container::from_ast 遇到 union 直接记一个错误 Serde does not support derive for unions 并返回 None,这是 from_ast 返回 None 的唯一情况。其余情况它依次做四件事:解析容器属性;逐个变体、逐个字段解析属性(attr::Variant::from_ast、attr::Field::from_ast);把 rename_all / rename_all_fields 的规则套到没有单独 rename 的变体和字段上;最后 check::check(cx, &mut item, derive)。属性解析的细节在属性。

from_ast 的最后一个参数 derive 说明这次是为哪个 trait 解析。同一个类型上同时写 #[derive(Serialize, Deserialize)],两个宏各自从头解析一遍,彼此不共享结果;check.rs 里只有 transparent 的检查看这个参数:哪个字段算「透明字段」,序列化一侧看它是否 skip_serializing,反序列化一侧看它是否 skip_deserializing 或带 default。

Ctxt:错误先攒着

属性解析和检查都不返回 Result。attr.rs 开头的注释把原因说得很清楚:

// This module handles parsing of `#[serde(...)]` attributes. The entrypoints
// are `attr::Container::from_ast`, `attr::Variant::from_ast`, and
// `attr::Field::from_ast`. Each returns an instance of the corresponding
// struct. Note that none of them return a Result. Unrecognized, malformed, or
// duplicated attributes result in a span_err but otherwise are ignored. The
// user will see errors simultaneously for all bad attributes in the crate
// rather than just the first.

错误进了 Ctxt:

pub struct Ctxt {
    // The contents will be set to `None` during checking. This is so that checking can be
    // enforced.
    errors: RefCell<Option<Vec<syn::Error>>>,
}
  • 用 RefCell 是因为 Ctxt 以 &Ctxt 传遍整个解析过程(每个 Attr 里都存一份 &'c Ctxt),记错误只需要共享引用。
  • check 按值拿走 self,take() 出整个 Vec,把第一个错误和其余的用 syn::Error::combine 合成一个 syn::Error。合并后的错误经 into_compile_error 变成多个 compile_error!,每个带自己的 span。
  • Option 那一层是为了「必须检查」:Drop 里发现还是 Some(没人调过 check)就 panic!("forgot to check for errors")。忘了检查是 serde_derive 自己的 bug,panic 让测试立刻失败,而不是悄悄吞掉用户的错误。

效果是一次编译看到全部错误。src/bin/two_errors.rs 里有三处写错(容器上一个不存在的大小写规则、字段上拼错的属性名、同一个属性写了两次):

use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "camel")]
struct Post {
  #[serde(skip_serializing_iff = "Option::is_none")]
  summary: Option<String>,
  #[serde(rename = "a", rename = "b")]
  title: String,
}

fn main() {}
error: unknown rename rule `rename_all = "camel"`, expected one of "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", "SCREAMING-KEBAB-CASE"
 --> src/bin/two_errors.rs:4:22
  |
4 | #[serde(rename_all = "camel")]
  |                      ^^^^^^^

error: unknown serde field attribute `skip_serializing_iff`
 --> src/bin/two_errors.rs:6:11
  |
6 |   #[serde(skip_serializing_iff = "Option::is_none")]
  |           ^^^^^^^^^^^^^^^^^^^^

error: duplicate serde attribute `rename`
 --> src/bin/two_errors.rs:8:25
  |
8 |   #[serde(rename = "a", rename = "b")]
  |                         ^^^^^^

error: could not compile `exp-serde-derive1` (bin "two_errors") due to 3 previous errors

三条错误来自同一个 Ctxt,顺序就是记录的顺序:先容器属性,再按字段顺序。「攒着一起报」也有边界:同一个 #[serde(...)] 里遇到不认识的属性名时,解析这一个属性列表的闭包返回 Err,这个列表后面的内容就不再看了,详见属性。

❗IMPORTANT
动脑筋:同一个写错的属性,派生两次会报几次?

把上面的 Post 缩成一个字段,同时派生两个 trait(src/bin/both_derives.rs):

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Post {
  #[serde(rename = "a", rename = "b")]
  title: String,
}

fn main() {}

两个派生宏各有一个 Ctxt,各自解析一遍属性。cargo build 最后一行的「due to N previous errors」里,N 是几?

check.rs:跨属性的检查

只看一个属性就能判断的错误(不认识、重复、值不是字符串、用错了位置)在 attr.rs 解析时就记下了。check.rs 管的是要同时看好几个属性、或者要看字段类型才能判断的组合:

// Cross-cutting checks that require looking at more than a single attrs object.
// Simpler checks should happen when parsing and building the attrs.
pub fn check(cx: &Ctxt, cont: &mut Container, derive: Derive) {
    check_default_on_tuple(cx, cont);
    check_remote_generic(cx, cont);
    check_getter(cx, cont);
    check_flatten(cx, cont);
    check_identifier(cx, cont);
    check_variant_skip_attrs(cx, cont);
    check_internal_tag_field_name_conflict(cx, cont);
    check_adjacent_tag_conflict(cx, cont);
    check_transparent(cx, cont, derive);
    check_from_and_try_from(cx, cont);
}
检查规则(报错原文的要点)
check_default_on_tuple元组 struct 里某个字段带 default,它后面的字段也必须带(field must have #[serde(default)] because previous field ... has #[serde(default)]),除非容器级有 default:元组字段只有序列提前结束时才用默认值,前面缺了后面一定也缺
check_remote_genericremote = "Generic<T>" 不许写泛型参数(remove generic parameters from this path)
check_gettergetter 不能用在 enum 上,只能用在带 remote 的 struct 上
check_flattenflatten 不能用在元组 struct / newtype struct 的字段上
check_identifierother 必须是最后一个、unit 变体,不能用在 untagged enum 或 variant_identifier 上;field_identifier / variant_identifier 只能有 unit 变体(field_identifier 的最后一个可以是 newtype)
check_variant_skip_attrs变体的 serialize_with 和变体或其字段的 skip_serializing(_if) 冲突;deserialize_with 和 skip_deserializing 冲突
check_internal_tag_field_name_conflicttag = "type" 时,struct 变体的字段(或别名)不能也叫 type
check_adjacent_tag_conflicttag 和 content 不能同名
check_transparenttransparent 不能和 from / try_from / into 同用,不能用在 enum、unit struct 上;除 PhantomData 和被跳过的字段外恰好剩一个字段(反序列化一侧还要求它没有 default);同时给那个字段打上 mark_transparent
check_from_and_try_fromfrom 和 try_from 不能同时写

check 拿的是 &mut Container,原因在 check_transparent:它在检查的同时找出那个透明字段并标记,生成代码时 serialize_transparent 直接找带标记的字段。

check 在 from_ast 里跑,记进的是同一个 Ctxt,所以解析错误和组合错误会一起出来(src/bin/check_error.rs):

use serde::Deserialize;

#[derive(Deserialize)]
#[serde(transparent)]
struct Meters {
  value: f64,
  #[serde(defualt)]
  unit: String,
}

fn main() {}
error: unknown serde field attribute `defualt`
 --> src/bin/check_error.rs:7:11
  |
7 |   #[serde(defualt)]
  |           ^^^^^^^

error: #[serde(transparent)] requires struct to have at most one transparent field
 --> src/bin/check_error.rs:4:1
  |
4 | / #[serde(transparent)]
5 | | struct Meters {
6 | |   value: f64,
7 | |   #[serde(defualt)]
8 | |   unit: String,
9 | | }
  | |_^

第二条的 span 是整个类型(error_spanned_by(cont.original, ...)),因为这个错误不属于任何一个属性。

Fragment:表达式还是语句块

生成函数体的几十个函数各自返回一段 token。问题是同一段 token 会被嵌到不同位置:函数体里、match 分支里、另一个表达式中间。一段 let 语句加一个尾表达式放在函数体里没问题,放进 match 分支就得加花括号;单个表达式放进 match 分支则要补一个逗号。fragment.rs 让每段 token 带上自己的语法形态:

pub enum Fragment {
    /// Tokens that can be used as an expression.
    Expr(TokenStream),
    /// Tokens that can be used inside a block. The surrounding curly braces are
    /// not part of these tokens.
    Block(TokenStream),
}

生成函数用 quote_expr! / quote_block!(就是 Fragment::Expr(quote!(...)) / Fragment::Block(quote!(...)))返回 Fragment。调用方按嵌入的位置选一个包装,三个包装各自实现 ToTokens:

包装用在Expr 的处理Block 的处理
Expr表达式位置原样加 { }
Stmts函数体原样原样(函数自带花括号)
Matchmatch 分支的值后面加 ,加 { }

上面 expand_derive_serialize 里的 Stmts(serialize_body(...)) 就是这样用的:unit struct 的函数体是一个表达式(quote_expr! { _serde::Serializer::serialize_unit_struct(__serializer, #type_name) }),具名 struct 的是一串 let 加尾表达式(quote_block!),两者都直接放进 fn serialize 的花括号里。enum 的每个变体分支写成 #case => #body,body 是 Match(...)。

📝NOTE
冷知识:Fragment 是为了消灭「神秘的逗号」

#777(issue「Expressions vs blocks vs match clauses in generated code」)指出,当时 ser.rs 里有的生成代码末尾带着一个逗号,「the comma is there for seemingly no reason but if you remove it, stuff breaks because somewhere up the chain these tokens are used as the value in a match expression」。issue 里提的方案就是现在的样子:一个 enum Fragment { Expr, Block },外加只对包装类型实现 ToTokens,「we can reason about each function individually without caring precisely how the caller interpolates the result - no more mystery comma」。提交「Fragments that keep track of type of syntax」(「Fixes #777.」)加了 fragment.rs,首次发布在 v0.9.8。Fragment 本身至今不实现 ToTokens,忘了选包装就编译不过。

dummy.rs:const _ 包装

wrap_in_const 是整个 dummy.rs:

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
        };
    }
}

src/bin/expand_post.rs 派生 struct Post { title: String, tags: Vec<String> } 的 Serialize,cargo expand 的结果(只摘外层):

#[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 Post {
        fn serialize<__S>(
            &self,
            __serializer: __S,
        ) -> _serde::__private229::Result<__S::Ok, __S::Error>
        where
            __S: _serde::Serializer,
        {
            // ...
        }
    }
};

逐项对照:

  • const _: () = { ... };:一个没有名字的常量,块里可以放 use、extern crate、辅助类型(__Field、__Visitor、__SerializeWith),这些名字都只在块里可见,不会和用户模块里的名字冲突,也不会在用户模块里多出任何名字;块里的 impl 照样对全局生效。两个派生各包一个 const _,互不干扰。

  • extern crate serde as _serde;:生成代码一律写 _serde::...,不依赖用户在当前模块里 use 了什么,也不怕用户自己有一个叫 serde 的模块。#[allow(unused_extern_crates)] 压掉 2018 idiom lint。

  • #[serde(crate = "...")] 走 Some(path) 分支,换成 use #path as _serde;,给重新导出 serde 的 crate 用。src/bin/crate_path.rs 里写 #[serde(crate = "reexport::my_serde")],展开成:

    const _: () = {
        use reexport::my_serde as _serde;
        #[automatically_derived]
        impl _serde::Serialize for Post {
    
  • _serde::__require_serde_not_serde_core!();:在 cargo expand 的输出里看不到,因为 serde 里这个宏展开成空;如果 _serde 实际指向 serde_core,它展开成 compile_error!。为什么要拦,见源码地图。

  • #[automatically_derived] 不在 dummy.rs 里,是 ser.rs / de.rs 写在每个 impl 上的(__Visitor、__FieldVisitor 这些辅助 impl 上也有),标明这是派生出来的 impl。

  • 外层的 allow:non_upper_case_globals、unused_attributes、unused_qualifications 三个在 2017 年 serde_codegen 并入 serde_derive 时就已经写在当时的 const _IMPL_SERIALIZE_FOR_... 上(那个名字含小写字母,正是 non_upper_case_globals 要报的);unused_qualifications 针对的是生成代码处处写全路径;clippy::absolute_paths 是 2025 年的提交「fix clippy absolute paths warning」加的。#[doc(hidden)] 让 rustdoc 不显示这个常量。

📝NOTE
冷知识:const _ 之前,常量是有名字的

在 1.0.113 之前,包装常量叫 _IMPL_SERIALIZE_FOR_Post 这样的名字,dummy.rs 用 format!("_IMPL_{}_FOR_{}", trait_, unraw(ty)) 拼出来。#1683 提出改用 Rust 1.37 引入的匿名常量 const _,理由是「This would tend to produce more readable error messages, because the paths currently emitted by rustc in error messages are silly」:rustc 报「trait 没实现」时,trait 的路径要经过 extern crate serde as _serde 所在的那个常量。#1827(「Use underscore consts on Rust 1.37+」)用 build.rs 探测编译器版本,1.37 以上生成 const _,PR 里贴出的新报错是 the trait `_::_serde::Serialize` is not implemented for `S` ,常量名那一段变成了 _。首次发布在 1.0.113,发布说明写的是「Improve diagnostic when a serde trait is not implemented (#1827)」。2023 年的提交「Remove build script from serde_derive」把 build.rs 和旧分支一起删掉,理由是「The current serde_derive's MSRV is 1.56, and both underscore consts and ptr::addr_of! are always available.」,现在只剩 const _。

pretend.rs:remote 派生的假装使用

#[serde(remote = "...")] 给别的 crate 的类型派生:用户在本地照抄一份定义(PointDef),派生宏生成的不是 impl Serialize for PointDef,而是 PointDef 上的一个固有方法 fn serialize(__self: &other::Point, ...),读的是远端类型的字段。这样一来,PointDef 自己的字段从来没有被读过,变体从来没有被构造过,rustc 会报 dead_code。pretend.rs 开头的注释说的就是这件事:

// Suppress dead_code warnings that would otherwise appear when using a remote
// derive. Other than this pretend code, a struct annotated with remote derive
// never has its fields referenced and an enum annotated with remote derive
// never has its variants constructed.

它只在 remote 分支里被调用(ser.rs 和 de.rs 里 if let Some(remote) = cont.attrs.remote() 那一支的 let used = pretend::pretend_used(&cont, params.is_packed);),普通派生不生成这段代码。src/bin/remote_pretend.rs:

mod other {
  pub struct Point {
    pub x: i32,
    pub y: i32,
  }
}

#[derive(Serialize)]
#[serde(remote = "other::Point")]
struct PointDef {
  x: i32,
  y: i32,
}

展开后的方法体开头:

    impl PointDef {
        fn serialize<__S>(
            __self: &other::Point,
            __serializer: __S,
        ) -> _serde::__private229::Result<__S::Ok, __S::Error>
        where
            __S: _serde::Serializer,
        {
            match _serde::__private229::None::<&PointDef> {
                _serde::__private229::Some(PointDef { x: __v0, y: __v1 }) => {}
                _ => {}
            }
            let mut __serde_state = _serde::Serializer::serialize_struct(
                __serializer,
                "PointDef",
                false as usize + 1 + 1,
            )?;
            // ...
                _serde::__private229::ser::constrain::<i32>(&__self.x),

match None::<&PointDef> 的 Some 分支永远进不去,但它在模式里把 PointDef 的每个字段绑定了一遍,对 dead_code 分析来说字段就「被读过」了。证明这段 match 确实有用:照着展开结果手写同样的方法,一份不写这段 match(src/bin/no_pretend.rs),一份写(src/bin/with_pretend.rs),其余一字不差:

struct PointDef {
  x: i32,
  y: i32,
}

impl PointDef {
  fn serialize<S: Serializer>(this: &other::Point, serializer: S) -> Result<S::Ok, S::Error> {
    // with_pretend.rs 在这里多了:
    // match None::<&PointDef> {
    //   Some(PointDef { x: _v0, y: _v1 }) => {}
    //   _ => {}
    // }
    let mut state = serializer.serialize_struct("PointDef", 2)?;
    state.serialize_field("x", &this.x)?;
    state.serialize_field("y", &this.y)?;
    state.end()
  }
}
=== no_pretend
warning: fields `x` and `y` are never read
  --> src/bin/no_pretend.rs:12:3
   |
11 | struct PointDef {
   |        -------- fields in this struct
12 |   x: i32,
   |   ^
13 |   y: i32,
   |   ^
   |
   = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default

{"x":1,"y":2}
=== with_pretend
{"x":1,"y":2}

enum 要多一步:除了读字段,还要假装构造每个变体。src/bin/remote_enum.rs 给 enum Shape { Dot, Circle(f64), Rect { w: f64, h: f64 } } 做 remote 派生,展开出来是一个读字段的 match(只列带字段的变体,元组变体写成 Circle { 0: __v0 }),再加每个变体一个构造它的 match(pretend_variants_used):

            match _serde::__private229::None::<&ShapeDef> {
                _serde::__private229::Some(ShapeDef::Circle { 0: __v0 }) => {}
                _serde::__private229::Some(ShapeDef::Rect { w: __v0, h: __v1 }) => {}
                _ => {}
            }
            match _serde::__private229::None {
                _serde::__private229::Some(()) => {
                    let _ = ShapeDef::Dot;
                }
                _ => {}
            }
            match _serde::__private229::None {
                _serde::__private229::Some((__v0,)) => {
                    let _ = ShapeDef::Circle(__v0);
                }
                _ => {}
            }
            // ... Rect 同理

Some((__v0,)) 里的 __v0 没有写类型,由后面 ShapeDef::Circle(__v0) 推断成 f64,所以不用知道字段类型就能写出构造表达式。#[repr(packed)] 的 struct 不能直接在模式里绑定字段的引用,pretend_fields_used_struct_packed 改成 Some(__v @ T { a: _, b: _ }) 加 addr_of!(__v.a)。

📝NOTE
没有蠢问题

问: 普通的 #[derive(Serialize)] 为什么不需要 pretend?

答: 普通派生生成的是 impl Serialize for Post,函数体里写 &self.title,字段确实被读了;Deserialize 一侧构造 Post { title: __field0, ... },类型确实被构造了。remote 派生的方法读的是 __self: &other::Point 的字段,构造的是 other::Point,本地那份 PointDef 只是一张「字段清单」,除了 pretend 那段代码,没有任何地方碰它。

pretend 的覆盖范围也是跟着 rustc 的 lint 一起长的:2018 年加入时(提交「Pretend remote derives are not dead code」,1.0.45)只处理具名字段的 struct 和 struct 变体;nightly-2024-01-06 的 rustc 开始对元组字段也报「field 0 is never read」,#2671(「Fix new dead_code warning in tuple struct and tuple variant remote defs」,1.0.195)把 Style::Tuple | Style::Newtype 加进了 pretend_fields_used 和 pretend_fields_used_enum 的匹配,发布说明是「Prevent remote definitions of tuple struct or tuple variant from triggering dead_code warning (#2671)」。

deprecated.rs:给 impl 加 allow(deprecated)

类型本身带 #[deprecated] 时,派生出来的 impl Serialize for Legacy 里提到 Legacy、读 self.title 都算「使用已弃用的项」。should_allow_deprecated 检查容器和每个变体的属性,只要有 #[deprecated] 或 #[allow(deprecated)],就在 impl 上加 #[allow(deprecated)]:

fn should_allow_deprecated(input: &syn::DeriveInput) -> bool {
    if contains_deprecated(&input.attrs) {
        return true;
    }
    if let syn::Data::Enum(data_enum) = &input.data {
        for variant in &data_enum.variants {
            if contains_deprecated(&variant.attrs) {
                return true;
            }
        }
    }
    false
}

对照实验:src/bin/deprecated_derive.rs 派生 #[deprecated(note = "use Post")] struct Legacy { title: String },src/bin/deprecated_hand.rs 手写一个同样的 impl Serialize,两个文件都只在 main 上写 #[allow(deprecated)](main 里要构造 Legacy):

=== deprecated_derive
{"title":"x"}
=== deprecated_hand
warning: use of deprecated struct `Legacy`: use Post
 --> src/bin/deprecated_hand.rs:9:20
  |
9 | impl Serialize for Legacy {
  |                    ^^^^^^
  |
  = note: `#[warn(deprecated)]` on by default

warning: use of deprecated field `Legacy::title`: use Post
  --> src/bin/deprecated_hand.rs:12:37
   |
12 |     state.serialize_field("title", &self.title)?;
   |                                     ^^^^^^^^^^

{"title":"x"}

派生一侧 cargo expand 里是 #[automatically_derived] #[allow(deprecated)] impl _serde::Serialize for Legacy。这个文件是 #2879(「add #[allow(deprecated)] to derive implementations」)加的,首次发布在 1.0.225,发布说明:「Avoid triggering a deprecation warning in derived Serialize and Deserialize impls for a data structure that contains its own deprecations (#2879)」。

this.rs:类型位置和值位置的路径

this.rs 两个函数,回答「生成代码里怎么称呼被派生的类型」:普通派生就是类型名 Post;remote 派生是 remote = "..." 里写的路径。区别只在泛型参数:this_type 去掉 ::,用在类型位置(&other::Wrapper<T>);this_value 补上 ::,用在表达式和模式位置(other::Wrapper::<T> { .. })。它们怎么和生成的泛型参数配合,在泛型与 bound。

动脑筋答案

src/bin/both_derives.rs 的完整输出:

error: duplicate serde attribute `rename`
 --> src/bin/both_derives.rs:5:25
  |
5 |   #[serde(rename = "a", rename = "b")]
  |                         ^^^^^^

error: could not compile `exp-serde-derive1` (bin "both_derives") due to 1 previous error

N 是 1。两个派生确实各报了一次:把 Serialize 去掉、只派生 Deserialize,同样报这一条。但两条诊断的文字和位置完全相同,rustc 只保留一条,--message-format=json 的输出里也只有一条 compiler-message。错误的 span 指向第二个 rename,因为 Attr::set 在发现已经有值时,用的是这一次传进来的 token 报错。

小结

  • 两个入口 derive_serialize / derive_deserialize 走同一条直线:replace_receiver → Container::from_ast(属性解析 + check.rs)→ precondition → Ctxt::check → 生成函数体 → wrap_in_const;出错时输出 compile_error!,不输出 impl。
  • internals::ast 的 Container / Data / Variant / Field 借用 syn 的输入,多带一份解析好的 attrs;Style 把 struct 和变体统一成 Struct / Tuple / Newtype / Unit;union 直接报错。
  • Ctxt 把错误攒在 RefCell<Option<Vec<syn::Error>>> 里,check 用 combine 一次报出;不 check 就 drop 会 panic。
  • check.rs 只做跨属性的组合检查(default 在元组里的顺序、transparent、other、内部标签冲突等),和属性解析错误记在同一个 Ctxt 里。
  • Fragment 记住一段 token 是表达式还是语句块,Expr / Stmts / Match 按嵌入位置补花括号或逗号(#777,v0.9.8)。
  • const _: () = { extern crate serde as _serde; ... } 把辅助名字关在块里,#[serde(crate)] 改成 use path as _serde;匿名常量来自 #1827(1.0.113),为了让报错路径可读。
  • pretend.rs 只服务 remote 派生:用永远走不到的 match None 假装读了本地定义的字段、构造了变体,压住 dead_code;deprecated.rs 给自带弃用标记的类型加 #[allow(deprecated)](#2879,1.0.225)。

💬 关联讨论

发起讨论
加载中...