源码地图
主线位置:不在主线上。本页说明两条主线经过的代码在哪个文件,哪些 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.rs | 2 029 | BytesMut 和它自己的 Shared;KIND_*、原始容量、偏移这些位编码常量;split、freeze、reserve;Buf / BufMut 的实现;冻结后用的那张 SHARED_VTABLE | 03 BytesMut |
bytes.rs | 1 657 | Bytes 与 Vtable;STATIC、PROMOTABLE_EVEN / ODD、SHARED、Owned<T> 各张 vtable 和它们的函数;From<Vec<u8>> 等转换;切片 | 02 Bytes |
lib.rs | 219 | crate 文档;pub use;几个公用函数:abort、range、panic_advance、panic_does_not_fit,以及 TryGetError | 本页 |
serde.rs | 89 | serde feature 下 Bytes / BytesMut 的 Serialize / Deserialize,一个 serde_impl! 宏生成两份 | 只提一句 |
loom.rs | 33 | 原子类型从哪个 crate 来:core、portable-atomic 或 loom | 本页 |
buf/buf_impl.rs | 2 967 | Buf trait;生成 get_* / try_get_* 的两个宏;&[u8]、std::io::Cursor、&mut T、Box<T> 的实现 | Buf |
buf/buf_mut.rs | 1 671 | BufMut trait;Vec<u8>、&mut [u8]、&mut [MaybeUninit<u8>] 的实现 | BufMut 与 UninitSlice |
buf/uninit_slice.rs | 257 | UninitSlice:包着 [MaybeUninit<u8>],只许写不许读 | BufMut 与 UninitSlice |
buf/chain.rs | 240 | Chain:两个 Buf / BufMut 首尾相接 | 适配器 |
buf/take.rs | 187 | Take:最多读 limit 个字节 | 适配器 |
buf/iter.rs | 127 | IntoIter:把 Buf 当成逐字节的迭代器 | 适配器 |
buf/writer.rs | 88 | Writer:BufMut 转 std::io::Write(要 std) | 适配器 |
buf/reader.rs | 81 | Reader:Buf 转 std::io::Read / BufRead(要 std) | 适配器 |
buf/limit.rs | 75 | Limit:最多写 limit 个字节 | 适配器 |
buf/vec_deque.rs | 40 | VecDeque<u8> 的 Buf 实现:环形缓冲有两段,chunk 只给第一段 | Buf |
buf/mod.rs | 39 | 声明子模块、重新导出 | — |
fmt/debug.rs | 40 | Debug:打印成 b"...",可打印 ASCII 原样输出,其余写成 \n、\x.. 等转义 | — |
fmt/hex.rs | 27 | LowerHex / UpperHex:{:x} 打印成十六进制 | — |
fmt/mod.rs | 15 | fmt_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: ... |
TryGetError | try_get_* 的错误类型,带 requested 和 available 两个字段;有 std 时能转成 std::io::Error |
panic_advance | advance、get_* 越界时的 panic:advance out of bounds: the len is {} but advancing by {} |
panic_does_not_fit | get_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 里那份。
改动来自「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.
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-platforms | portable-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.rs | 1 798 | 118 | Bytes 和 BytesMut 的几乎所有行为 |
test_buf.rs | 461 | 22 | Buf 的 get_*、try_get_*、chunks_vectored |
test_buf_mut.rs | 284 | 23 | BufMut 的 put_*、Vec<u8> 与切片的实现 |
test_chain.rs | 177 | 7 | Chain |
test_bytes_odd_alloc.rs | 147 | 8 | 分配器故意给出奇数地址 |
test_bytes_vec_alloc.rs | 146 | 4 | 分配器核对释放时的大小 |
test_limit.rs | 86 | 8 | Limit |
test_take.rs | 84 | 4 | Take |
test_reader.rs | 38 | 3 | Reader |
test_debug.rs | 35 | 1 | Debug 输出 |
test_iter.rs | 21 | 2 | IntoIter |
test_serde.rs | 20 | 2 | serde 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.sh | MIRIFLAGS="-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.sh | cargo 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-version | MSRV 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的写法本该是一个Acquirefence,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.rs | 266 | 18 个:分配大中小三种 BytesMut;唯一与共享时 deref;clone 冻结后的 Bytes;写入后 split_to、反复写入再取走、fmt::Write、extend;put_slice / put_u8 和 Vec<u8> 上同样操作的对比 |
bytes.rs | 120 | 10 个:唯一、共享、静态三种存储的 deref;静态和共享 Bytes 的 clone,以及作为对照的 Arc<Vec<u8>> 的 clone;从长切片拷贝构造;空切片和短切片的 slice;split_off 后 drop |
buf.rs | 186 | bench_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。