首页
/ comprehensive-rust 课程详解:用整数偏移(Offset)实现 Rust 自引用缓冲区

comprehensive-rust 课程详解:用整数偏移(Offset)实现 Rust 自引用缓冲区

2026-09-09 22:05:47作者:卓炯娓

导读

本文围绕 Google Android 团队 Rust 课程(comprehensive-rust)中"自引用缓冲区"一节的 offset 方案展开,讲解如何用 usize 整数偏移替代原始指针,在纯安全 Rust 中实现一个自引用(self-referential)数据结构。你将理解为何这类结构在 Rust 中天然难以表达、偏移方案相比原始指针和 Pin 方案各自的取舍,并拿到一份可直接运行的完整实现代码。

问题背景:为什么 Rust 中难以表达自引用结构

"自引用缓冲区"(Self-Referential Buffer)是一种内部某个字段指向自己另一字段内存的类型。课程在 self-referential-buffer.md 中给出的最小形态如下:

pub struct SelfReferentialBuffer {
    data: [u8; 1024],
    cursor: *mut u8,
}

data 是 1KB 的字节数组,cursor 是指向 data 内部某个位置的指针。

这种结构在 Rust 中并不典型,因为 Rust 没有在实例发生移动(move)时自动更新内部指针地址的机制。如 what-a-move-is.md 所述,Rust 中的 move 始终是一次按位拷贝(bitwise copy),即使类型没有实现 Copy 也是如此——课程展示了 DynamicBuffer 被传递时在 LLVM IR 层面直接生成 llvm.memcpy 指令。这意味着:

一个值的内存地址不是稳定的(a value's memory address is not stable)。

只要 SelfReferentialBuffer 被移动,原先指向 datacursor 指针就会悬空,指向旧地址上的旧数据。而在 C++ 中,这类类型可以正常工作,因为语言允许用户定义移动/拷贝时的行为。课程在 cpp.md 给出了 C++ 版本:其移动构造函数会通过 data + (other.cursor - other.data) 重新计算 cursor,使指针始终指向新对象内部的 data

没有垃圾回收、又不允许重载移动行为的 Rust,就需要另寻出路。课程在 rust.md 中并列给出了三种建模思路:

  • 原始指针(raw pointer):与 C++ 最接近,但使用结果类型极其危险;
  • 整数偏移(integer offset):在 Rust 中更自然,但需要手动创建引用;
  • Pinning:允许使用原始指针的同时减少 unsafe 代码块。

本文的关联文档 rust-offset.md 正是其中的第二种方案。

核心实现:用 usize 偏移替代指针

偏移方案的核心思想是:不保存指向 data 内部的指针,而是保存一个 usize 索引(position),在需要时按需(on-demand)构造切片引用

#[derive(Debug)]
pub struct SelfReferentialBuffer {
    data: [u8; 1024],
    position: usize,
}

impl SelfReferentialBuffer {
    pub fn new() -> Self {
        SelfReferentialBuffer { data: [0; 1024], position: 0 }
    }

    pub fn read(&self, n_bytes: usize) -> &[u8] {
        let available = self.data.len().saturating_sub(self.position);
        let len = n_bytes.min(available);
        &self.data[self.position..self.position + len]
    }

    pub fn write(&mut self, bytes: &[u8]) {
        let available = self.data.len().saturating_sub(self.position);
        let len = bytes.len().min(available);
        self.data[self.position..self.position + len].copy_from_slice(&bytes[..len]);
        self.position += len;
    }
}

对照课程 rust.md 中的骨架声明 data: [u8; 1024], cursor: usize,这里的 position 字段正是那个整数偏移——它记录了"当前读写位置距离缓冲区起始处的字节数"。

逐段解析

1. new():零成本初始化

pub fn new() -> Self {
    SelfReferentialBuffer { data: [0; 1024], position: 0 }
}

data 初始化为全零的 1KB 数组,position 从 0 开始。整个构造过程完全发生在安全 Rust 中,不需要任何 unsafe,也不像原始指针方案那样需要事后"修正"内部指针(对比 rust-raw-pointers.mdnew()buffer.update_cursor() 的步骤)。

2. read():按需构造借用

pub fn read(&self, n_bytes: usize) -> &[u8] {
    let available = self.data.len().saturating_sub(self.position);
    let len = n_bytes.min(available);
    &self.data[self.position..self.position + len]
}

这里有两个值得注意的细节:

  • saturating_sub:当 position 已经等于缓冲区长度(1024)时,1024 - 1024 = 0 不会下溢;由于 position 只会由 write 递增且上限就是缓冲区长度,saturating_sub 在此是防御性写法,保证极端情况下也不会触发 panic;
  • 返回值 &self.data[...] 是一个生命周期由 &self 决定的借用,借用检查器会在编译期保证它不会比缓冲区存活得更久——这正是"引用按需创建"方案的核心优势:不存在悬空引用,因为引用本身就是在访问时实时构造出来的

3. write():边界检查后写入

pub fn write(&mut self, bytes: &[u8]) {
    let available = self.data.len().saturating_sub(self.position);
    let len = bytes.len().min(available);
    self.data[self.position..self.position + len].copy_from_slice(&bytes[..len]);
    self.position += len;
}

写入逻辑与读取对称:先计算剩余可用空间 available,再取 bytes.len()available 的较小值,随后通过切片索引 self.data[self.position..self.position + len] 完成就地拷贝,最后推进 position。切片索引本身会执行边界检查,越界会直接 panic 而不是产生未定义行为。

注意 write 的一个隐含语义:它不会返回实际写入的字节数,如果缓冲区剩余空间不足,多余字节会被静默丢弃(bytes[..len] 只拷贝前 len 个字节)。如果需要"写入多少、剩余多少"的反馈,可以在调用后通过 position 的变化自行推导。

为什么说这是更惯用的 Rust 写法

课程的配套说明(rust-offset.md 的 <details> 块)给出结论:

在 Rust 中,使用 offset 变量并按需创建引用更为惯用(In Rust, it's more idiomatic to use an offset variable and to create references on-demand)。

这个结论建立在三点事实上:

  1. 全程无需 unsafe:整个结构体及其 read/write/new 都是安全代码,借用检查器和切片边界检查接管了所有内存安全责任;
  2. move 安全position 是一个普通整数,无论实例如何移动,position 的值始终正确——它描述的是"缓冲区内部的逻辑位置",而不是"某个绝对内存地址";
  3. Copy 语义友好usizeCopy 类型,SelfReferentialBuffer 即便被移动、赋值,所有字段(包括 position)都按位拷贝后依然自洽,不存在指针悬空问题。

三种方案横向对比:为什么 offset 更适合安全代码

维度 原始指针(raw pointer) 整数偏移(offset) Pinning(Pin<Box<Self>>
内部状态 cursor: *mut u8 position: usize cursor: *mut u8 + _pin: PhantomPinned
是否需要 unsafe 频繁出现,遍布 read/write/new 完全不需要 初始化与 read/write 中均有 unsafe
move 安全性 移动后指针悬空,需手动 update_cursor() 天然安全,position 随拷贝保持正确 依靠 PhantomPinned 在编译期禁止移动
与 C++ 的相似度 最高,几乎逐字对应 较高
主要风险 空指针、越界、悬空,均可能 UB 手动构造引用时需自行保证索引合法 get_unchecked_mut 破坏 pin 保证的窗口期

原始指针版本(rust-raw-pointers.md)虽然语义上与 C++ 类最接近,但课程明确指出了它的问题:

  • unsafe 频繁出现,这是"另一种设计可能更合适"的信号;
  • unsafe 块缺乏安全注释(safety comments),因此这段代码实际是**不健全(unsound)**的;
  • unsafe 块范围过大,良好实践应使用更小的 unsafe 块、明确的前置条件与安全注释;
  • 课程还提出了一个思考题:read()write() 是否应该标记为 unsafe?答案是需要——因为除非先写入,否则 cursor 会是空指针。

Pinning 版本(rust-pin.md)则通过 PhantomPinned 在类型层面宣告"不可移动",使原始指针方案变得可用:new() 返回 Pin<Box<Self>> 而非 Self(这会带来一次堆分配),初始化时通过 Pin::get_unchecked_mut 在 pin 之后设置 cursor——这本身是 unsafe 的,因为那一刻暂时破坏了 pin 保证,要求开发者承诺此后不再移动该值。

三种方案中,只有 offset 方案能做到零 unsafe,这正是课程将其标注为"更惯用"的根本原因。

在 unsafe-deep-dive 课程中的定位

本文档位于课程的 unsafe-deep-dive 大章节下,属于 pinning 子章节"自引用缓冲区"示例的一部分。该示例的教学顺序是:先用 self-referential-buffer.md 抛出问题,再依次用 C++ 建模(cpp.md)、三种 Rust 方案对比(rust.md)、原始指针(rust-raw-pointers.md)、offset 方案(rust-offset.md)和 pin 方案(rust-pin.md)逐步展开。

从课程的宏观叙事看,pinning 被定位为"Rust 中最具挑战性的概念之一",平时只在 async 代码(poll(self: Pin<&mut Self>))中见到,但它的适用范围更广:自引用结构体、侵入式数据结构,以及 Rust 与 C++ 的 FFI——Rust 必须假设任何持引用的 C++ 都可能是自引用数据结构。offset 方案为读者提供了一条完全不借助 pinning、不借助 unsafe 就能安全建模自引用语义的中间路径,也为后续理解 pin 方案为什么需要"把内存钉死在一个固定地址"提供了对照背景。

使用与验证建议

  • 直接运行:本文代码在课程中标记为 rust,editable,即可以在 Rust Playground 中直接编辑运行。本地验证时可放入任意 Rust 项目的 src/main.rs,配合 #[derive(Debug)] 打印 read 的结果进行观察;
  • 注意 read 的边界语义position 到达缓冲区末尾后,read 会返回空切片 &[] 而不是 panic,这是 saturating_submin 组合的结果,可作为"读完即空"的终止判断;
  • 对比学习:建议将本方案与 rust-raw-pointers.md 并排阅读:同样的 read/write 接口,一个零 unsafe 且由编译器保证安全,另一个需要大量 unsafe 且存在健全性隐患,这种对照能直观体会"偏移替代指针"在 API 设计上的价值。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525