Rust黑魔法(2)
Rust黑魔法(2) —— 深入 repr(...):Rust 数据布局真正的控制面板
为什么会出现repr(...)
Rust 默认使用 优化布局(optimized layout):
- 字段顺序可以被重排(为了减少 padding)
- 对齐由平台 ABI+ Rust 自己的规则决定
- 枚举的表示(tag、payload)是可优化的,不保证稳定
- ZST、DST 会被做特殊处理
我们在代码里面构造的结构体:
struct Foo {
a: u8,
b: u32,
c: u16,
}Rust 内部其实是把它变成这样:
a: u8 padding(3)
b: u32
c: u16 padding(2)
大小 12,对齐 4Rust 甚至可以重排成 (u32,b),(u16,c),(u8,a) 以达到更佳布局。也就是说默认 repr(Rust) 完全不保证布局稳定性,甚至两个版本的 Rust 编译器可能排出不同顺序。这就是为什么我们需要 repr(...)。 以下我们逐个介绍Rust支持的repr
repr(C)
当我们需要去和C语言进行交互的时候,数据的调用以及数据布局也要兼顾C语言,那么Rust提供了这么一个方式,于此同时它还为 Rust 类型给予:
- 稳定 ABI
- 字段顺序固定
- 对齐行为与 C ABI 一致
- padding 行为与 C ABI 一致
- 无重排
- 枚举使用 C-style tag + payload(不做 niche 优化[^1]) 最主要是,Rust 1.77 明确宣布不保证默认 repr 的 ABI 在未来版本稳定。所以要采用FFI的形式,交互的结构体必须
repr(C),否则会变成UB(未定义行为)。以下是例子:
#[repr(C)]
struct Foo {
a: u8,
b: u32,
c: u16,
}对应的布局:
offset 0: a (u8)
offset 1: padding
offset 2–3: b (u16)
offset 4–7: c (u32)
size = 8
align = 4repr(transparent)
你的例子 OK,我补强更黑魔法的内容。 这个注解仅用于一个元组结构体:
rust
#[repr(transparent)]
struct FooT(u32);布局和u32保持一致:
完全与 u32 相同
size = 4
align = 4那它可以干什么用呢?它允许把一个类型“安全外壳化”而不改变 ABI:
#[repr(transparent)] struct NonZeroI32(std::num::NonZeroI32);设置完成之后,编译器保证:
- 大小、对齐完全等同内部第一个字段
- 可以在 FFI 中无成本转化 透明 repr 在两个地方极度重要
自定义 newtype FFI 包装器
例如:
#[repr(transparent)] struct CStringRef(*const c_char);你可以安全地在 C ↔ Rust 之间无障碍传输。
自定义类型增强(zero-cost wrapper)
比如:
#[repr(transparent)] struct UserId(u64);ABI 与 u64 完全一样,因此:
- 没有额外开销
- 在内存中等价,但在类型系统中有强约束
repr(u*) / repr(i*)
而这两个注解只作用于枚举类型,并且根据计算得出枚举数据最大布局:
#[repr(u8)]
enum FooU {
A(Foo),
B,
}实际布局:
A: 1-byte 用于标签 + 枚举包裹的类型大小 (8 bytes)
B: 标签 (1 byte)
最终大小 = 1 + 8 = 9
因为需要和二进制对齐,所以:align(4) = 12 bytes
align = 4它决定:
- 枚举 discriminant 的宽度(1/2/4/8 bytes)
- 禁止 niche 优化[^1]
- 禁止布局压缩
niche优化[^1]会被关闭
例如 Option<&T> 本来可以缩成一个指针(null niche)。
但如果你强制 repr(u8),niche优化[^1]就没了。
repr(packed) / repr(packed(n))
repr(packed)
这个表示结构体完全关闭数据对齐,注意,一旦采用这个注解,就会导致UB(因为在某些arm架构下,架构要求强制对齐,但是这里设置之后就会导致结构体解析错误)
#[repr(C, packed)]
struct FooPacked {
a: u8,
b: u16,
c: u32,
}布局:
offset 0: a (u8)
offset 1–2: b (u16) <-- 不对齐
offset 3–6: c (u32) <-- 不对齐
size = 7
align = 1repr(packed(n))
结构体字段必须遵循: field_align = min(original_align, n) 这里的n一般是2的倍数,与上面一个的区别在于降低而不是移除对齐,当时强制对齐的长度为n,以下采用n=2为例。
#[repr(C, packed(2))]
struct FooPacked2 {
a: u8,
b: u16,
c: u32,
}布局:
offset 0: a (u8)
offset 1: padding
offset 2–3: b (u16)
offset 4–7: c (u32) <-- 原要对齐 4,但被限制为 2
size = 8
align = 2这个对于repr(packed)好处:
- 不像 repr(packed) 那样完全失去对齐
- 兼容一些二进制协议(例如 2-byte 对齐) 坏处:
- 依旧可能出现 misaligned
- 某些字段对齐仍然不足,需要
read_unaligned
repr(align(n))
这个和repr(C)十分类似,align(n) 不改变字段顺序,也不改变字段布局,它只改变整个 struct的最小对齐要求。通常用于SIMD/FFI。
#[repr(C, align(8))]
struct FooAlign8 {
a: u8,
b: u16,
c: u32,
}布局:
offset 0: a (u8)
offset 1: padding
offset 2–3: b (u16)
offset 4–7: c (u32)
offset 8–15: struct-level padding
size = 16
align = 8各 repr 的总结表
| repr | 作用 | 是否稳定ABI | 是否允许重排 | niche 优化[^1] |
|---|---|---|---|---|
| default(Rust) | 最优布局 | ❌ | ✔️ | ✔️ |
| C | C ABI | ✔️ | ❌ | ❌ |
| transparent | 透明包装器 | ✔️ | ❌ | ❌ |
| u*/i* | 强制 tag 大小 | ✔️ | ❌ | ❌ |
| packed | 禁用对齐 | ✔️ | ❌ | ❌ |
| packed(n) | 降低对齐 | ✔️ | ❌ | ❌ |
| align(n) | 强制更大对齐 | ✔️ | ✔️ | ✔️ |
主要用途
那这些东西的主要用途是什么呢?分别是以下场景:
- 序列化协议(binary protocol) 例如网络字节流、文件头,用 repr(C)/packed 强制固定布局。
- FFI(C/C++) 与 C ABI 交互时必须 repr(C)/transparent。
- Lock-free / 原子结构 需要 align(16) 保证 atomic128 正常对齐。
- 内存映射文件 (mmap) packed(n) 常用于解析磁盘格式。
- 构建 unsafe DSL 例如用 repr(transparent) 构建零成本 newtype
总结
在第一篇中,我们理解了Rust默认的数据布局是由编译器自动优化、自动重排、自动对齐的; 而本篇介绍的 repr(...),则是让开发者直接干预布局的唯一手段。 repr(C) 保证与 C ABI 一致,repr(transparent) 提供零成本包装,repr(u*/i*) 固定枚举标签大小并关闭优化,repr(packed) 让结构体紧凑但带来未对齐风险,repr(align(n)) 则用于提升整体对齐。这些开关让类型在 FFI、二进制协议、自定义底层结构等场景中变得 可预测、可控、可依赖,而不再受到编译器重排与优化的影响。 如果第一篇回答了“数据在内存中到底长什么样”,那么第二篇回答的是——“我们如何亲手控制它应该长什么样”。
这也是进入 Rust unsafe 世界前,必须掌握的基础能力。
[^1]:什么是 niche 优化?在 Rust 中,一个类型的所有可能取值构成“取值空间”(value space)。如果某个类型的取值空间中,有一些“本来就不存在的值”,那么 Rust 就可以利用这些值来存放额外信息。这些“本来就用不到的值”就叫 niche(空洞/空位)。Rust 编译器会自动利用这些 niche 来压缩复合类型(尤其是 Option<T>),这就是 niche 优化(niche optimization)。