Rust in Everything
基于 serde_json 1.0.154

反序列化一侧

主线位置:反序列化主线里「格式」那一半。一次 from_str 里,类型给出一个 deserialize_* 提示和一个 Visitor,本页讲 serde_json 拿到提示之后做什么:从哪里取字节、看到哪个首字节调哪个 visit_*、数字和字符串怎么解析、嵌套多深就停、读完之后检查什么。Visitor 一侧的机制在 Deserialize、Deserializer 与 Visitor,SeqAccess / MapAccess / EnumAccess 在 SeqAccess、MapAccess 与 EnumAccess,借用能不能成立在 生命周期 'de 与借用,错误怎么带上行列号在 错误。

flowchart TB
  entry["from_str / from_slice / from_reader"] --> rd["StrRead / SliceRead / IoRead"]
  rd --> ft[from_trait]
  ft --> de["T::deserialize(&mut de)"]
  de --> hint["deserialize_* 提示"]
  hint --> ws[parse_whitespace]
  ws -->|"n t f"| ident[parse_ident]
  ws -->|"- 0-9"| num[parse_integer]
  ws -->|引号| strnode[parse_str]
  ws -->|"[ {"| depth[check_recursion]
  depth --> acc["visit_seq / visit_map"]
  acc -->|每个元素| hint
  ident --> visit["visit_*"]
  num --> visit
  strnode --> visit
  ft --> endck["end:只许剩空白"]

代码在 src/de.rs(2714 行)和 src/read.rs(1140 行)。本页的实验都在 M2 Max、macOS、rustc 1.99.0 上跑,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154";用到 feature 的实验另起一个 crate,单独说明。

Deserializer:一个 Read、一块 scratch、一个深度

Deserializer 的字段:

pub struct Deserializer<R> {
    read: R,
    scratch: Vec<u8>,
    remaining_depth: u8,
    #[cfg(feature = "float_roundtrip")]
    single_precision: bool,
    #[cfg(feature = "unbounded_depth")]
    disable_recursion_limit: bool,
}
  • read:输入。R 是 serde_json 自己的 Read trait,不是 std::io::Read,下一节讲。
  • scratch:一块复用的缓冲。字符串里有转义时解码到这里(Reference::Copied),ignore_value 拿它当括号栈,开了 float_roundtrip 的长数字也先拷进这里。它属于 Deserializer,不属于某次调用,所以每次用之前都要 scratch.clear(),容量留着给下一次。
  • remaining_depth:还能再进几层 [ / {。Deserializer::new 把它设成 128,后面「递归深度」一节讲实际能进几层。

三个公开入口只差在把输入包成哪种 Read:from_str 用 StrRead,from_slice 用 SliceRead,from_reader 用 IoRead;然后都交给 from_trait,它建一个 Deserializer,调 T::deserialize(&mut de),最后调 de.end()(代码在 一次往返 里引过)。实现 serde::Deserializer 的是 &mut Deserializer<R>,所以同一个 Deserializer 可以被嵌套的 deserialize 反复借出去。

Read:密封的输入抽象

Read 是公开的 trait,但用户实现不了它:

/// Trait used by the deserializer for iterating over input. This is manually
/// "specialized" for iterating over `&[u8]`. Once feature(specialization) is
/// stable we can use actual specialization.
///
/// This trait is sealed and cannot be implemented for types outside of
/// `serde_json`.
pub trait Read<'de>: private::Sealed {
    #[doc(hidden)]
    fn next(&mut self) -> Result<Option<u8>>;
    #[doc(hidden)]
    fn peek(&mut self) -> Result<Option<u8>>;

    /// Only valid after a call to peek(). Discards the peeked byte.
    #[doc(hidden)]
    fn discard(&mut self);
    // ...
    #[doc(hidden)]
    fn parse_str<'s>(&'s mut self, scratch: &'s mut Vec<u8>) -> Result<Reference<'de, 's, str>>;
    // ...
}

超 trait private::Sealed 在一个私有模块里,外面的类型拿不到它,也就实现不了 Read;方法全部 #[doc(hidden)]。这样 serde_json 可以随时改这些方法的签名而不算破坏兼容。trait 和三个实现类型仍然是公开的,因为 Deserializer::new(read: R)、StreamDeserializer::new(read: R) 的签名里要出现它们。

文档里的「manually specialized」是这个 trait 存在的原因:读字节的方法(next / peek / discard)每种输入都能写,但 parse_str 不行。对一段内存,没有转义的字符串可以直接切一刀返回;对一个 io::Read,字节读过就没了,只能拷。如果只有 next,&[u8] 就享受不到零拷贝。于是把整段「解析一个字符串」也做成 trait 方法,让每种输入各写各的。

三个实现(加上一个 &mut R 的转发实现):

实现字段peek位置怎么算parse_str
SliceRead<'a>slice: &'a [u8]、index只读 slice[index]出错时才用 memchr 从头数换行(position_of_index)无转义返回 Borrowed,有转义返回 Copied,校验 UTF-8
StrRead<'a>delegate: SliceRead<'a>同上同上同上,但跳过 UTF-8 校验
IoRead<R>iter: LineColIterator<io::Bytes<R>>、ch: Option<u8>从迭代器取一个字节存进 ch每读一个字节就更新行列一律 Copied

SliceRead 的注释说了它快在哪:「peek() can be read-only and we can compute line/col position only if an error happens」。IoRead 做不到:peek 必须真的读一个字节出来,存进 ch;字节读过就不能回头再数换行,所以 LineColIterator 每个字节都要更新一次计数。

StrRead 的 parse_str 和 SliceRead 走同一个 parse_str_bytes,只是把最后一步的 UTF-8 校验换掉:

    fn parse_str<'s>(&'s mut self, scratch: &'s mut Vec<u8>) -> Result<Reference<'a, 's, str>> {
        self.delegate.parse_str_bytes(scratch, true, |_, bytes| {
            // The deserialization input came in as &str with a UTF-8 guarantee,
            // and the \u-escapes are checked along the way, so don't need to
            // check here.
            Ok(unsafe { str::from_utf8_unchecked(bytes) })
        })
    }

from_str 比 from_slice 少的就是这一次 str::from_utf8:&str 本身保证了 UTF-8,\u 转义在解码时单独检查过。from_slice 和 from_reader 用的是 as_str,校验失败报 invalid unicode code point,本页最后一节讲它的报错位置。

parse_str_bytes 怎么在 Borrowed 和 Copied 之间选、IoRead 为什么只能 Copied、这对 &str 字段意味着什么,都在 生命周期 'de 与借用,这里不再重复。还剩它循环开头那一步 skip_to_escape:在字符串里找第一个需要停下来的字节。

skip_to_escape:一次看 8 个字节

字符串里需要停下的字节只有三种:结尾的 "、转义的 \、JSON 不允许出现在字符串里的控制字符(0x00..=0x1F)。skip_to_escape 不逐个字节比较,而是把 8 个字节装进一个 u64 一起判断:

        #[cfg(fast_arithmetic = "64")]
        type Chunk = u64;
        #[cfg(fast_arithmetic = "32")]
        type Chunk = u32;

        const STEP: usize = mem::size_of::<Chunk>();
        const ONE_BYTES: Chunk = Chunk::MAX / 255; // 0x0101...01

        for chunk in rest.chunks_exact(STEP) {
            let chars = Chunk::from_le_bytes(chunk.try_into().unwrap());
            let contains_ctrl = chars.wrapping_sub(ONE_BYTES * 0x20) & !chars;
            let chars_quote = chars ^ (ONE_BYTES * Chunk::from(b'"'));
            let contains_quote = chars_quote.wrapping_sub(ONE_BYTES) & !chars_quote;
            let chars_backslash = chars ^ (ONE_BYTES * Chunk::from(b'\\'));
            let contains_backslash = chars_backslash.wrapping_sub(ONE_BYTES) & !chars_backslash;
            let masked = (contains_ctrl | contains_quote | contains_backslash) & (ONE_BYTES << 7);
            if masked != 0 {
                // SAFETY: chunk is in-bounds for slice
                self.index = unsafe { chunk.as_ptr().offset_from(self.slice.as_ptr()) } as usize
                    + masked.trailing_zeros() as usize / 8;
                return;
            }
        }

        self.index += rest.len() / STEP * STEP;
        self.skip_to_escape_slow();

这是注释里说的 Mycroft 算法的变体。只看一个字节 b:(b - k) & !b 的最高位为 1,当且仅当 b < k(k 不超过 0x80)。b < k 时减法回绕到高位为 1 的值,而 !b 的高位也是 1;k ≤ b < 0x80 时差的高位是 0;b ≥ 0x80 时 !b 的高位是 0。k = 0x20 就是「是不是控制字符」;先异或 " 再取 k = 1,就是「异或之后是不是 0」,即「是不是 "」,反斜杠同理。乘上 ONE_BYTES 让 8 个字节同时做这件事,三个结果或起来,只留每个字节的最高位(& (ONE_BYTES << 7)),不为 0 就说明这 8 个字节里有要停的;小端序下 trailing_zeros() / 8 正好是第一个命中字节的下标。借位会向高位字节传播,可能让命中字节后面的字节出现假阳性,但第一个命中永远是准的,而这里只要第一个。剩下不满 8 个字节的尾巴交给逐字节的 skip_to_escape_slow。

fast_arithmetic 是 build.rs 按目标架构设的:aarch64、x86_64 等 64 位平台用 u64,其余用 u32。

不校验控制字符的那条路(parse_str_raw,给 deserialize_bytes 用)只要找 " 和 \,直接用 memchr::memchr2。

📝NOTE
冷知识:这段 SWAR 代码 1.0.123 才有

之前 SliceRead::parse_str_bytes 逐个字节查一张 ESCAPE 表。#1161(首次发布于 1.0.123)把它换成了上面一次处理一个字(SIMD within a register)的写法。PR 说明了为什么不直接用 memchr:要找的是 ch == b'"' || ch == b'\\' || ch < 0x20,「memchr3 supports searches of kind ch == a || ch == b || ch == c, which is almost what we need」,差的正是 < 0x20;而先用 memchr2 找引号和反斜杠、再单独找控制字符也不更快。PR 最初给的数字有升有降,长字符串多的 twitter.json 结构体解析快了 23%,短字符串多的 citm 慢了 2.6%(「the new implementation is slower when most of the strings are very short」)。1.0.123 的发布说明写的是:「Optimize string parsing by applying SIMD-within-a-register: 30.3% improvement on twitter.json from 613 MB/s to 799 MB/s」。

parse_whitespace 与 deserialize_any:看首字节

除了直接把自己交给 visit_newtype_struct 的 deserialize_newtype_struct,每个 deserialize_* 的第一步都是 parse_whitespace:

    /// Returns the first non-whitespace byte without consuming it, or `None` if
    /// EOF is encountered.
    fn parse_whitespace(&mut self) -> Result<Option<u8>> {
        loop {
            match tri!(self.peek()) {
                Some(b' ' | b'\n' | b'\t' | b'\r') => {
                    self.eat_char();
                }
                other => {
                    return Ok(other);
                }
            }
        }
    }

它用 peek 看、用 eat_char(即 discard)吃,返回的那个非空白字节没有被消费。JSON 的每种值都能由第一个字节认出来:n 是 null,t / f 是布尔,- 和数字是数," 是字符串,[ 和 { 是数组和对象。所以 serde_json 不需要回溯,看一个字节就能决定往哪走。

deserialize_any 就是把这张表直接写出来:

        let value = match peek {
            b'n' => {
                self.eat_char();
                tri!(self.parse_ident(b"ull"));
                visitor.visit_unit()
            }
            b't' => {
                self.eat_char();
                tri!(self.parse_ident(b"rue"));
                visitor.visit_bool(true)
            }
            // ... b'f' 同理
            b'-' => {
                self.eat_char();
                tri!(self.parse_any_number(false)).visit(visitor)
            }
            b'0'..=b'9' => tri!(self.parse_any_number(true)).visit(visitor),
            b'"' => {
                self.eat_char();
                self.scratch.clear();
                match tri!(self.read.parse_str(&mut self.scratch)) {
                    Reference::Borrowed(s) => visitor.visit_borrowed_str(s),
                    Reference::Copied(s) => visitor.visit_str(s),
                }
            }
            b'[' => {
                check_recursion! {
                    self.eat_char();
                    let ret = visitor.visit_seq(SeqAccess::new(self));
                }

                match (ret, self.end_seq()) {
                    (Ok(ret), Ok(())) => Ok(ret),
                    (Err(err), _) | (_, Err(err)) => Err(err),
                }
            }
            // ... b'{' 同理:visit_map(MapAccess::new(self)),然后 end_map()
            _ => Err(self.peek_error(ErrorCode::ExpectedSomeValue)),
        };

数组和对象的右括号不归 Visitor 管:visit_seq 返回之后,end_seq 检查下一个非空白字节是不是 ]。Visitor 提前不读了(比如只要前两个元素),剩下的元素就会让 end_seq 报错,后面「end」一节有例子。

一个只报告「收到了哪个 visit_*」的 Visitor,src/bin/dispatch.rs:

use std::fmt;

use serde::de::{Deserializer as _, IgnoredAny, MapAccess, SeqAccess, Visitor};

// Reports which visit_* deserialize_any picked, and nothing else.
struct Which;

impl<'de> Visitor<'de> for Which {
  type Value = String;

  fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
    f.write_str("anything")
  }
  fn visit_unit<E>(self) -> Result<String, E> {
    Ok("visit_unit".into())
  }
  fn visit_bool<E>(self, v: bool) -> Result<String, E> {
    Ok(format!("visit_bool({v})"))
  }
  fn visit_u64<E>(self, v: u64) -> Result<String, E> {
    Ok(format!("visit_u64({v})"))
  }
  // ... visit_i64、visit_f64、visit_borrowed_str、visit_str 同样各打印一行
  fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<String, A::Error> {
    let mut n = 0;
    while seq.next_element::<IgnoredAny>()?.is_some() {
      n += 1;
    }
    Ok(format!("visit_seq({n} elements)"))
  }
  // ... visit_map 同理,用 next_entry::<IgnoredAny, IgnoredAny>() 计数
}

fn main() {
  let inputs = [
    "null", "true", "  12", "-12", "-0", "1.5", "1e3",
    "18446744073709551615", "18446744073709551616",
    r#""plain""#, r#""a\nb""#, "[1, [2], {}]", r#"{"a": 1}"#, "nul", "x",
  ];
  for input in inputs {
    let mut de = serde_json::Deserializer::from_str(input);
    let out = match de.deserialize_any(Which) {
      Ok(s) => s,
      Err(e) => format!("Err({e})"),
    };
    println!("{input:<24} {out}");
  }
}
null                     visit_unit
true                     visit_bool(true)
  12                     visit_u64(12)
-12                      visit_i64(-12)
-0                       visit_f64(-0.0)
1.5                      visit_f64(1.5)
1e3                      visit_f64(1000.0)
18446744073709551615     visit_u64(18446744073709551615)
18446744073709551616     visit_f64(1.8446744073709552e19)
"plain"                  visit_borrowed_str("plain")
"a\nb"                   visit_str("a\nb")
[1, [2], {}]             visit_seq(3 elements)
{"a": 1}                 visit_map(1 entries)
nul                      Err(EOF while parsing a value at line 1 column 3)
x                        Err(expected value at line 1 column 1)
  • 非负整数一律 visit_u64,负整数 visit_i64,带 . 或 e 的一律 visit_f64:JSON 文本里没有类型宽度,serde_json 只能给出这三种(ParserNumber)。u8 之类的 Visitor 怎么接 u64 再查范围,在 标准库类型的实现。
  • -0 走的是 visit_f64(-0.0),不是 visit_i64(0);u64::MAX + 1 不报错,变成了浮点数。原因在下一节的数字解析里。
  • "a\nb" 有转义,只能 visit_str(拷进了 scratch);"plain" 是 visit_borrowed_str。
  • 1e3 是 1000.0,不是整数 1000:只要出现了指数就是浮点。

deserialize_any 是给不知道自己要什么的类型(Value、untagged、Content)用的。其余 30 个 deserialize_* 都由 serde_json 自己实现,同样先 parse_whitespace,但只接受提示对应的那几个首字节,别的交给 peek_invalid_type:标量值它会完整读出来(数组和对象只看括号),只为了在报错里说清楚「实际是什么」(invalid type: string "7", expected u32)。为什么不直接都转给 deserialize_any,在 Deserialize、Deserializer 与 Visitor 那一页。

数字:先按 u64 累加

整数提示(deserialize_i8 到 deserialize_u64)和 f32 / f64 都由 deserialize_number! 宏生成,转到同一个 deserialize_number:看到 - 或数字就调 parse_integer,结果照样是 ParserNumber 三选一,宽度的检查留给 Visitor。

parse_integer 的快路径是一个 u64 累加循环:

            c @ b'1'..=b'9' => {
                let mut significand = (c - b'0') as u64;

                loop {
                    match tri!(self.peek_or_null()) {
                        c @ b'0'..=b'9' => {
                            let digit = (c - b'0') as u64;

                            // We need to be careful with overflow. If we can,
                            // try to keep the number as a `u64` until we grow
                            // too large. At that point, switch to parsing the
                            // value as a `f64`.
                            if overflow!(significand * 10 + digit, u64::MAX) {
                                return Ok(ParserNumber::F64(tri!(
                                    self.parse_long_integer(positive, significand),
                                )));
                            }

                            self.eat_char();
                            significand = significand * 10 + digit;
                        }
                        _ => {
                            return self.parse_number(positive, significand);
                        }
                    }
                }
            }

overflow! 展开成 a >= c / 10 && (a > c / 10 || b > c % 10),在乘之前判断会不会溢出。溢出了就转 parse_long_integer:默认构建下它把剩下的数字只数个数、记进指数,交给 f64_from_parts,所以 18446744073709551616 得到的是 1.8446744073709552e19 而不是错误。前导 0 后面不许再跟数字(01 报 invalid number)。

累加完,parse_number 看下一个字节:. 进 parse_decimal,e / E 进 parse_exponent,否则就是整数:

            _ => {
                if positive {
                    ParserNumber::U64(significand)
                } else {
                    let neg = (significand as i64).wrapping_neg();

                    // Convert into a float if we underflow, or on `-0`.
                    if neg >= 0 {
                        ParserNumber::F64(-(significand as f64))
                    } else {
                        ParserNumber::I64(neg)
                    }
                }
            }

负数先按无符号读,最后取负。wrapping_neg 之后还是非负,只有两种情况:绝对值超过了 i64 的范围(回绕),或者就是 -0。两种都转成浮点,于是 -0 保住了符号,-9223372036854775809 也不报错。

src/bin/numbers.rs 把几个边界值读进 Value:

use serde_json::Value;

fn main() {
  for input in [
    "18446744073709551615", "18446744073709551616", "-9223372036854775808",
    "-9223372036854775809", "-0", "01", "1.", "1e400", "1e-400", "-1e-400",
  ] {
    match serde_json::from_str::<Value>(input) {
      Ok(v) => println!("{input:<22} Ok({v:?})"),
      Err(e) => println!("{input:<22} Err({e})"),
    }
  }
}
18446744073709551615   Ok(Number(18446744073709551615))
18446744073709551616   Ok(Number(1.8446744073709552e+19))
-9223372036854775808   Ok(Number(-9223372036854775808))
-9223372036854775809   Ok(Number(-9.223372036854776e+18))
-0                     Ok(Number(-0.0))
01                     Err(invalid number at line 1 column 2)
1.                     Err(EOF while parsing a value at line 1 column 2)
1e400                  Err(number out of range at line 1 column 5)
1e-400                 Ok(Number(0.0))
-1e-400                Ok(Number(-0.0))

整数这一侧从不报「超出范围」,超出就变浮点;浮点这一侧上溢报 number out of range(serde_json 不产生无穷大),下溢静默变成带符号的 0。i64::MIN 恰好放得下:significand 是 2^63,wrapping_neg 得到负数。

带小数点或指数时,parse_decimal 继续把小数部分累加进同一个 significand、记下十进制指数,最后由 f64_from_parts(positive, significand, exponent) 算出浮点数。这个函数有两个版本:

构建f64_from_parts结果
默认significand as f64 再乘或除 POW10 表里的 `10^exponent
float_roundtriplexical::parse_concise_float,长数字走 parse_truncated_float正确舍入

significand as f64 一次舍入,乘或除 10^k 又一次舍入,两次加起来就可能偏一个 ULP。Cargo.toml 对 float_roundtrip 的说明是「Use sufficient precision when parsing fixed precision floats from JSON to ensure that they maintain accuracy when round-tripped through JSON. This comes at an approximately 2x performance cost for parsing floats compared to the default best-effort precision.」默认是 best-effort。

偏得多不多,跑一下就知道。src/bin/floats.rs 生成一百万个 f64,用 to_string 写成最短表示,再用 from_str 读回来比较位模式:

// Parses the shortest representation (to_string) of many f64s back with from_str.
fn check(label: &str, mut make: impl FnMut(u64) -> f64) {
  let mut x: u64 = 0x2545F4914F6CDD1D;
  let (mut total, mut differ) = (0, 0);
  let mut example = None;
  for _ in 0..1_000_000 {
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    let f = make(x);
    if !f.is_finite() {
      continue;
    }
    let text = serde_json::to_string(&f).unwrap();
    let parsed: f64 = serde_json::from_str(&text).unwrap();
    assert_eq!(text.parse::<f64>().unwrap(), f);
    total += 1;
    if parsed != f {
      differ += 1;
      example.get_or_insert((text, f.to_bits(), parsed.to_bits()));
    }
  }
  println!("{label}: {total} round trips, {differ} not identical");
  if let Some((text, a, b)) = example {
    println!("  e.g. {text}: bits {a:#x} -> {b:#x}");
  }
}

fn main() {
  check("random bits ", f64::from_bits);
  check("[0, 1)      ", |x| (x >> 11) as f64 / (1u64 << 53) as f64);
  check("cents       ", |x| (x % 1_000_000) as f64 / 100.0);
}

assert_eq! 那一行确认文本本身没问题:标准库的 str::parse 每个都能读回原值。默认 feature,--release:

random bits : 999509 round trips, 296281 not identical
  e.g. 6.178787134922198e+305: bits 0x7f6c280beaa8e3e7 -> 0x7f6c280beaa8e3e8
[0, 1)      : 1000000 round trips, 105294 not identical
  e.g. 0.38595771669529844: bits 0x3fd8b387feb616c6 -> 0x3fd8b387feb616c5
cents       : 1000000 round trips, 0 not identical

同一个程序放进开了 float_roundtrip 的 crate(serde_json = { version = "=1.0.154", features = [..., "float_roundtrip"] }):

random bits : 999509 round trips, 0 not identical
[0, 1)      : 1000000 round trips, 0 not identical
cents       : 1000000 round trips, 0 not identical
  • 均匀分布在 [0, 1) 的数,大约十个里有一个读回来差了最后一位(例子里的位模式只差 1)。
  • 「分」这种两位小数一个都没错:有效数字不超过 2^53、指数不超过 22 时,significand as f64 和 10^k 都是精确的,只剩一次舍入,结果就是正确舍入的值。
  • 需要 f64 → JSON → f64 逐位不变(缓存、签名、比较哈希),就开 float_roundtrip。

i128 / u128 是另一条路(do_deserialize_i128):把数字字符扫进一个 String,再用标准库的 parse,不经过 ParserNumber。所以 deserialize_any 永远给不出 128 位整数,Value、Content 也就装不下超过 u64 的整数(arbitrary_precision 是另一回事,见 Value)。

每个提示各自的首字节

除了 deserialize_any,deserialize_* 只认自己的首字节,但有几个提示接受不止一种写法。src/bin/shapes.rs:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct Post {
  title: String,
  views: u32,
  draft: Option<bool>,
}

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
enum Status {
  Draft,
  Published { at: u64 },
}

fn show<T: std::fmt::Debug + for<'de> Deserialize<'de>>(input: &str) {
  match serde_json::from_str::<T>(input) {
    Ok(v) => println!("{input:<34} Ok({v:?})"),
    Err(e) => println!("{input:<34} Err({e})"),
  }
}

fn main() {
  show::<Post>(r#"{"title":"a","views":1,"draft":null}"#);
  show::<Post>(r#"{"title":"a","views":1}"#);
  show::<Post>(r#"["a",1,true]"#);
  show::<Post>(r#"["a",1]"#);
  show::<Post>(r#"["a",1,true,4]"#);
  show::<Post>(r#""a""#);
  show::<Option<u32>>("null");
  show::<Option<u32>>("nil");
  show::<Option<u32>>("\"7\"");
  show::<Status>(r#""Draft""#);
  show::<Status>(r#"{"Published":{"at":9}}"#);
  show::<Status>(r#"{"Published":{"at":9},"x":1}"#);
  show::<Status>("[\"Draft\"]");
}
{"title":"a","views":1,"draft":null} Ok(Post { title: "a", views: 1, draft: None })
{"title":"a","views":1}            Ok(Post { title: "a", views: 1, draft: None })
["a",1,true]                       Ok(Post { title: "a", views: 1, draft: Some(true) })
["a",1]                            Err(invalid length 2, expected struct Post with 3 elements at line 1 column 7)
["a",1,true,4]                     Err(trailing characters at line 1 column 13)
"a"                                Err(invalid type: string "a", expected struct Post at line 1 column 3)
null                               Ok(None)
nil                                Err(expected ident at line 1 column 2)
"7"                                Err(invalid type: string "7", expected u32 at line 1 column 3)
"Draft"                            Ok(Draft)
{"Published":{"at":9}}             Ok(Published { at: 9 })
{"Published":{"at":9},"x":1}       Err(expected value at line 1 column 21)
["Draft"]                          Err(expected value at line 1 column 1)

struct 收对象,也收数组。 deserialize_struct 对 { 调 visit_map,对 [ 调 visit_seq,两个分支的代码一模一样,只差 visit_* 和收尾的 end_map / end_seq。derive 生成的 visit_seq 按声明顺序取字段,少了报 invalid length(生成的 Deserialize)。多了的那个 4 不是 Visitor 发现的:visit_seq 取够三个就返回,end_seq 看到的下一个字节是 , 而不是 ],于是报 trailing characters,位置在 4 上。serde_json 序列化 struct 总是写对象,数组这条路是给反序列化宽容用的。

Option 只认 null。 deserialize_option 看到 n 就吃掉 null 调 visit_none,否则 visit_some(self),把整个 Deserializer 交给 T 接着读,"7" 的错误来自 u32 而不是 Option。字段缺失时为什么也是 None、为什么 JSON 表示不出 Some(None),在 标准库类型的实现。nil 的错误发生在 parse_ident(b"ull") 读到 i 时:n 开头就认定是 null,不会再试别的。

enum 收字符串或单键对象。 deserialize_enum 看到 " 交出 UnitVariantAccess,看到 { 交出 VariantAccess(两者的细节在 SeqAccess、MapAccess 与 EnumAccess,外部标签这种表示在 外部标签)。变体读完之后,它自己检查对象只有这一个键:

                let value = tri!(ret);

                match tri!(self.parse_whitespace()) {
                    Some(b'}') => {
                        self.eat_char();
                        Ok(value)
                    }
                    Some(_) => Err(self.error(ErrorCode::ExpectedSomeValue)),
                    None => Err(self.error(ErrorCode::EofWhileParsingObject)),
                }

所以多一个键的报错是 expected value,不像 struct 那样报 unknown field;self.error 用的是已消费的位置,column 21 是内层对象的 },多出来的 , 在它后面。["Draft"] 首字节是 [,直接 ExpectedSomeValue。

递归深度:最多 127 层

[ 和 { 的分支外面都套着 check_recursion!:

macro_rules! check_recursion {
    ($this:ident $($body:tt)*) => {
        if_checking_recursion_limit! {
            $this.remaining_depth -= 1;
            if $this.remaining_depth == 0 {
                return Err($this.peek_error(ErrorCode::RecursionLimitExceeded));
            }
        }

        $this $($body)*

        if_checking_recursion_limit! {
            $this.remaining_depth += 1;
        }
    };
}

每进一层 [ / { 减一,出来加一。限制要防的是栈:递归不在 serde_json 的某一个函数里,而是绕着 Visitor 转圈,deserialize_seq 调 visit_seq,visit_seq 里 next_element 调 T::deserialize,T 又是数组时再进 deserialize_seq……每一层 JSON 嵌套就是几层 Rust 栈帧。攻击者送来十万个 [,没有限制的解析器会栈溢出,进程直接 abort,Result 都来不及返回。

从 128 开始,先减再判断是否为 0,所以第 128 个 [ 把它减到 0 时就报错,实际最多 127 层。128 放得进 u8,这个字段只占一个字节。src/bin/depth.rs:

use serde::Deserialize;
use serde::de::IgnoredAny;
use serde_json::Value;

fn nested(depth: usize) -> String {
  format!("{}{}", "[".repeat(depth), "]".repeat(depth))
}

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct Doc {
  id: u32,
}

fn main() {
  for depth in [127, 128, 129] {
    match serde_json::from_str::<Value>(&nested(depth)) {
      Ok(_) => println!("Value, depth {depth}: Ok"),
      Err(e) => println!("Value, depth {depth}: Err({e})"),
    }
  }
  let deep = nested(100_000);
  let r = serde_json::from_str::<IgnoredAny>(&deep);
  println!("IgnoredAny, depth 100000: {r:?}");
  let doc = format!(r#"{{"id":1,"extra":{deep}}}"#);
  println!("Doc with unknown field of depth 100000: {:?}", serde_json::from_str::<Doc>(&doc));
}
Value, depth 127: Ok
Value, depth 128: Err(recursion limit exceeded at line 1 column 128)
Value, depth 129: Err(recursion limit exceeded at line 1 column 128)
IgnoredAny, depth 100000: Ok(IgnoredAny)
Doc with unknown field of depth 100000: Ok(Doc { id: 1 })
  • 127 层能读,128 层就报错;129 层报在同一个位置,因为第 128 个 [ 就停了。column 128 是 peek_error 给的位置:第 128 个 [ 还没被吃掉,peek_position 指向它。
  • IgnoredAny 读十万层也没事,嵌在未知字段里的十万层同样被跳过了。deserialize_ignored_any 调的 ignore_value 不递归,也不经过 check_recursion!。
📝NOTE
没有蠢问题

问: ignore_value 不受深度限制,那给一个只认识 id 的结构体送一百万层嵌套,会不会把它撑爆?

答: 撑不爆栈。ignore_value 是一个循环,进一层就把当前的括号(b'[' 或 b'{')压进 scratch(self.scratch.extend(enclosing.take())),出一层就 scratch.pop()。嵌套深度换成了堆上每层一个字节,一百万层是 1 MB,而这一百万层括号本身就占了 2 MB 输入。深度限制保护的是栈,这里根本没有用栈,自然不需要它。

unbounded_depth:关掉限制

开了 unbounded_depth feature,disable_recursion_limit 把 disable_recursion_limit 字段设成 true,if_checking_recursion_limit! 就跳过加减。文档写得很直白:「Parse arbitrarily deep JSON structures without any consideration for overflowing the stack.」并建议配合 serde_stacker 的动态增长栈,还要小心解析结果之后的 Display、Debug、Drop 也会递归。

src/bin/unbounded.rs(feature crate,serde_json 开了 arbitrary_precision、preserve_order、raw_value、unbounded_depth、float_roundtrip,和这里有关的只有 unbounded_depth):

use serde::Deserialize;
use serde_json::Value;

fn nested(depth: usize) -> String {
  format!("{}{}", "[".repeat(depth), "]".repeat(depth))
}

// Drops a deep Value without recursing, as the disable_recursion_limit docs advise.
fn drop_flat(value: Value) {
  let mut stack = vec![value];
  while let Some(value) = stack.pop() {
    if let Value::Array(array) = value {
      stack.extend(array);
    }
  }
}

fn main() {
  let depth: usize = std::env::args().nth(1).unwrap().parse().unwrap();
  let json = nested(depth);

  let mut de = serde_json::Deserializer::from_str(&json);
  println!("limit on:  {:?}", Value::deserialize(&mut de).map(drop_flat).map_err(|e| e.to_string()));

  let mut de = serde_json::Deserializer::from_str(&json);
  de.disable_recursion_limit();
  let value = Value::deserialize(&mut de);
  println!("limit off: parsed = {}", value.is_ok());
  drop_flat(value.unwrap());
  println!("dropped");
}

debug 构建,主线程栈 8 MB(ulimit -s 是 8176),用一个 shell 循环依次传 128、3000、10000(== depth 和 exit= 两行是循环打印的):

== depth 128
limit on:  Err("recursion limit exceeded at line 1 column 128")
limit off: parsed = true
dropped
exit=0
== depth 3000
limit on:  Err("recursion limit exceeded at line 1 column 128")
limit off: parsed = true
dropped
exit=0
== depth 10000
limit on:  Err("recursion limit exceeded at line 1 column 128")

thread 'main' (2655343) has overflowed its stack
fatal runtime error: stack overflow, aborting
exit=134

关掉限制之后 3000 层还能读,10000 层在解析途中栈溢出,进程以 SIGABRT(134)退出:这正是限制要防的情况。程序里特意用 drop_flat 拆掉结果,因为 Value 默认的 Drop 也是递归的。

📝NOTE
冷知识:128 层的限制早在 1.0 之前就有,关掉它要等到 1.0.38

限制来自 #163「Limit recursion to 128 levels」,首次发布于 0.8.4,PR 没有正文。在那之后很长一段时间,限制无法关闭。#509「Unbounded depth」(首次发布于 1.0.38)加了 unbounded_depth feature 和 disable_recursion_limit,PR 说明:「Provides a method disable_recursion_limit to parse arbitrarily deep JSON structures without any consideration for overflowing the stack. When using this method, the caller will want to provide some other way to protect against stack overflows, such as by wrapping the Deserializer in the dynamically growing stack adapter provided by the serde_stacker crate.」所以它是一个 feature 后面的方法而不是默认行为:关掉限制等于把防栈溢出的责任交给调用方,上面的 10000 层就是没接这个责任的下场。

end:值后面只许有空白

from_trait 在 T::deserialize 之后调 end:

    pub fn end(&mut self) -> Result<()> {
        match tri!(self.parse_whitespace()) {
            Some(_) => Err(self.peek_error(ErrorCode::TrailingCharacters)),
            None => Ok(()),
        }
    }

T::deserialize 只读它要的那一个值,之后还有什么它不知道。end 是 from_str 这个函数加的约束,不是 Deserializer 的:自己建 Deserializer 时不调 end,尾部的东西就没人看。src/bin/trailing.rs 的前半:

use serde::Deserialize;
use serde_json::{Deserializer, Value};

fn main() {
  for input in ["1 ", "1 2", "{} x", "[1]\n\n,"] {
    println!("from_str {input:?}: {:?}", serde_json::from_str::<Value>(input).map_err(|e| e.to_string()));
  }

  // Without end(): the trailing "2" is simply never looked at.
  let mut de = Deserializer::from_str("1 2");
  let one = u32::deserialize(&mut de).unwrap();
  println!("manual: {one}, end() = {:?}", de.end().map_err(|e| e.to_string()));
  // ... 后半是下一节的 StreamDeserializer
}
from_str "1 ": Ok(Number(1))
from_str "1 2": Err("trailing characters at line 1 column 3")
from_str "{} x": Err("trailing characters at line 1 column 4")
from_str "[1]\n\n,": Err("trailing characters at line 3 column 1")
manual: 1, end() = Err("trailing characters at line 1 column 3")

手动的那次,u32::deserialize 成功返回了 1,尾部的 2 只在显式调 end() 时才报出来。trailing characters 被 classify 归为 Syntax(错误)。前面 ["a",1,true,4] 报的也是它,但那次来自 end_seq,不是 end。

StreamDeserializer:一段输入里的多个值

不想要「只许一个值」,就用 into_iter 把 Deserializer 变成 StreamDeserializer,每次 next 读一个值,适合 JSON Lines 或者一个 socket 上接连发来的消息。它的 next:

    fn next(&mut self) -> Option<Result<T>> {
        if R::should_early_return_if_failed && self.failed {
            return None;
        }

        // skip whitespaces, if any
        // this helps with trailing whitespaces, since whitespaces between
        // values are handled for us.
        match self.de.parse_whitespace() {
            Ok(None) => {
                self.offset = self.de.read.byte_offset();
                None
            }
            Ok(Some(b)) => {
                // If the value does not have a clear way to show the end of the value
                // (like numbers, null, true etc.) we have to look for whitespace or
                // the beginning of a self-delineated value.
                let self_delineated_value = match b {
                    b'[' | b'"' | b'{' => true,
                    _ => false,
                };
                self.offset = self.de.read.byte_offset();
                let result = de::Deserialize::deserialize(&mut self.de);

                Some(match result {
                    Ok(value) => {
                        self.offset = self.de.read.byte_offset();
                        if self_delineated_value {
                            Ok(value)
                        } else {
                            self.peek_end_of_value().map(|()| value)
                        }
                    }
                    Err(e) => {
                        self.de.read.set_failed(&mut self.failed);
                        Err(e)
                    }
                })
            }
            // ...
        }
    }
  • 数组、对象、字符串有自己的结束符,读完就算完,后面可以紧跟下一个值({}[] 是两个值)。数字和 null / true / false 没有:12 后面紧跟 3 会被读成 123。所以这类值读完之后,peek_end_of_value 要求下一个字节是空白、结构字符(" [ ] { } , :)或输入结束,否则报 trailing characters。
  • byte_offset() 是最近一个值结束的位置,读失败时可以据此知道成功消费到了哪里。
  • 解析出错后流就结束了。IoRead 靠 failed 标记;SliceRead / StrRead 的 set_failed 把 slice 截到当前位置,下次 parse_whitespace 直接见到输入结束,省掉每次 next 都查一次标记。should_early_return_if_failed 这个关联常量就是为了这个区别。

src/bin/trailing.rs 的后半:

  let data = r#"{"k":3}1"cool" 3{}  [0,1] null"#;
  let mut stream = Deserializer::from_str(data).into_iter::<Value>();
  while let Some(item) = stream.next() {
    println!("offset {:>2}: {:?}", stream.byte_offset(), item.map_err(|e| e.to_string()));
  }

  let mut stream = Deserializer::from_str("1 2x 3").into_iter::<Value>();
  while let Some(item) = stream.next() {
    println!("offset {:>2}: {:?}", stream.byte_offset(), item.map_err(|e| e.to_string()));
  }
offset  7: Ok(Object {"k": Number(3)})
offset  8: Ok(Number(1))
offset 14: Ok(String("cool"))
offset 16: Ok(Number(3))
offset 18: Ok(Object {})
offset 25: Ok(Array [Number(0), Number(1)])
offset 30: Ok(Null)
offset  1: Ok(Number(1))
offset  3: Err("trailing characters at line 1 column 4")
offset  3: Err("expected value at line 1 column 4")

第一段里 {"k":3}1 和 1"cool" 中间都没有空白,能分开是因为对象和字符串自带结束符,而 1 后面跟的 " 是结构字符。第二段:2 解析成功,但后面紧跟 x,peek_end_of_value 报错;这个错误不走 set_failed,所以下一次 next 又从 x 开始,这次是解析本身失败(expected value),set_failed 把输入截断,流到此结束,后面的 3 不会再读到。

from_reader:一个字节一次 read

IoRead::new 里那一行决定了 from_reader 的性能:

    pub fn new(reader: R) -> Self {
        IoRead {
            iter: LineColIterator::new(reader.bytes()),
            // ...
        }
    }

io::Read::bytes() 返回的迭代器每次 next 取一个字节。serde_json 自己不加缓冲,from_reader 和 IoRead::new 的文档都提醒了:「When reading from a source against which short reads are not efficient, such as a File, you will want to apply your own buffering because serde_json will not buffer the input. See std::io::BufReader.」from_reader 的文档还多一句:「counter to intuition, this function is usually slower than reading a file completely into memory and then applying from_str or from_slice on it」,链到 issue #160(至今未关闭)。

数一数。src/bin/reader_count.rs 写一个 20000 篇 Post 的 JSON 文件,用一个数 read 调用次数的包装器读三遍:

use std::fs::File;
use std::io::{self, BufReader, Read};
use std::time::Instant;

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Post {
  id: u32,
  title: String,
  tags: Vec<String>,
}

// Counts how many times read() is called and how many bytes it returns.
struct Counting<R> {
  inner: R,
  calls: usize,
  bytes: usize,
}

impl<R: Read> Read for Counting<R> {
  fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
    self.calls += 1;
    let n = self.inner.read(buf)?;
    self.bytes += n;
    Ok(n)
  }
}

fn counting(path: &str) -> Counting<File> {
  Counting { inner: File::open(path).unwrap(), calls: 0, bytes: 0 }
}

fn main() {
  let path = std::env::args().nth(1).unwrap();
  let posts: Vec<Post> = (0..20_000)
    .map(|id| Post { id, title: format!("post number {id}"), tags: vec!["rust".into(), "serde".into()] })
    .collect();
  std::fs::write(&path, serde_json::to_vec(&posts).unwrap()).unwrap();

  let t = Instant::now();
  let mut r = counting(&path);
  let v: Vec<Post> = serde_json::from_reader(&mut r).unwrap();
  println!("from_reader(File):            {:>4} ms, read calls = {:>7}, bytes = {}, posts = {}", t.elapsed().as_millis(), r.calls, r.bytes, v.len());

  // ... 同样两段:from_reader(BufReader::new(&mut r)),以及 read_to_end 之后 from_slice
}
❗IMPORTANT
动脑筋:这个文件 1277781 字节。三种读法各调了几次 read?

M2 Max、macOS、rustc 1.99.0,--release。from_reader 直接读 File、from_reader 读 BufReader<File>(默认容量)、read_to_end 之后 from_slice:Counting::read 各被调了多少次?哪一种最慢,慢多少?

BufReader 只是让每次 read 拿一大块,serde_json 那边依然逐字节 next,只是这些 next 落在 BufReader 的内存里,不再是一次系统调用。from_slice 更进一步:SliceRead 能 memchr、能一次扫 8 个字节、能直接借用,这些 IoRead 都做不到。还有一点和性能无关但同样来自这里:from_reader 只收 DeserializeOwned(生命周期 'de 与借用)。

非法 UTF-8 的报错位置

from_slice 和 from_reader 的字符串最后都经过 as_str:

fn as_str<'de, 's, R: Read<'de>>(read: &R, slice: &'s [u8]) -> Result<&'s str> {
    match str::from_utf8(slice) {
        Ok(s) => Ok(s),
        Err(err) => {
            // Point at the first invalid UTF-8 byte. `read` is positioned at
            // the closing quote of the string whose content is `slice`. If the
            // JSON string contained escape sequences, the column position is
            // approximate.
            let position = read.position();
            Err(Error::syntax(
                ErrorCode::InvalidUnicodeCodePoint,
                position.line,
                position
                    .column
                    .saturating_sub(slice.len() - err.valid_up_to()),
            ))
        }
    }
}

UTF-8 是整段字符串扫完、找到结尾引号之后才校验的,那时 read 的位置已经在引号上。slice.len() - err.valid_up_to() 是「坏字节到字符串末尾还有几个字节」,从当前列号里减掉,就退回到坏字节本身。src/bin/utf8_pos.rs:

fn main() {
  let cases: [&[u8]; 3] = [
    b"\"\x80   \"",
    b"{\"name\": \"caf\xe9 au lait\"}",
    b"[\"ok\",\n \"bad \xff here\"]",
  ];
  for json in cases {
    let err = serde_json::from_slice::<serde_json::Value>(json).unwrap_err();
    println!("{:<34} {err}", json.escape_ascii().to_string());
  }
  let err = std::str::from_utf8(cases[1]).unwrap_err();
  println!("std::str::from_utf8: {err}");
}

1.0.154:

\"\x80   \"                        invalid unicode code point at line 1 column 2
{\"name\": \"caf\xe9 au lait\"}    invalid unicode code point at line 1 column 14
[\"ok\",\n \"bad \xff here\"]      invalid unicode code point at line 2 column 7
std::str::from_utf8: invalid utf-8 sequence of 1 bytes from index 13

同一个程序换成 serde_json = "=1.0.151"(修复之前):

\"\x80   \"                        invalid unicode code point at line 1 column 6
{\"name\": \"caf\xe9 au lait\"}    invalid unicode code point at line 1 column 23
[\"ok\",\n \"bad \xff here\"]      invalid unicode code point at line 2 column 13
std::str::from_utf8: invalid utf-8 sequence of 1 bytes from index 13

列号从 1 数,\xe9 在下标 13,1.0.154 报 column 14,正好是它;1.0.151 报的 column 23 是结尾引号。第三例在第二行,两版的行号都对,差别只在列。

注释里的「approximate」也能复现。slice 是解码之后的内容,有转义时比输入短,往回退的字节数就算少了(src/bin/utf8_escape.rs,坏字节都在 column 2):

\"\xff\\n\\n\\n\"    invalid unicode code point at line 1 column 5
\"\xff\\u0041\"      invalid unicode code point at line 1 column 7

每个 \n 在输入里两个字节、解码后一个,三个转义差 3 列;A 六个字节变一个,差 5 列。

📝NOTE
冷知识:1.0.153 之前,非法 UTF-8 永远报在字符串末尾

#1345(首次发布于 1.0.153)之前,as_str 就一行:str::from_utf8(slice).or_else(|_| error(read, ErrorCode::InvalidUnicodeCodePoint)),位置直接取 read.position()。PR 的说明:「When from_utf8 failed while parsing a string, the error used the reader's current position, which is already past the closing quote. The column therefore always pointed at the end of the string, not at the invalid byte. This uses Utf8Error::valid_up_to so InvalidUnicodeCodePoint reports the first bad byte, the same way control-character errors already do.」它关掉了两个 issue(#1083、#1110),其中 #1110 给的例子 "\x80 " 就是上面实验的第一行:修复前 column 6,修复后 column 2。同一个 PR 也改了 raw_value 下 end_raw_buffering 的位置计算,PR 的第二个提交「Correct multiline raw UTF-8 error positions」修正了多行原始值的行列号。

动脑筋答案

src/bin/reader_count.rs,--release,连跑两次输出相同:

from_reader(File):             487 ms, read calls = 1277782, bytes = 1277781, posts = 20000
from_reader(BufReader<File>):    4 ms, read calls =     157, bytes = 1277781, posts = 20000
read_to_end + from_slice:        3 ms, read calls =      18, bytes = 1277781, posts = 20000
  • 直接读 File:1277782 次,比字节数多 1。每个字节一次 read(io::Bytes 每次只要一个字节),最后一次返回 0 表示文件结束。每次都是一次系统调用,487 ms。
  • BufReader<File>:157 次。BufReader 默认容量 8 KiB,1277781 / 8192 向上取整是 156,再加一次读到 0。serde_json 的 next 次数没有变,只是绝大多数落在 BufReader 的内存里,4 ms,快了一百多倍。
  • read_to_end + from_slice:18 次,read_to_end 自己管理缓冲,一次读一大块。3 ms,比 BufReader 还快一点:SliceRead 有 skip_to_escape 和借用,IoRead 每个字节都要经过 LineColIterator 和 peek 的 ch 槽。

时间在这台机器上测一次,只看量级;read 次数是确定的。

小结

  • Deserializer<R> = 输入 read + 复用的 scratch + remaining_depth。from_str / from_slice / from_reader 只差在包成 StrRead / SliceRead / IoRead,都经 from_trait 读一个值再 end()。
  • Read 是密封的,方法隐藏,把「解析一个字符串」也做成 trait 方法,让内存输入能零拷贝、IoRead 只能拷贝。StrRead 跳过 UTF-8 校验;skip_to_escape 一次用一个 u64 查 8 个字节(1.0.123 起)。
  • 每个 deserialize_* 先 parse_whitespace 看首字节;deserialize_any 按首字节分派,数字只给 u64 / i64 / f64 三种,-0 和超出 u64 / i64 的整数变成 f64。
  • 整数在 u64 里累加,溢出转浮点。默认的浮点解析是 best-effort:[0, 1) 的最短表示约 10% 读回来差一个 ULP;float_roundtrip 用正确舍入的 lexical 算法,实验里零差异。
  • deserialize_struct 同时收 { 和 [;deserialize_option 只认 null;deserialize_enum 收字符串或单键对象,多出的键报 expected value。
  • 每进一层 [ / { 减一次 remaining_depth(从 128 开始,先减后判),最多 127 层;ignore_value 用 scratch 当栈,不受限制。unbounded_depth 能关掉限制,debug 构建下 10000 层就栈溢出。
  • end() 是 from_* 函数加的约束,值后面只许有空白;要读多个值用 StreamDeserializer,数字之类的值后面必须是空白或结构字符。
  • from_reader 每个字节一次 read:1.28 MB 文件 127 万次调用、487 ms,套上 BufReader 157 次、4 ms。
  • 1.0.153(#1345)起,非法 UTF-8 报在第一个坏字节上;字符串里有转义时列号仍是近似值。

💬 关联讨论

发起讨论
加载中...