类型布局
类型的布局(Layout)是其大小(Size)、对齐(Alignment)和字段的相对偏移量。对于枚举,判别值(Discriminant)的布局和解释也是类型布局的一部分。
类型布局可以在每次编译时更改。我们只记录今天保证的内容,而不是试图记录具体做了什么。
请注意,即使具有相同布局的类型在跨函数边界传递时仍可能有所不同。有关类型的函数调用 ABI 兼容性,请参阅这里。
大小和对齐
所有值都具有对齐和大小。
值的对齐指定存储该值的有效地址。对齐为 n 的值必须仅存储在 n 的倍数的地址处。例如,对齐为 2 的值必须存储在偶数地址处,而对齐为 1 的值可以存储在任何地址处。对齐以字节为单位,必须至少为 1,且始终为 2 的幂。值的对齐可以用 align_of_val 函数检查。
值的大小是具有该项类型的数组中连续元素之间的字节偏移量,包括对齐填充。值的大小始终是其对齐的倍数。请注意,某些类型是零大小的;0 被认为是任何对齐的倍数(例如,在某些平台上,类型 [u16; 0] 的大小为 0,对齐为 2)。值的大小可以用 size_of_val 函数检查。
所有值具有相同大小和对齐,并且两者在编译时已知的类型实现 Sized trait,可以用 size_of 和 align_of 函数检查。不是 Sized 的类型称为动态大小类型。由于 Sized 类型的所有值共享相同的大小和对齐,我们将这些共享值分别称为类型的大小和类型的对齐。
原始数据布局
大多数原始类型的大小在此表中给出。
| 类型 | size_of::<Type>() |
|---|---|
bool | 1 |
u8 / i8 | 1 |
u16 / i16 | 2 |
u32 / i32 | 4 |
u64 / i64 | 8 |
u128 / i128 | 16 |
usize / isize | 见下文 |
f32 | 4 |
f64 | 8 |
char | 4 |
usize 和 isize 的大小足以包含目标平台上的每个地址。例如,在 32 位目标上,这是 4 字节,在 64 位目标上,这是 8 字节。
usize 和 isize 具有相同的大小和对齐。
原始类型的对齐是特定于平台的。在大多数情况下,它们的对齐等于它们的大小,但可能更小。特别是,i128 和 u128 通常对齐到 4 或 8 字节,即使它们的大小是 16,并且在许多 32 位平台上,i64、u64 和 f64 仅对齐到 4 字节,而不是 8。
对于相同指定大小的固定宽度有符号和无符号整数变体,对齐保证相同 — 也就是说,对于给定大小 N,align_of::<uN>() == align_of::<iN>()。
指针和引用布局
指针和引用具有相同的布局。指针或引用的可变性不会改变布局。
指向有大小类型的指针具有与 usize 相同的大小和对齐。
指向未大小类型的指针是有大小的。指向未大小类型的指针的大小和对齐保证分别大于或等于指向有大小类型的指针的大小和对齐。
Note
虽然您不应该依赖于此,但目前所有指向 DST 的指针都是
usize大小的两倍,并且具有相同的对齐。
数组布局
[T; N] 的数组大小为 size_of::<T>() * N,对齐与 T 相同。数组的布局使得数组的从零开始的第 n 个元素从数组开头偏移 n * size_of::<T>() 字节。
切片布局
切片与它们切片的数组部分具有相同的布局。
Note
这是关于原始
[T]类型,而不是指向切片的指针(&[T]、Box<[T]>等)。
str 布局
字符串切片是字符的 UTF-8 表示,与 [u8] 类型的切片具有相同的布局。引用 &str 与引用 &[u8] 具有相同的布局。
元组布局
元组根据 Rust 表示布局。
此规则的例外是单元元组(()),它作为零大小类型保证大小为 0,对齐为 1。
Trait 对象布局
Trait 对象与 trait 对象所具有的值具有相同的布局。
Note
这是关于原始 trait 对象类型,而不是指向 trait 对象的指针(
&dyn Trait、Box<dyn Trait>等)。
闭包布局
闭包没有布局保证。
表示
所有用户定义的复合类型(struct、enum 和 union)都具有指定类型布局的表示。
类型的可能表示是:
Rust(默认)C- 原始表示
transparent
可以通过将 repr 属性应用于类型来更改类型的表示。以下示例显示了具有 C 表示的结构体。
#![allow(unused)]
fn main() {
#[repr(C)]
struct ThreeInts {
first: i16,
second: i8,
third: i32
}
}
对齐可以分别使用 align 和 packed 修饰符提高或降低。它们会改变属性中指定的表示。如果未指定表示,则改变默认表示。
#![allow(unused)]
fn main() {
// Default representation, alignment lowered to 2.
#[repr(packed(2))]
struct PackedStruct {
first: i16,
second: i8,
third: i32
}
// C representation, alignment raised to 8
#[repr(C, align(8))]
struct AlignedStruct {
first: i16,
second: i8,
third: i32
}
}
Note
由于表示是项上的属性,因此表示不依赖于泛型参数。任何两个具有相同名称的类型具有相同的表示。例如,
Foo<Bar>和Foo<Baz>都具有相同的表示。
类型的表示可以更改字段之间的填充,但不会更改字段本身的布局。例如,包含具有 Rust 表示的结构体 Inner 的具有 C 表示的结构体不会更改 Inner 的布局。
Rust 表示
Rust 表示是没有 repr 属性的具名类型的默认表示。通过 repr 属性显式使用此表示保证与完全省略属性相同。
此表示做出的唯一数据布局保证是健全性所需的那些。这些是:
- 字段的偏移量可被该字段的对齐整除。
- 类型的对齐至少是其字段的最大对齐。
对于结构体,进一步保证字段不重叠。也就是说,字段可以排序,使得任何字段的偏移量加上大小小于或等于排序中下一个字段的偏移量。排序不必与类型声明中指定的字段顺序相同。
请注意,此保证并不意味着字段具有不同的地址:零大小类型可能与同一结构体中的其他字段具有相同的地址。
此表示不提供其他数据布局保证。
C 表示
C 表示设计用于双重目的。一个目的是创建与 C 语言互操作的类型。第二个目的是创建可以安全地执行依赖于数据布局的操作(例如将值重新解释为不同类型)的类型。
由于这种双重目的,可以创建对与 C 编程语言接口无用的类型。
此表示可以应用于结构体、联合体和枚举。例外是零变体枚举,对于它们 C 表示是错误的。
#[repr(C)] 结构体
结构体的对齐是其中对齐最大的字段的对齐,如果没有字段则为 1。
字段的大小和偏移量由以下算法确定。
从当前偏移量 0 字节开始。
对于结构体中按声明顺序的每个字段,首先确定字段的大小和对齐。如果当前偏移量不是字段对齐的倍数,则向当前偏移量添加填充字节,直到它是字段对齐的倍数。字段的偏移量是当前偏移量的值。然后将当前偏移量增加字段的大小。
最后,结构体的大小是当前偏移量向上舍入到结构体对齐的最近倍数。
以下是用伪代码描述的此算法。
/// Returns the amount of padding needed after `offset` to ensure that the
/// following address will be aligned to `alignment`.
fn padding_needed_for(offset: usize, alignment: usize) -> usize {
let misalignment = offset % alignment;
if misalignment > 0 {
// round up to next multiple of `alignment`
alignment - misalignment
} else {
// already a multiple of `alignment`
0
}
}
struct.alignment = struct.fields().map(|field| field.alignment).max();
let current_offset = 0;
for field in struct.fields_in_declaration_order() {
// Increase the current offset so that it's a multiple of the alignment
// of this field. For the first field, this will always be zero.
// The skipped bytes are called padding bytes.
current_offset += padding_needed_for(current_offset, field.alignment);
struct[field].offset = current_offset;
current_offset += field.size;
}
struct.size = current_offset + padding_needed_for(current_offset, struct.alignment);
Warning
此伪代码使用朴素算法,为了清晰起见忽略了溢出问题。要在实际代码中执行内存布局计算,请使用
Layout。
Note
此算法可以产生零大小的结构体。在 C 中,像
struct Foo { }这样的空结构体声明是非法的。但是,gcc 和 clang 都支持启用此类结构体的选项,并将它们的大小分配为零。相比之下,C++ 给空结构体大小为 1,除非它们被继承或它们是具有[[no_unique_address]]属性的字段,在这种情况下它们不会增加结构体的总大小。
#[repr(C)] 联合体
使用 #[repr(C)] 声明的联合体将具有与目标平台 C 语言中等效 C 联合体声明相同的大小和对齐。
联合体的大小为其所有字段的最大大小舍入到其对齐,对齐为其所有字段的最大对齐。这些最大值可能来自不同的字段。每个字段位于联合体开头的字节偏移量 0 处。
#![allow(unused)]
fn main() {
#[repr(C)]
union Union {
f1: u16,
f2: [u8; 4],
}
assert_eq!(std::mem::size_of::<Union>(), 4); // From f2
assert_eq!(std::mem::align_of::<Union>(), 2); // From f1
assert_eq!(std::mem::offset_of!(Union, f1), 0);
assert_eq!(std::mem::offset_of!(Union, f2), 0);
#[repr(C)]
union SizeRoundedUp {
a: u32,
b: [u16; 3],
}
assert_eq!(std::mem::size_of::<SizeRoundedUp>(), 8); // Size of 6 from b,
// rounded up to 8 from
// alignment of a.
assert_eq!(std::mem::align_of::<SizeRoundedUp>(), 4); // From a
assert_eq!(std::mem::offset_of!(SizeRoundedUp, a), 0);
assert_eq!(std::mem::offset_of!(SizeRoundedUp, b), 0);
}
#[repr(C)] 无字段枚举
对于无字段枚举,C 表示具有目标平台 C ABI 的默认 enum 大小和对齐。
Note
C 中的枚举表示是实现定义的,所以这实际上是一个“最佳猜测“。特别是,当感兴趣的 C 代码使用某些标志编译时,这可能是不正确的。
Warning
C 语言中的
enum和 Rust 的具有此表示的无字段枚举之间存在关键差异。C 中的enum主要是typedef加上一些命名常量;换句话说,enum类型的对象可以持有任何整数值。例如,这在C中经常用于位标志。相比之下,Rust 的无字段枚举只能合法地持有判别值,其他一切都是未定义行为。因此,在 FFI 中使用无字段枚举来建模 Cenum通常是错误的。
#[repr(C)] 带字段的枚举
带字段的 repr(C) 枚举的表示是一个具有两个字段的 repr(C) 结构体,在 C 中也称为“标记联合体“:
- 枚举的
repr(C)版本,所有字段已移除(“标记”)
- 每个具有字段的变体的字段的
repr(C)结构体的repr(C)联合体(“有效载荷”)
Note
由于
repr(C)结构体和联合体的表示,如果变体具有单个字段,将该字段直接放在联合体中或将其包装在结构体中没有区别;因此,希望操作此类enum表示的任何系统可以使用对他们来说更方便或更一致的任何形式。
#![allow(unused)]
fn main() {
// This Enum has the same representation as ...
#[repr(C)]
enum MyEnum {
A(u32),
B(f32, u64),
C { x: u32, y: u8 },
D,
}
// ... this struct.
#[repr(C)]
struct MyEnumRepr {
tag: MyEnumDiscriminant,
payload: MyEnumFields,
}
// This is the discriminant enum.
#[repr(C)]
enum MyEnumDiscriminant { A, B, C, D }
// This is the variant union.
#[repr(C)]
union MyEnumFields {
A: MyAFields,
B: MyBFields,
C: MyCFields,
D: MyDFields,
}
#[repr(C)]
#[derive(Copy, Clone)]
struct MyAFields(u32);
#[repr(C)]
#[derive(Copy, Clone)]
struct MyBFields(f32, u64);
#[repr(C)]
#[derive(Copy, Clone)]
struct MyCFields { x: u32, y: u8 }
// This struct could be omitted (it is a zero-sized type), and it must be in
// C/C++ headers.
#[repr(C)]
#[derive(Copy, Clone)]
struct MyDFields;
}
原始表示
原始表示是与原始整数类型同名的表示。即:u8、u16、u32、u64、u128、usize、i8、i16、i32、i64、i128 和 isize。
原始表示只能应用于枚举,并且根据枚举是否有字段具有不同的行为。零变体枚举具有原始表示是错误的。将两个原始表示组合在一起是错误的。
无字段枚举的原始表示
对于无字段枚举,原始表示将大小和对齐设置为与同名原始类型相同。例如,具有 u8 表示的无字段枚举只能在 0 到 255(含)之间具有判别值。
带字段枚举的原始表示
原始表示枚举的表示是每个具有字段的变体的 repr(C) 结构体的 repr(C) 联合体。联合体中每个结构体的第一个字段是枚举的原始表示版本,所有字段已移除(“标记”),其余字段是该变体的字段。
Note
如果标记在联合体中拥有自己的成员,此表示是不变的,如果这使操作对您更清晰的话(尽管要遵循 C++ 标准,标记成员应包装在
struct中)。
#![allow(unused)]
fn main() {
// This enum has the same representation as ...
#[repr(u8)]
enum MyEnum {
A(u32),
B(f32, u64),
C { x: u32, y: u8 },
D,
}
// ... this union.
#[repr(C)]
union MyEnumRepr {
A: MyVariantA,
B: MyVariantB,
C: MyVariantC,
D: MyVariantD,
}
// This is the discriminant enum.
#[repr(u8)]
#[derive(Copy, Clone)]
enum MyEnumDiscriminant { A, B, C, D }
#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantA(MyEnumDiscriminant, u32);
#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantB(MyEnumDiscriminant, f32, u64);
#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantC { tag: MyEnumDiscriminant, x: u32, y: u8 }
#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantD(MyEnumDiscriminant);
}
将带字段枚举的原始表示与 #[repr(C)] 结合
对于带字段的枚举,也可以将 repr(C) 和原始表示组合(例如 repr(C, u8))。这通过将判别枚举的表示更改为所选原始类型来修改 repr(C)。因此,如果您选择 u8 表示,则判别枚举将具有 1 字节的大小和对齐。
前面示例中的判别枚举变为:
#![allow(unused)]
fn main() {
#[repr(C, u8)] // `u8` was added
enum MyEnum {
A(u32),
B(f32, u64),
C { x: u32, y: u8 },
D,
}
// ...
#[repr(u8)] // So `u8` is used here instead of `C`
enum MyEnumDiscriminant { A, B, C, D }
// ...
}
例如,使用 repr(C, u8) 枚举不可能有 257 个唯一判别值(“标记”),而仅具有 repr(C) 属性的相同枚举将没有任何问题地编译。
除了 repr(C) 之外使用原始表示可以更改枚举从 repr(C) 形式的大小:
#![allow(unused)]
fn main() {
#[repr(C)]
enum EnumC {
Variant0(u8),
Variant1,
}
#[repr(C, u8)]
enum Enum8 {
Variant0(u8),
Variant1,
}
#[repr(C, u16)]
enum Enum16 {
Variant0(u8),
Variant1,
}
// The size of the C representation is platform dependent
assert_eq!(std::mem::size_of::<EnumC>(), 8);
// One byte for the discriminant and one byte for the value in Enum8::Variant0
assert_eq!(std::mem::size_of::<Enum8>(), 2);
// Two bytes for the discriminant and one byte for the value in Enum16::Variant0
// plus one byte of padding.
assert_eq!(std::mem::size_of::<Enum16>(), 4);
}
对齐修饰符
align 和 packed 修饰符可用于分别提高或降低 struct 和 union 的对齐。packed 也可以更改字段之间的填充(尽管它不会更改任何字段内部的填充)。单独使用时,align 和 packed 不提供关于结构体布局中字段顺序或枚举变体布局的保证,尽管它们可以与提供此类保证的表示(如 C)组合使用。
对齐以整数参数形式指定,形式为 #[repr(align(x))] 或 #[repr(packed(x))]。对齐值必须是从 1 到 229 的 2 的幂。对于 packed,如果未给出值(如 #[repr(packed)]),则值为 1。
对于 align,如果指定的对齐小于没有 align 修饰符的类型的对齐,则对齐不受影响。
对于 packed,如果指定的对齐大于没有 packed 修饰符的类型的对齐,则对齐和布局不受影响。
出于定位字段的目的,每个字段的对齐是指定对齐和字段类型对齐中的较小者。
字段间填充保证是满足每个字段(可能更改的)对齐所需的最小值(尽管请注意,单独使用时,packed 不提供关于字段顺序的任何保证)。这些规则的一个重要后果是,具有 #[repr(packed(1))](或 #[repr(packed)])的类型将没有字段间填充。
align 和 packed 修饰符不能应用于同一类型,并且 packed 类型不能传递地包含另一个 aligned 类型。align 和 packed 只能应用于 Rust 和 C 表示。
align 修饰符也可以应用于 enum。当应用时,对 enum 对齐的效果与将 enum 包装在具有相同 align 修饰符的新类型 struct 中相同。
Note
不允许引用未对齐的字段,因为这是未定义行为。当字段由于对齐修饰符而未对齐时,请考虑以下使用引用和解引用的选项:
#![allow(unused)] fn main() { #[repr(packed)] struct Packed { f1: u8, f2: u16, } let mut e = Packed { f1: 1, f2: 2 }; // Instead of creating a reference to a field, copy the value to a local variable. let x = e.f2; // Or in situations like `println!` which creates a reference, use braces // to change it to a copy of the value. println!("{}", {e.f2}); // Or if you need a pointer, use the unaligned methods for reading and writing // instead of dereferencing the pointer directly. let ptr: *const u16 = &raw const e.f2; let value = unsafe { ptr.read_unaligned() }; let mut_ptr: *mut u16 = &raw mut e.f2; unsafe { mut_ptr.write_unaligned(3) } }
transparent 表示
transparent 表示只能用于具有单个变体的 struct 或 enum,该变体具有:
- 任意数量的大小为 0 且对齐为 1 的字段(例如
PhantomData<T>),以及 - 最多一个其他字段。
具有此表示的结构体和枚举与唯一的非大小 0 非对齐 1 字段(如果存在)或单元(否则)具有相同的布局和 ABI。
这与 C 表示不同,因为具有 C 表示的结构体将始终具有 C struct 的 ABI,而例如,具有原始字段的 transparent 表示的结构体将具有原始字段的 ABI。
由于此表示将类型布局委托给另一个类型,因此不能与任何其他表示一起使用。