offset_of! 切片
摘要
一篇博客文章,介绍了一个 Rust 宏,该宏在编译时计算结构体中切片字段的偏移量,通过伪造分配来绕过当前稳定版 Rust 的限制。
<p><a href="https://lobste.rs/s/yas7ik/offset_slices">评论</a></p>
查看缓存全文
缓存时间: 2026/06/18 16:04
# 切片字段的 `offset_of!` | arya dradjica
来源:https://bal-e.org/blog/2026/offset-of-slices/
`std::mem::offset\_of\!`(https://doc.rust-lang.org/stable/std/mem/macro.offset_of.html)是一个实用的小宏,它允许你在编译时计算 `struct` 中某个特定字段的偏移量。我个人用到它的机会不多,但一位朋友(https://terts.dev/)正在用它来开发一个用于 Rust 的酷炫 JIT 脚本语言(https://codeberg.org/NLnetLabs/roto),所以偶尔会碰到它。不过,它有几个奇怪的地方:你不能将其用于不定长字段(例如 `Foo { a: u8, b: [u8] }` 中的 `b`)。这种情况并不常见,但如果你在做一些足够奇怪的事情,以至于需要计算字段的字节偏移量,那么你可能已经熟悉并乐于(滥用)动态大小类型。
像所有令人沮丧的事情一样,这背后是有原因的:在 Rust 中,实现 `Sized` 的类型具有固定的大小*和对齐*,而其他类型则没有。对于像 `Bar { a: u8, b: dyn Debug }` 这样的类型,`b` 可以是任意类型,从而具有任意对齐,因此具有任意偏移量。切片在这里有点特殊:它们没有实现 `Sized`,其大小在编译时不是固定的,但它们的对齐**是固定的**。`[T]` 的对齐就是 `T` 的对齐。因此,理论上可以计算 `struct` 中切片字段的偏移量。`std::mem::offset\_of\!` 在未稳定的 `offset\_of\_slice` 特性下支持这一点……但如果你希望在稳定版 Rust 上获得这种行为呢?请看(也可以在 Rust Playground(https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=05491a0a69fb38c99d1dde6fdf0051ee)上测试):
```
macro_rules! fun_offset_of {
($t:ty, $field:ident) => {
const {
// 我们可以在此内部玩耍的空白区域。
const EMPTY: &[u8] = &[0u8; 65536];
let empty: *const [u8] = &raw const *EMPTY;
// 只有当 `$t` 是 `Sized` 或具有切片元数据时,此转换才能编译。
let container = empty as *const $t;
// 现在,提取该字段。从技术上讲这是 `unsafe`,
// 但由于我们在 `const` 块中,未定义行为将导致编译错误。
let field = unsafe { &raw const (*container).$field };
// 计算 `container` 和 `field` 之间的偏移量,以字节为单位。
// 我们满足 `offset_from` 的基本要求:两者都源自同一分配 `*EMPTY`。
let container = container.cast::<u8>();
let field = field.cast::<u8>();
unsafe { field.offset_from(container) }
}
};
}
```
思路很简单:我们可以构造一个包含类型的假实例,获取指向其内部字段的指针,然后返回该指针的偏移量。所有这些操作都可以在编译时完成!由于指针转换的有趣语义(https://doc.rust-lang.org/reference/expressions/operator-expr.html#pointer-to-pointer-cast),该宏仅当包含类型是 `Sized` 或以切片字段结尾(这正是我们想要支持的情况)时才能编译。
不幸的是,Rust 要求(对于 `&raw const (*container).$field`)假实例必须指向有效内存,并且偏移量必须在界限内。我们不能仅仅使用空指针。它不关心该内存的内容,所以我们简单处理,构建一个大字节数组。字段的偏移量必须位于此内存内,因此大于 64k 的偏移量无法工作,但我认为这是一个可管理的限制。编译时的未定义行为(总是)会变成编译错误!
在 `std::mem::offset\_of\!` 稳定之前,有几个 crate 提供了类似功能,例如 `offset`(https://docs.rs/offset)、`memoffset`(https://docs.rs/memoffset)、`repr\_offset`(https://docs.rs/repr_offset)等。据我所知,它们现在大多只使用 `std::mem::offset\_of\!`,并且从未支持过获取切片字段的偏移量。也许以前有人发明过类似的技巧并将其隐藏在一个不相关的 crate 中,但我没有找到明显的发现。
我并不需要这个功能,但想出来还是挺有趣的。如果这个故事需要一个寓意,那就是你应该享受乐趣并犯下 Rust 罪行 :3
相似文章
在Rust中的缓存感知数据布局:字段分区、伪共享与128字节规则
这篇博客文章解释了Rust中针对多线程结构的缓存感知数据布局,涵盖了字段分区和避免伪共享的128字节规则,并以一个SPSC环形缓冲区为例。
Rust 中的安全 SIMD,即使内部也安全
Rust 的 SIMD 抽象现在允许在不使用 unsafe 代码的情况下安全使用,这得益于 Rust 1.87 引入的 CPU 特性令牌,从而实现了简洁且可移植的向量操作。
Only Bounds
这篇博客文章介绍了'only bounds',这是对Rust泛型系统的一个提议改进,它用一组更丰富的大小特性代替了当前的Sized/?Sized层次结构,以适应动态大小类型和ARM's Scalable Vector Extension。
不是我,是编译器
一位Rust程序员发现了一个编译器错误,其中将'bool as u32'进行类型转换会产生不正确的结果,导致解析器错误。该错误已报告并链接到GitHub issue #158206。
安全 Rust 的边界
TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。