Rust in Everything
基于 bytes 1.12.1

index out of bounds: the len is 0 but the index is 0

主线位置:构建主线的起点。with_capacity 和 reserve 只改容量;长度只在写入(put_*、extend_from_slice、advance_mut)时增长。下标、Deref 出来的 &mut [u8] 只覆盖已经写过的那一段。

flowchart TB
  wc["with_capacity 16"] --> st["len 0 cap 16"]
  st -->|"buf[0] = 1"| p["panic"]
  st -->|"read(buf)"| z["Ok 0 像 EOF"]
  st -->|"put_u8 / resize"| ok["len 增长"]
  st -->|"read_buf(buf)"| rb["写进容量 len 增长"]

BytesMut 的容量和长度存在哪、with_capacity 怎么变成一个 Vec,见 data 字段的编码;写入时怎么拿到未初始化的尾部,见 BufMut 与 UninitSlice。这一页是它们在使用者那一侧最常见的误会:把容量当成了长度。

本页的实验在 M2 Max、rustc 1.99.0、--release 下运行,依赖 bytes = "=1.12.1"、tokio = { version = "=1.54.1", features = ["full"] }。

复现

use std::panic;

use bytes::{BufMut, BytesMut};
use tokio::io::AsyncReadExt;

#[tokio::main(flavor = "current_thread")]
async fn main() {
  panic::set_hook(Box::new(|info| {
    let msg = info.payload().downcast_ref::<String>().cloned()
      .or_else(|| info.payload().downcast_ref::<&str>().map(|s| s.to_string()))
      .unwrap_or_default();
    let loc = info.location().unwrap();
    println!("    panicked at {}:{}: {msg}", loc.file(), loc.line());
  }));

  // 1. index into capacity
  let r = panic::catch_unwind(|| {
    let mut buf = BytesMut::with_capacity(16);
    println!("with_capacity(16): len={} capacity={}", buf.len(), buf.capacity());
    buf[0] = 1;
  });
  println!("buf[0] = 1 -> panicked: {}", r.is_err());

  // 2. read(&mut buf) sees a zero-length slice
  let mut src: &[u8] = b"hello";
  let mut buf = BytesMut::with_capacity(1024);
  let n = src.read(&mut buf).await.unwrap();
  println!("read(&mut buf)     -> Ok({n}), buf.len()={}, src left={}", buf.len(), src.len());
  let n = src.read_buf(&mut buf).await.unwrap();
  println!("read_buf(&mut buf) -> Ok({n}), buf.len()={}, buf={:?}", buf.len(), buf);

  // 3. ways to get a length
  let mut a = BytesMut::with_capacity(16);
  a.put_u8(1);
  let mut b = BytesMut::with_capacity(16);
  b.resize(16, 0);
  b[0] = 1;
  let c = BytesMut::zeroed(16);
  println!("put_u8:  len={} cap={}", a.len(), a.capacity());
  println!("resize:  len={} cap={}", b.len(), b.capacity());
  println!("zeroed:  len={} cap={}", c.len(), c.capacity());

  // 4. reserve and split_off work on capacity, not length
  let mut d = BytesMut::new();
  d.reserve(64);
  println!("reserve(64): len={} cap={}", d.len(), d.capacity());
  let tail = d.split_off(32);
  println!("split_off(32) on len 0: self len={} cap={}, tail len={} cap={}", d.len(), d.capacity(), tail.len(), tail.capacity());
}

跑 3 次,输出相同:

with_capacity(16): len=0 capacity=16
    panicked at src/bin/cap.rs:20: index out of bounds: the len is 0 but the index is 0
buf[0] = 1 -> panicked: true
read(&mut buf)     -> Ok(0), buf.len()=0, src left=5
read_buf(&mut buf) -> Ok(5), buf.len()=5, buf=b"hello"
put_u8:  len=1 cap=16
resize:  len=16 cap=16
zeroed:  len=16 cap=16
reserve(64): len=0 cap=64
split_off(32) on len 0: self len=0 cap=32, tail len=0 cap=32
  1. with_capacity(16) 之后 len 是 0。buf[0] = 1 panic,信息是标准库的切片下标越界,位置是自己的 cap.rs:20。
  2. read(&mut buf) 返回 Ok(0),一个字节没读,src 里的 5 个字节原封不动。换成 read_buf 读到了 5 个字节。
  3. put_u8、resize、zeroed 都会让长度增长,之后才能按下标写。
  4. reserve(64) 也只给容量。split_off(32) 在长度为 0 的缓冲上不 panic,切出来的两半长度都是 0、各有 32 字节容量。

源码:Deref 只到 len

with_capacity 就是一个空的 Vec:

    pub fn with_capacity(capacity: usize) -> BytesMut {
        BytesMut::from_vec(Vec::with_capacity(capacity))
    }

buf[0]、&mut buf 当 &mut [u8] 用,都经过 DerefMut → as_mut → as_slice_mut:

    fn as_slice_mut(&mut self) -> &mut [u8] {
        unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
    }

切片的长度是 self.len,不是 self.cap。len 之后的容量还没初始化,Rust 不允许把未初始化的内存当成 &mut [u8] 交出去,所以这里也不可能是 cap。要写进那段容量,得走 BufMut:chunk_mut 给出的是 &mut UninitSlice,写完 advance_mut 把 len 往后挪(BufMut 与 UninitSlice)。put_u8、extend_from_slice 内部就是这么做的。

read 和 read_buf

tokio 的 AsyncReadExt::read 接受 &mut [u8]。传 &mut buf 时,自动解引用得到的就是上面那个长度为 0 的切片。它的文档写明 Ok(0) 有两种含义:

        /// 1. This reader has reached its "end of file" and will likely no longer
        ///    be able to produce bytes. Note that this does not mean that the
        ///    reader will *always* no longer be able to produce bytes.
        /// 2. The buffer specified was 0 bytes in length.

典型的读循环写的是 if n == 0 { break; },把第二种当成了第一种:连接上明明有数据,循环第一圈就以为对端关了。标准库的 std::io::Read::read 也一样。

read_buf 接受 B: BufMut,往 chunk_mut() 里读,再 advance_mut(n):

        if !me.buf.has_remaining_mut() {
            return Poll::Ready(Ok(0));
        }

        let n = {
            let dst = me.buf.chunk_mut();
            // ...
        };

        // ...
        unsafe {
            me.buf.advance_mut(n);
        }

BytesMut 的 remaining_mut 是 isize::MAX as usize - self.len(),第一个判断不会成立;容量用完时 chunk_mut 自己先 reserve(64)(BufMut 与 UninitSlice)。所以 read_buf 不需要先给长度,读进来多少,len 就涨多少。

split_off 量的是容量

BytesMut::split_off 的上限是 capacity(),上面 split_off(32) 因此合法:两半各拿 32 字节容量,内容都是空的。它在 split 与 freeze 的「越界时的 panic 信息」里量过另一种情形(len 11、cap 64 时 split_off(20):self 保留全部 11 字节、容量缩到 20)。按「切成前后两段内容」的意思写 split_off(n) 却传了大于 len 的 n,不会报错,只会得到一个空的后半段,并把前半段的容量削掉一截。

修法

想做的事写法
追加数据put_u8 / put_slice / extend_from_slice,不要先「开空间」再按下标写
要一段定长、可按下标写的缓冲BytesMut::zeroed(n),或 with_capacity(n) 后 resize(n, 0)
从 AsyncRead / Read 读进 BytesMuttokio 用 read_buf(&mut buf);只能用 read(&mut [u8]) 时,先 resize 出长度,读完再 truncate(n)
切成两段内容split_off(at) / split_to(at) 前确认 at <= len()

zeroed 和 resize 会把整段先写成 0,是一次真实的写入;read_buf 直接写进未初始化的容量,省掉这一步。hyper 的读缓冲走的是后者(读写缓冲)。

怎么确认自己没踩

  • 搜 with_capacity( 和 reserve( 之后紧跟下标写入、copy_from_slice、.read(&mut 的地方。

  • clippy 有一条 read_zero_byte_vec,管的正是「with_capacity 之后 read」,但它默认不开,而且只认 Vec。同一个文件里 Vec 和 BytesMut 各写一遍:

    use std::io::Read;
    
    use bytes::BytesMut;
    
    fn main() {
      let mut src: &[u8] = b"hello";
      let mut v: Vec<u8> = Vec::with_capacity(1024);
      let n1 = src.read(&mut v).unwrap();
      let mut m = BytesMut::with_capacity(1024);
      let n2 = src.read(&mut m).unwrap();
      println!("Vec: Ok({n1}), BytesMut: Ok({n2})");
    }
    

    运行 3 次都打印 Vec: Ok(0), BytesMut: Ok(0)。普通的 cargo clippy 什么都不报;显式打开这条 lint(cargo clippy -- -W clippy::read_zero_byte_vec)只报 Vec 那一行:

    warning: reading zero byte data to `Vec`
     --> src/bin/cap_lint.rs:8:12
      |
    8 |   let n1 = src.read(&mut v).unwrap();
      |            ^^^^^^^^^^^^^^^^
      |
      = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#read_zero_byte_vec
      = note: requested on the command line with `-W clippy::read-zero-byte-vec`
    help: try
      |
    8 ~   v.resize(1024, 0);
    9 ~   let n1 = src.read(&mut v).unwrap();
      |
    

    BytesMut 那一行要靠人看。

  • 读循环里的 n == 0 分支加一条断言或日志,确认进来时缓冲长度不为 0。

小结

  • with_capacity / reserve 只给容量,len 不变;下标和 Deref 出的 &mut [u8] 只到 len,所以 with_capacity(16) 后 buf[0] = 1 panic:index out of bounds: the len is 0 but the index is 0。
  • read(&mut buf) 对空的 BytesMut 返回 Ok(0),和 EOF 无法区分;用 read_buf,它写进容量并推进 len。
  • 要按下标写,先 zeroed / resize;要追加,直接 put_*。
  • BytesMut::split_off 以容量为上限,切进没写的部分不报错。
  • clippy 的 read_zero_byte_vec 默认不开,且只认 Vec,不认 BytesMut。

💬 关联讨论

发起讨论
加载中...