类型布局

类型的布局(layout)是指其大小(size)、对齐(alignment)及其字段的相对偏移。对于枚举(enum),判别式(discriminant)的布局和解释方式也是类型布局的一部分。

类型布局可以在每次编译时更改。我们不试图精确记录编译器如何实现布局,只记录当前保证的布局。

请注意,即使布局相同的类型,在函数边界传递时也可能有所不同。有关类型的函数调用 ABI 兼容性,请参见此处

大小和对齐

所有值都有一个对齐要求和大小。

值的对齐(alignment)指定了存储该值的有效地址。对齐要求为 n 的值只能存储在地址是 n 的倍数的地址上。例如,对齐要求为 2 的值必须存储在偶数地址,而对齐要求为 1 的值可以存储在任意地址。对齐以字节为单位衡量,必须至少为 1,并且始终是 2 的幂。可以使用 align_of_val 函数检查值的对齐要求。

值的大小(size)是指在该项类型的数组中,连续元素之间的字节偏移量,包括对齐填充。值的大小始终是其对齐要求的倍数。请注意,有些类型是零大小的;0 被认为是任何对齐要求的倍数(例如,在某些平台上,类型 [u16; 0] 的大小为 0,对齐要求为 2)。可以使用 size_of_val 函数检查值的大小。

所有值都具有相同大小和对齐要求,并且两者都在编译时已知,这类类型实现了 Sized trait,可以使用 size_ofalign_of 函数进行检查。不是 Sized 的类型称为动态大小类型。由于 Sized 类型的所有值共享相同的大小和对齐要求,我们将这些共享值分别称为类型的大小和类型的对齐要求。

原始数据布局

大多数原始类型的大小如下表所示。

类型size_of::<Type>()
bool1
u8 / i81
u16 / i162
u32 / i324
u64 / i648
u128 / i12816
usize / isize见下文
f324
f648
char4

usizeisize 的大小足以包含目标平台上的任何地址。例如,在 32 位目标平台上,大小为 4 字节;在 64 位目标平台上,大小为 8 字节。

原始类型的对齐要求是平台特定的。大多数情况下,它们的对齐要求等于其大小,但也可能小于大小。特别是,i128u128 尽管大小为 16 字节,但通常对齐到 4 或 8 字节;在许多 32 位平台上,i64u64f64 只对齐到 4 字节,而不是 8 字节。

指针和引用布局

指针和引用具有相同的布局。指针或引用的可变性不会改变布局。

指向 Sized 类型的指针与 usize 具有相同的大小和对齐要求。

指向 Unsized 类型的指针是 Sized 的。大小和对齐要求保证至少等于普通指针的大小和对齐要求。

注意

虽然你不应该依赖这一点,但目前所有指向 DST 的指针的大小都是 usize 大小的两倍,并且具有相同的对齐要求。

数组布局

类型为 [T; N] 的数组大小为 size_of::<T>() * N,对齐要求与 T 相同。数组的布局方式使得数组中从零开始的第 nth 个元素相对于数组起始位置的偏移量为 n * size_of::<T>() 字节。

切片布局

切片具有与其所切取的数组部分相同的布局。

注意

这里讨论的是原始的 [T] 类型,而不是指向切片的指针(&[T], Box<[T]> 等)。

str 布局

字符串切片是字符的 UTF-8 表示,其布局与类型 [u8] 的切片相同。

元组布局

元组按照 Rust 表示进行布局。

此规则的例外是单元元组(()),作为零大小类型,它保证大小为 0,对齐要求为 1。

Trait 对象布局

Trait 对象与其所代表的值具有相同的布局。

注意

这里讨论的是原始的 trait 对象类型,而不是指向 trait 对象的指针(&dyn Trait, Box<dyn Trait> 等)。

闭包布局

闭包没有布局保证。

表示(Representations)

所有用户定义的复合类型(structenumunion)都有一个表示(representation),它指定了该类型的布局。

类型的可能表示有

可以通过对类型应用 repr 属性来更改其表示。以下示例显示了一个具有 C 表示的 struct。

#![allow(unused)]
fn main() {
#[repr(C)]
struct ThreeInts {
    first: i16,
    second: i8,
    third: i32
}
}

对齐要求可以使用 alignpacked 修饰符分别提高或降低。它们改变属性中指定的表示。如果未指定表示,则改变默认表示。

#![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 表示的 struct,如果其中包含一个具有 Rust 表示的 struct Inner,则不会改变 Inner 的布局。

Rust 表示

Rust 表示是未指定 repr 属性的具名类型(nominal types)的默认表示。通过 repr 属性显式使用此表示保证与完全省略该属性效果相同。

此表示仅提供健全性(soundness)所需的数据布局保证。它们是

  1. 字段正确对齐。
  2. 字段不重叠。
  3. 类型的对齐要求至少等于其字段的最大对齐要求。

形式上,第一条保证意味着任何字段的偏移量都可以被该字段的对齐要求整除。

第二条保证意味着可以对字段进行排序,使得任何字段的偏移量加上其大小小于或等于排序中下一个字段的偏移量。这个排序不必与类型声明中指定字段的顺序相同。

请注意,第二条保证并不意味着字段具有不同的地址:零大小类型可能与同一 struct 中的其他字段具有相同的地址。

此表示不提供其他数据布局保证。

C 表示

C 表示旨在用于双重目的。一个目的是创建与 C 语言可互操作的类型。第二个目的是创建可以安全地执行依赖于数据布局的操作的类型,例如将值重新解释为不同的类型。

由于这种双重目的,可能会创建一些对于与 C 编程语言进行接口操作并不有用的类型。

此表示可以应用于 struct、union 和 enum。例外情况是 零变体枚举,对其应用 C 表示会报错。

#[repr(C)] Structs

struct 的对齐要求是其内部对齐要求最高的字段的对齐要求。

字段的大小和偏移量由以下算法确定。

从当前偏移量 0 字节开始。

对于 struct 中声明顺序的每个字段,首先确定字段的大小和对齐要求。如果当前偏移量不是字段对齐要求的倍数,则在当前偏移量上添加填充字节,直到它是字段对齐要求的倍数。此时的当前偏移量即为该字段的偏移量。然后将当前偏移量增加字段的大小。

最后,struct 的大小是当前偏移量向上取整到 struct 对齐要求的最近倍数。

以下是该算法的伪代码描述。

/// 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

注意

此算法可以生成零大小的 struct。在 C 语言中,像 struct Foo { } 这样的空 struct 声明是非法的。然而,gcc 和 clang 都支持启用此类 struct 的选项,并将其大小设为零。相比之下,C++ 给空 struct 指定大小为 1,除非它们是继承的,或者它们是带有 [[no_unique_address]] 属性的字段,在这种情况下它们不增加 struct 的总大小。

#[repr(C)] Unions

使用 #[repr(C)] 声明的 union 将具有与目标平台 C 语言中等效的 C union 声明相同的大小和对齐要求。

union 的大小将是所有字段的最大大小向上取整到其对齐要求,对齐要求是所有字段的最大对齐要求。这些最大值可能来自不同的字段。

#![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 语言中 enum 的表示是实现定义的,因此这实际上是一个“最佳猜测”。特别是,当相关 C 代码使用某些标志编译时,这可能不正确。

警告

C 语言中的 enum 与 Rust 具有此表示的 无字段枚举 之间存在关键差异。C 语言中的 enum 主要是一个 typedef 加上一些命名常量;换句话说,enum 类型的对象可以持有任何整数值。例如,这在 C 中常用于位标志。相比之下,Rust 的 无字段枚举 只能合法持有判别式值,其他任何值都是未定义行为。因此,在 FFI 中使用无字段枚举来模拟 C enum 通常是错误的。

#[repr(C)] 带字段枚举

带字段的 repr(C) 枚举的表示是一个包含两个字段的 repr(C) struct,在 C 中也称为“带标签的联合体”(tagged union)

  • 一个移除了所有字段的 repr(C) 版本枚举(“标签”)
  • 一个包含每个变体字段(如果存在)的 repr(C) struct 的 repr(C) union(“载荷”)

注意

由于 repr(C) struct 和 union 的表示方式,如果一个变体只有一个字段,将其直接放入 union 或将其包装在一个 struct 中没有区别;因此,任何希望操作此类 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

原始表示

原始表示(primitive representations)是指与原始整数类型同名的表示。即:u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, 和 isize

原始表示只能应用于枚举,并且根据枚举是否包含字段而有不同的行为。对 零变体枚举 应用原始表示会报错。将两个原始表示组合在一起会报错。

无字段枚举的原始表示

对于 无字段枚举,原始表示将大小和对齐要求设置为与同名原始类型相同。例如,具有 u8 表示的无字段枚举的判别式只能在 0 到 255(含)之间。

带字段枚举的原始表示

原始表示枚举的表示是一个 repr(C) union,其中包含每个带有字段的变体的 repr(C) struct。union 中每个 struct 的第一个字段是移除了所有字段的原始表示版本枚举(“标签”),其余字段是该变体的字段。

注意

即使在 union 中为标签分配了自己的成员,这个表示也不会改变,如果这样使操作更清晰的话(不过为了遵循 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);
}

对齐修饰符

alignpacked 修饰符可以分别用于提高或降低 structunion 的对齐要求。packed 也可能改变字段之间的填充(尽管它不会改变任何字段内部的填充)。就它们自身而言,alignpacked 不提供关于 struct 布局中字段顺序或枚举变体布局的保证,但它们可以与提供此类保证的表示(例如 C)结合使用。

对齐值以整数参数的形式指定,例如 #[repr(align(x))]#[repr(packed(x))]。对齐值必须是 1 到 229 之间的 2 的幂。对于 packed,如果未给定值,例如 #[repr(packed)],则值为 1。

对于 align,如果指定的对齐值小于不带 align 修饰符的类型的对齐值,则对齐要求不受影响。

对于 packed,如果指定的对齐值大于不带 packed 修饰符的类型的对齐值,则对齐和布局不受影响。

每个字段的对齐要求,用于定位字段时,取指定对齐值与字段类型对齐值中的较小者。

保证字段之间的填充是满足每个字段(可能已更改的)对齐要求所需的最小填充(但请注意,就其自身而言,packed 不提供任何关于字段顺序的保证)。这些规则的一个重要结果是,带有 #[repr(packed(1))](或 #[repr(packed)])的类型将没有字段间填充。

alignpacked 修饰符不能应用于同一类型,并且 packed 类型不能传递地包含另一个带有 align 的类型。alignpacked 只能应用于 RustC 表示。

align 修饰符也可以应用于 enum。此时,它对 enum 对齐要求的影响与将 enum 包装在一个具有相同 align 修饰符的新类型 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,且该变体包含

  • 任意数量的 size 为 0 且对齐要求为 1 的字段(例如 PhantomData<T>),以及
  • 至多一个其他字段。

带有此表示的 struct 和 enum 具有与唯一的非 size 0 且非对齐要求 1 的字段(如果存在)相同的布局和 ABI,否则与 unit 类型相同。

这与 C 表示不同,因为带有 C 表示的 struct 始终具有 C struct 的 ABI,而例如,带有 transparent 表示且包含原始类型字段的 struct 则具有该原始类型字段的 ABI。

由于此表示将类型布局委托给另一种类型,因此不能与其他任何表示一起使用。