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
with_capacity(16)之后len是 0。buf[0] = 1panic,信息是标准库的切片下标越界,位置是自己的cap.rs:20。read(&mut buf)返回Ok(0),一个字节没读,src里的 5 个字节原封不动。换成read_buf读到了 5 个字节。put_u8、resize、zeroed都会让长度增长,之后才能按下标写。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 读进 BytesMut | tokio 用 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] = 1panic: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。