Rust in Everything
基于 bytes 1.12.1

Buf

主线位置:两条主线的读端。上层拿到一个 Bytes(共享主线)或直接读 BytesMut(构建主线)时,都通过 Buf 往前吃字节。get_u32 这类方法只碰游标;copy_to_bytes 在 Bytes 上就是共享主线的 split_to,在 BytesMut 上就是构建主线的 split_to + freeze。

flowchart TB
  getn["get_u32"] --> enough{"remaining 够 4"}
  enough -->|否| err["TryGetError"]
  err --> pnc["get_*:panic"]
  err --> ret["try_get_*:Err"]
  enough -->|是| fast{"chunk 够 4"}
  fast -->|是| conv["直接转换,advance 4"]
  fast -->|否| slow["逐块拷进 4 字节数组"]
  slow --> conv2["转换"]

Buf 是读游标:它记着「读到哪了」,每读一点就往前挪。存储可以是连续的一块,也可以是几段。buf_impl.rs 有 2 967 行,其中 1 918 行是文档注释(见 源码地图)。trait 里有 38 个 get_* 和 38 个 try_get_*,每个都带一段文档和示例,函数体大多是一行宏调用;末尾的转发宏又把这 76 个各写了一遍。真正的机制只有三处:三个必需方法的契约、生成 get_* 的两个宏、copy_to_bytes。为什么要把游标从存储里抽出来,见 bytes 解决什么问题;写的一侧在 BufMut 与 UninitSlice,Chain、Take 这些组合在 适配器。

本页的程序都依赖:

bytes = "=1.12.1"

平台是 M2 Max、rustc 1.99.0。

三个必需方法

去掉文档注释,trait 的必需部分是:

pub trait Buf {
    fn remaining(&self) -> usize;

    fn chunk(&self) -> &[u8];

    fn advance(&mut self, cnt: usize);

    // ...
}

签名里看不出来的东西,写在每个方法文档的「Implementer notes」里。默认方法全靠这些约定才是对的:

方法返回什么实现者要保证
remaining当前位置到末尾的字节数,「greater than or equal to the length of the slice returned by chunk()」除非调用了 advance 或其他文档写明会移动位置的方法,返回值不变
chunk从当前位置开始的一段,长度在 0 到 remaining() 之间,「this can return a shorter slice」不 panic;返回空切片当且仅当 remaining() 为 0
advance游标前进 cnt,下一次 chunk() 从 cnt 字节之后开始cnt > remaining() 时建议 panic;不 panic 就必须当作 cnt == remaining() 处理;cnt == 0 不 panic、什么也不做

chunk 可以只给一部分,这是 Buf 能表示不连续存储的原因:VecDeque<u8> 绕回之后有两段,Chain 有两个 Buf,它们的 chunk 都只给当前这一段。代价落在调用方:拿到的 chunk() 不一定是全部数据,按它解析的代码要么循环,要么用下面这些会自己循环的默认方法。

「空当且仅当 remaining() == 0」这条看起来啰嗦,默认方法却离不开它。try_copy_to_slice 是所有跨块读取的落脚点:

    fn try_copy_to_slice(&mut self, mut dst: &mut [u8]) -> Result<(), TryGetError> {
        if self.remaining() < dst.len() {
            return Err(TryGetError {
                requested: dst.len(),
                available: self.remaining(),
            });
        }

        while !dst.is_empty() {
            let src = self.chunk();
            let cnt = usize::min(src.len(), dst.len());

            dst[..cnt].copy_from_slice(&src[..cnt]);
            dst = &mut dst[cnt..];

            self.advance(cnt);
        }
        Ok(())
    }

开头查过 remaining() 够,循环里就不再查。如果某个实现在 remaining() > 0 时返回空的 chunk(),cnt 是 0,dst 不变,advance(0) 什么也不做,这个循环永远出不来。反过来,remaining() 报多了,chunk() 迟早给出空切片,同样卡死。

advance 的约定在各个实现里写法不同。&[u8] 和 Cursor 越界时调用 panic_advance,Bytes 和 BytesMut 用自己的 assert!,VecDeque<u8> 直接 self.drain(..cnt),越界时是标准库的 panic。各自的原文在后面「数据不够时」一节。

chunks_vectored:默认只交一段

第四个和底层存储有关的方法是 chunks_vectored,要 std feature。它把从当前位置开始的若干段填进一组 IoSlice,好直接交给 writev。默认实现只填一段:

    fn chunks_vectored<'a>(&'a self, dst: &mut [IoSlice<'a>]) -> usize {
        if dst.is_empty() {
            return 0;
        }

        if self.has_remaining() {
            dst[0] = IoSlice::new(self.chunk());
            1
        } else {
            0
        }
    }

文档对它的保证只有这些:写进 dst 的总长度不超过 remaining();返回值是写了几段;remaining() 不为 0 时至少写一段非空的。不保证写完所有段。VecDeque<u8> 覆盖了它,一次交出两段;Chain 覆盖成「先让第一段填,再让第二段接着填」。hyper 的写缓冲正是靠这个把响应头和 body 合成一次 writev(读写缓冲)。

「不保证写完」和 Chain 的写法放在一起,就有一个坑。下面的 Trickle 是一个合法的 Buf:chunk 每次只给 1 个字节,chunks_vectored 用默认实现:

use std::io::IoSlice;

use bytes::Buf;

// 合法但「吝啬」的 Buf:chunk 每次只给 1 个字节,chunks_vectored 用默认实现
struct Trickle<'a>(&'a [u8]);

impl Buf for Trickle<'_> {
  fn remaining(&self) -> usize {
    self.0.len()
  }
  fn chunk(&self) -> &[u8] {
    &self.0[..self.0.len().min(1)]
  }
  fn advance(&mut self, n: usize) {
    self.0 = &self.0[n..];
  }
}

fn show(label: &str, buf: &impl Buf) {
  let mut io = [IoSlice::new(&[]); 16];
  let n = buf.chunks_vectored(&mut io);
  let parts: Vec<String> = io[..n]
    .iter()
    .map(|s| String::from_utf8_lossy(s).into_owned())
    .collect();
  println!("{label:<16} remaining={:<2} n={n} slices={parts:?}", buf.remaining());
}

fn main() {
  show("Trickle", &Trickle(b"hello"));
  show("&[u8] chain", &(&b"hello"[..]).chain(&b"world"[..]));
  show("Trickle chain", &Trickle(b"hello").chain(&b"world"[..]));
}

跑 3 次,输出相同:

Trickle          remaining=5  n=1 slices=["h"]
&[u8] chain      remaining=10 n=2 slices=["hello", "world"]
Trickle chain    remaining=10 n=2 slices=["h", "world"]
  • Trickle 单独用:只交出 "h",remaining 是 5。这符合文档,调用方写完 1 个字节、advance(1) 之后再来取就行。
  • 两个 &[u8] 拼起来:两段都交出来,没有问题。
  • Trickle 拼 &[u8]:第一段只交了 "h",Chain 接着让第二段填,得到 ["h", "world"]。把这组 IoSlice 原样交给 writev,写出去的是 hworld,"ello" 被跳过了。

Chain::chunks_vectored 没法从返回值知道第一段是否交全。所以只有在每一段都是「chunk 就是全部」的类型(&[u8]、Bytes、BytesMut)时,拼起来的 chunks_vectored 才是完整的。

📝NOTE
冷知识:chunks_vectored 曾经承诺「没填满就说明已经交全」

1.10.0 之前,文档里有一句:「If chunk_vectored does not fill every entry in dst, then dst is guaranteed to contain all remaining slices in self.」上面那个默认实现本身就违反它:dst 有 16 格只填 1 格,却不一定交全了。#701 用的正是 Trickle 拼 &b"world"[..] 的例子,得到 [[h], [world]],最早在 hyper 那边被报告;讨论里指出 #474 早就发现过。#754(标题「Remove incorrect guarantee for chunks_vectored」,首次发布于 1.10.0)删掉了这句,换成现在的「The return value is the number of slices written to dst. If Buf::remaining() is non-zero, then this writes at least one non-empty slice to dst.」只改了文档。#701 关闭之后,讨论里还有人指出「I think Chain::chunks_vectored is still wrong」,到 1.12.1 chain.rs 里这个方法没有改过,上面的输出就是它现在的样子。

get_*:两个宏,一条快路径

除了 get_u8 和 get_i8 是手写的(读 self.chunk()[0] 再 advance(1)),定长整数和浮点数的 get_* 都是一行宏调用,比如 get_u32:

    fn get_u32(&mut self) -> u32 {
        buf_get_impl!(self, u32::from_be_bytes);
    }

_le、_ne 版本只是把 from_be_bytes 换成 from_le_bytes、from_ne_bytes;get_f32 是 f32::from_bits(self.get_u32())。真正的逻辑在 buf_try_get_impl! 的第一个分支:

macro_rules! buf_try_get_impl {
    ($this:ident, $typ:tt::$conv:tt) => {{
        const SIZE: usize = core::mem::size_of::<$typ>();

        if $this.remaining() < SIZE {
            return Err(TryGetError {
                requested: SIZE,
                available: $this.remaining(),
            });
        }

        // try to convert directly from the bytes
        // this Option<ret> trick is to avoid keeping a borrow on self
        // when advance() is called (mut borrow) and to call bytes() only once
        let ret = $this
            .chunk()
            .get(..SIZE)
            .map(|src| unsafe { $typ::$conv(*(src as *const _ as *const [_; SIZE])) });

        if let Some(ret) = ret {
            // if the direct conversion was possible, advance and return
            $this.advance(SIZE);
            return Ok(ret);
        } else {
            // if not we copy the bytes in a temp buffer then convert
            let mut buf = [0; SIZE];
            $this.copy_to_slice(&mut buf); // (do the advance)
            return Ok($typ::$conv(buf));
        }
    }};
    // ...
}

三步:

  1. 先查总量。 remaining() 不够就返回 TryGetError,游标不动。
  2. 快路径。 chunk().get(..SIZE) 在当前块至少有 SIZE 字节时得到一个长度正好是 SIZE 的切片,把它的指针转成 *const [u8; SIZE] 读出一个数组,交给 from_be_bytes。[u8; N] 的对齐是 1,任何地址都能这样读;长度已经由 get(..SIZE) 保证,所以这个 unsafe 成立。结果先放进 Option,等 chunk() 的借用结束了再 advance。注释里的 bytes() 是 chunk() 的旧名(1.0 改名,#450)。
  3. 慢路径。 当前块不够 SIZE 字节,就开一个栈上的 [0; SIZE],用 copy_to_slice 逐块拷满(它走上面的 try_copy_to_slice,又查一次 remaining),再转换。

panic 的版本 buf_get_impl! 不另写一遍,而是把上面的宏包进一个闭包:

macro_rules! buf_get_impl {
    ($this:ident, $typ:tt::$conv:tt) => {{
        return (|| buf_try_get_impl!($this, $typ::$conv))()
            .unwrap_or_else(|error| panic_advance(&error));
    }};
    // ...
}
📝NOTE
没有蠢问题

问: (|| ...)() 定义一个闭包又立刻调用它,为什么不直接写 buf_try_get_impl!(...).unwrap_or_else(...)?

答: 因为 buf_try_get_impl! 展开出来的是带 return 的语句,不是一个表达式。它是给 try_get_u32 这种返回 Result<u32, TryGetError> 的函数用的,return Err(...) / return Ok(ret) 直接从那个函数返回。展开在 get_u32(返回 u32)里,这些 return 的类型就不对。包进闭包后,return 退出的是闭包,闭包的返回值是 Result,外面再 unwrap_or_else 成 panic。快慢路径只有一份代码,不是复制两遍。

变长的 get_uint(nbytes) / get_int(nbytes) 走宏的另外两个分支(be => 和 le =>)。它们没有快路径,直接开一个 8 字节数组,用 try_copy_to_slice 填进低位那几个字节(大端填后面,小端填前面),注释说「The same trick as above does not improve the best case speed」。nbytes 超过 8 时调用 panic_does_not_fit,这是参数错误,try_get_uint 也照样 panic,不返回 Err。

实验:跨块读出来的值一样,代价不一样

同样 5 个字节,分别放在一个 &[u8]、一个 2 + 3 的 Chain、一个绕回的 VecDeque<u8> 里,各读一次 get_u32。程序后半段是下面两节要用的 try_get_u32 和 get_int:

use std::collections::VecDeque;

use bytes::{Buf, Bytes};

fn main() {
  let data = [0x0A, 0x0B, 0x0C, 0x0D, 0xEE];

  // 1. 连续:一个 &[u8]
  let mut flat = &data[..];
  println!("flat   chunk={:?} get_u32={:#010x} remaining={}", flat.chunk().len(), flat.get_u32(), flat.remaining());

  // 2. 跨块:前 2 字节在 a,后 3 字节在 b
  let mut chain = (&data[..2]).chain(&data[2..]);
  println!("chain  chunk={:?} get_u32={:#010x} remaining={}", chain.chunk().len(), chain.get_u32(), chain.remaining());

  // 3. VecDeque:环形缓冲绕回之后有两段
  let mut dq: VecDeque<u8> = VecDeque::with_capacity(8);
  dq.extend([0u8; 6]);
  dq.drain(..5); // 留 1 个在第 5 格
  dq.extend(data); // 写进第 6、7 格,再绕回第 0..3 格
  dq.pop_front(); // 去掉留下的那个
  let (s1, s2) = dq.as_slices();
  println!("deque  slices=({}, {}) get_u32={:#010x} remaining={}", s1.len(), s2.len(), dq.get_u32(), dq.remaining());

  // 4. try_get_u32:够与不够
  let mut short = Bytes::from_static(&[1, 2, 3]);
  println!("try_get_u32 on 3 bytes: {:?}, remaining={}", short.try_get_u32(), short.remaining());
  let err = short.try_get_u32().unwrap_err();
  println!("Display: {err}");
  let mut ok = Bytes::from_static(&[1, 2, 3, 4]);
  println!("try_get_u32 on 4 bytes: {:?}, remaining={}", ok.try_get_u32(), ok.remaining());

  // 5. get_int 与 try_get_int:3 字节的 0xFFFFFF
  let ff = [0xFF, 0xFF, 0xFF];
  println!("get_int(3)      = {}", (&ff[..]).get_int(3));
  println!("try_get_int(3)  = {:?}", (&ff[..]).try_get_int(3));
  println!("get_uint(3)     = {}", (&ff[..]).get_uint(3));

  // 6. nbytes == 0
  let mut b = &b"hello"[..];
  println!("get_int(0) = {}, remaining={}", b.get_int(0), b.remaining());
}

跑 3 次,输出相同:

flat   chunk=5 get_u32=0x0a0b0c0d remaining=1
chain  chunk=2 get_u32=0x0a0b0c0d remaining=1
deque  slices=(2, 3) get_u32=0x0a0b0c0d remaining=1
try_get_u32 on 3 bytes: Err(TryGetError { requested: 4, available: 3 }), remaining=3
Display: Not enough bytes remaining in buffer to read value (requested 4 but only 3 available)
try_get_u32 on 4 bytes: Ok(16909060), remaining=0
get_int(3)      = -1
try_get_int(3)  = Ok(16777215)
get_uint(3)     = 16777215
get_int(0) = 0, remaining=5

前三行:Chain 的 chunk() 只有 2 个字节,VecDeque 的两段是 2 和 3,都走了慢路径,读出的值和连续的 &[u8] 一样,都是 0x0a0b0c0d,游标都停在最后 1 个字节前。

值一样,代价不一样。用 criterion 测三种情况下一次 get_u32(每次迭代重新构造游标,所以三个数里都含构造的开销):

[dev-dependencies]
criterion = "0.5"
use std::hint::black_box;

use bytes::Buf;
use criterion::{Criterion, criterion_group, criterion_main};

fn bench(c: &mut Criterion) {
  let data = [0x0Au8, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11];

  // 连续的 &[u8]:快路径
  c.bench_function("slice", |b| {
    b.iter(|| {
      let mut buf = black_box(&data[..]);
      buf.get_u32()
    })
  });
  // Chain,4 字节都在第一段:快路径
  c.bench_function("chain_in_a", |b| {
    b.iter(|| {
      let mut buf = black_box(&data[..4]).chain(black_box(&data[4..]));
      buf.get_u32()
    })
  });
  // Chain,2 + 2 跨段:慢路径
  c.bench_function("chain_split", |b| {
    b.iter(|| {
      let mut buf = black_box(&data[..2]).chain(black_box(&data[2..]));
      buf.get_u32()
    })
  });
}

criterion_group!(benches, bench);
criterion_main!(benches);

cargo bench 跑 3 次,下面是每次的 time: 行(criterion 的置信区间下界、估计值、上界):

slice                   time:   [499.59 ps 503.92 ps 508.39 ps]
chain_in_a              time:   [2.3786 ns 2.4046 ns 2.4313 ns]
chain_split             time:   [6.3531 ns 6.3654 ns 6.3781 ns]
slice                   time:   [514.68 ps 519.39 ps 523.69 ps]
chain_in_a              time:   [2.3307 ns 2.3482 ns 2.3689 ns]
chain_split             time:   [6.3170 ns 6.3243 ns 6.3320 ns]
slice                   time:   [496.23 ps 498.66 ps 500.98 ps]
chain_in_a              time:   [2.3384 ns 2.3463 ns 2.3542 ns]
chain_split             time:   [6.3247 ns 6.3312 ns 6.3383 ns]

三次之间相差不超过 5%,结论稳定:

  • slice 约 0.5 ns。&[u8] 的 remaining、chunk、advance 都是标了 #[inline] 的短函数,快路径上只有一次长度比较和一次 4 字节读。
  • chain_in_a 约 2.35 ns。仍是快路径,但 Chain 的每个方法都要先问第一段还有没有剩,remaining 是两段相加,advance 要判断落在哪一段。
  • chain_split 约 6.33 ns,是 chain_in_a 的 2.7 倍。chunk().get(..4) 得到 None,转到 copy_to_slice:再查一次 remaining,循环两轮,每轮一次 chunk、一次 copy_from_slice、一次 advance。

慢路径的绝对开销只是几纳秒,但它出现在每一次跨块读上。bytes 自己的 benches/buf.rs 也专门测了这件事:把数据拆成第一段 1 字节的两段,逼出慢路径。

数据不够时:panic 的原文

get_* 和 copy_to_slice 数据不够时都调用 lib.rs 的 panic_advance:

/// Panic with a nice error message.
#[cold]
fn panic_advance(error_info: &TryGetError) -> ! {
    panic!(
        "advance out of bounds: the len is {} but advancing by {}",
        error_info.available, error_info.requested
    );
}

它标了 #[cold],让 get_* 的热路径里不展开 panic 的格式化代码。下面的程序用 catch_unwind 收集各种越界的 panic 消息:

use std::collections::VecDeque;
use std::panic;

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

fn try_it(label: &str, f: impl FnOnce() + panic::UnwindSafe) {
  match panic::catch_unwind(f) {
    Ok(()) => println!("{label:<28} ok"),
    Err(e) => {
      let msg = e
        .downcast_ref::<String>()
        .cloned()
        .or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string()))
        .unwrap_or_default();
      println!("{label:<28} panic: {msg}");
    }
  }
}

fn main() {
  panic::set_hook(Box::new(|_| {})); // 不让默认 hook 打印,下面自己打印消息

  try_it("&[u8] get_u32 (3B)", || {
    (&[1u8, 2, 3][..]).get_u32();
  });
  try_it("Bytes get_u32 (3B)", || {
    Bytes::from_static(&[1, 2, 3]).get_u32();
  });
  try_it("Chain get_u32 (1+2B)", || {
    (&[1u8][..]).chain(&[2u8, 3][..]).get_u32();
  });
  try_it("&[u8] get_u8 (0B)", || {
    (&[][..]).get_u8();
  });
  try_it("&[u8] advance(5) (3B)", || {
    (&[1u8, 2, 3][..]).advance(5);
  });
  try_it("Bytes advance(5) (3B)", || {
    Bytes::from_static(&[1, 2, 3]).advance(5);
  });
  try_it("BytesMut advance(5) (3B)", || {
    BytesMut::from(&[1u8, 2, 3][..]).advance(5);
  });
  try_it("VecDeque advance(5) (3B)", || {
    VecDeque::from(vec![1u8, 2, 3]).advance(5);
  });
  try_it("&[u8] copy_to_bytes(5) (3B)", || {
    (&[1u8, 2, 3][..]).copy_to_bytes(5);
  });
  try_it("Bytes copy_to_bytes(5) (3B)", || {
    Bytes::from_static(&[1, 2, 3]).copy_to_bytes(5);
  });
  try_it("&[u8] get_uint(9) (16B)", || {
    (&[0u8; 16][..]).get_uint(9);
  });
}

跑 3 次,输出相同:

&[u8] get_u32 (3B)           panic: advance out of bounds: the len is 3 but advancing by 4
Bytes get_u32 (3B)           panic: advance out of bounds: the len is 3 but advancing by 4
Chain get_u32 (1+2B)         panic: advance out of bounds: the len is 3 but advancing by 4
&[u8] get_u8 (0B)            panic: advance out of bounds: the len is 0 but advancing by 1
&[u8] advance(5) (3B)        panic: advance out of bounds: the len is 3 but advancing by 5
Bytes advance(5) (3B)        panic: cannot advance past `remaining`: 5 <= 3
BytesMut advance(5) (3B)     panic: cannot advance past `remaining`: 5 <= 3
VecDeque advance(5) (3B)     panic: range end index 5 out of range for slice of length 3
&[u8] copy_to_bytes(5) (3B)  panic: advance out of bounds: the len is 3 but advancing by 5
Bytes copy_to_bytes(5) (3B)  panic: split_to out of bounds: 5 <= 3
&[u8] get_uint(9) (16B)      panic: size too large: the integer type can fit 8 bytes, but nbytes is 9
  • 所有 get_* 不管底层是什么类型,消息都一样,都说「advance out of bounds」。其实 get_u32 一次 advance 也没调:宏在读之前就查了 remaining,把 TryGetError 交给 panic_advance。消息里的「len」是 available。
  • 直接调 advance 时,消息取决于类型自己的实现:&[u8] 用 panic_advance;Bytes 和 BytesMut 用 assert!,消息是「cannot advance past `remaining`: 5 <= 3」(读起来像是断言的条件,而不是实际的关系);VecDeque<u8> 的 drain(..5) 报的是标准库的区间越界。
  • copy_to_bytes 也分两种:默认实现在开头查 remaining,用 panic_advance;Bytes 覆盖成了 split_to,消息来自 split_to(各种切片 panic 的原文见 切片)。
  • get_uint(9) 是参数错,不是数据不够:16 字节足够,但 u64 只装得下 8 个。

try_get_*:不想 panic 时

try_get_u32 这一族在 1.10.0 加入(#753,CHANGELOG:「try_get_* methods for Buf trait」),每个 get_* 都有一个对应的 try_get_*,外加 try_copy_to_slice。它们直接用 buf_try_get_impl!:

    fn try_get_u32(&mut self) -> Result<u32, TryGetError> {
        buf_try_get_impl!(self, u32::from_be_bytes)
    }

错误类型 TryGetError 只有两个公开字段 requested 和 available。有 std 时它实现了 std::error::Error,也能 From 成 std::io::Error(ErrorKind::Other),所以在返回 io::Result 的解码函数里可以直接用 ?。

上面实验里的第 4 段:3 字节上 try_get_u32 得到 Err(TryGetError { requested: 4, available: 3 }),remaining 仍是 3,游标没有动;Display 的原文是「Not enough bytes remaining in buffer to read value (requested 4 but only 3 available)」。4 字节上得到 Ok(16909060),也就是 0x01020304。

「失败时游标不动」来自宏的结构:总量检查在读任何字节之前,检查通过之后就一定能读完。这让解码帧的代码可以写成「试着读,Err 就等更多数据」,不用先自己算一遍 remaining。

合并 #753 之前,buf_get_impl! 是唯一的宏,数据不够时直接调用 panic_advance(SIZE, $this.remaining())。#753 把它改名成 buf_try_get_impl!、把 panic 改成 return Err(...),再新写一个用闭包包住它的 buf_get_impl!,也就是上一节看到的样子;copy_to_slice 同样改成调用 try_copy_to_slice 再 unwrap_or_else。所以两族方法的快慢路径是同一份代码。

get_int:符号扩展

get_int 读 nbytes 个字节当作有符号数。它先用 get_uint 读出无符号值,再做符号扩展:

    fn get_int(&mut self, nbytes: usize) -> i64 {
        sign_extend(self.get_uint(nbytes), nbytes)
    }

sign_extend:

// https://en.wikipedia.org/wiki/Sign_extension
fn sign_extend(val: u64, nbytes: usize) -> i64 {
    if nbytes == 0 {
        // avoid `val << 64` panic
        0
    } else {
        let shift = (8 - nbytes) * 8;
        (val << shift) as i64 >> shift
    }
}

把 nbytes 个字节的值左移到 u64 的最高位,转成 i64 再算术右移回来,最高位(符号位)就被复制到高位。实验第 5 段:3 字节的 0xFFFFFF,get_uint(3) 是 16777215,get_int(3) 是 -1。

nbytes == 0 时 shift 是 64,移位量等于位宽,注释说这个分支是为了「avoid val << 64 panic」,直接返回 0。实验第 6 段:get_int(0) 返回 0,remaining 仍是 5,读 0 个字节不动游标。

但同一个实验里,try_get_int(3) 得到的是 Ok(16777215),不是 -1。try_get_int 在 1.12.1 里没有调用 sign_extend:

    fn try_get_int(&mut self, nbytes: usize) -> Result<i64, TryGetError> {
        buf_try_get_impl!(be => self, i64, nbytes);
    }

宏把 3 个字节拷进一个全零的 8 字节数组的低位,再 i64::from_be_bytes,高位是 0,负数就成了正数。try_get_int_le 和委托给它们的 try_get_int_ne 同样如此。这个版本里需要 try_ 又需要负数时,可以先 try_get_uint,再自己做上面那个移位。

📝NOTE
冷知识:sign_extend 修过两次,try_ 那一侧到 1.12.1 还没修

1.7.2 之前,get_int 和 try_get_int 现在一样,直接把补零的字节数组当 i64 读。#732(首次发布于 1.7.2,CHANGELOG:「Fix default impl of Buf::{get_int, get_int_le}」)说这是去掉 byteorder 依赖时(#280)引入的回归:「the representation of the integer was simply padded with 0 bytes and then interpreted as i64」,于是加了 sign_extend,写法照 byteorder 的实现。这个写法在 nbytes == 0 时会左移 64 位:#798 报告「get_int panics if nbytes is zero due to attempt to right shift by 64」,#806(首次发布于 1.12.0,CHANGELOG:「Fix panic in get_int if nbytes is zero」)加上了 if nbytes == 0 那个分支。1.10.0 加 try_get_int 时(#753)走的是宏,没有经过 sign_extend。master 上已经合并了 #839,它的说明是「For example, the 3-byte big-endian value 0xffffff (which get_int(3) returns as -1) was returned by try_get_int(3) as 16777215」,改成 Ok(sign_extend(self.try_get_uint(nbytes)?, nbytes))。写作时它还没有发版,不在 1.12.1 里。

copy_to_bytes:默认拷贝,Bytes 不拷贝

copy_to_bytes 取出接下来的 len 个字节,返回一个 Bytes。默认实现只能拷贝:

    fn copy_to_bytes(&mut self, len: usize) -> crate::Bytes {
        use super::BufMut;

        if self.remaining() < len {
            panic_advance(&TryGetError {
                requested: len,
                available: self.remaining(),
            });
        }

        let mut ret = crate::BytesMut::with_capacity(len);
        ret.put(self.take(len));
        ret.freeze()
    }

with_capacity(len) 分配一次,put 把 take(len) 的每一段拷进去,freeze 时长度正好等于容量,走 From<Vec<u8>> 的可提升路径,不再分配(见 五种存储)。所以默认实现是一次分配、len 字节拷贝。

文档说「This function may be optimized by the underlying type to avoid actual copies」。覆盖了它的有四个:

类型覆盖成代价
Bytesself.split_to(len)不拷贝;可提升存储第一次时分配 24 字节的 Shared
BytesMutself.split_to(len).freeze()不拷贝;KIND_VEC 第一次 split 时分配 40 字节的 Shared
Chain落在一段内就交给那一段的 copy_to_bytes,跨段才拷贝见 适配器
Take转给内层的 copy_to_bytes同上

对同一个 1000 字节的源连做三次 copy_to_bytes(100),用计数分配器看每次分配几次、多少字节:

use std::alloc::{GlobalAlloc, Layout, System};
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};

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

struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static BYTES: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counting {
  unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
    ALLOCS.fetch_add(1, Relaxed);
    BYTES.fetch_add(layout.size(), 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;

// 对同一个 Buf 连做三次 copy_to_bytes(100),分别记分配次数和字节数
fn run<B: Buf>(label: &str, mut buf: B) {
  let mut cols = Vec::with_capacity(3);
  let mut keep = Vec::with_capacity(3);
  for _ in 0..3 {
    let (a0, b0) = (ALLOCS.load(Relaxed), BYTES.load(Relaxed));
    let piece = buf.copy_to_bytes(100);
    let (a1, b1) = (ALLOCS.load(Relaxed), BYTES.load(Relaxed));
    cols.push(format!("{}次/{}B", a1 - a0, b1 - b0));
    keep.push(piece);
  }
  println!("{label:<20} {}", cols.join("  "));
}

fn main() {
  let raw = vec![7u8; 1000];
  let vec_for_bytes = raw.clone(); // len == cap
  let mut bm = BytesMut::with_capacity(1000);
  bm.extend_from_slice(&raw);
  let deque: VecDeque<u8> = raw.iter().copied().collect();

  run("&[u8]", &raw[..]);
  run("Bytes::from_static", Bytes::from_static(&[7u8; 1000]));
  run("Bytes::from(Vec)", Bytes::from(vec_for_bytes));
  run("BytesMut", bm);
  run("VecDeque<u8>", deque);
}

跑 3 次,输出相同:

&[u8]                1次/100B  1次/100B  1次/100B
Bytes::from_static   0次/0B  0次/0B  0次/0B
Bytes::from(Vec)     1次/24B  0次/0B  0次/0B
BytesMut             1次/40B  0次/0B  0次/0B
VecDeque<u8>         1次/100B  1次/100B  1次/100B
  • &[u8] 和 VecDeque<u8> 用默认实现:每次一个 100 字节的新缓冲,数据拷一遍。
  • Bytes::from_static:split_to 只 clone 一次静态 vtable,什么也不分配。
  • Bytes::from(Vec):len == cap 的 Vec 转成可提升存储,第一次 split_to 内部的 clone 把它提升,分配 24 字节的 Shared(clone 与提升),之后只加计数。
  • BytesMut:第一次 split_to 把 KIND_VEC 提升成 KIND_ARC,分配 40 字节的 Shared(split 与 freeze),之后为 0。

Bytes 和 BytesMut 那几行里没有一次是 100 字节:数据始终留在原来那块内存里。hyper 交给上层的 body 分块就是 Bytes,上层用 copy_to_bytes 切帧不会拷贝;http-body-util 的 to_bytes 在只有一块时也靠这一点(http-body-util)。

📝NOTE
冷知识:Bytes::copy_to_bytes 以前是 slice 加 advance

1.7.0 之前,Bytes 的覆盖是:len == self.remaining() 时 mem::replace(self, Bytes::new()),否则 let ret = self.slice(..len); self.advance(len); ret。两种都不拷贝,但要算两次范围、检查两次边界。#688(「perf: improve Bytes::copy_to_bytes」,首次发布于 1.7.0)把它换成一行 self.split_to(len),说明是「We already use this tech for BytesMut, but not bring it for Bytes. Namely, reduce the overhead of slice.」也就是向 BytesMut 那边早就在用的 self.split_to(len).freeze() 看齐。

现成的实现

crate 里实现了 Buf 的类型,以及各自覆盖了什么:

类型位置chunk覆盖的默认方法
&[u8]buf_impl.rs整个切片copy_to_slice(一次 copy_from_slice)
std::io::Cursor<T: AsRef<[u8]>>buf_impl.rsposition 之后的部分无
VecDeque<u8>vec_deque.rs第一段(为空就给第二段)chunks_vectored(两段)
Bytesbytes.rs整个 Bytescopy_to_bytes
BytesMutbytes_mut.rs整个已写部分copy_to_bytes
Chain<T, U>、Take<T>chain.rs、take.rs见 适配器chunks_vectored、copy_to_bytes
&mut T、Box<T>(T: Buf + ?Sized)buf_impl.rs转给 T全部

&[u8] 的 advance 就是把切片本身往后缩:

    fn advance(&mut self, cnt: usize) {
        if self.len() < cnt {
            panic_advance(&TryGetError {
                requested: cnt,
                available: self.len(),
            });
        }

        *self = &self[cnt..];
    }

游标就是切片的起点,没有额外的状态,所以 let mut buf = &data[..] 之后读,data 本身不受影响。Vec<u8> 没有 Buf 实现:它没有地方记「读到哪了」。要读一个 Vec<u8>,用 &v[..]、Cursor::new(v),或者 Bytes::from(v)。

Cursor 的 advance 允许 position 已经超过长度时 advance(0),注释是「We intentionally allow cnt == 0 here even if pos > len」,对应契约里「cnt == 0 不 panic」那一条:Cursor::set_position 可以设成任意值,remaining 用饱和减法算成 0。

最后一行的两个转发实现由 deref_forward_buf! 生成,它把 trait 里的每一个方法都写一遍 (**self).xxx(...),包括所有 get_*、try_get_* 和 copy_to_bytes:

impl<T: Buf + ?Sized> Buf for &mut T {
    deref_forward_buf!();
}

impl<T: Buf + ?Sized> Buf for Box<T> {
    deref_forward_buf!();
}

只转发三个必需方法也能工作,但那样 &mut Bytes 的 copy_to_bytes 会退回默认实现,每次拷贝。全部转发之后,把 &mut bytes 交给 fn parse<B: Buf>(buf: B),用的仍是 Bytes 自己的版本。T: ?Sized 让 Box<dyn Buf> 也是 Buf;文件末尾的 _assert_trait_object 是一个参数为 &dyn Buf 的空函数,注释说它的存在让编译器检查 Buf 是否仍然 object-safe。

❗IMPORTANT
动脑筋:同样是 Bytes::from_static,下面两次 copy_to_bytes(100) 各分配几次?

一个装进 Box<dyn Buf>;另一个包在 Wrapper 里,Wrapper 只实现了三个必需方法,每个都转给里面的 Bytes。两者都通过 &mut dyn Buf 调用:

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};

use bytes::{Buf, Bytes};

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

// 只实现三个必需方法,其余全用默认实现
struct Wrapper(Bytes);

impl Buf for Wrapper {
  fn remaining(&self) -> usize {
    self.0.remaining()
  }
  fn chunk(&self) -> &[u8] {
    self.0.chunk()
  }
  fn advance(&mut self, cnt: usize) {
    self.0.advance(cnt)
  }
}

fn count(label: &str, buf: &mut dyn Buf) {
  let before = ALLOCS.load(Relaxed);
  let piece = buf.copy_to_bytes(100);
  let after = ALLOCS.load(Relaxed);
  println!("{label:<16} allocs={}", after - before);
  drop(piece);
}

fn main() {
  static DATA: [u8; 1000] = [7; 1000];
  let mut boxed: Box<dyn Buf> = Box::new(Bytes::from_static(&DATA));
  let mut wrapped = Wrapper(Bytes::from_static(&DATA));
  count("Box<dyn Buf>", &mut boxed);
  count("Wrapper", &mut wrapped);
}

答案在 动脑筋答案。

动脑筋答案

跑 3 次,输出相同:

Box<dyn Buf>     allocs=0
Wrapper          allocs=1
  • Box<dyn Buf>:0 次。&mut boxed 是 &mut Box<dyn Buf>,Box<T> 的实现由 deref_forward_buf! 生成,copy_to_bytes 转给 dyn Buf,再经 trait object 的 vtable 找到 Bytes 覆盖的版本,也就是 split_to。静态存储的 clone 不分配。
  • Wrapper:1 次,就是默认实现里 BytesMut::with_capacity(100) 那一次。Wrapper 没有覆盖 copy_to_bytes,里面的 Bytes 再便宜也用不上:默认实现只看得见 chunk 和 advance,只能拷贝。自己给 Buf 包一层时,要把 copy_to_bytes(以及需要的话 chunks_vectored)也转发下去。

小结

  • Buf 只要求 remaining、chunk、advance。chunk 可以只给当前一段,这是表示不连续存储的方式;它必须在且仅在 remaining() == 0 时为空,否则 try_copy_to_slice 的循环会卡死。
  • chunks_vectored 默认只交一段,文档从 1.10.0(#754)起不再保证交全;Chain 把一个只交一部分的 Buf 和别的拼起来时,交出的 IoSlice 会跳过数据,1.12.1 里仍是这样。
  • 定长的 get_* 由 buf_get_impl! 生成:先查 remaining,当前块够长就直接读数组,否则拷进栈上的临时数组。buf_get_impl! 用一个立即调用的闭包包住 buf_try_get_impl!,两族方法共用一份代码。实测连续 &[u8] 约 0.5 ns,Chain 快路径约 2.35 ns,跨段慢路径约 6.33 ns。
  • 数据不够时所有 get_* 的消息都是「advance out of bounds: the len is N but advancing by M」;直接 advance 的消息取决于类型。try_get_*(1.10.0)返回 TryGetError,失败时游标不动,能转成 io::Error。
  • get_int 用 sign_extend 做符号扩展(#732,1.7.2),nbytes == 0 返回 0(#806,1.12.0);1.12.1 的 try_get_int 没有符号扩展,0xFFFFFF 读成 16777215,修复(#839)还没发版。
  • copy_to_bytes 默认分配一次并拷贝;Bytes 覆盖成 split_to,BytesMut 覆盖成 split_to().freeze(),都不拷贝。&mut T 和 Box<T> 转发所有方法,这些覆盖不会因为多包一层而丢失;自己写的包装类型要自己转发。

💬 关联讨论

发起讨论
加载中...