类型布局
类型的布局是指其大小、对齐方式及其字段的相对偏移量。对于枚举,判别式如何布局和解释也是类型布局的一部分。
类型布局可能在每次编译时更改。我们只记录今天保证的内容,而不是试图确切地记录所做的事情。
请注意,即使布局相同的类型,在跨函数边界传递时也可能有所不同。有关类型的函数调用 ABI 兼容性,请参阅此处。
大小和对齐
所有值都具有对齐方式和大小。
值的对齐方式指定了存储该值的有效地址。对齐方式为 n
的值只能存储在 n
的倍数的地址上。例如,对齐方式为 2 的值必须存储在偶数地址,而对齐方式为 1 的值可以存储在任何地址。对齐方式以字节为单位度量,并且必须至少为 1,并且始终是 2 的幂。可以使用 align_of_val
函数检查值的对齐方式。
值的大小是具有该项类型的数组中,连续元素之间(包括对齐填充)的字节偏移量。值的大小始终是其对齐方式的倍数。请注意,某些类型是零大小的;0 被认为是任何对齐方式的倍数(例如,在某些平台上,类型 [u16; 0]
的大小为 0,对齐方式为 2)。可以使用 size_of_val
函数检查值的大小。
所有值都具有相同大小和对齐方式,并且两者在编译时都已知的类型,实现了 Sized
特征,并且可以使用 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 个字节。
原始类型的对齐方式是平台特定的。在大多数情况下,它们的对齐方式等于它们的大小,但可能小于它们的大小。特别是,即使 i128
和 u128
的大小为 16,它们通常也对齐到 4 或 8 个字节,并且在许多 32 位平台上,i64
、u64
和 f64
仅对齐到 4 个字节,而不是 8 个字节。
指针和引用布局
指针和引用具有相同的布局。指针或引用的可变性不会改变布局。
指向定大小类型的指针与 usize
具有相同的大小和对齐方式。
指向不定大小类型的指针是定大小的。保证大小和对齐方式至少等于指针的大小和对齐方式。
注意:虽然你不应该依赖这一点,但目前所有指向DST的指针的大小都是
usize
大小的两倍,并且具有相同的对齐方式。
数组布局
[T; N]
数组的大小为 size_of::<T>() * N
,并且具有与 T
相同的对齐方式。数组的布局方式是,数组中从零开始的第 n
个元素从数组的开头偏移 n * size_of::<T>()
个字节。
切片布局
切片与它们切片的数组部分具有相同的布局。
注意:这是关于原始的
[T]
类型,而不是指向切片的指针(&[T]
、Box<[T]>
等)。
str
布局
字符串切片是字符的 UTF-8 表示形式,其布局与 [u8]
类型的切片相同。
元组布局
元组根据Rust
表示形式进行布局。
此规则的例外是单元元组 (()
),它被保证为零大小类型,其大小为 0,对齐方式为 1。
特征对象布局
特征对象与特征对象的值具有相同的布局。
注意:这是关于原始特征对象类型,而不是指向特征对象的指针(
&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 } }
注意:由于表示形式是项上的属性,因此表示形式不依赖于泛型参数。任何两个具有相同名称的类型都具有相同的表示形式。例如,
Foo<Bar>
和Foo<Baz>
都具有相同的表示形式。
类型的表示形式可以更改字段之间的填充,但不会更改字段本身的布局。例如,具有 C
表示形式的结构体,其中包含具有 Rust
表示形式的结构体 Inner
,将不会更改 Inner
的布局。
Rust
表示形式
Rust
表示形式是名义类型(nominal types)的默认表示形式,没有 repr
属性。通过 repr
属性显式使用此表示形式保证与完全省略该属性相同。
此表示形式做出的唯一数据布局保证是健全性所需的内容。它们是
- 字段已正确对齐。
- 字段不重叠。
- 类型的对齐方式至少是其字段的最大对齐方式。
形式上,第一个保证意味着任何字段的偏移量都可被该字段的对齐方式整除。
第二个保证意味着可以对字段进行排序,使得任何字段的偏移量加上大小小于或等于排序中下一个字段的偏移量。排序不必与类型声明中指定的字段顺序相同。
请注意,第二个保证并不意味着字段具有不同的地址:零大小类型可能与同一结构体中的其他字段具有相同的地址。
此表示形式没有做出其他数据布局保证。
C
表示形式
C
表示形式旨在用于双重目的。一个目的是创建可与 C 语言互操作的类型。第二个目的是创建可以可靠地执行依赖于数据布局的操作的类型,例如将值重新解释为不同的类型。
由于这种双重目的,可能会创建对与 C 编程语言接口无用的类型。
此表示形式可以应用于结构体、联合体和枚举。例外是零变体枚举,对于它们,C
表示形式是一个错误。
#[repr(C)]
结构体
结构体的对齐方式是其中对齐方式最大的字段的对齐方式。
字段的大小和偏移量由以下算法确定。
从 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);
警告: 此伪代码使用了一个幼稚的算法,为了清晰起见,忽略了溢出问题。要在实际代码中执行内存布局计算,请使用
Layout
。
注意:此算法可以生成零大小的结构体。在 C 语言中,像
struct Foo { }
这样的空结构体声明是非法的。但是,gcc 和 clang 都支持启用此类结构体的选项,并为其分配大小零。相比之下,C++ 给空结构体的大小为 1,除非它们是从继承而来的,或者是具有[[no_unique_address]]
属性的字段,在这种情况下,它们不会增加结构体的总大小。
#[repr(C)]
联合体
使用 #[repr(C)]
声明的联合体将具有与目标平台上 C 语言中的等效 C 联合体声明相同的大小和对齐方式。
联合体的大小将是其所有字段的最大大小(舍入到其对齐方式),对齐方式将是其所有字段的最大对齐方式。这些最大值可能来自不同的字段。
#![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 #[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 }
#[repr(C)]
无字段枚举
对于无字段枚举,C
表示形式具有目标平台 C ABI 的默认 enum
大小和对齐方式的大小和对齐方式。
注意:C 中的枚举表示形式是实现定义的,因此这实际上是“最佳猜测”。特别是,当感兴趣的 C 代码使用某些标志编译时,这可能是错误的。
警告: C 语言中的
enum
和 Rust 的无字段枚举(使用此表示形式)之间存在关键差异。C 中的enum
主要是typedef
加上一些命名常量;换句话说,enum
类型的对象可以保存任何整数值。例如,这通常用于C
中的位标志。相比之下,Rust 的无字段枚举只能合法地保存判别式值,其他一切都是未定义行为。因此,在 FFI 中使用无字段枚举来建模 Cenum
通常是错误的。
#[repr(C)]
带字段枚举
具有字段的 repr(C)
枚举的表示形式是带有两个字段的 repr(C)
结构体,在 C 中也称为“标记联合体”
- 一个删除了所有字段的
repr(C)
版本的枚举(“标记”)
- 每个具有字段的变体的字段的
repr(C)
结构体的repr(C)
联合体(“有效负载”)
注意:由于
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; }
注意:具有非
Copy
字段的union
是不稳定的,请参阅 55149。
原始表示形式
原始表示形式是与原始整数类型同名的表示形式。即:u8
、u16
、u32
、u64
、u128
、usize
、i8
、i16
、i32
、i64
、i128
和 isize
。
原始表示形式只能应用于枚举,并且枚举是否有字段具有不同的行为。零变体枚举具有原始表示形式是一个错误。将两个原始表示形式组合在一起是一个错误。
无字段枚举的原始表示形式
对于无字段枚举,原始表示形式将大小和对齐方式设置为与同名的原始类型相同。例如,具有 u8
表示形式的无字段枚举只能具有介于 0 和 255 之间的判别式。
带字段枚举的原始表示形式
原始表示形式枚举的表示形式是每个具有字段的变体的 repr(C)
结构体的 repr(C)
联合体。联合体中每个结构体的第一个字段是删除了所有字段的枚举的原始表示形式版本(“标记”),其余字段是该变体的字段。
注意:如果标记在联合体中被赋予了自己的成员,则此表示形式不会更改,如果这可以使操作对你来说更清晰(尽管为了遵循 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); }
注意:具有非
Copy
字段的union
是不稳定的,请参阅 55149。
将带字段枚举的原始表示形式与 #[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 dependant 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))]
。对齐值必须是 2 的幂,从 1 到 229。对于 packed
,如果未给出值,如 #[repr(packed)]
中所示,则值为 1。
对于 align
,如果指定的对齐方式小于没有 align
修饰符的类型的对齐方式,则对齐方式不受影响。
对于 packed
,如果指定的对齐方式大于没有 packed
修饰符的类型的对齐方式,则对齐方式和布局不受影响。
每个字段的对齐方式(用于定位字段)是指定的对齐方式和字段类型对齐方式中的较小者。
保证字段间填充是满足每个字段(可能已更改)的对齐方式所需的最小值(尽管请注意,packed
本身不提供有关字段顺序的任何保证)。这些规则的一个重要结果是,具有 #[repr(packed(1))]
(或 #[repr(packed)]
)的类型将没有字段间填充。
align
和 packed
修饰符不能应用于同一类型,并且 packed
类型不能传递地包含另一个 align
类型。align
和 packed
只能应用于 Rust
和 C
表示形式。
align
修饰符也可以应用于 enum
。应用时,对 enum
对齐方式的影响与将 enum
包装在具有相同 align
修饰符的 newtype struct
中相同。
注意:不允许引用未对齐的字段,因为它是未定义行为。当字段由于对齐方式修饰符而未对齐时,请考虑以下使用引用和解引用的选项
#![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,否则具有单元类型的布局和 ABI。
这与 C
表示形式不同,因为具有 C
表示形式的结构体将始终具有 C
struct
的 ABI,而例如,具有 transparent
表示形式且具有原始类型字段的结构体将具有原始类型字段的 ABI。
由于此表示形式将类型布局委托给另一种类型,因此它不能与任何其他表示形式一起使用。