Representation

reference : https://doc.rust-lang.org/nomicon/other-reprs.html

  • discuss
    • why memory representation is immportant in driver development
    • The types of memory representation
      • #[repr(rust)]
      • #[repr(C)]
      • #[repr(transparent)]
      • #[repr(u8)], #[repr(u16)], #[repr(u32)] // applied to an enum
      • #[repr(align(2))]
    • When one should use certain memory representations
      • defining accurate register alignments/sizes
      • FFI-ing
  • Look into core::mem and use it to demonstrate the different alignment representations in practical way

What is alignment?

Alignment is the process of placing data in starting addresses that are a multiple of a certain number. For example, if I say that data_x has an alignment of 2, it means that data_x's starting address will be divisible by 2^2. eg address 4.

So why is the divisibility of starting addresses important?

  • Performance: Many hardware architectures, particularly modern CPUs, are optimized for accessing aligned data. Accessing data that is aligned according to the architecture's requirements often results in faster and more efficient memory access. Misaligned data access may require the CPU to perform additional operations or fetch data in multiple memory transactions, leading to decreased performance.

  • Hardware Requirements: Some hardware architectures have strict alignment requirements for certain types of data. Failure to adhere to these requirements can result in hardware exceptions, crashes, or undefined behavior. Aligning data properly ensures compatibility with the target hardware and prevents such issues. For example, the riscv, instructions have an alignment of 5 -referring to the fact that instructions must start at memory addresses that are multiples of 4 bytes. If they are not aligned, the instruction address misaligned exception will be thrown.

  • Atomic Operations: Some atomic operations, such as atomic read-modify-write operations, may require that memory addresses be aligned to specific boundaries. Misaligned data can prevent these operations from being performed atomically, leading to potential data corruption or race conditions in concurrent programs.

  • Portability: Writing code that adheres to memory alignment requirements ensures portability across different hardware architectures and platforms. By following alignment guidelines, code can be more easily ported and maintained on various systems without encountering unexpected behavior or performance issues.

What is minimum alignment?

Minimum alignment refers to the strictest alignment requirement for a particular data type, which is often dictated by the platform's architecture or the ABI (Application Binary Interface).

When we talk about the minimum alignment of a type, we are referring to the smallest power of 2 that the memory address of an object of that type must be divisible by. This is to ensure that the data can be accessed efficiently by the hardware, as many CPUs require certain types to be aligned to specific boundaries for optimal performance.

For example:
If the minimum alignment of a type is 8 bytes, it means that any object of that type must be located at a memory address that is divisible by 8.

Alignments and how they affect space occupied by data-types

Alignment is not directly related to the space occupied by the data type. However, the alignment can influence the amount of space occupied by data in memory, as padding may be inserted between fields of a struct or at the end of an array to ensure that each element meets the required alignment.

for example :

#![allow(unused)]

fn main() {
// These two structs occupy different sizes even though they have the same field types.  
// This is because struct::s1 has added paddings to help align field_1 to the alignment of 6 ie (2^6 = u64)
// s1 is 16 bytes long  
// s2 is 12 bytes long

#[repr(Rust)]
struct s1{
  field_1: u32,
  field_2: u64
}

#[repr(packed)]
struct s1{
  field_1: u32,
  field_2: u64
}
}

repr(transparent)

The #[repr(transparent)] attribute in Rust is used to ensure that a struct has the same memory layout as its single non-zero-sized field. This attribute guarantees that the struct does not introduce any additional padding or alignment requirements beyond what its field requires.

When you apply #[repr(transparent)] to a struct, it ensures that the struct's layout in memory is exactly the same as the layout of its single non-zero-sized field. This can be useful for creating newtypes that wrap existing types but have different behavior or semantics, without introducing any additional overhead in terms of memory layout or performance.

Example :

#[repr(transparent)]
struct Wrapper(u32);

fn main() {
    let w = Wrapper(42);
    println!("Size of Wrapper: {}", std::mem::size_of::<Wrapper>());
    println!("Value inside Wrapper: {}", w.0);
}

If a struct has multiple fields of different sizes, applying #[repr(transparent)] will result in a compilation error. This is because #[repr(transparent)] can only be applied to structs with a single non-zero-sized field.

This code will produce a compilation error :

#[repr(transparent)]
struct Wrapper {
    field1: u32,
    field2: u64,
}

fn main() {
    let w = Wrapper { field1: 42, field2: 123 };
}

Using core::mem to inspect Representations.

To check the ABI-required minimum alignment of a type in bytes