A normal struct is laid out for efficient access.
const Point = struct {
x: u8,
y: u32,
};The compiler may place padding between fields. Padding is unused space inserted to satisfy alignment.
A u32 is usually aligned more strictly than a u8. Therefore this struct may occupy more than five bytes.
Packed memory uses an exact bit layout.
const Point = packed struct {
x: u8,
y: u32,
};In a packed struct, fields are placed according to their declared bit sizes. This is useful when the program must match an external representation.
Common cases are hardware registers, network headers, file formats, instruction encodings, and binary protocols.
A packed struct can describe bit fields.
const Control = packed struct {
enable: bool,
mode: u3,
reserved: u4,
};This struct occupies one byte. The fields use exactly eight bits:
enable 1 bit
mode 3 bits
reserved 4 bitsA value can be used like any other struct value.
const std = @import("std");
const Control = packed struct {
enable: bool,
mode: u3,
reserved: u4,
};
pub fn main() void {
var c = Control{
.enable = true,
.mode = 5,
.reserved = 0,
};
std.debug.print("{} {d}\n", .{ c.enable, c.mode });
}The type u3 is a three-bit unsigned integer. It stores values from 0 to 7.
Packed structs make small integer widths practical.
const Header = packed struct {
version: u4,
length: u12,
flags: u8,
};This layout uses 24 bits.
The exact layout matters. The program is now coupled to the order and width of every field.
Packed memory is not the same as portable serialization.
If a file format defines byte order, the program must still handle byte order explicitly.
For many formats, byte-level code is clearer.
const std = @import("std");
pub fn main() void {
const bytes = [_]u8{ 0x34, 0x12 };
const x = std.mem.readInt(u16, bytes[0..2], .little);
std.debug.print("{x}\n", .{x});
}This says plainly that the integer is stored little-endian.
A packed struct says how fields occupy memory. It does not make every binary format safe to read by casting a pointer.
Alignment must still be respected.
const Header = packed struct {
tag: u8,
length: u32,
};
const ptr: *Header = @ptrCast(@alignCast(raw_ptr));The cast asserts that raw_ptr points to a valid Header at a valid alignment.
For byte streams, copying and decoding is often safer than pointer casting.
Packed structs are especially useful for memory-mapped registers.
const Status = packed struct {
ready: bool,
error_flag: bool,
code: u6,
};
const status: *volatile Status =
@ptrFromInt(0x4000_0004);Reading:
if (status.ready) {
// device is ready
}accesses the register as a structured value.
This is much clearer than manual masks:
const raw = reg.*;
const ready = (raw & 0x01) != 0;Manual masks are still useful when the representation is complex or shared with C code.
A packed struct can also have a backing integer type.
const Flags = packed struct(u8) {
read: bool,
write: bool,
execute: bool,
reserved: u5,
};The backing type says the whole struct is represented as a u8.
This makes the intended size explicit.
Packed memory has costs.
Accessing packed fields may require masking and shifting.
Taking pointers to packed fields can be restricted or require special pointer types.
Unaligned loads may be slower or invalid on some targets.
The benefit is control. The cost is responsibility.
Use ordinary structs for ordinary program data.
const User = struct {
id: u64,
name: []const u8,
};Use packed structs when the memory layout itself is part of the problem.
const TcpFlags = packed struct(u8) {
fin: bool,
syn: bool,
rst: bool,
psh: bool,
ack: bool,
urg: bool,
ece: bool,
cwr: bool,
};The rule is simple:
| Need | Use |
|---|---|
| Normal data model | struct |
| Exact bit layout | packed struct |
| Tagged alternatives | union(enum) |
| Raw byte format | explicit byte parsing |
Packed memory is a low-level tool. It should appear at the boundary of the program, where Zig meets hardware, wire formats, or stored binary data.
Exercise 19-21. Define a packed struct that fits in one byte and contains three fields.
Exercise 19-22. Add a backing integer type to the struct from Exercise 19-21.
Exercise 19-23. Decode a two-byte little-endian integer using std.mem.readInt.
Exercise 19-24. Explain why an ordinary struct should be preferred for normal application data.