A struct is not only a list of fields. It also has a layout in memory.
For a normal struct, Zig chooses a layout suitable for the target machine.
const Point = struct {
x: i32,
y: i32,
};This type contains two i32 fields. On most targets, each i32 uses 4 bytes, so the struct uses 8 bytes.
const std = @import("std");
const Point = struct {
x: i32,
y: i32,
};
pub fn main() void {
std.debug.print("{d}\n", .{@sizeOf(Point)});
}The output is usually:
8@sizeOf(T) gives the size of a type in bytes.
Alignment also matters.
const std = @import("std");
pub fn main() void {
std.debug.print("u8 size = {d}, align = {d}\n", .{
@sizeOf(u8),
@alignOf(u8),
});
std.debug.print("u32 size = {d}, align = {d}\n", .{
@sizeOf(u32),
@alignOf(u32),
});
}A value’s alignment is the address boundary where the target expects that value to be placed. A u32 often has alignment 4. That means its address should be divisible by 4.
Padding is added to satisfy alignment.
const A = struct {
a: u8,
b: u32,
};This struct has one byte field followed by a four byte field. The compiler may place padding between them so that b is aligned.
On a common 64-bit target, this type often has size 8, not 5.
const std = @import("std");
const A = struct {
a: u8,
b: u32,
};
pub fn main() void {
std.debug.print("size = {d}\n", .{@sizeOf(A)});
std.debug.print("align = {d}\n", .{@alignOf(A)});
}A different field order may use less space.
const B = struct {
b: u32,
a: u8,
};This still may have padding at the end, but there is no need for padding before b.
Do not rely on field order or padding for ordinary structs when talking to C, files, or hardware. A normal Zig struct is for Zig data. Its layout is chosen by the compiler.
When layout must match C, use extern struct.
const CPoint = extern struct {
x: c_int,
y: c_int,
};An extern struct has a layout compatible with the C ABI for the target.
Use it at C boundaries.
extern fn draw_point(p: CPoint) void;When layout must match exact bits, use packed struct.
const Register = packed struct {
carry: bool,
zero: bool,
interrupt: bool,
decimal: bool,
brk: bool,
unused: bool,
overflow: bool,
negative: bool,
};The three common struct forms are:
| Form | Purpose |
|---|---|
struct | Ordinary Zig data |
extern struct | C ABI layout |
packed struct | Exact bit layout |
A normal struct is the right default.
const User = struct {
id: u64,
name: []const u8,
active: bool,
};This says what the program means. It does not promise a file format or a C layout.
Use extern struct when another language must see the same layout.
const Stat = extern struct {
size: c_long,
mode: c_uint,
};Use packed struct when individual bits have names.
const Flags = packed struct {
read: bool,
write: bool,
execute: bool,
};The address of a field can be taken for normal structs.
var p = Point{
.x = 1,
.y = 2,
};
const xp = &p.x;For packed structs, fields may not be addressable in the same way. A packed field can start at a bit offset that has no ordinary address. Read and write packed fields as values.
Data layout becomes important when the program crosses a boundary: C calls, system calls, binary files, network packets, memory-mapped registers, or disk formats.
Inside ordinary program code, prefer clear fields and simple types. Let the compiler choose the layout unless the layout is part of the contract.
Exercises.
7-25. Use @sizeOf to print the size of a struct with fields u8 and u32.
7-26. Change the field order and compare the result.
7-27. Use @alignOf on u8, u16, u32, and u64.
7-28. Write one normal struct, one extern struct, and one packed struct, each with a short comment saying why that form was chosen.