类型布局
类型的布局是其大小、对齐方式以及其字段的相对偏移量。对于枚举,判别式的布局和解释方式也是类型布局的一部分。
类型布局可以随着每次编译而改变。我们不试图准确地记录所做的工作,而只是记录今天保证的内容。
大小和对齐方式
所有值都有对齐方式和大小。
值的*对齐方式*指定了哪些地址可以用来存储该值。对齐方式为 `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
通常对齐到 4 或 8 个字节,即使它们的大小为 16 个字节,并且在许多 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
)都有一个*表示形式*,用于指定该类型的布局。类型的可能表示形式如下:
可以通过对类型应用 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
表示形式的结构体包含一个具有默认表示形式的结构体 Inner
,这不会更改 Inner
的布局。
Rust
表示形式
Rust
表示形式是没有 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 中使用无字段枚举来模拟 C enum
通常是错误的。
#[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))]
形式的整数参数指定。对齐值必须是 1 到 229 之间的 2 的幂。对于 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 = std::ptr::addr_of!(e.f2); let value = unsafe { ptr.read_unaligned() }; let mut_ptr: *mut u16 = std::ptr::addr_of_mut!(e.f2); unsafe { mut_ptr.write_unaligned(3) } }
transparent
表示形式
transparent
表示形式只能用于具有以下特征的 struct
或具有单个变体的 enum
- 具有非零大小的单个字段,以及
- 任意数量的大小为 0 且对齐方式为 1 的字段(例如
PhantomData<T>
)。
具有此表示形式的结构体和枚举与单个非零大小字段具有相同的布局和 ABI。
这与 C
表示形式不同,因为具有 C
表示形式的结构体将始终具有 C
struct
的 ABI,而例如,具有带有基元字段的 transparent
表示形式的结构体将具有基元字段的 ABI。
因为此表示形式将类型布局委托给另一种类型,所以它不能与任何其他表示形式一起使用。