Rust in Everything
基于 bytes 1.12.1

源码地图

主线位置:不在主线上。本页说明两条主线经过的代码在哪个文件,哪些 feature 会改变编译出来的东西,以及仓库用哪些测试和 CI 脚本守住这些 unsafe 代码。

lib.rs 只声明模块、重新导出公开类型;Bytes 和 BytesMut 互相引用(freeze、try_into_mut、From<Bytes> for BytesMut),buf/ 里的两个 trait 被它们实现:

flowchart TB
  lib["lib.rs:入口与公用函数"] --> bytes["bytes.rs:Bytes"]
  lib --> bmut["bytes_mut.rs:BytesMut"]
  lib --> buf["buf/:Buf 与 BufMut"]
  lib --> fmtm["fmt/:Debug 与 Hex"]
  lib --> loom["loom.rs:原子类型从哪来"]
  lib -.-> serde["serde.rs:serde feature"]
  bytes ---|"互相转换"| bmut
  bytes --> buf
  bmut --> buf
  bytes --> loom
  bmut --> loom

src/ 的布局

下面的行数是 v1.12.1 下每个文件的总行数,含文档注释和测试,只用来估分量。src/ 一共 19 个文件、9 881 行,其中 4 495 行是 /// 或 //! 文档注释。

文件行数管什么本卷
bytes_mut.rs2 029BytesMut 和它自己的 Shared;KIND_*、原始容量、偏移这些位编码常量;split、freeze、reserve;Buf / BufMut 的实现;冻结后用的那张 SHARED_VTABLE03 BytesMut
bytes.rs1 657Bytes 与 Vtable;STATIC、PROMOTABLE_EVEN / ODD、SHARED、Owned<T> 各张 vtable 和它们的函数;From<Vec<u8>> 等转换;切片02 Bytes
lib.rs219crate 文档;pub use;几个公用函数:abort、range、panic_advance、panic_does_not_fit,以及 TryGetError本页
serde.rs89serde feature 下 Bytes / BytesMut 的 Serialize / Deserialize,一个 serde_impl! 宏生成两份只提一句
loom.rs33原子类型从哪个 crate 来:core、portable-atomic 或 loom本页
buf/buf_impl.rs2 967Buf trait;生成 get_* / try_get_* 的两个宏;&[u8]、std::io::Cursor、&mut T、Box<T> 的实现Buf
buf/buf_mut.rs1 671BufMut trait;Vec<u8>、&mut [u8]、&mut [MaybeUninit<u8>] 的实现BufMut 与 UninitSlice
buf/uninit_slice.rs257UninitSlice:包着 [MaybeUninit<u8>],只许写不许读BufMut 与 UninitSlice
buf/chain.rs240Chain:两个 Buf / BufMut 首尾相接适配器
buf/take.rs187Take:最多读 limit 个字节适配器
buf/iter.rs127IntoIter:把 Buf 当成逐字节的迭代器适配器
buf/writer.rs88Writer:BufMut 转 std::io::Write(要 std)适配器
buf/reader.rs81Reader:Buf 转 std::io::Read / BufRead(要 std)适配器
buf/limit.rs75Limit:最多写 limit 个字节适配器
buf/vec_deque.rs40VecDeque<u8> 的 Buf 实现:环形缓冲有两段,chunk 只给第一段Buf
buf/mod.rs39声明子模块、重新导出—
fmt/debug.rs40Debug:打印成 b"...",可打印 ASCII 原样输出,其余写成 \n、\x.. 等转义—
fmt/hex.rs27LowerHex / UpperHex:{:x} 打印成十六进制—
fmt/mod.rs15fmt_impl! 宏:Bytes、BytesMut 都转给内部的 BytesRef—

两个 trait 文件占了 src/ 的近一半,但大部分是文档:buf_impl.rs 2 967 行里有 1 918 行文档注释,buf_mut.rs 1 671 行里有 1 013 行。Buf trait 里有 76 个 get_* / try_get_* 方法(每种整数、浮点数分大端、小端、本机序;转发宏 deref_forward_buf! 里又写了一遍,文件里一共 152 处),BufMut trait 里有 46 个 put_*(转发宏里另有 21 处),每个都带一段文档和示例。真正的机制集中在 bytes.rs 和 bytes_mut.rs 两个文件里,加起来 3 686 行。

Debug 的格式就是本卷实验输出里那种 b"/a":HTTP 这样大部分是 ASCII 的数据直接能读,比标准库 [u8] 的 [47, 97] 好认。

lib.rs 里的公用函数

lib.rs 除了文档和 pub use,还放了几个各文件共用的小函数。它们的 panic 信息会在常见坑一部分原样出现:

函数做什么
abort引用计数溢出时调用(shallow_clone_arc、owned_clone、increment_shared 里的 old_size > usize::MAX >> 1 一类检查)。有 std 时是 std::process::abort();没有 std 时在一个 Drop 里再 panic 一次,用「panic 中 panic」逼出 abort
range把 RangeBounds 换成 (begin, end) 并检查边界,Bytes::slice 等用它;panic 信息是 range start must not be greater than end: ... 和 range end out of bounds: ...
TryGetErrortry_get_* 的错误类型,带 requested 和 available 两个字段;有 std 时能转成 std::io::Error
panic_advanceadvance、get_* 越界时的 panic:advance out of bounds: the len is {} but advancing by {}
panic_does_not_fitget_uint(n) 这类按字节数读整数时 n 超过类型宽度:size too large: the integer type can fit {} bytes, but nbytes is {}

Cargo.toml:feature、依赖与 MSRV

Cargo.toml 第 22 行起:

[features]
default = ["std"]
std = []

[dependencies]
serde = { version = "1.0.60", optional = true, default-features = false, features = ["alloc"] }
# Use portable-atomic crate to support platforms without atomic CAS.
# See "no_std support" section in readme for more information.
#
# Enable require-cas feature to provide a better error message if the end user forgets to use the cfg or feature.
extra-platforms = { package = "portable-atomic", version = "1.3", optional = true, default-features = false, features = ["require-cas"] }

[dev-dependencies]
serde_test = "1.0"

[target.'cfg(loom)'.dev-dependencies]
loom = "0.7"

[features] 里只写了 std 一个;serde 和 extra-platforms 是两个可选依赖自动生成的同名 feature。extra-platforms 这个名字是依赖改名得来的:包名是 portable-atomic,在代码里以 extra_platforms 出现。

feature默认打开了什么
std是Buf::reader / BufMut::writer 和 Reader / Writer 两个类型;chunks_vectored(std::io::IoSlice);std::io::Cursor 的 Buf 实现;TryGetError 的 std::error::Error 实现和到 io::Error 的转换;abort 用 std::process::abort
serde否Bytes / BytesMut 的序列化与反序列化(serde.rs)
extra-platforms否原子类型改用 portable-atomic 的,给没有原子 CAS 指令的平台(如 thumbv6m-none-eabi)用;见下面的 loom.rs

crate 本身是 #![no_std] 加 extern crate alloc,Bytes、BytesMut、Buf、BufMut 在没有 std 时都在,少的只是上表第一行那些和 std::io 打交道的部分。

三种写法下 cargo tree -e normal 的输出(M2 Max、rustc 1.99.0,路径已删)。默认:

bytes = "=1.12.1"
exp-bytes-intro v0.1.0
└── bytes v1.12.1

打开两个可选依赖:

bytes = { version = "=1.12.1", features = ["serde", "extra-platforms"] }
exp-bytes-intro v0.1.0
└── bytes v1.12.1
    ├── portable-atomic v1.15.0
    └── serde v1.0.229
        └── serde_core v1.0.229

MSRV 是 1.57:Cargo.toml 的 rust-version = "1.57",clippy.toml 里也写着 msrv = "1.57"(1.11.0 起,#788)。CI 的 minrust 任务用 cargo hack check --feature-powerset --optional-deps --rust-version 在 1.57 上把每种 feature 组合都编译一遍,但只编译不跑测试,注释说「Tests are not run as tests may require newer versions of rust」。

发布的包里有什么

Cargo.toml 第 20 行有一个 include 白名单:

include = ["CHANGELOG.md", "LICENSE", "README.md", "SECURITY.md", "Cargo.toml", "src/**/*.rs", "tests/**/*.rs", "clippy.toml"]

所以 crates.io 上的包里有 tests/,没有 ci/ 和 benches/。下面是本机 cargo 缓存里两个版本解开后的顶层目录(ls -1F):

bytes-1.11.1:
benches/ Cargo.lock Cargo.toml Cargo.toml.orig CHANGELOG.md ci/ clippy.toml LICENSE README.md SECURITY.md src/ tests/
bytes-1.12.1:
Cargo.lock Cargo.toml Cargo.toml.orig CHANGELOG.md clippy.toml LICENSE README.md SECURITY.md src/ tests/

Cargo.lock 和 Cargo.toml.orig 是 cargo package 自己加的。想对照本页读 ci/ 和 benches/,要看 git 仓库,不能看 ~/.cargo/registry 里那份。

📝NOTE
冷知识:1.12.0 之前,依赖 bytes 会顺带拿到四个 shell 脚本

改动来自「Exclude development scripts from published package」(#810),首次随 1.12.0 发布。之前 Cargo.toml 既没有 include 也没有 exclude,仓库里的 ci/*.sh 和 benches/ 都进了发布的包,上面 1.11.1 的目录就是这样。PR 的说明是:

During a dependency review we noticed that the bytes crate includes various
development scripts. ... As of now they prevent any downstream user from
enabling the `[bans.build.interpreted]` option of cargo deny.
也就是说,下游项目想用 cargo deny 禁止依赖里带解释型脚本,bytes 会被拦下来。改法选的是白名单而不是黑名单:「to make sure that everything that's included is an conscious choice」,以后仓库里多出什么文件,都不会自动进包。

loom.rs:原子类型从哪来

bytes.rs 和 bytes_mut.rs 都不直接用 core::sync::atomic,而是 use crate::loom::sync::atomic::{AtomicPtr, AtomicUsize, Ordering}。loom.rs 只有 33 行,按配置三选一:

条件原子类型来自
默认core::sync::atomic
打开 extra-platformsportable-atomic(extra_platforms::{AtomicPtr, AtomicUsize, Ordering})
cfg(all(test, loom))loom::sync::atomic

loom 是 tokio 组织的并发测试工具:在 loom::model 里跑的代码,所有原子操作和线程切换都由 loom 接管,它会系统地枚举这些操作可能的交错顺序,每种都跑一遍。bytes 只有两个 loom 测试,都叫 fuzz 模块,都在测「两个线程同时 clone 同一个 Bytes」:

  • bytes.rs 的 fuzz:bytes_cloning_vec,Bytes::from(b"abcdefgh".to_vec()) 放进 Arc,两个线程各 clone 一次。这正是可提升存储第一次 clone 时两个线程抢着 compare_exchange 的场景,见 clone 与提升。
  • bytes_mut.rs 的 fuzz:bytes_mut_cloning_frozen,先 split().freeze(),再两个线程各 clone 一次。

CI 的 loom 任务是 RUSTFLAGS="--cfg loom -Dwarnings" cargo test --lib:只跑 src/ 里的单元测试,cfg(loom) 让 Cargo.toml 里的 [target.'cfg(loom)'.dev-dependencies] 生效、让 loom.rs 选第三行。为了 loom,Bytes::new 和 Bytes::from_static 各写了两份:平时是 const fn new,cfg(all(loom, test)) 下是普通的 fn new。

tests/:12 个测试文件

tests/ 下每个文件都会被编译成一个独立的测试程序:

文件行数#[test] 数测什么
test_bytes.rs1 798118Bytes 和 BytesMut 的几乎所有行为
test_buf.rs46122Buf 的 get_*、try_get_*、chunks_vectored
test_buf_mut.rs28423BufMut 的 put_*、Vec<u8> 与切片的实现
test_chain.rs1777Chain
test_bytes_odd_alloc.rs1478分配器故意给出奇数地址
test_bytes_vec_alloc.rs1464分配器核对释放时的大小
test_limit.rs868Limit
test_take.rs844Take
test_reader.rs383Reader
test_debug.rs351Debug 输出
test_iter.rs212IntoIter
test_serde.rs202serde feature

为什么两个测试单独成文件

test_bytes_odd_alloc.rs 和 test_bytes_vec_alloc.rs 各自声明了一个 #[global_allocator]。一个程序只能有一个全局分配器,写进 test_bytes.rs 就会让那 118 个测试全都换上它;两个写在同一个文件里则根本编译不过。把两个 System 都标成 #[global_allocator] 试一下:

use std::alloc::System;

#[global_allocator]
static ODD: System = System;

#[global_allocator]
static LEDGER: System = System;

fn main() {}
error: cannot define multiple global allocators
 --> src/bin/two_allocators.rs:7:1
  |
4 | static ODD: System = System;
  | ---------------------------- previous global allocator defined here
5 |
6 | #[global_allocator]
  | ------------------- in this attribute macro expansion
7 | static LEDGER: System = System;
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot define a new global allocator

error: could not compile `exp-bytes-intro` (bin "two_allocators") due to 1 previous error

tests/ 下一个文件一个程序,正好让每个分配器只管自己那几个测试。两个分配器在查不同的东西:

  • Odd(test_bytes_odd_alloc.rs):对齐为 1、大小非零的分配,多要 1 个字节,把返回的指针加 1,于是每个 Vec<u8> 的地址都是奇数;释放时减回去。第一个测试 sanity_check_odd_allocator 先确认 vec![33u8; 1024] 的地址最低位确实是 1,其余测试把 From<Vec<u8>>、clone、Vec::from(bytes)、BytesMut::from(bytes)、split_off 后再转换都跑一遍。可提升存储要拿指针最低位当标记,地址本身是奇数时就得换一张表;常见的系统分配器给出的地址都对齐到 8 或 16,这条路径平时走不到,这个分配器专门把它逼出来。文件和奇偶两张表来自同一个 PR,「Fix Bytes when Vec pointer's LSB is set」(#346,0.5.4)。细节见几种存储。
  • Ledger(test_bytes_vec_alloc.rs):一张 1024 * 1024 项的表,分配时记下「指针 → 大小」,释放时查表,传进来的 layout.size() 和当初分配的不一样就 panic:bad dealloc: alloc size was {}, dealloc size is {}。Bytes 释放可提升存储时要自己算出原来的容量(数据可能已经被 advance、truncate 过),算错了就是用错误的 Layout 去 dealloc,这是未定义行为,普通分配器下多半不报错。测试正是 advance、truncate、两者组合、各种 Vec::from(bytes)。文件来自「Fix Bytes::truncate losing the original Vec's capacity」(#361,0.5.4)。细节见切片。

两个文件开头都有 #![cfg(not(miri))],test_bytes_odd_alloc.rs 的注释写着原因:「Miri does not support custom allocators (also, Miri is "odd" by default with 50% chance)」。

ci/:四个脚本,加上 CI 里的另外几项

ci/ 下四个脚本,加上 .github/workflows/ci.yml 里直接写的几个任务,说明这个库对 unsafe 代码要求到什么程度。CI 统一用 RUSTFLAGS: -Dwarnings,需要 nightly 的任务固定用 nightly-2024-09-15。

脚本或任务跑什么查什么
ci/miri.shMIRIFLAGS="-Zmiri-strict-provenance" 下 cargo miri test;再对 mips64-unknown-linux-gnuabi64 跑一遍;再加 --release 跑一遍未定义行为:越界、释放后使用、用错 Layout、违反指针来源(provenance)规则;mips64 是大端目标;--release 下整数溢出是回绕而不是 panic
ci/tsan.sh先 -Z sanitizer=address 跑 test_bytes、test_buf、test_buf_mut;再 -Z sanitizer=thread 加 -Zbuild-std 跑同样三个名字叫 tsan,其实 ASAN(内存错误)和 TSAN(数据竞争)各跑一遍;ASAN 关了泄漏检测(detect_leaks=0)
ci/panic-abort.sh-Cpanic=abort -Zpanic-abort-tests 下 cargo test --all-features --test '*'在 panic 即 abort 的配置下集成测试照样能过;依赖 catch_unwind 的两个测试标了 #[cfg_attr(not(panic = "unwind"), ignore)]
ci/test-stable.shcargo hack test --each-feature --optional-deps,再 --all-features;nightly 下额外 cargo check --benches 和最低版本依赖检查每个 feature 单独开、全部开都能过
loom 任务RUSTFLAGS="--cfg loom -Dwarnings" cargo test --lib上面两个 fuzz 测试的所有线程交错
minrust 任务cargo hack check --feature-powerset --optional-deps --rust-versionMSRV 1.57 下每种 feature 组合都能编译
no-std 任务thumbv7m-none-eabi 下关掉 std 编译所有 feature 组合;thumbv6m-none-eabi 下再加 extra-platforms 和 portable-atomic 的 critical-section没有 std、没有原子 CAS 的平台
cross 任务i686、armv7、powerpc、powerpc64、wasm32-wasip1 上 cargo test覆盖 32 位平台(BytesMut 能在 data 里记下的偏移上限 MAX_VEC_POS = usize::MAX >> VEC_POS_OFFSET 随指针宽度变)和大端平台

这些工具反过来也塑造了源码的写法,读的时候会碰到:

  • 为 TSAN 改写的 fence。release_shared 在引用计数减到零之后,按标准库 Arc 的写法本该是一个 Acquire fence,bytes 写的是一次 Acquire 读,注释说「Thread sanitizer does not support atomic fences. Use an atomic load instead.」bytes_mut.rs 里的同名函数也一样。
  • 为 miri 准备的两份 ptr_map。可提升存储要在指针上设、清最低位。cfg(miri) 那份用 wrapping_add 在原指针上挪,保留指针来源;平时那份直接把整数转回指针。注释的说法是:前者「is strict provenance compatible, but it results in worse codegen」,所以只在 miri 下用,好让用 miri 测自己代码的下游得到更好的诊断。
  • miri 跑不动的测试。test_bytes.rs 里有 #[cfg_attr(miri, ignore)] // Miri is too slow,以及 if cfg!(miri) { 16 } else { 256 } 这样在 miri 下缩小规模的写法;两个自定义分配器的文件整个 cfg(not(miri))。
  • --release 那一行的来历。ci/miri.sh 最后的 cargo miri test --release(注释「run with wrapping integer overflow instead of panic」)是 1.11.1 修 BytesMut::reserve 整数溢出的那个提交加的。被修的是 reserve_inner 里一个没有检查的 v_capacity >= new_cap + offset:debug 构建下加法溢出直接 panic,release 构建下回绕成一个小数,比较就可能错误地成立。miri 默认按 debug 跑,只有加上 --release 才走得到回绕那条路。修复本身见 reserve。

本卷的实验在 M2 Max 上用 stable 跑,没有装 miri;要读私有字段的实验在页面上注明「只用于观察」。

benches/

三个文件,用的是 nightly 才有的 #![feature(test)] 和 #[bench],CI 只在 nightly 下 cargo check --benches,不实际跑:

文件行数测什么
bytes_mut.rs26618 个:分配大中小三种 BytesMut;唯一与共享时 deref;clone 冻结后的 Bytes;写入后 split_to、反复写入再取走、fmt::Write、extend;put_slice / put_u8 和 Vec<u8> 上同样操作的对比
bytes.rs12010 个:唯一、共享、静态三种存储的 deref;静态和共享 Bytes 的 clone,以及作为对照的 Arc<Vec<u8>> 的 clone;从长切片拷贝构造;空切片和短切片的 slice;split_off 后 drop
buf.rs186bench_group! 宏为 get_u8、get_u16、get_u32、get_u64、get_f32、get_f64、get_uint(3) 七种读法各生成 5 个:普通的 &[u8],以及一个测试用 Buf,数据只有一段或被拆成两段(第一段 1 字节,逼出跨段的慢路径),再各配一个三个方法都 #[inline(never)] 的版本

bytes.rs 里的 clone_arc_vec 拿 Arc<Vec<u8>> 的 clone 和 Bytes 的 clone 对比,正是bytes 解决什么问题要回答的那个问题。

小结

  • src/ 19 个文件、9 881 行,近一半是文档。机制集中在 bytes.rs(1 657)和 bytes_mut.rs(2 029);buf_impl.rs 和 buf_mut.rs 虽大,主要是成百个 get_* / put_* 的文档和示例。
  • feature 只有默认的 std,加两个可选依赖 serde 和 extra-platforms(portable-atomic);默认 feature 下没有任何依赖。crate 是 no_std + alloc,std 只多出 std::io 相关的部分。MSRV 1.57,CI 只编译不测试。
  • 1.12.0 起发布的包用 include 白名单(#810),里面有 tests/,没有 ci/ 和 benches/。
  • loom.rs 决定原子类型来自 core、portable-atomic 还是 loom;两个 loom 测试都在测两个线程同时 clone。
  • test_bytes_odd_alloc.rs 和 test_bytes_vec_alloc.rs 各带一个全局分配器,一个程序只能有一个,所以单独成文件:Odd 逼出奇数地址走 PROMOTABLE_ODD_VTABLE,Ledger 核对释放时的大小。
  • CI 用 miri(strict provenance、大端、release)、ASAN、TSAN、panic=abort、loom、no-std、跨平台把 unsafe 代码围起来;源码里为 TSAN 把 fence 改成 load,为 miri 准备了第二份 ptr_map。

💬 关联讨论

发起讨论
加载中...