Rust in Everything
基于 serde 1.0.229

升级到 1.0.229 与 1.0.154

主线位置:不在某一条主线上,而是看两条主线的两端各变了什么。serde 一侧的改动落在 derive 生成的反序列化代码(反序列化主线的 visit_enum);serde_json 一侧的改动落在解析器报错的位置(反序列化主线的 parse_str)和几个本站没开的 feature 上。

flowchart TB
  item["一条改动"] --> feat{"本站开了这个 feature"}
  feat -->|没开| none1["不影响"]
  feat -->|开了或无条件| api{"本站走到这段代码"}
  api -->|没走到| none2["不影响"]
  api -->|走到| run["跑两遍对比输出"]

本站的提交 89a61b1 锁的是 serde 1.0.228、serde_json 1.0.150。两段区间里每一条改动是什么,怎么读这一卷的「锁定版本」一节已经逐条列过,这里不再重复,只加一列:对本站有没有影响,凭什么这么说。

先看本站开了哪些 feature

serde_json 的五条改动里有三条只在某个 feature 下生效,所以先查 feature。在 89a61b1 的一份拷贝里离线跑(--locked 不改 Cargo.lock),服务端构建和 wasm 客户端构建各一次:

cargo tree --offline --locked -p app --features server -e normal -f '{p} [{f}]' --prefix none --color never \
  | grep -E '^serde_(json|yaml) v' | sed 's/ (\*)//' | sort -u
cargo tree --offline --locked -p app --target wasm32-unknown-unknown -e normal -f '{p} [{f}]' --prefix none --color never \
  | grep -E '^serde_(json|yaml) v' | sed 's/ (\*)//' | sort -u

两次输出相同:

serde_json v1.0.150 [default,raw_value,std]
serde_yaml v0.9.34+deprecated []

本站自己的 Cargo.toml 只写了 serde_json = "1.0",没开任何 feature。raw_value 是别人带进来的,cargo tree -e features -i serde_json 列出了是谁:

├── serde_json feature "raw_value"
│   ├── axum v0.8.9 (*)
│   ├── sqlx-core v0.8.6 (*)
│   └── sqlx-postgres v0.8.6 (*)

arbitrary_precision、preserve_order、unbounded_depth、float_roundtrip 都没开。站点代码里也搜不到 RawValue、as_f32、arbitrary_precision、preserve_order。

serde 1.0.228 → 1.0.229

改动本站依据
syn 2 → 3(#3085)不新增 crate本站的 Cargo.lock 里已经有 syn 3.0.6(async-trait 0.1.92 用它);升级后 serde_derive 的依赖从 syn 2.0.118 换成 syn 3.0.6,syn 2 仍被其他过程宏使用
生成代码少用 ?(#2936)10 个 enum 的生成代码换了写法,行为不变见下一节的展开对比
serde_derive 最低编译器 1.61 → 1.71不影响镜像用 rust:1-trixie(当前稳定版),本机 rustc 1.99.0
减少 serde_derive 自身的 llvm-lines(#3034 等)不影响只改 serde_derive 自己编译出来的 IR,不改生成的代码
seq_impl! 去掉 mut_mut、unstable 的 never_type不影响前者行为不变;unstable 本站没开(serde 的 feature 是 alloc,default,derive,rc,serde_derive,std)
私有模块 __private228 → __private229不影响只出现在生成代码里,站点代码没有直接写它

#2936 落在本站的哪些类型上

#2936 改的是外部标签 enum 的 visit_enum、相邻标签 enum 的 visit_seq 和 deserialize_in_place 的 visit_seq。本站没有相邻标签,也没有开 serde_derive 的 deserialize_in_place;派生了 Deserialize 的外部标签 enum 有 10 个:PayError、PayAction、LessonKind、Region、ProductKind、Language、LlmFailureAction、ModerationLabel、LlmProvider、LlmRole。内部标签的 LlmContentBlock 不在其中。

挑 LlmFailureAction 和 ModerationThresholdsConfig,原样抄进两个 crate,一个依赖 serde = "=1.0.229"、serde_json = "=1.0.154",一个依赖 serde = "=1.0.228"、serde_json = "=1.0.150":

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LlmFailureAction {
  #[default]
  Review,
  Reject,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModerationThresholdsConfig {
  pub block_above: Option<f32>,
  pub flag_above: Option<f32>,
}

fn main() {
  let a: LlmFailureAction = serde_json::from_str("\"reject\"").unwrap();
  println!("{a:?} {}", serde_json::to_string(&a).unwrap());
  let t: ModerationThresholdsConfig = serde_json::from_str(r#"{"flag_above":0.5}"#).unwrap();
  println!("{t:?}");
}

两边的输出相同:

Reject "reject"
ModerationThresholdsConfig { block_above: None, flag_above: Some(0.5) }

cargo expand 的结果:1.0.229 一侧 586 行,1.0.228 一侧 583 行。把 1.0.228 一侧的 __private228 换成 __private229 再 diff,只剩 14 行,全在 LlmFailureAction 的 visit_enum 里;两个 Serialize impl 和 ModerationThresholdsConfig 的 Deserialize 一字不差:

212,213c212,213
<                     match _serde::de::EnumAccess::variant(__data)? {
<                         (__Field::__field0, __variant) => {
---
>                     match _serde::de::EnumAccess::variant(__data) {
>                         _serde::__private229::Ok((__Field::__field0, __variant)) => {
217c217
<                         (__Field::__field1, __variant) => {
---
>                         _serde::__private229::Ok((__Field::__field1, __variant)) => {
219a220,222
>                         }
>                         _serde::__private229::Err(__err) => {
>                             _serde::__private229::Err(__err)

1.0.229 一侧完整的 visit_enum:

                fn visit_enum<__A>(
                    self,
                    __data: __A,
                ) -> _serde::__private229::Result<Self::Value, __A::Error>
                where
                    __A: _serde::de::EnumAccess<'de>,
                {
                    match _serde::de::EnumAccess::variant(__data) {
                        _serde::__private229::Ok((__Field::__field0, __variant)) => {
                            _serde::de::VariantAccess::unit_variant(__variant)?;
                            _serde::__private229::Ok(LlmFailureAction::Review)
                        }
                        _serde::__private229::Ok((__Field::__field1, __variant)) => {
                            _serde::de::VariantAccess::unit_variant(__variant)?;
                            _serde::__private229::Ok(LlmFailureAction::Reject)
                        }
                        _serde::__private229::Err(__err) => {
                            _serde::__private229::Err(__err)
                        }
                    }
                }

variant(__data)? 那个 ? 并进了 match,多出一个原样返回错误的 Err 分支;每个变体里的 unit_variant(__variant)? 没动。对应 serde_derive 的 enum_externally.rs 第 72 行。? 隐含的 From::from 在这里是同一个错误类型之间的转换,去掉它不改变任何返回值,所以 10 个 enum 读到的结果和报错都和以前一样。外部标签 enum 的 visit_enum 其余部分见外部标签。

serde_json 1.0.150 → 1.0.154

版本改动本站依据
1.0.151#1331 RawValue::from_string_unchecked不影响raw_value 开着(axum、sqlx 带进来),但这是新增的函数,站点和这两个依赖在锁定版本里都不调它也不可能调(1.0.150 里没有)
1.0.152#1346 arbitrary_precision 下保留 -0不影响没开 arbitrary_precision;改的是 #[cfg(feature = "arbitrary_precision")] 的 parse_any_number
1.0.152#1347 Value == f32 对超出 f32 范围的数为假不影响站点没有 Value 和 f32 的比较;读阈值走的是 f32 的 Deserialize,不经 Number::as_f32
1.0.153#1345 非法 UTF-8 的报错位置报错文本变了见下一节
1.0.154#1333 preserve_order 不再要求 std不影响没开 preserve_order;它新增的可选依赖 foldhash 不会进本站的 Cargo.lock

唯一看得见的变化:非法 UTF-8 的列号

#1345 改的是 read.rs 的 as_str,它在字符串的字节不是合法 UTF-8 时报 invalid unicode code point。这个函数不受任何 feature 控制,from_slice 读字节输入时都会走到;from_str 的输入已经是 &str,不会出现非法 UTF-8。机制和 PR 的来历见 serde_json 反序列化一侧。

本站用 from_slice 读外部字节的地方,是 dioxus server 函数的请求体(dioxus-fullstack 0.7.9 src/magic.rs 第 551 行)。上一页的 server 函数实验,在两套版本下跑出来只有一行不同:

< 服务端 字符串里有非法 UTF-8: Err(invalid unicode code point at line 1 column 16)
---
> 服务端 字符串里有非法 UTF-8: Err(invalid unicode code point at line 1 column 11)

输入是 {"name":"a\xff.png","data_base64":"x"},\xff 是第 11 个字节,第 16 个字节是 .png 后面的引号。1.0.150 报的是字符串读完时的位置,1.0.154 报的是第一个坏字节。对本站来说,这只改变 server 函数回给客户端的那条 500 错误里的列号;站点的测试里没有断言这个列号(crates/ 里搜不到 invalid unicode 或 column 加数字的断言)。

实际升一次

上面是逐条推理。再在 89a61b1 的另一份拷贝里真的升一次(只在拷贝里做,不动站点仓库):

cargo update --offline -p serde --precise 1.0.229
cargo update --offline -p serde_json --precise 1.0.154
    Updating serde v1.0.228 -> v1.0.229
    Updating serde_core v1.0.228 -> v1.0.229
    Updating serde_derive v1.0.228 -> v1.0.229
note: pass `--verbose` to see 203 unchanged dependencies behind latest
    Updating serde_json v1.0.150 -> v1.0.154
note: pass `--verbose` to see 202 unchanged dependencies behind latest

Cargo.lock 的 diff 只有四个包的版本号和校验和,外加 serde_derive 的依赖那一行:

6845c6845
<  "syn 2.0.118",
---
>  "syn 3.0.6",

没有新增或删除任何包。升级前后各跑一次 app-core 和 llm 两个 crate 的单元测试(site.json 的解析、LLM 请求体的形状都在里面),结果相同:

test result: ok. 112 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 44 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

(这是升级后的一次;升级前的一次两行相同,只有 finished in 的耗时是 0.01s 和 0.02s。)

上一页的五个实验程序(site.json 的解析、经 Value 写回、LLM 的请求与响应、前置元数据、server 函数)也在两套版本下各跑了一次,diff 的结果是四个完全相同,server_fn 只差上面那一行。

📝NOTE
没有蠢问题

问: 本站的 Cargo.toml 写的是 serde = "1.0",1.0.229 早就发布了,为什么构建出来还是 1.0.228?要不要把 serde_derive 也写进 Cargo.toml 一起升?

答: "1.0" 只是允许的范围,实际用哪个版本由 Cargo.lock 决定,cargo build 不会自己改锁文件,要 cargo update 才会动。serde_derive 不用单独写:cargo update -p serde --precise 1.0.229 一条命令就把 serde_core、serde_derive 一起带到了 1.0.229(上面的输出)。这是因为 serde_core 用一个永远不成立的 cfg(any()) 依赖精确锁住了 serde_derive = "=1.0.229",三者只能同版本,见源码地图的「版本怎么锁在一起」。

小结

  • 本站的 serde_json 开了 default、std、raw_value,raw_value 来自 axum 和 sqlx;arbitrary_precision、preserve_order 都没开。
  • serde 1.0.229:唯一改变生成代码的 #2936 落在本站 10 个外部标签 enum 的 visit_enum 上,? 并进 match,行为不变;LlmFailureAction 两版展开除私有模块名外只差 14 行。syn 3 本站已有,不新增 crate;最低编译器 1.71 不是问题。
  • serde_json 1.0.154:五条改动里四条在本站不生效(feature 没开或 API 没用);#1345 让 server 函数遇到非法 UTF-8 时报的列号从字符串结尾移到第一个坏字节。
  • 实际升级只改 Cargo.lock 里四个包的版本和 serde_derive 的 syn 依赖;两个 crate 的 156 个单元测试和五个实验程序在升级前后结果相同,只有那一个列号不同。

💬 关联讨论

发起讨论
加载中...