Skip to content

langref: improve packed struct memory layout description #21741

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 25 additions & 19 deletions doc/langref.html.in
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,7 @@ unwrapped == 1234{#endsyntax#}</pre>
<li>{#link|Floats#}</li>
<li>{#link|bool|Primitive Types#}</li>
<li>{#link|type|Primitive Types#}</li>
<li>{#link|packed struct#}</li>
</ul>
</td>
<td>
Expand Down Expand Up @@ -2224,31 +2225,36 @@ or

{#header_open|packed struct#}
<p>
Unlike normal structs, {#syntax#}packed{#endsyntax#} structs have guaranteed in-memory layout:
{#syntax#}packed{#endsyntax#} structs, like {#syntax#}enum{#endsyntax#}, are based on the concept
of interpreting integers differently. All packed structs have a <strong>backing integer</strong>,
which is implicitly determined by the total bit count of fields, or explicitly specified.
Packed structs have well-defined memory layout - exactly the same ABI as their backing integer.
</p>
<p>
Each field of a packed struct is interpreted as a logical sequence of bits, arranged from
least to most significant. Allowed field types:
</p>
<ul>
<li>Fields remain in the order declared, least to most significant.</li>
<li>There is no padding between fields.</li>
<li>Zig supports arbitrary width {#link|Integers#} and although normally, integers with fewer
than 8 bits will still use 1 byte of memory, in packed structs, they use
exactly their bit width.
</li>
<li>{#syntax#}bool{#endsyntax#} fields use exactly 1 bit.</li>
<li>An {#link|integer|Integers#} field uses exactly as many bits as its
bit width. For example, a {#syntax#}u5{#endsyntax#} will use 5 bits of
the backing integer.</li>
<li>A {#link|bool|Primitive Types#} field uses exactly 1 bit.</li>
<li>An {#link|enum#} field uses exactly the bit width of its integer tag type.</li>
<li>A {#link|packed union#} field uses exactly the bit width of the union field with
the largest bit width.</li>
<li>Packed structs support equality operators.</li>
<li>A {#syntax#}packed struct{#endsyntax#} field uses the bits of its backing integer.</li>
</ul>
<p>
This means that a {#syntax#}packed struct{#endsyntax#} can participate
in a {#link|@bitCast#} or a {#link|@ptrCast#} to reinterpret memory.
This even works at {#link|comptime#}:
</p>
{#code|test_packed_structs.zig#}

<p>
The backing integer is inferred from the fields' total bit width.
Optionally, it can be explicitly provided and enforced at compile time:
The backing integer can be inferred or explicitly provided. When
inferred, it will be unsigned. When explicitly provided, its bit width
will be enforced at compile time to exactly match the total bit width of
the fields:
</p>
{#code|test_missized_packed_struct.zig#}

Expand Down Expand Up @@ -2290,18 +2296,18 @@ or

<p>
Equating packed structs results in a comparison of the backing integer,
and only works for the `==` and `!=` operators.
and only works for the {#syntax#}=={#endsyntax#} and {#syntax#}!={#endsyntax#} {#link|Operators#}.
</p>
{#code|test_packed_struct_equality.zig#}

<p>
Using packed structs with {#link|volatile#} is problematic, and may be a compile error in the future.
For details on this subscribe to
<a href="https://github.com/ziglang/zig/issues/1761">this issue</a>.
TODO update these docs with a recommendation on how to use packed structs with MMIO
(the use case for volatile packed structs) once this issue is resolved.
Don't worry, there will be a good solution for this use case in zig.
Field access and assignment can be understood as shorthand for bitshifts
on the backing integer. These operations are not {#link|atomic|Atomics#},
so beware using field access syntax when combined with memory-mapped
input-output (MMIO). Instead of field access on {#link|volatile#} {#link|Pointers#},
construct a fully-formed new value first, then write that value to the volatile pointer.
</p>
{#code|packed_struct_mmio.zig#}
{#header_close#}

{#header_open|Struct Naming#}
Expand Down
19 changes: 19 additions & 0 deletions doc/langref/packed_struct_mmio.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
pub const GpioRegister = packed struct(u8) {
GPIO0: bool,
GPIO1: bool,
GPIO2: bool,
GPIO3: bool,
reserved: u4 = 0,
};

const gpio: *volatile GpioRegister = @ptrFromInt(0x0123);

pub fn writeToGpio(new_states: GpioRegister) void {
// Example of what not to do:
// BAD! gpio.GPIO0 = true; BAD!

// Instead, do this:
gpio.* = new_states;
}

// syntax