Rust in Everything
基于 serde 1.0.229

Serialize 与 Serializer

主线位置:序列化主线的全部接口。一次 to_string 里,derive 生成的 Serialize::serialize 调 serializer.serialize_struct(...),拿到一个状态对象,逐个 serialize_field,最后 end;本页讲这些方法和状态对象在 trait 层面的约定。derive 具体生成了什么在 生成的 Serialize,serde_json 怎么实现它们在 序列化一侧。

flowchart TB
  user["to_string(&post)"] --> sz["Post::serialize(ser)"]
  sz --> start["serialize_struct 名字和字段数"]
  start --> state["SerializeStruct 状态对象"]
  state -->|"serialize_field 每个字段"| field["字段的 serialize"]
  field -->|"递归,新的 Serializer"| sz2["值写进同一个输出"]
  state --> endnode["end:返回 S::Ok"]

这一页的代码在 serde_core/src/ser/ 的三个文件里:mod.rs(2010 行,大部分是文档和例子)定义 Serialize、Serializer 和七个复合类型的 trait;impossible.rs(216 行)是一个不能被构造的辅助类型;fmt.rs(170 行)给 &mut fmt::Formatter 实现了 Serializer。标准库类型的 Serialize 实现在 ser/impls.rs,见 标准库类型的实现。

本页的实验都在 M2 Max、macOS、rustc 1.99.0 上跑,依赖锁定 serde = "=1.0.229"(derive)、serde_json = "=1.0.154"、bincode = "=1.3.3"、postcard = "1"(实际解析到 1.1.3,开 alloc),没有特别说明的都是 debug 构建(cargo run)。

Serialize:一个方法

Serialize 只有一个方法:

pub trait Serialize {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer;
}

三点:

  • &self:序列化不消耗值,也不改它。
  • S 是方法上的泛型参数,不是 trait 上的。同一个 impl Serialize for Post 对每一种格式各单态化一份,Post 不需要知道有哪些格式。
  • 返回 S::Ok:输出是什么由格式决定。serde_json::to_writer 用的 &mut serde_json::Serializer<W> 的 Ok 是 ()(结果已经写进 W);serde_json::to_value 用的 serde_json::value::Serializer 的 Ok 是 Value(结果就是返回值)。同一份 Serialize 代码两边都能用。

trait 上还有一个 #[diagnostic::on_unimplemented],把报错改写成 the trait bound `{Self}: serde::Serialize` is not satisfied,并提示「for local types consider adding #[derive(serde::Serialize)]」。源码注释说明了原因:trait 实际定义在 serde_core 里,不改写的话,没有直接依赖 serde_core 的项目会在报错里看到 serde_core::ser::Serialize 这个陌生路径。

Serializer:按值拿 self 的格式

Serializer 的开头是九个关联类型:

pub trait Serializer: Sized {
    type Ok;
    type Error: Error;
    type SerializeSeq: SerializeSeq<Ok = Self::Ok, Error = Self::Error>;
    type SerializeTuple: SerializeTuple<Ok = Self::Ok, Error = Self::Error>;
    type SerializeTupleStruct: SerializeTupleStruct<Ok = Self::Ok, Error = Self::Error>;
    type SerializeTupleVariant: SerializeTupleVariant<Ok = Self::Ok, Error = Self::Error>;
    type SerializeMap: SerializeMap<Ok = Self::Ok, Error = Self::Error>;
    type SerializeStruct: SerializeStruct<Ok = Self::Ok, Error = Self::Error>;
    type SerializeStructVariant: SerializeStructVariant<Ok = Self::Ok, Error = Self::Error>;
    // ...
}
关联类型作用
Ok序列化成功后返回的值。写进 writer 的格式用 (),在内存里建树的用树的类型
Error格式自己的错误类型,必须实现 ser::Error(只有一个 custom 构造函数,见 错误)
SerializeSeq … SerializeStructVariant七种复合类型各自的状态对象,serialize_seq 等方法返回它

七个复合类型的约束都写成 <Ok = Self::Ok, Error = Self::Error>:状态对象的 end 返回的必须就是整个 Serializer 的 Ok,元素出错报的必须是同一种 Error。这样 Serialize 的实现里 seq.end() 的返回值可以直接作为 serialize 的返回值。

接下来是 30 个 serialize_* 方法,数据模型的 29 种类型各一个,Option 拆成 serialize_none 和 serialize_some 两个(29 种类型的表在 serde 解决什么问题)。其中 28 个是必须实现的;serialize_i128 和 serialize_u128 有默认实现,直接报错:

    fn serialize_i128(self, v: i128) -> Result<Self::Ok, Self::Error> {
        let _ = v;
        Err(Error::custom("i128 is not supported"))
    }

所有方法的接收者都是 self,按值。一个 Serializer 值只序列化一个值,用完就没了。这也是 Serializer: Sized 的原因。写进 writer 的格式于是不给 Serializer 本身实现,而是给它的 &mut 实现,serde_json 的 impl ser::Serializer for &'a mut Serializer<W, F>:

impl<'a, W, F> ser::Serializer for &'a mut Serializer<W, F>
where
    W: io::Write,
    F: Formatter,
{
    type Ok = ();
    type Error = Error;

    type SerializeSeq = Compound<'a, W, F>;
    type SerializeTuple = Compound<'a, W, F>;
    // ... 七个复合类型都是 Compound<'a, W, F>

&mut 引用是 Copy 不了的,但可以重借用:序列化一个字段时,serde_json 把 &mut **ser 交给字段的 serialize,字段写完,借用结束,外层继续用同一个 Serializer。在内存里建树的格式没有共享的输出,就直接给一个零大小的值实现:serde_json::value::Serializer 是 struct Serializer;,Ok = Value,每层递归用一个新的 Serializer。

源码里到处是 tri!(...) 而不是 ?。crate_root.rs 的注释说明了原因:serde 内部的错误处理都不需要 ? 隐含的 From::from 转换,这个简化的宏比 ? 编译快 9%。读代码时把它当成 ? 即可。

复合类型:开始、逐个元素、end

标量一次调用就完了;seq、tuple、map、struct 这些复合类型要分三步:先调 Serializer::serialize_seq(len) 之类的方法开始,拿到一个状态对象;对每个元素调状态对象的方法;最后调状态对象的 end。SerializeSeq:

pub trait SerializeSeq {
    type Ok;
    type Error: Error;
    fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
    where
        T: ?Sized + Serialize;
    fn end(self) -> Result<Self::Ok, Self::Error>;
}

七个 trait 形状相同,区别只在逐个元素那一步的方法名和参数:

开始状态对象逐个元素长度参数
serialize_seqSerializeSeqserialize_element(&T)Option<usize>
serialize_tupleSerializeTupleserialize_element(&T)usize
serialize_tuple_structSerializeTupleStructserialize_field(&T)usize
serialize_tuple_variantSerializeTupleVariantserialize_field(&T)usize
serialize_mapSerializeMapserialize_key(&K)、serialize_value(&V),或 serialize_entryOption<usize>
serialize_structSerializeStructserialize_field(&'static str, &T)、skip_fieldusize
serialize_struct_variantSerializeStructVariantserialize_field(&'static str, &T)、skip_fieldusize

签名本身就规定了调用顺序。开始的方法按值拿走 Serializer,返回状态对象,之后不能再对同一个 Serializer 调别的方法;逐个元素的方法拿 &mut self,可以调任意多次;end 按值拿走状态对象,调完之后再想 serialize_element,编译器不让。「开始之后必须以 end 结束」这一条类型系统管不住(忘了调 end,状态对象被 drop 掉,代码照样编译),但这种代码拿不到 S::Ok,serialize 也就没有值可以返回,除非另外造一个。

状态对象里放的是格式自己的状态,类型这边看不见。serde_json 七种复合类型共用一个 Compound,里面是对 Serializer 的借用和一个 State(Empty / First / Rest),靠它决定元素前面要不要写逗号:

pub enum Compound<'a, W: 'a, F: 'a> {
    Map {
        ser: &'a mut Serializer<W, F>,
        state: State,
    },
    // ... arbitrary_precision、raw_value 两个 feature 下的变体
}

serde_json::value::Serializer 则用三种不同的状态对象:seq、tuple、tuple struct 共用 SerializeVec(里面是一个 Vec<Value>),map 和 struct 共用 SerializeMap,两种 variant 各一个。状态对象做成关联类型,格式就可以按需要选,不用全塞进一个类型。

SerializeMap 把键和值拆成 serialize_key、serialize_value 两步,serialize_entry 有默认实现,就是依次调这两个。文档给的理由是:有的 Serialize 类型不能同时在内存里拿着键和值,所以实现方必须支持分开调;键和值都在手上时鼓励调 serialize_entry,让格式有机会优化。SerializeStruct::skip_field 默认什么也不做:derive 对被 skip_serializing_if 跳过的字段调它,需要知道「这里本来有一个字段」的格式可以覆盖它。

📝NOTE
没有蠢问题

问: 为什么不设计成 serialize_seq(len, |seq| { ... }) 这样传一个闭包,开始和结束都由格式来调,就不会忘了 end?

答: 闭包里要序列化的元素类型各不相同。serialize_field 是一个泛型方法(fn serialize_field<T: ?Sized + Serialize>),一个 struct 的每个字段各用一个 T 调一次。Rust 的闭包不能是泛型的,闭包参数只能是一个具体类型;要让闭包里能对任意 T 调用,参数就得是一个 trait 对象,而带泛型方法的 trait 做不成 trait 对象,只能退回到动态分派、把每个值擦除类型,这正是 serde 要避免的。状态对象是一个普通的具体类型,它的泛型方法每次调用都单态化、能内联。代价就是上面说的:end 由调用方负责调。

len:声明,不是约束

serialize_seq 和 serialize_map 的长度参数是 Option<usize>,其余五个是 usize。tuple、struct 的长度在编译期就知道,seq 和 map 的长度却可能要遍历一遍才知道。serialize_seq 的文档说:这个数「可能在遍历之前算得出来,也可能算不出来。有的 serializer 只支持事先知道长度的序列」。

谁传 None

标准库的集合类型都不直接调 serialize_seq,而是调 collect_seq / collect_map。Vec、VecDeque、BTreeSet、HashSet 等 和切片都是 serializer.collect_seq(self),HashMap 和 BTreeMap 是 serializer.collect_map(self)。collect_seq 的默认实现:

    fn collect_seq<I>(self, iter: I) -> Result<Self::Ok, Self::Error>
    where
        I: IntoIterator,
        <I as IntoIterator>::Item: Serialize,
    {
        let mut iter = iter.into_iter();
        let mut serializer = tri!(self.serialize_seq(iterator_len_hint(&iter)));
        tri!(iter.try_for_each(|item| serializer.serialize_element(&item)));
        serializer.end()
    }

长度来自文件末尾的 iterator_len_hint:

fn iterator_len_hint<I>(iter: &I) -> Option<usize>
where
    I: Iterator,
{
    match iter.size_hint() {
        (lo, Some(hi)) if lo == hi => Some(lo),
        _ => None,
    }
}

size_hint 的上下界相等才算知道长度。标准库集合的迭代器都给出精确的 size_hint,所以它们总是传 Some(n);filter 之后下界变成 0,就是 None。collect_map 同理,逐对调 serialize_entry。

derive 一侧,serialize_struct 的 len 是运行时算的:被 skip_serializing_if 跳过的字段不计入。结构体里有 #[serde(flatten)] 字段时,derive 改调 serialize_map(None)(serialize_struct_as_map),因为被展开的那个字段会贡献几个键,编译期不知道,见 flatten。

下面用一个只记录「调了哪个开始方法、len 是多少、之后来了几个元素」的 Serializer 验证这几条。它不递归序列化元素,只数个数;不关心的复合类型都用 Impossible(本页后面讲)。src/bin/len_hint.rs,删掉了一些重复的样板:

use std::collections::{BTreeSet, HashMap};
use std::fmt;

use serde::Serialize;
use serde::ser::{self, Impossible, SerializeMap, SerializeSeq, SerializeStruct, Serializer};

// A Serializer that only reports which compound method was called and with what len.
// Elements are counted, not serialized, so the probe never recurses.
struct Probe;

#[derive(Debug)]
struct ProbeError(String);
// ... impl Display、std::error::Error、ser::Error for ProbeError

struct Counter {
  what: String,
  n: usize,
}

impl SerializeSeq for Counter {
  type Ok = String;
  type Error = ProbeError;
  fn serialize_element<T: ?Sized + Serialize>(&mut self, _: &T) -> Result<(), ProbeError> {
    self.n += 1;
    Ok(())
  }
  fn end(self) -> Result<String, ProbeError> {
    Ok(format!("{} -> {} elements", self.what, self.n))
  }
}
// ... SerializeMap(serialize_key 计数)和 SerializeStruct(serialize_field 计数)同理

impl Serializer for Probe {
  type Ok = String;
  type Error = ProbeError;
  type SerializeSeq = Counter;
  type SerializeTuple = Impossible<String, ProbeError>;
  type SerializeTupleStruct = Impossible<String, ProbeError>;
  type SerializeTupleVariant = Impossible<String, ProbeError>;
  type SerializeMap = Counter;
  type SerializeStruct = Counter;
  type SerializeStructVariant = Impossible<String, ProbeError>;

  // ... 标量和其余复合类型的方法都返回 Err(方法名)
  fn serialize_seq(self, len: Option<usize>) -> Result<Counter, ProbeError> {
    Ok(Counter { what: format!("serialize_seq({len:?})"), n: 0 })
  }
  fn serialize_map(self, len: Option<usize>) -> Result<Counter, ProbeError> {
    Ok(Counter { what: format!("serialize_map({len:?})"), n: 0 })
  }
  fn serialize_struct(self, name: &'static str, len: usize) -> Result<Counter, ProbeError> {
    Ok(Counter { what: format!("serialize_struct({name:?}, {len})"), n: 0 })
  }
}

// Goes through the default collect_seq with an iterator whose size_hint is not exact.
struct Evens<'a>(&'a [u32]);

impl Serialize for Evens<'_> {
  fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
    s.collect_seq(self.0.iter().filter(|x| *x % 2 == 0))
  }
}

#[derive(Serialize)]
struct Post {
  title: String,
  #[serde(skip_serializing_if = "Option::is_none")]
  draft: Option<bool>,
  #[serde(flatten)]
  extra: HashMap<String, u32>,
}

#[derive(Serialize)]
struct Plain {
  title: String,
  #[serde(skip_serializing_if = "Option::is_none")]
  draft: Option<bool>,
}

fn show<T: Serialize>(label: &str, v: &T) {
  match v.serialize(Probe) {
    Ok(s) => println!("{label:<26} {s}"),
    Err(e) => println!("{label:<26} Err({e})"),
  }
}

fn main() {
  let v = vec![1u32, 2, 3, 4, 5];
  show("Vec<u32>", &v);
  show("&[u32][1..]", &&v[1..]);
  show("BTreeSet<u32>", &v.iter().copied().collect::<BTreeSet<_>>());
  show("HashMap<String, u32>", &HashMap::from([("a".to_string(), 1u32), ("b".to_string(), 2)]));
  show("Evens(filter)", &Evens(&v));
  show("Plain { draft: None }", &Plain { title: "t".into(), draft: None });
  show(
    "Post (flatten)",
    &Post { title: "t".into(), draft: None, extra: HashMap::from([("views".to_string(), 7u32)]) },
  );
  show("u32", &5u32);
  show("(u8, u8)", &(1u8, 2u8));
}
Vec<u32>                   serialize_seq(Some(5)) -> 5 elements
&[u32][1..]                serialize_seq(Some(4)) -> 4 elements
BTreeSet<u32>              serialize_seq(Some(5)) -> 5 elements
HashMap<String, u32>       serialize_map(Some(2)) -> 2 entries
Evens(filter)              serialize_seq(None) -> 2 elements
Plain { draft: None }      serialize_struct("Plain", 1) -> 1 fields
Post (flatten)             serialize_map(None) -> 2 entries
u32                        Err(serialize_u32)
(u8, u8)                   Err(serialize_tuple)
  • 集合类型和切片都传 Some(n),n 等于实际元素数。
  • filter 之后是 None,元素照样一个个来。
  • Plain 两个字段,draft 是 None 被跳过,len 是 1。
  • Post 有 flatten,走 serialize_map(None);draft 同样被跳过,剩下 title 和展开出来的 views 两个键。
  • 最后两行是 Probe 不支持的方法返回的错误,元组走的是 serialize_tuple,不是 serialize_seq。

谁需要 len

格式拿这个数做什么,各不相同:

格式Some(n)None
bincode 1.3.3先写 n 作为长度前缀(默认配置下是 8 字节小端 u64),再写元素len.ok_or(ErrorKind::SequenceMustHaveLength)?,报错
postcard 1.1.3先写 n 的 varint,再写元素len.ok_or(Error::SerializeSeqLengthUnknown)?,报错
serde_json 写出器只看是不是 Some(0):是就立刻写 ],状态设为 Empty和 Some(n) 一样
serde_json::value::SerializerVec::with_capacity(n) 预分配len.unwrap_or(0),不预分配

serde_json 的 serialize_seq:

    fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
        tri!(self
            .formatter
            .begin_array(&mut self.writer)
            .map_err(Error::io));
        if len == Some(0) {
            tri!(self
                .formatter
                .end_array(&mut self.writer)
                .map_err(Error::io));
            Ok(Compound::Map {
                ser: self,
                state: State::Empty,
            })
        } else {
            Ok(Compound::Map {
                ser: self,
                state: State::First,
            })
        }
    }

JSON 的数组本来就有结束符,不需要事先知道长度;Some(0) 的特殊处理让空数组的 [ 和 ] 在一次调用里写完,end 看到 Empty 就什么也不写。二进制格式没有结束符,读的一方要先读长度才知道后面有几个元素,所以 bincode 和 postcard 必须在第一个元素之前写出长度,None 就只能报错。(对应地,map 一侧 serialize_map 的处理和这里完全一样。)

没有一个格式检查 len 和实际元素数是否相符。下面这个程序用同样三个元素、三种 len 分别交给四个 serializer,src/bin/len_formats.rs:

use serde::Serialize;
use serde::ser::{SerializeSeq, Serializer};

// Same elements, three ways of announcing the length.
struct Seq {
  len: Option<usize>,
  items: Vec<u32>,
}

impl Serialize for Seq {
  fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
    let mut seq = s.serialize_seq(self.len)?;
    for x in &self.items {
      seq.serialize_element(x)?;
    }
    seq.end()
  }
}

fn run(label: &str, v: &Seq) {
  println!("{label}");
  println!("  serde_json::to_string : {:?}", serde_json::to_string(v).map_err(|e| e.to_string()));
  println!("  serde_json::to_value  : {:?}", serde_json::to_value(v).map(|v| v.to_string()).map_err(|e| e.to_string()));
  match bincode::serialize(v) {
    Ok(bytes) => {
      println!("  bincode::serialize    : Ok({bytes:?})");
      println!("    back as Vec<u32>    : {:?}", bincode::deserialize::<Vec<u32>>(&bytes).map_err(|e| e.to_string()));
    }
    Err(e) => println!("  bincode::serialize    : Err({e})"),
  }
  println!("  postcard::to_allocvec : {:?}", postcard::to_allocvec(v).map_err(|e| e.to_string()));
}

fn main() {
  let items = vec![1u32, 2, 3];
  run("len = Some(3)", &Seq { len: Some(3), items: items.clone() });
  run("len = None", &Seq { len: None, items: items.clone() });
  run("len = Some(1), 3 elements", &Seq { len: Some(1), items });
}
len = Some(3)
  serde_json::to_string : Ok("[1,2,3]")
  serde_json::to_value  : Ok("[1,2,3]")
  bincode::serialize    : Ok([3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0])
    back as Vec<u32>    : Ok([1, 2, 3])
  postcard::to_allocvec : Ok([3, 1, 2, 3])
len = None
  serde_json::to_string : Ok("[1,2,3]")
  serde_json::to_value  : Ok("[1,2,3]")
  bincode::serialize    : Err(Bincode can only encode sequences and maps that have a knowable size ahead of time)
  postcard::to_allocvec : Err("The length of a sequence must be known")
len = Some(1), 3 elements
  serde_json::to_string : Ok("[1,2,3]")
  serde_json::to_value  : Ok("[1,2,3]")
  bincode::serialize    : Ok([1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0])
    back as Vec<u32>    : Ok([1])
  postcard::to_allocvec : Ok([1, 1, 2, 3])
  • Some(3):bincode 前 8 字节是长度 3,然后三个 4 字节的 u32;postcard 是 varint 的 3 加三个单字节 varint。
  • None:两个 JSON 路径不受影响;两个二进制格式在开始那一步就报错,什么也没写出来。这就是 Evens 那种 filter 迭代器、以及带 flatten 的结构体不能用 bincode 序列化的原因。
  • Some(1) 却给了三个元素:所有格式都成功了。bincode 照写长度前缀 1,再写三个元素;读回 Vec<u32> 只读出 [1],后面 8 个字节被当成多余的尾巴忽略(bincode::deserialize 的默认配置带 allow_trailing_bytes()),也没有报错。

所以 len 是写 Serialize 的一方对格式的承诺,格式信任它,serde 不核对。手写 Serialize 时,要么给准确的数,要么给 None。

❗IMPORTANT
动脑筋:serialize_seq(Some(0)) 之后又塞两个元素,serde_json::to_string 返回什么?

M2 Max、macOS、rustc 1.99.0,debug 构建。src/bin/quiz_zero.rs:

use serde::Serialize;
use serde::ser::{SerializeSeq, Serializer};

// Announces an empty sequence, then pushes two elements anyway.
struct Liar;

impl Serialize for Liar {
  fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
    let mut seq = s.serialize_seq(Some(0))?;
    seq.serialize_element(&1)?;
    seq.serialize_element(&2)?;
    seq.end()
  }
}

fn main() {
  let s = serde_json::to_string(&Liar);
  println!("to_string        : {s:?}");
  println!("from_str back    : {:?}", serde_json::from_str::<serde_json::Value>(&s.unwrap()).map_err(|e| e.to_string()));
  println!("to_value         : {:?}", serde_json::to_value(&Liar).map(|v| v.to_string()));
  println!("to_string_pretty : {:?}", serde_json::to_string_pretty(&Liar));
}

上面 serialize_seq 的代码已经给出了全部线索。先写出第一行的字符串,再看文末答案(另外三行也在那里)。

collect_seq、collect_map、collect_str

Serializer 上有三个带默认实现的 collect_* 方法。collect_seq 和 collect_map 上面已经读过:拿一个迭代器,按 size_hint 算出 len,开始、逐个、end。它们的意义在于格式可以覆盖:标准库集合都经由它们序列化,一个格式如果对「从迭代器序列化一个序列」有更好的做法,覆盖这一个方法,所有集合类型都受益。

collect_str 接收任何 Display 值,序列化成字符串。默认实现分两种编译配置:

    #[cfg(any(feature = "std", feature = "alloc"))]
    fn collect_str<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
    where
        T: ?Sized + Display,
    {
        self.serialize_str(&value.to_string())
    }

    #[cfg(not(any(feature = "std", feature = "alloc")))]
    fn collect_str<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
    where
        T: ?Sized + Display;

有分配器时先 to_string() 到一个临时 String,再 serialize_str;没有分配器时这个方法没有默认实现,格式必须自己写。标准库里调它的是 fmt::Arguments,所以 format_args!(...) 可以直接序列化;第三方类型(时间、UUID 之类)常在 Serialize 里调 serializer.collect_str(self)。

serde_json 覆盖了它(collect_str):写一个开引号,用一个实现了 fmt::Write 的 Adapter 把 Display 的输出边转义边写进 writer,再写一个闭引号,中间不分配。postcard 1.1.3 也覆盖了它,但二进制格式要先写长度:它的注释说,格式化要跑两遍,第一遍只数字节,第二遍才真正写。bincode 1.3.3 没有覆盖,用的是默认实现。下面数一下三者各分配几次,src/bin/collect_str_alloc.rs:

use std::alloc::{GlobalAlloc, Layout, System};
use std::fmt;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};

use serde::Serialize;

struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counting {
  unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
    ALLOCS.fetch_add(1, Relaxed);
    unsafe { System.alloc(layout) }
  }
  unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
    unsafe { System.dealloc(ptr, layout) }
  }
}

#[global_allocator]
static GLOBAL: Counting = Counting;

// A Display value with no String behind it.
struct Version(u32, u32, u32);

impl fmt::Display for Version {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "{}.{}.{}", self.0, self.1, self.2)
  }
}

impl Serialize for Version {
  fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
    s.collect_str(self)
  }
}

fn count(label: &str, f: impl FnOnce(&mut Vec<u8>)) {
  let mut buf = Vec::with_capacity(1024);
  let before = ALLOCS.load(Relaxed);
  f(&mut buf);
  let n = ALLOCS.load(Relaxed) - before;
  println!("{label:<28} allocs={n} bytes={buf:?}");
}

fn main() {
  let v = Version(1, 0, 229);
  count("serde_json::to_writer", |buf| serde_json::to_writer(buf, &v).unwrap());
  count("postcard::to_slice", |buf| {
    let mut stack = [0u8; 64];
    let used = postcard::to_slice(&v, &mut stack).unwrap();
    buf.extend_from_slice(used);
  });
  count("bincode::serialize_into", |buf| bincode::serialize_into(buf, &v).unwrap());
}

输出缓冲事先预留了 1024 字节,写入本身不会触发分配。跑 3 次,每次输出相同:

serde_json::to_writer        allocs=0 bytes=[34, 49, 46, 48, 46, 50, 50, 57, 34]
postcard::to_slice           allocs=0 bytes=[7, 49, 46, 48, 46, 50, 50, 57]
bincode::serialize_into      allocs=1 bytes=[7, 0, 0, 0, 0, 0, 0, 0, 49, 46, 48, 46, 50, 50, 57]

三者写出的都是 1.0.229 这 7 个字节:serde_json 前后加引号(34),postcard 前面是 varint 长度 7,bincode 前面是 8 字节的长度 7。只有走默认实现的 bincode 分配了一次,就是 to_string() 的那个 String。

📝NOTE
冷知识:collect_str 是为了省掉一次 to_string 加进来的

issue #786 的出发点是:「I've seen a fair number of Serialize implementations that involve calling to_string and I wonder if this extra allocation could be removed somehow.」它举的例子是 chrono 的 serializer.serialize_str(&format!("{:?}", self)),提议让格式自己把格式化结果直接写进输出。

PR #789 加了 Serializer::collect_str,首次发布于 v0.9.11,release notes 写的是「to enable zero-allocation serialization of dynamically constructed strings」。提交说明里描述了当时的默认实现:「The default implementation collects the Display value into a String and then passes that to Serializer::serialize_str when the std or collections features are enabled, otherwise it unconditionally returns an error.」

现在的 1.0.229 里,有分配器时的默认实现还是先收集成 String(上面实验里 bincode 那一次分配);没有 std / alloc 时它不再无条件报错,而是变成了必须实现的方法。省不省掉那次分配,取决于格式有没有像 serde_json 那样覆盖它。

is_human_readable

is_human_readable 是 Serializer 上唯一一个拿 &self 的方法,默认返回 true:

    #[inline]
    fn is_human_readable(&self) -> bool {
        true
    }

它让 Serialize 的实现在写之前问一句:这个格式是给人读的,还是要紧凑?文档说,JSON、YAML 这类文本格式倾向人类可读的形式,Postcard 这类二进制格式倾向紧凑的形式;格式可以把它覆盖成 false,而改变这个返回值应当被视为破坏性改动,因为可读模式下写出的数据不要求能在紧凑模式下读回来。

serde_json 没有覆盖它(用默认的 true);bincode 1.3.3 和 postcard 1.1.3 都覆盖成了 false。

serde_core/src/ser/impls.rs 里按它分支的只有六个网络地址类型:IpAddr、Ipv4Addr、Ipv6Addr、SocketAddr、SocketAddrV4、SocketAddrV6。Duration 和 SystemTime 不分支,在任何格式里都是一个两字段的 struct。IpAddr 的实现:

impl Serialize for net::IpAddr {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        if serializer.is_human_readable() {
            match *self {
                net::IpAddr::V4(ref a) => a.serialize(serializer),
                net::IpAddr::V6(ref a) => a.serialize(serializer),
            }
        } else {
            match *self {
                net::IpAddr::V4(ref a) => {
                    serializer.serialize_newtype_variant("IpAddr", 0, "V4", a)
                }
                net::IpAddr::V6(ref a) => {
                    serializer.serialize_newtype_variant("IpAddr", 1, "V6", a)
                }
            }
        }
    }
}

可读模式下 IpAddr 直接交给里面的 Ipv4Addr / Ipv6Addr,后者写成 "127.0.0.1" 这样的字符串,不带变体名;紧凑模式下是一个 newtype variant,Ipv4Addr 写成 [u8; 4]。Ipv4Addr 的可读分支没有用 to_string(),而是在一个 15 字节的栈上数组里手写十进制数字再 serialize_str;其余几个地址类型用 serialize_display_bounded_length! 宏,同样格式化进一个定长的栈上缓冲(Ipv6Addr 39 字节,SocketAddrV6 58 字节),都不分配。反序列化一侧 de/impls.rs 里同样按 Deserializer::is_human_readable 分支,见 标准库类型的实现。

同样几个值在三种格式里的样子,src/bin/human.rs:

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
use std::time::{Duration, UNIX_EPOCH};

use serde::Serialize;

fn show<T: Serialize>(label: &str, v: &T) {
  let json = serde_json::to_string(v).unwrap();
  let bin = bincode::serialize(v).unwrap();
  let post = postcard::to_allocvec(v).unwrap();
  println!("{label}");
  println!("  json     ({:>2} bytes): {json}", json.len());
  println!("  bincode  ({:>2} bytes): {bin:?}", bin.len());
  println!("  postcard ({:>2} bytes): {post:?}", post.len());
}

fn main() {
  let v4 = Ipv4Addr::new(127, 0, 0, 1);
  show("IpAddr::V4(127.0.0.1)", &IpAddr::V4(v4));
  show("Ipv4Addr 127.0.0.1", &v4);
  let v6 = SocketAddrV6::new(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1), 8080, 0, 0);
  show("SocketAddr::V6([2001:db8::1]:8080)", &SocketAddr::V6(v6));
  show("SocketAddrV6 [2001:db8::1]:8080", &v6);
  show("Duration 1.5s", &Duration::from_millis(1500));
  show("SystemTime epoch+1.5s", &(UNIX_EPOCH + Duration::from_millis(1500)));
}
IpAddr::V4(127.0.0.1)
  json     (11 bytes): "127.0.0.1"
  bincode  ( 8 bytes): [0, 0, 0, 0, 127, 0, 0, 1]
  postcard ( 5 bytes): [0, 127, 0, 0, 1]
Ipv4Addr 127.0.0.1
  json     (11 bytes): "127.0.0.1"
  bincode  ( 4 bytes): [127, 0, 0, 1]
  postcard ( 4 bytes): [127, 0, 0, 1]
SocketAddr::V6([2001:db8::1]:8080)
  json     (20 bytes): "[2001:db8::1]:8080"
  bincode  (22 bytes): [1, 0, 0, 0, 32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 144, 31]
  postcard (19 bytes): [1, 32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 144, 63]
SocketAddrV6 [2001:db8::1]:8080
  json     (20 bytes): "[2001:db8::1]:8080"
  bincode  (18 bytes): [32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 144, 31]
  postcard (18 bytes): [32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 144, 63]
Duration 1.5s
  json     (28 bytes): {"secs":1,"nanos":500000000}
  bincode  (12 bytes): [1, 0, 0, 0, 0, 0, 0, 0, 0, 101, 205, 29]
  postcard ( 6 bytes): [1, 128, 202, 181, 238, 1]
SystemTime epoch+1.5s
  json     (52 bytes): {"secs_since_epoch":1,"nanos_since_epoch":500000000}
  bincode  (12 bytes): [1, 0, 0, 0, 0, 0, 0, 0, 0, 101, 205, 29]
  postcard ( 6 bytes): [1, 128, 202, 181, 238, 1]
  • IpAddr::V4 在 bincode 里是 4 字节的变体下标 0(bincode 把变体下标写成 u32)加 4 个字节;postcard 的变体下标是 varint,1 字节。
  • SocketAddrV6 在两个二进制格式里都是 16 字节地址加端口:bincode 端口是小端 u16(8080 = 0x1F90 → 144, 31),postcard 是 varint(144, 63),恰好也是 2 字节。紧凑形式是 (ip, port) 二元组,所以 flowinfo 和 scope_id 都没写;可读形式用 Display,scope_id 为 0 时也不出现。
  • Duration 和 SystemTime 在三种格式里结构相同,JSON 里只是字段名不同。
📝NOTE
冷知识:is_human_readable 是为了让 SocketAddrV6 在 bincode 里从最多 55 字节变成 18 字节

issue #790 提出:同一个类型应当能在可读格式和二进制格式里用不同的表示,「The idea is that chrono::DateTime, std::net::IpAddr etc can serialize themselves as a string in JSON and YAML but compact bytes in Bincode. Currently a SocketAddrV6 takes up to 55 bytes to represent in Bincode but should only be 18 bytes.」当时的初步设想是在 Serializer 上加一个方法,再配两个新 trait(SerializeReadable、SerializeCompact)。

PR #1044 没有加 trait,只加了一个返回 bool 的方法:「This implements the KISS suggested in #790.」理由是:「Personally I find the original suggestion of adding two traits perhaps slightly cleaner in theory but I think it ends up more complicated in the end since the added traits also need to be duplicated to to the Seed traits.」同一个 PR 把网络地址类型改成按它分支。首次发布于 v1.0.16,release notes:「which allow types like IpAddr to serialize in a human-readable representation in formats like JSON but in a compact binary representation in formats like Bincode」。

现在:上面实验里 SocketAddrV6 在 bincode 1.3.3 下正好是 18 字节。

Impossible:给不支持的复合类型占位

不是每个 Serializer 都支持全部七种复合类型。serde_json 序列化 map 的键时用的 MapKeySerializer 只接受字符串、数字、bool 这类能写成 JSON 字符串的标量,遇到 seq、map 就报 key must be a string(axum 卷的 状态码覆盖 500 碰到的就是这句)。但 trait 要求七个关联类型都得填一个实现了对应 trait 的类型,哪怕 serialize_seq 一定返回 Err。Impossible 就是填这个空的:

pub struct Impossible<Ok, Error> {
    void: Void,
    ok: PhantomData<Ok>,
    error: PhantomData<Error>,
}

enum Void {}

impl<Ok, Error> SerializeSeq for Impossible<Ok, Error>
where
    Error: ser::Error,
{
    type Ok = Ok;
    type Error = Error;

    fn serialize_element<T>(&mut self, value: &T) -> Result<(), Error>
    where
        T: ?Sized + Serialize,
    {
        let _ = value;
        match self.void {}
    }

    fn end(self) -> Result<Ok, Error> {
        match self.void {}
    }
}

Void 是一个没有变体的 enum,没有值;含有它的 Impossible 也就构造不出来。match self.void {} 没有分支,类型检查时它可以是任何类型,所以每个方法体都只有这一行,不需要 unreachable!()。七个 trait 都这样实现了一遍,Ok 和 Error 是类型参数,好让 Ok = Self::Ok, Error = Self::Error 的约束对得上:serde_json 的 MapKeySerializer 写 type SerializeSeq = Impossible<(), Error>;,serde_json::value 里的键序列化器(Ok = String)写 Impossible<String, Error>。

用了 Impossible 的 Serializer,对应的开始方法只能返回 Err,因为它没有办法造出一个 Impossible 来返回 Ok。上面的 Probe 也是这样,(u8, u8) 那一行的 Err(serialize_tuple) 就来自这里。

📝NOTE
没有蠢问题

问: 不支持的复合类型反正都填 Impossible,为什么不让它当七个关联类型的默认值,只支持一部分复合类型的 Serializer 就不用逐个写了?

答: 关联类型的默认值在稳定版 Rust 里还不能用。在 rustc 1.99.0 上编译一个最小的例子:

trait Format {
  type Seq = ();
}
fn main() {}
error[E0658]: associated type defaults are unstable
 --> atd.rs:2:3
  |
2 |   type Seq = ();
  |   ^^^^^^^^^^^^^^
  |
  = note: see issue #29661 <https://github.com/rust-lang/rust/issues/29661> for more information

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0658`.

提出 Impossible 的 issue #694 末尾就写了:「And these can be the defaults once we have associated type defaults.」Impossible 在 v0.9.4 加入,这个特性至今没有稳定,所以每个 Serializer 还是要把七个关联类型写全,像 fmt.rs 那样连写七行 Impossible<(), fmt::Error>。

fmt.rs:把 fmt::Formatter 当成 Serializer

ser/fmt.rs 给 &mut fmt::Formatter 实现了 Serializer(impl Serializer for &mut fmt::Formatter)。用途写在它的文档例子里:一个 derive 了 Serialize、带 rename_all 的 enum,可以直接用 serde 的名字实现 Display:

impl Display for MessageType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.serialize(f)
    }
}

实现很短:

  • Ok = (),Error = fmt::Error;impl Error for fmt::Error 的 custom 丢掉消息,只返回 fmt::Error(它本来就不带内容)。
  • 七个复合类型全是 Impossible<(), fmt::Error>,开始方法都返回 Err(fmt::Error)。
  • bool、全部整数(含 i128 / u128)、浮点、char、str、unit_struct(写结构体名)都是 Display::fmt(&v, self)。
  • serialize_unit_variant 写变体名(已经过 rename / rename_all),serialize_newtype_struct 转去序列化里面的值。
  • bytes、none、some、unit、newtype_variant 返回 Err(fmt::Error)。
  • collect_str 直接 Display::fmt(value, self),不经过 String。

src/bin/fmt_ser.rs 把几种值经由 &mut fmt::Formatter 写进一个 String:

use std::fmt::{self, Display, Write};

use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
enum MessageType {
  StartRequest,
  EndRequest,
}

impl Display for MessageType {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    self.serialize(f)
  }
}

#[derive(Serialize)]
struct UserId(u64);

#[derive(Serialize)]
struct Point {
  x: i32,
  y: i32,
}

// Routes any Serialize value through `&mut fmt::Formatter`.
struct ViaFormatter<T>(T);

impl<T: Serialize> Display for ViaFormatter<T> {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    self.0.serialize(f)
  }
}

fn show<T: Serialize>(label: &str, v: T) {
  let mut out = String::new();
  let r = write!(out, "{}", ViaFormatter(v));
  println!("{label:<22} {r:?} {out:?}");
}

fn main() {
  println!("{} {}", MessageType::StartRequest, MessageType::EndRequest);
  show("UserId(42)", UserId(42));
  show("u128::MAX", u128::MAX);
  show("'x'", 'x');
  show("Some(1)", Some(1));
  show("()", ());
  show("Point", Point { x: 1, y: 2 });
  show("format_args!", format_args!("{}-{}", 1, 2));
}
start-request end-request
UserId(42)             Ok(()) "42"
u128::MAX              Ok(()) "340282366920938463463374607431768211455"
'x'                    Ok(()) "x"
Some(1)                Err(Error) ""
()                     Err(Error) ""
Point                  Err(Error) ""
format_args!           Ok(()) "1-2"

unit variant 写出的是 rename_all 之后的名字;newtype struct 透明;Option、() 和普通 struct 都得到 fmt::Error;format_args! 经由 fmt::Arguments 的 collect_str 原样写出。它只适合「只有 unit variant 的 enum」和标量 newtype 这类值,别的都会让 Display 失败。

📝NOTE
冷知识:这个 impl 只能放在 serde 里

PR #1705 的说明是:「Allow using &mut fmt::Formatter as a serializer for primitive types. One use-case I have in mind in particular is implementing Display for enums that also implement Serialize. If you believe this impl should exist, due to coherence, this crate is the only place it could exist.」Serializer 和 fmt::Formatter 对第三方 crate 来说都是外部的,孤儿规则不允许别处写这个 impl。

讨论里有人说「I frequently end up with a dev dependency on serde_json, just to do a string comparison with a serialised value.」,也有人指出不一定要给 Formatter 本身实现,用一个包装类型放在别的 crate 里也行。最终合并的是直接给 &mut fmt::Formatter 实现的版本,首次发布于 v1.0.108,release notes:「Provide a Serializer impl that can write primitives and unit variants to a &mut fmt::Formatter」,附的例子就是现在 fmt.rs 里那段 MessageType。PR 原来的提交把代码写在 ser/impls.rs 里;合并时又跟了几个提交一起进入主分支,标题依次是「Support no-default-features mode in Formatter's impl」「Move the Formatter Serializer to a module」「Remove serialize_unit from Formatter's impl」「Directly display to the Formatter」「Allow serializing newtype struct to Formatter」「Add 128-bit integer support for Formatter」。其中「Remove serialize_unit」把 serialize_unit 从什么也不写、返回 Ok(()) 改成了返回 Err(fmt::Error),上面实验里 () 那一行的 Err 就是这么来的。

动脑筋答案

to_string        : Ok("[],1,2]")
from_str back    : Err("trailing characters at line 1 column 3")
to_value         : Ok("[1,2]")

thread 'main' (2475833) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.154/src/ser.rs:1989:9:
attempt to subtract with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

答案是 [],1,2],而且 to_string 返回的是 Ok。逐个调用看:

  • serialize_seq(Some(0)):写 [,因为 len == Some(0) 立刻写 ],状态设为 Empty。
  • 第一个 serialize_element:begin_array_value 的 first 参数是 *state == State::First,Empty 不等于 First,于是先写 ,,再写 1,状态改成 Rest。
  • 第二个:再写 ,2。
  • end:状态已经被改成 Rest,不是 Empty,于是又写一个 ]。

这个字符串不是合法的 JSON:读回去时 [] 已经是一个完整的值,第 3 列的 , 是多余的尾部字符。to_value 不受影响,因为 value::Serializer 只把 len 当容量提示,元素照样 push 进 Vec。

to_string_pretty 在 debug 构建下 panic:PrettyFormatter 的 end_array 每次把 current_indent 减 1,这个序列一共 begin_array 一次、end_array 两次,第二次从 0 减 1 溢出。release 构建下溢出不检查,current_indent 变成 usize::MAX,接下来的 indent 循环要写这么多次缩进:同一个程序 --release 跑,前三行输出相同,最后一行一直不返回,进程 RSS 在第 10、20、30 秒分别是 1779344、12113504、22385472 KB,我在 30 秒时终止了它。len 和实际元素数不一致,格式不保证任何结果。

小结

  • Serialize 只有一个方法,S: Serializer 是方法上的泛型,返回什么由格式的 Ok 决定(serde_json 写出器是 (),to_value 是 Value)。
  • Serializer 的方法都按值拿 self,一个值只序列化一次;写进 writer 的格式给 &mut Serializer 实现,递归时重借用。
  • 30 个 serialize_* 对应数据模型 29 种类型(Option 两个),i128 / u128 默认报 i128 is not supported。
  • 复合类型拆成「开始 → 逐个元素 → end」的状态对象:签名决定了调用顺序,状态对象装格式自己的状态(serde_json 的 Compound + State),泛型方法保证单态化。
  • len: Option<usize> 是声明,不核对:集合类型经 collect_seq / collect_map 传精确值,filter 和 flatten 传 None;bincode、postcard 遇到 None 报错,serde_json 只用它特判 Some(0),给错了会写出非法 JSON。
  • collect_str 默认先 to_string(),serde_json 和 postcard 覆盖成不分配;is_human_readable 默认 true,标准库里只有六个网络地址类型按它分支。
  • Impossible 用一个空 enum 填上不支持的复合类型;fmt.rs 用它给 &mut fmt::Formatter 实现了一个只认标量和 unit variant 的 Serializer。

💬 关联讨论

发起讨论
加载中...