We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Performance and Data Layout step 7 of 20
The other reprs: transparent, packed, align, integer enums
A representation attribute, written #[repr(...)], tells the compiler how
to lay out a type in memory. Without one, Rust uses its default
representation, whose field order and padding the compiler may choose.
#[repr(C)] asks for the field order, padding and alignment that the C
rules for the target platform would give. Matching an existing C header
sometimes needs more than that. This problem covers three more attributes:
packed, transparent and an integer type such as u8 on an enum.
Two terms are used throughout. A type's alignment is the number its
address must be a multiple of: a u32 usually has alignment 4, so it must
start at an address divisible by 4. Padding is the unused bytes the
compiler inserts between fields so that each field starts at a correctly
aligned address.
The header you are matching
#pragma pack(push, 1)
struct wire_header {
uint8_t version;
uint32_t length;
uint8_t flags;
uint16_t checksum;
};
#pragma pack(pop)
typedef int32_t handle_t;
enum status : uint8_t { STATUS_OK = 0, STATUS_RETRY = 1, STATUS_FATAL = 2 };
#pragma pack(push, 1) is a compiler extension supported by GCC, Clang and
MSVC that removes padding from the structs between it and pack(pop). The
enum status : uint8_t form, which fixes the enum's storage type, is C23
and C++ syntax.
#[repr(C, packed)]
packed removes all padding: each field starts at the byte right after the
previous one, and the struct's alignment becomes 1. Under ordinary C rules,
wire_header would take 12 bytes: version at offset 0, three padding
bytes, length at offset 4, flags at offset 8, one padding byte, and
checksum at offset 10. Packed, it takes exactly 8 bytes, the sum of its
field sizes, with the fields at offsets 0, 1, 5 and 6. That is the layout a
wire format with no gaps needs.
Write #[repr(C, packed)], not #[repr(packed)]. packed on its own
removes the padding but keeps Rust's default representation, so the field
order is not guaranteed to match the declaration and the struct is not
guaranteed to match the header. Clippy's repr_packed_without_abi lint,
which warns by default, reports this, and the grade treats Clippy warnings
as errors.
References to packed fields are rejected
let r = &h.length; // error[E0793]
With Rust 1.98.1 the error reads:
error[E0793]: reference to field of packed struct is unaligned
= note: this struct is 1-byte aligned, but the type of this field may require higher alignment
= note: creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
Every &T in Rust must point to an address that is correctly aligned for
T, and the compiler is allowed to generate code that relies on it. In a
packed struct, length sits at offset 1, so &h.length would usually be
misaligned. Creating such a reference is undefined behavior even if it is
never read, so the compiler rejects it.
This also catches code that does not mention &. println!("{}", h.length)
is rejected with the same error, because the formatting macros take a
reference to each argument, and so is a method call such as
h.length.to_string() that borrows the field because the method takes &self.
Writing the argument as a block, println!("{}", { h.length }), copies the
value first and compiles.
There are two ways to read a packed field:
-
Copy the value out.
let n = h.length;reads the field by value and creates no reference. For aCopyfield such as an integer, this is the usual fix and needs nounsafe. When you access a field by name, the compiler knows it may be misaligned and generates a load that is correct for the target. -
Take a raw pointer and read it unaligned.
unsafe { (&raw const h.checksum).read_unaligned() }&raw const placecreates a raw pointer (*const u16here) without creating a reference first. It has been stable since Rust 1.82, and the olderstd::ptr::addr_of!macro does the same thing.read_unalignedreads a value through the pointer without requiring the address to be aligned. Reading with*ptrinstead would still require alignment. This form is useful when you need the field's address, for example to pass it to other code. For a field that is notCopy, such as aString,read_unalignedmakes a bitwise duplicate, and you become responsible for making sure only one of the two copies is ever dropped.
This problem uses both: read_length copies the field out, and
read_checksum uses the raw-pointer form.
Packed structs have costs. On targets that do not support unaligned loads,
or support them slowly, the compiler may read a misaligned field in several
smaller pieces. Use packed when an external format requires the exact
byte layout, not as a general way to save memory.
#[repr(transparent)]
#[repr(transparent)]
pub struct Handle(pub i32);
A wrapper with a single field is called a newtype. #[repr(transparent)]
guarantees that the newtype has the same size, alignment and calling
convention (ABI) as its field. Handle can therefore stand in for a C
int32_t such as handle_t in an extern "C" function signature, while
Rust code still cannot mix a Handle up with a plain i32 or another
newtype around i32. On the grading platform, i32 and therefore Handle
have size 4 and alignment 4.
If the C side instead declared a struct, such as
typedef struct { int32_t raw; } handle_t;, the matching Rust type would be
#[repr(C)]. Some platforms pass a one-field struct differently from a
plain integer, and transparent promises the integer's calling convention.
The rule is that a transparent type may contain at most one field that is
not trivial. A trivial field has size 0 and alignment 1, such as () or
PhantomData<T>; any number of those may sit next to the real field, which
is how phantom-typed handles carry a type marker at no cost. A second real
field, or a zero-size field with larger alignment such as [u16; 0], is
rejected with E0690.
#[repr(u8)] on an enum
#[repr(u8)]
pub enum Status { Ok = 0, Retry = 1, Fatal = 2 }
Each variant of an enum has an integer tag called its discriminant; the
= 0, = 1 and = 2 set them. #[repr(u8)] stores the discriminant as a
u8, so Status has size 1 and alignment 1, matching the C enum. Without
the attribute, the size is chosen by the compiler and is not guaranteed.
s as u8 converts a variant to its discriminant, so Status::Retry as u8
is 1.
Option<Status> also has size 1 with the current compiler. A Status byte
can only hold 0, 1 or 2, so the compiler can use one of the other 253 byte
values to represent None. Which value it picks is not specified. An
unused value that the compiler borrows this way is called a niche. The
standard library guarantees that Option<T> has the same size as T only
for specific types such as references, Box and NonZero integers, not
for an enum like Status, so do not rely on this size in an FFI signature.
What to write
Give the three types the representations above and implement:
pub fn read_length(h: &WireHeader) -> u32 // copy the field out
pub fn read_checksum(h: &WireHeader) -> u16 // &raw const + read_unaligned
pub fn status_code(s: Status) -> u8 // the discriminant
pub fn handle_raw(h: Handle) -> i32 // the wrapped value
pub fn describe() -> Vec<(String, usize, usize)>
The test builds a WireHeader from four values (version and flags in
0 to 255, length in 0 to 4294967295, checksum in 0 to 65535) and a
Handle from any i32, including i32::MIN and i32::MAX. read_length,
read_checksum and handle_raw must return those values unchanged, and
status_code must return 0, 1 and 2 for Ok, Retry and Fatal.
describe returns exactly four (name, size, align) rows, in this order and
with these names: "WireHeader", "Handle", "Status" and
"Option<Status>". Use std::mem::size_of and std::mem::align_of to
compute the numbers. The expected rows are 8/1, 4/4, 1/1 and 1/1. The first
three follow from the representations on the grading platform. The last is
the niche result described above, which the test records for the compiler
it runs.
The tests cannot detect every mistake. Rust's default representation also
gives Handle size 4 and Status size 1 with the current compiler, so the
tests pass without transparent or u8. The attributes are what turn those
numbers into guarantees that the C side can depend on.
Related errors and lints
- E0793: a reference to a field of a packed struct, covered above.
- E0587: a type has both
packedandalign(N).packedlowers the alignment to 1 andalignraises it, so the compiler rejects the combination: "type has conflicting packed and align representation hints". - E0690: a
transparenttype with more than one non-trivial field. - E0084: an integer representation such as
#[repr(u8)]on an enum with no variants: "unsupported representation for zero-variant enum". - Putting
#[repr(C)]on something that is not a data type, such as a function, type alias,useor statement, is also an error. With Rust 1.98.1 the message is, for example, "#[repr(C)]attribute cannot be used on functions"; older compilers reported this as E0517. - Clippy's
enum_clike_unportable_variantlint is denied by default. It reports a fieldless enum whose discriminant values would not fit on a 32-bit target, for example0x1_0000_0000in a#[repr(isize)]enum. - Clippy's
default_union_representation(in the opt-inrestrictiongroup) flags aunionwithout areprattribute, andtrailing_empty_array(in the opt-innurserygroup) flags a struct ending in a zero-length array without areprattribute. Both shapes usually mean an FFI type is missing#[repr(C)].
The grade requires the code to compile, pass the tests, and produce no
warnings under cargo clippy -- -D warnings.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.