一个类型的布局是它的大小、对齐和字段的相对偏移。对于枚举,如何布局和8.解释也是类型布局的一部分。

每次编译类型布局都可能发生改变。我们只会保证今天的内容,而不会精确地描述它是如何完成的。

尺寸和对齐

所有的值都有对齐和尺寸。

值的对齐描述了储存值的有效地址是怎样的。对齐到 n 的值被储存的地址必然能够整除 n。比如,一个值对齐为 2,那么只能被储存到偶数地址上;而对齐到 1 的值能够被储存到任意地址上。对齐的单位为 bytes,最小为 1,必定为 2 的幂次。值的对齐属性能够通过函数 align_of_val 获得。

将多个值储存在一个数组中,那么相邻元素之间的地址差值就是值的尺寸。值的尺寸必定为其对齐的幂次。注意一些值的大小为 0,0 被视作能够被任意对齐整除。(比如,一些平台上 [u16; 0] 的对齐为 2,尺寸为 0)。值的大小能够通过函数 size_of_val 获得。

尺寸和对齐都是类型的属性(而不是值的),而且都能够在编译期获得,而且都实现了 Sized trait,且能够通过 size_ofalign_of 获得。没有实现 Sized trait 的类型被称为 dynamically sized types

原生数据布局

大多数原生数据的大小如下表:

Table 1. 原生数据大小
类型size_of::<Type>()

bool

1

u8/i8

1

u16/i16

2

u32/i32

4

u64/i64

8

u128/i128

16

f32

4

f64

8

char

4

usize/isize 的大小和 CPU 相关。32bit CPU 上大小为 4,64bit 上大小为 8。

大多数原生类型都对齐到它们的大小,但是这是一个平台相关的行为。比如在 x86 上 u64/f64 都只是对齐到 32bits。

指针和引用布局

指针和引用具备相同的类型布局。指针或引用的可变性并不会影响内存布局。

指向 sized 类型的指针大小和对齐都均为 usize

指向 unsized 类型的指针大小是固定的。大小和对齐都至少和 sized 类型的相等。

尽管不应当依赖于此,但是目前指向 DST 类型的指针大小和对齐都是 2usize。

数组布局

数组 [T; N] 的大小为 size_of::<T>() * N ,对齐和 T 相同。数组中的元素是按顺序排列的。

切片布局

切片和数组的布局是一样的。

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

str 布局

String 切片是 UTF-8 字符的切片表示。因此它和切片 [u8] 布局相同。

元组布局

元组根据 <默认布局> 进行排列。

一个例外是单元元组( () ),其为 zero-sized 类型,因而大小为零,对齐为 1。

特征对象布局

特征对象布局和特征对象实际类型布局相同。

这里指的是原生对象类型,而不是指针( &dyn Trait, Box<dyn Trait, )。

闭包布局

闭包没有布局保证。

布局表现

所有用户定义的类型( struct, enum, union )都有一个布局规范。可能的布局表现为:

  • Default

  • c

  • 原生G表示

  • transparent

类型的布局形式可以通过 repr 属性更改。下面是对结构体应用 c 布局的例子:

#[repr(C)]
struct ThreeInts {
    first: i16,
    second: i8,
    third: i32
}

对齐属性可以通过 align, packed 进行修改。如果没有指定布局,则使用默认布局:

// 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 的布局形式相同。

类型的布局可能会更改字段之间的填充,但是并不会更改字段本身的布局形式。比如,一个包含了结构体 Inner 布局为 c 的结构体。其不会将 Inner 的布局形式更改为 c。

默认布局

没有 repr 属性的类型具备默认布局。这种布局在形式上被称为 rust 表示。

此种数据布局只保证了:

  1. 所有字段正确对齐。

  2. 字段之间不会重叠。

  3. 类型的对齐属性至少为它的字段的对齐属性的最大值。

形式上,第一个保证意味着任何字段的偏移量都可以被该字段的对齐方式整除。第二个保证意味着可以对字段进行排序,使得偏移量加上任何字段的大小小于或等于排序中下一个字段的偏移量。排序不必与在类型声明中指定字段的顺序相同。

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

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

C 布局

C 布局是为两个目的设计的。其一是创建可与 C 语言互操作的类型,其二是创建类型。你可与在这些类型上可靠地执行依赖于数据布局的操作,例如将值解释为不同的类型。

由于这种双重目的,可与创建对 C 语言交互没有用的类型。

这种表示可与应用于结构体、联合和枚举。但是 zero-variant-enums 例外,因此它对于 C 布局是一个错误。

结构体

Union

声明为 #[repr(C)] 的 union 在大小和对齐上和 C union 等价。union 的大小是所有字段最大值,对齐值是对最大值进行四舍五入得到的。所有字段按最大最齐方式进行对齐。

#[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

无字段枚举

对于 无字段枚举 ,内存布局和 C 的相同。

C 中的枚举是实现定义的,因而这实际上是“最佳猜测”。尤其是当某些编译器标志影响了 C 代码时。
Rust 的 https://doc.rust-lang.org/reference/items/enumerations.html#field-less-enum[无字段枚举] h和 C 的枚举是不同的。C 中的枚举实际上更接近使用 typedef 定义的常量。也就是说枚举对象可以保存整数值,这在 C 中经常用作 bitflags。而 Rust 的 https://doc.rust-lang.org/reference/items/enumerations.html#field-less-enum[无字段枚举] 只能持有合法的 discriminant values,除此之外全部是 https://doc.rust-lang.org/reference/behavior-considered-undefined.html[未定义行为]。因此,在 FFI 中使用无字段枚举取模拟 C 中的枚举一般是错误的。

带字段枚举

reor(C) 带字段枚举表示的是具有两个字段的结构体,在 C 中也被称为“标记 Union”:

  • 移除了所有字段 tag 的、有 repr(C) 标记的枚举。

  • 带有所有字段的、由 repr(C) 标记的 union。

由于 `repr(C)` 结构体和 Union 表示。如果枚举只有一个字段,那么将此字段放在 union 还是结构体中没任何区别。因此,任何希望操纵这种枚举的系统可以使用对他们来讲更方便或一致的形式。
// 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 trait 的字段的 union 是不稳定的,详见 55149

原生布局

原生布局是与原始整数类型同名的表示。即:u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128 和 isize。

原生表示只能用与枚举,并且无论枚举是否有字段,都有不同的行为。 zero-variant-enums 使用原生表示是错误的,将两个原生表示组合在一起也是错误的。

无字段枚举

对于 无字段枚举 。原生表示与其同名的原生类型相同。例如,具有 u8 表示的无字段枚举只能储存 0~255 之间的值。

带字段枚举

带有字段的枚举可以视为一个被 repr© 修饰的 union,此枚举是由带有 repr© 修饰的字段构成的。The first field of each struct in the union is the primitive representation version of the enum with all fields removed ("the tag") and the remaining fields are the fields of that variant.

Note: This representation is unchanged if the tag is given its own member in the union, should that make manipulation more clear for you (although to follow the C++ standard the tag member should be wrapped in a struct).
// 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 trait 的字段的 union 是不稳定的,详见 55149

同时使用原生表示和 #[repr(C)]

对于带有字段的枚举,可以同时使用 repr© 和原生表示(例如 repr(C, u8) )。This modifies the repr© by changing the representation of the discriminant enum to the chosen primitive instead. So, if you chose the u8 representation, then the discriminant enum would have a size and alignment of 1 byte.

The discriminant enum from the example earlier then becomes:

#[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© 的原生表示能够更改 repr(C) 表示的大小:

#[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);

For example, with a repr(C, u8) enum it is not possible to have 257 unique discriminants ("tags") whereas the same enum with only a repr(C) attribute will compile without any problems.

Using a primitive representation in addition to repr(C) can change the size of an enum from the repr(C) form:

#[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 可以用来修改类型的对齐。其中 packed 还可能会修改字段之间的填充。

对齐通过一个整数指定,使用 [repr(align(x))] 或者 [repr(packed(x))] 。其中 x 必须为 2 的幂次。对于 packed ,如果没有给定值,也就是使用 #[repr(packed)] 则默认为 1。

对于 align,如果指定的值比应当需要的小,则此参数没有影响。

对于 packed,如果指定的值比需要的值大,则此参数没有影响。 repr(packed) 将会导致字段之间没有填充。

两个属性不能同时用于有一个类型。

align 也可以用于枚举。这种情况下,就好像把枚举装到了一个 newtype 的结构体中,而此属性用于结构体上。

对于一个没有对齐的指针解引用是 https://doc.rust-lang.org/reference/behavior-considered-undefined.html[未定义行为] 。而且可以 https://github.com/rust-lang/rust/issues/27060[safely 地创建一个指向未对齐字段的指针] 。和其余能够 safe 创建未定义行为的方式一样,这是 Rust 的 bug。

transparent

transparent 只能用于结构体或带有一个 variant 的枚举,且其必须:

  • 具有非零大小的单个字段以及:

  • 大小为零且对齐方式为 1 的任意数量的字段。(例如 PhantomData<T> )。

使用这种表示的结构体和枚举和单个非零大小的字段具有相同的 ABI。

这和 c 布局是不同的,因为 c 布局必定和 c 结构体具备相同的 ABI。例如一个带有 transparent 布局的结构体,其唯一字段是一个原生字段,则此结构体和此原生字段类型具有相同的 ABI。

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

Last moify: 2023-10-06 03:46:34
Build time:2025-07-18 09:41:42
Powered By asphinx