Rust in Everything
基于 bytes 1.12.1

怎么读这一卷

主线位置:两条主线的总览。本页把共享主线和构建主线各画一遍,标出每一步落在哪个函数上;下一部分的「一块缓冲的一生」用一个程序把两条线同时跑一遍,并打印每一步之后的内部形态。

flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| use1[01 全景]
  use1 --> use2["02 的 vtable 与存储"]
  use2 --> use3[04 Buf 与 BufMut]
  use3 --> use4[06 常见坑]
  q -->|读懂实现| all[00 到 08 按顺序]

锁定版本

项值
cratebytes 1.12.1
tagv1.12.1
commit76c0fbb(2026-07-08,「Release bytes v1.12.1 (#838)」)
依赖默认 feature 下没有;可选的 serde、portable-atomic(feature 名 extra-platforms),见源码地图
实验用的 tokio1.54.1,和 tokio 卷相同(只有少数实验用到)

源码链接都用 tag,形如 https://github.com/tokio-rs/bytes/blob/v1.12.1/src/bytes.rs#L101。想在本地对照着读:

git clone https://github.com/tokio-rs/bytes
git -C bytes checkout v1.12.1

仓库的 master 已经走过了这个 tag。写作时 master 比 v1.12.1 多 4 个提交,都还没有发版:「Simplify shared alignment assertions」(#841)、「Release shared reference on zero truncate」(#842,改了 Bytes::truncate)、「fix: sign-extend n-byte integers in try_get_int/try_get_int_le」(#839)、「inline (hint) put_slice of BytesMut」(#860)。这些都不在本卷的版本里,读 master 会看到对不上的代码和行号。

本站自己的 Cargo.lock 锁的是 bytes 1.12.0,比本卷早一个补丁版本。两者的差异就是下一节这一个修复;它对本站有没有影响,放在「在本站中」一部分讲。

1.12.0 到 1.12.1:只改了一件事

git diff v1.12.0 v1.12.1 只碰了四个文件:CHANGELOG.md、Cargo.toml(版本号)、src/bytes.rs、src/bytes_mut.rs。两个源文件的改动来自同一个提交「Handle unwinding from Box::new」(#837),CHANGELOG 里只有一行:

# 1.12.1 (July 8th, 2026)

### Fixed
- Properly handle when `Box::new` panics (#837)

问题出在「先构造一个已经拥有缓冲的值,再 Box::new 它」这个顺序上。1.12.0 的 BytesMut::promote_to_shared 是这样写的:

        let shared = Box::new(Shared {
            vec: rebuild_vec(self.ptr.as_ptr(), self.len, self.cap, off),
            original_capacity_repr,
            ref_count: AtomicUsize::new(ref_cnt),
        });

        let shared = Box::into_raw(shared);

rebuild_vec 先用 self 的 ptr、len、cap 拼出一个 Vec<u8>,它和 self 指向同一块缓冲。这时如果 Box::new 分配失败并且 panic(默认是 abort,但可以把分配器配置成 OOM 时 panic),栈展开会先 drop 这个临时的 Shared(连同里面的 Vec),再 drop self 这个 BytesMut,同一块缓冲被释放两次。报告问题的 #836 标题就是「Exception safety violations in BytesMut, leading to Double-Free」,它指出了两处:BytesMut::promote_to_shared 和 Bytes 的 shallow_clone_vec。#837 的说明:

This fixes a few cases where a panic from `Box::new` has incorrect behavior
because it runs the destructor of the argument passed to `new`. They are
fixed by ensuring that the allocation is performed before creating the value
to be stored in the box.

1.12.1 的写法是先分配一块未初始化的 Box<MaybeUninit<Shared>>,分配成功之后才构造 Shared,再用新加的 Shared::init_to_raw 原地写进去。promote_to_shared 现在是:

        // Explicitly allocate before invoking rebuild_vec() so that
        // the vector is not dropped if Box::new() panics.
        let shared = Box::new(MaybeUninit::<Shared>::uninit());
        let shared = Shared::init_to_raw(
            shared,
            Shared {
                vec: rebuild_vec(self.ptr.as_ptr(), self.len, self.cap, off),
                original_capacity_repr,
                ref_count: AtomicUsize::new(ref_cnt),
            },
        );

bytes.rs 里的 Shared::init_to_raw 和 bytes_mut.rs 里的那份一模一样,因为两个文件各有一个自己的 Shared:

    fn init_to_raw(b: Box<MaybeUninit<Self>>, v: Self) -> *mut Self {
        let shared = Box::into_raw(b).cast::<Self>();
        // SAFETY: The Box has the right layout.
        unsafe { shared.write(v) };
        shared
    }

同样的改法用在了三处:BytesMut::promote_to_shared(BytesMut 第一次 split 时)、shallow_clone_vec(可提升的 Bytes 第一次 clone 时),以及 From<Vec<u8>> for Bytes。后者在 1.12.0 里已经把 vec 包进了 ManuallyDrop,issue 没有点它的名,这次是一并按同样的顺序改写:先判断 len == cap,再分配,再把 vec 交给 ManuallyDrop。正常运行时三处的行为、分配次数都和 1.12.0 一样,只有「Box::new 本身 panic」这条路径不同。本卷没有复现这个 panic:它要求把 OOM 配置成 panic 并让分配器在恰好那一次失败。

这三个函数在后面各有一页:提升见 clone 与提升和 split 与 freeze,From<Vec<u8>> 见几种存储。

两条主线

bytes 只有两个核心类型,方向相反:Bytes 只读、可以随便 clone 和切片,BytesMut 可写、同一时刻只有一个句柄能写某一段。全卷沿这两个类型展开,每页开头都会标出本页在哪条主线上。

共享主线(Bytes 的 clone / slice / drop 经由 vtable):

flowchart TB
  mk["构造:选一张 vtable"] --> st["from_static:STATIC"]
  mk --> vec["From Vec:PROMOTABLE 或 SHARED"]
  mk --> own["from_owner:Owned T"]
  mk --> fz["BytesMut::freeze"]
  st --> cl["clone:vtable.clone"]
  vec --> cl
  own --> cl
  fz --> cl
  cl --> sl["slice:clone 后改 ptr 与 len"]
  sl --> dr["drop:vtable.drop"]
  dr --> rel["计数归零才释放"]
  1. 一个 Bytes 有四个字段:ptr、len、data: AtomicPtr<()> 和 vtable: &'static Vtable。Vtable 是一个手写的函数指针表:clone、into_vec、into_mut、is_unique、drop。源码在 data 字段上的注释是 // inlined "trait object"。见结构与 Vtable。
  2. 构造时选定 vtable,之后不再变。from_static 用 STATIC_VTABLE,data 为空,不分配;From<Vec<u8>> 在 len == cap 时转成 Box<[u8]>,按地址奇偶选 PROMOTABLE_EVEN_VTABLE 或 PROMOTABLE_ODD_VTABLE,否则马上分配一个 Shared、用 SHARED_VTABLE;from_owner 把任意 AsRef<[u8]> 包进 Owned<T>,每个 T 一张 Owned::<T>::VTABLE;BytesMut::freeze 来的用 bytes_mut.rs 里的另一张 SHARED_VTABLE。见几种存储。
  3. Clone for Bytes 只有一行:调用 vtable.clone。静态存储什么都不做;共享存储把引用计数加一;可提升存储第一次被 clone 时才走 shallow_clone_vec,分配 Shared、用一次 compare_exchange 把 data 换过去,抢输的一方释放自己的 Shared。见 clone 与提升。
  4. slice、split_to、split_off 都是「先 self.clone(),再改 ptr 和 len」;切出来的某一段为空时不 clone,空的那一段是一个挂在 STATIC_VTABLE 上、不指向任何存储的 Bytes。slice_ref 用地址判断一个 &[u8] 是不是自己的子切片。见切片。
  5. Drop for Bytes 调用 vtable.drop。共享存储走 release_shared:fetch_sub(1, Release),减到零的那个句柄再做一次 Acquire 读,然后释放 Shared 和缓冲。
  6. 反方向:is_unique、try_into_mut、Vec::from(bytes) 分别走 vtable 的 is_unique、into_mut、into_vec,唯一拥有时可以不拷贝地拿回可写的缓冲。

构建主线(BytesMut 的写入、split、reserve、freeze):

flowchart TB
  wc["with_capacity:KIND_VEC"] --> wr["写入:chunk_mut 与 advance_mut"]
  wr --> sp{"split_to"}
  sp -->|第一次| pr["promote_to_shared:KIND_ARC"]
  sp -->|已是 KIND_ARC| inc[引用计数加一]
  pr --> fr["freeze:换成 Bytes"]
  inc --> fr
  fr --> rs["剩下的部分 reserve"]
  rs -->|唯一拥有| reuse[原地复用]
  rs -->|还有别人| alloc["新分配:回到 KIND_VEC"]
  reuse --> wr
  alloc --> wr
  1. BytesMut 的字段是 ptr、len、cap、data: *mut Shared。with_capacity 只是 Vec::with_capacity 再交给 from_vec:这时 data 不是指针,而是一组位,最低位是 KIND_VEC,往上是「原始容量」的编码和「前面被切掉了多少字节」。见 data 字段的编码。
  2. 写入走 impl BufMut for BytesMut:chunk_mut 交出未初始化的尾部(满了先 reserve(64)),调用方写完再 advance_mut;put_slice 是 extend_from_slice。见 BufMut 与 UninitSlice。
  3. split_to 调用 shallow_clone:KIND_VEC 时先 promote_to_shared,分配一个引用计数为 2 的 Shared,data 变成指向它的指针(对齐保证最低位是 0,即 KIND_ARC);已经是 KIND_ARC 就只把计数加一。然后两个句柄各自改 ptr、len、cap,分到不重叠的两段。split_off 和 split 同理。见 split 与 freeze。
  4. freeze:KIND_ARC 时把同一个 Shared 指针挂到 bytes_mut.rs 的 SHARED_VTABLE 上,不拷贝也不分配;KIND_VEC 时用 rebuild_vec 拼回 Vec,走 Bytes::from(vec),再 advance 掉前面切掉的偏移。这一步把构建主线接到共享主线上。
  5. 被切走前半段的 BytesMut 继续用。容量不够时 reserve 进入 reserve_inner:KIND_VEC 时要么把数据挪回开头,要么交给 Vec::reserve;KIND_ARC 且只剩自己一个句柄时,就地复用那块分配(必要时把数据挪回开头或扩大它);还有别的句柄活着,就按 max(需要的容量, 原始容量) 新分配一个 Vec,拷贝数据、释放自己那份引用,回到 KIND_VEC。见 reserve。

两条线在 hyper 里是这样接起来的(hyper 1.12.0,见 读写缓冲和解析与编码):

步骤hyper 的代码bytes 的主线
读 socketpoll_read_from_io:read_buf.reserve(next),chunk_mut() 交给 poll_read,读到 n 字节后 advance_mut(n)构建主线 2、5
切出请求头buf.split_to(len).freeze()构建主线 3、4
取 URI紧接着的 slice_ref:slice.slice_ref(&slice[path_range])共享主线 4
取每个头部的值slice.slice(header.value.0..header.value.1)共享主线 4
body 分块read_mem:read_buf.split_to(n).freeze()构建主线 3、4

请求头、URI、每个头部的值和 body 分块都是对同一块读缓冲的 split_to、slice 或 slice_ref,切的时候不拷贝字节;这块缓冲在最后一个引用它的句柄 drop 之后才释放。

用分配次数看两条主线

照上表写一个最小程序,用一个只数次数的全局分配器,看每一步分配了几次。依赖:

bytes = "=1.12.1"
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

use bytes::{BufMut, Bytes, BytesMut};

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, Ordering::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;

fn step(name: &str, before: &mut usize) {
  let now = ALLOCS.load(Ordering::Relaxed);
  println!("{name:<28} +{} alloc", now - *before);
  *before = ALLOCS.load(Ordering::Relaxed);
}

fn main() {
  println!("start");
  let mut n = ALLOCS.load(Ordering::Relaxed);

  let mut buf = BytesMut::with_capacity(1024);
  step("BytesMut::with_capacity", &mut n);
  buf.put_slice(b"GET /a HTTP/1.1\r\n\r\nbody");
  step("put_slice", &mut n);
  let head = buf.split_to(19);
  step("split_to (first split)", &mut n);
  let head: Bytes = head.freeze();
  step("freeze", &mut n);
  let path = head.slice(4..6);
  step("slice", &mut n);
  let path2 = path.clone();
  step("clone", &mut n);
  let body = buf.split_to(4).freeze();
  step("split_to + freeze (again)", &mut n);
  println!("path = {:?}, body = {:?}", path2, body);
  println!("same allocation: {}", head.as_ptr() as usize + 4 == path.as_ptr() as usize);
  drop((head, path, path2, body, buf));
  step("drop all", &mut n);

  let v = b"hello".to_vec();
  step("vec (len == cap)", &mut n);
  let a = Bytes::from(v);
  step("Bytes::from(vec)", &mut n);
  let b = a.clone();
  step("first clone", &mut n);
  let c = a.clone();
  step("second clone", &mut n);
  drop((a, b, c));

  let s = Bytes::from_static(b"static");
  let t = s.clone();
  step("from_static + clone", &mut n);
  drop((s, t));
}

开头先打印一行 start,让标准输出在计数开始前就分配好自己的缓冲;step 打印完再重新读一次计数,打印本身的分配不算进下一步。M2 Max、rustc 1.99.0,cargo run --release 跑 3 次,输出完全相同:

start
BytesMut::with_capacity      +1 alloc
put_slice                    +0 alloc
split_to (first split)       +1 alloc
freeze                       +0 alloc
slice                        +0 alloc
clone                        +0 alloc
split_to + freeze (again)    +0 alloc
path = b"/a", body = b"body"
same allocation: true
drop all                     +0 alloc
vec (len == cap)             +1 alloc
Bytes::from(vec)             +0 alloc
first clone                  +1 alloc
second clone                 +0 alloc
from_static + clone          +0 alloc
  • 构建主线上只有两次分配:with_capacity 分配 1024 字节的缓冲,第一次 split_to 时 promote_to_shared 分配 Shared。之后的 freeze、slice、clone,以及第二次 split_to + freeze,都只动指针和引用计数。same allocation: true 说明 path 就指在 head 的第 4 个字节上。
  • Bytes::from(vec) 在 len == cap 时不分配(走可提升存储),第一次 clone 才分配那个 Shared,第二次 clone 只加计数。这就是「可提升」的意思。
  • from_static 和它的 clone 都不分配。

每一步之后 data 里到底是什么、引用计数是几,在一块缓冲的一生里逐步打印出来。

阅读路线

  • 只想会用:读 01 全景、02 的前两章(结构与 Vtable、几种存储)、04 Buf 与 BufMut,以及常见坑。读完能判断一个 Bytes 的 clone 会不会分配,知道 BytesMut::with_capacity 给的是容量不是长度,知道 Buf::chunk 只给当前这一段。
  • 想读懂实现:按顺序读。02 是重点,四页分别讲 vtable 的形状、每张 vtable 的函数、可提升存储第一次 clone 时的竞争、切片为什么只改两个字段;03 讲 BytesMut 怎么把元数据塞进 data 的指针位;05 把各章的取舍汇总。

前置知识

这一卷要求会写 Rust,读得懂 unsafe 块。bytes 不依赖任何运行时,下面几样东西在正文里反复出现:

概念讲它的页本卷用到它的地方
Arc 的引用计数与 Release / Acquire一个 usize 的状态机最后一节「引用计数:和 Arc 一样的取舍」Bytes 和 BytesMut 的 Shared 都是去掉了弱引用的手写 Arc;release_shared 照抄标准库的 drop 顺序
手写 vtable 擦掉类型任务的内存布局「RawTask 和 Harness」一节Bytes 的 Vtable 是同一种做法:一个裸指针加一张 'static 函数表
裸指针与对齐—用指针的最低位当标记:KIND_VEC / KIND_ARC,可提升存储的奇偶两张表
MaybeUninit 与未初始化内存—BufMut::chunk_mut 返回 &mut UninitSlice;1.12.1 的 Box<MaybeUninit<Shared>>
tokio 的 ReadBuf 与 poll_read从 TcpStream::read 到挂起hyper 把 BytesMut 的未初始化尾部交给 poll_read

本卷的约定

  • 每页开头的「主线位置」说明本页在共享主线(Bytes)、构建主线(BytesMut)上的哪一步,还是两条都不在(比如源码地图)。
  • 摘自 bytes 的代码片段一律保留原来的格式(4 空格缩进),方便和原文件对照,删掉的行用 // ... 标出;本站自己写的示例用 2 空格缩进。
  • 关于行为的说法(哪张 vtable、什么时候分配、reserve 之后容量是多少、什么时候 panic)尽量配一个小程序和它的真实输出。每个示例上方写出依赖,版本用 = 锁死:bytes = "=1.12.1"。实验的平台是 M2 Max、rustc 1.99.0。
  • 分配次数用上面那种只数次数的 #[global_allocator] 观察。内部状态尽量只通过公开 API 看:as_ptr、len、capacity、is_unique、try_into_mut、指针相等。个别实验要读私有字段(data 的位、用的是哪张 vtable)时,会先核对 size_of 再 transmute 到一个同布局的镜像结构体,并在页面上注明:这依赖 1.12.1 的私有布局,只用于观察,换个版本可能就不对了。
  • panic 信息照源码和实际输出原样引用,不翻译。

目录

部分内容
00 导读本页;源码地图
01 全景bytes 解决什么问题;一块缓冲的一生
02 Bytes本卷重点:结构与 Vtable、几种存储、clone 与提升、切片
03 BytesMutdata 字段的编码、split 与 freeze、reserve
04 Buf 与 BufMutBuf、BufMut 与 UninitSlice、适配器
05 设计取舍把各章的取舍汇总到一起
06 常见坑用 panic 原文或现象做标题,每个都有最小复现
07 在本站中本站(开源)代码里的 bytes;1.12.0 与 1.12.1 的差异对本站的影响
08 动手不用 bytes,从零写一个最小的 Bytes 和迷你 BytesMut

小结

  • 全卷对着 v1.12.1(76c0fbb,2026-07-08)读;master 上还有 4 个没发版的提交,读 master 会对不上行号。本站锁的是 1.12.0。
  • 1.12.1 只修了一件事(#837):先分配 Box<MaybeUninit<Shared>>、再构造拥有缓冲的 Shared,避免 Box::new panic 时同一块缓冲被释放两次。改动落在 promote_to_shared、shallow_clone_vec 和 From<Vec<u8>>,正常路径行为不变。
  • 共享主线:构造时选定 vtable → clone 走 vtable.clone(静态不做事、共享加计数、可提升的第一次才分配)→ slice / split_* 先 clone 再改 ptr、len → drop 走 vtable.drop,计数归零才释放。
  • 构建主线:with_capacity 得到 KIND_VEC → chunk_mut / advance_mut 写入 → 第一次 split_to 提升成 KIND_ARC → freeze 换一张 vtable 变成 Bytes → 剩下的部分 reserve 时能复用就复用,否则新分配回到 KIND_VEC。
  • 照 hyper 的路径写的最小程序里,bytes 只在 with_capacity 和第一次 split_to 时分配;请求头、URI、头部值、body 分块都切自同一块读缓冲。

💬 关联讨论

发起讨论
加载中...