1.13.1. The Basic Responsibility of Types
Every Rust value has a type, and the responsibility of that type is to tell you how to interpret the bits in memory.
For example, the bit pattern 0b10111101 has no meaning by itself, but:
- Interpreted as
u8, it becomes the number 189 - Interpreted as
i8, it becomes the number -67
When you define a custom type, the compiler decides where each part of that type is placed in memory.
1.13.2. Alignment
Alignment determines where a type’s bytes may be stored.
Once a type’s representation is determined, you might think it can be stored anywhere in memory. In theory that is possible, but in practice computer hardware places constraints on where a given type can live.
The most typical example is a pointer. A pointer points to bytes, not bits; one byte equals 8 bits. In other words, it does not point to an individual bit. So if a value of some type were placed at bit index 4 in memory, you would not be able to address it, because pointers address bytes rather than specific bits. That is why alignment is done at the byte level — that is, at 8-bit boundaries.
For this reason, all values, regardless of type, must begin on a byte boundary. All types must be at least byte-aligned. In other words, the storage address must be a multiple of 8 bits.
1.13.3. Stricter Alignment Rules
Some types have alignment requirements stricter than byte alignment. In CPU and memory systems, memory is often accessed in blocks larger than a single byte.
For example, on a 64-bit CPU, most values are accessed in 8-byte blocks, and each operation begins at an address that is 8-byte aligned. This is also called the CPU word size.
Of course, CPUs can also handle reads and writes of smaller values, as well as values that cross block boundaries. But as developers, we should try our best to ensure that hardware operates at its native alignment.
For example, if the i64 value you want to read begins in the middle of an 8-byte block, then reading it requires at least two reads. Because i64 is 8 bytes wide, beginning in the middle of two 8-byte blocks means it must span both blocks. So when reading it, the engine must read from both blocks: the first block provides the first part of the i64, the second block provides the remaining part, and then the pieces must be merged.
That is very inefficient and slows down program execution, so we should try to keep hardware operations aligned to their native boundaries whenever possible.
1.13.4. Misaligned Access
When a CPU accesses memory and the data address does not follow the alignment required by the architecture, it is called a "misaligned access". This can lead to poor performance and concurrency issues.
Many CPUs require, or strongly recommend, that their parameters be naturally aligned. A naturally aligned value has alignment that matches its size.
For example, if you want to load 8 bytes, the provided address should be 8-byte aligned.
1.13.5. The Compiler Tries to Use Alignment as Much as Possible
Based on the contents a type includes, the compiler computes an alignment for that type (or, in other words, assigns it an alignment scheme):
For primitive values, alignment usually matches their size. For example,
u8is aligned to 1 byte,u16to 2 bytes,u32to 4 bytes, andu64to 8 bytes.For compound types (types that contain other types), the alignment is usually the maximum alignment of the contained types. For example, if a type contains fields of
u8,u16, andu32, then the type should be 4-byte aligned (u32has the largest alignment, which is 4 bytes).
1.13.6. Layout
The layout of a type is how the compiler decides to represent that type in memory.
The Rust compiler does not provide many guarantees about how types are laid out.
Rust provides the repr attribute: it can be added to a type definition to request a specific representation.
1.13.7. repr(C)
One of the most common repr attributes is repr(C). The C in the name indicates that it is related to C.
repr(C) layout is compatible with the layout used by C/C++ compilers for the same type. This is useful for Rust code that interacts with other languages through FFI (Foreign Function Interface).
When using FFI to interact with other languages, Rust generates a layout that matches what the other language’s compiler expects. Because C layout is predictable and unlikely to change, repr(C) is very useful in unsafe contexts.
For example, you can use it when working with raw pointers to that type or when converting between two types with the same fields.
1.13.8. repr(transparent)
The transparent in repr(transparent) means transparent. It is used on newtype-style wrappers and guarantees that the outer type has the same layout as its single non-zero-sized field. (Other fields are allowed only if they are zero-sized types, such as () or PhantomData.)
This is very useful when combined with the newtype pattern.
Let’s briefly revisit the newtype pattern here: you use a tuple struct to create a new local type, which is essentially a thin wrapper.
For example, if you want to operate on the memory representation of struct A and struct NewA(A), then after using repr(transparent), the two memory representations should be the same. Without it, the Rust compiler cannot guarantee that.
1.13.9. An Example of Using repr
Let’s look at an example:
| Code | Field Type Size | Default Representation | Padding | Final Alignment |
|---|---|---|---|---|
#[repr(C)] |
||||
struct Foo { |
||||
tiny: bool, |
1 byte | 1-byte aligned | 3 bytes | |
normal: u32, |
4 bytes | 4-byte aligned | (tiny + normal) 8 bytes |
|
small: u8, |
1 byte | 1-byte aligned | 7 bytes | 8 bytes |
long: u64, |
8 bytes | 8-byte aligned | 8 bytes | |
short: u16, |
2 bytes | 2-byte aligned | 6 bytes | 8 bytes |
} |
||||
| Total 32 bytes |
This table shows the memory alignment and padding of a Rust struct under #[repr(C)]:
The code is in the leftmost column and uses the
repr(C)annotation. The struct contains several fieldsThe Rust compiler first sees that the
tinyfield is of typebool, which occupies 1 byte in memory, so it is aligned to 1 byteThe compiler then sees that the
normalfield is of typeu32, which occupies 4 bytes, so it only needs 4-byte alignment. At this point Rust notices thattinyis aligned to 1 byte, so the compiler inserts 3 bytes of padding to maketinyoccupy 4 bytesSince this field now occupies exactly 8 bytes, which is a multiple of 4 bytes, it is already aligned
The
smallfield is of typeu8, which occupies 1 byte and is aligned to 1 byte. Because the previous two fields are already aligned, Rust will decide how much padding to add based on the following bytes. At this point the compiler still has to wait and seelongis of typeu64, which occupies 8 bytes and is naturally 8-byte aligned. Since its field is 8 bytes or larger, we now see thattinyandnormaltogether form an 8-byte-aligned region, andlongis also 8-byte aligned. Rust understands that the structure should now be aligned to 8 bytes. Therefore the compiler has to add 7 bytes of padding tosmallto make it 8-byte alignedshortis of typeu16, which occupies 2 bytes. Since the structure should now be 8-byte aligned, the compiler adds 6 bytes so that it becomes 8-byte aligned
The process can be represented in a table like this:
| Field | Type Size | Required Alignment | Padding | Notes |
|---|---|---|---|---|
tiny: bool |
1 byte | 1 byte | 3 bytes | To align the next u32
|
normal: u32 |
4 bytes | 4 bytes | none | Aligned as u32
|
small: u8 |
1 byte | 1 byte | 7 bytes | To align the next u64
|
long: u64 |
8 bytes | 8 bytes | none | 8-byte aligned |
short: u16 |
2 bytes | 2 bytes | 6 bytes | Structure aligned to 8 bytes |
Top comments (0)