struct
test_structs.zig
// Declare a struct.
// Zig gives no guarantees about the order of fields and the size of
// the struct but the fields are guaranteed to be ABI-aligned.
const Point = struct {
x: f32,
y: f32,
};
// Maybe we want to pass it to OpenGL so we want to be particular about
// how the bytes are arranged.
const Point2 = packed struct {
x: f32,
y: f32,
};
// Declare an instance of a struct.
const p = Point {
.x = 0.12,
.y = 0.34,
};
// Maybe we're not ready to fill out some of the fields.
var p2 = Point {
.x = 0.12,
.y = undefined,
};
// Structs can have methods
// Struct methods are not special, they are only namespaced
// functions that you can call with dot syntax.
const Vec3 = struct {
x: f32,
y: f32,
z: f32,
pub fn init(x: f32, y: f32, z: f32) Vec3 {
return Vec3 {
.x = x,
.y = y,
.z = z,
};
}
pub fn dot(self: Vec3, other: Vec3) f32 {
return self.x * other.x + self.y * other.y + self.z * other.z;
}
};
const expect = @import("std").testing.expect;
test "dot product" {
const v1 = Vec3.init(1.0, 0.0, 0.0);
const v2 = Vec3.init(0.0, 1.0, 0.0);
try expect(v1.dot(v2) == 0.0);
// Other than being available to call with dot syntax, struct methods are
// not special. You can reference them as any other declaration inside
// the struct:
try expect(Vec3.dot(v1, v2) == 0.0);
}
// Structs can have declarations.
// Structs can have 0 fields.
const Empty = struct {
pub const PI = 3.14;
};
test "struct namespaced variable" {
try expect(Empty.PI == 3.14);
try expect(@sizeOf(Empty) == 0);
// you can still instantiate an empty struct
const does_nothing = Empty {};
_ = does_nothing;
}
// struct field order is determined by the compiler for optimal performance.
// however, you can still calculate a struct base pointer given a field pointer:
fn setYBasedOnX(x: *f32, y: f32) void {
const point: *Point = @fieldParentPtr("x", x);
point.y = y;
}
test "field parent pointer" {
var point = Point {
.x = 0.1234,
.y = 0.5678,
};
setYBasedOnX(&point.x, 0.9);
try expect(point.y == 0.9);
}
// You can return a struct from a function. This is how we do generics
// in Zig:
fn LinkedList(comptime T: type) type {
return struct {
pub const Node = struct {
prev: ?*Node,
next: ?*Node,
data: T,
};
first: ?*Node,
last: ?*Node,
len: usize,
};
}
test "linked list" {
// Functions called at compile-time are memoized. This means you can
// do this:
try expect(LinkedList(i32) == LinkedList(i32));
const list = LinkedList(i32){
.first = null,
.last = null,
.len = 0,
};
try expect(list.len == 0);
// Since types are first class values you can instantiate the type
// by assigning it to a variable:
const ListOfInts = LinkedList(i32);
try expect(ListOfInts == LinkedList(i32));
var node = ListOfInts.Node{
.prev = null,
.next = null,
.data = 1234,
};
const list2 = LinkedList(i32){
.first = &node,
.last = &node,
.len = 1,
};
// When using a pointer to a struct, fields can be accessed directly,
// without explicitly dereferencing the pointer.
// So you can do
try expect(list2.first.?.data == 1234);
// instead of try expect(list2.first.?.*.data == 1234);
}
Shell
$ zig test test_structs.zig
1/4 test_structs.test.dot product... OK
2/4 test_structs.test.struct namespaced variable... OK
3/4 test_structs.test.field parent pointer... OK
4/4 test_structs.test.linked list... OK
All 4 tests passed.
Default Field Values
Each struct field may have an expression indicating the default field value. Such expressions are executed at comptime, and allow the field to be omitted in a struct literal expression:
struct_default_field_values.zig
const Foo = struct {
a: i32 = 1234,
b: i32,
};
test "default struct initialization fields" {
const x: Foo = .{
.b = 5,
};
if (x.a + x.b != 1239) {
comptime unreachable;
}
}
Shell
$ zig test struct_default_field_values.zig
1/1 struct_default_field_values.test.default struct initialization fields... OK
All 1 tests passed.
Default field values are only appropriate when the data invariants of a struct cannot be violated by omitting that field from an initialization.
For example, here is an inappropriate use of default struct field initialization:
bad_default_value.zig
const Threshold = struct {
minimum: f32 = 0.25,
maximum: f32 = 0.75,
const Category = enum { low, medium, high };
fn categorize(t: Threshold, value: f32) Category {
assert(t.maximum >= t.minimum);
if (value < t.minimum) return .low;
if (value > t.maximum) return .high;
return .medium;
}
};
pub fn main() !void {
var threshold: Threshold = .{
.maximum = 0.20,
};
const category = threshold.categorize(0.90);
try std.io.getStdOut().writeAll(@tagName(category));
}
const std = @import("std");
const assert = std.debug.assert;
Shell
$ zig build-exe bad_default_value.zig
$ ./bad_default_value
thread 986647 panic: reached unreachable code
/home/ci/actions-runner/_work/zig-bootstrap/out/host/lib/zig/std/debug.zig:403:14: 0x103657d in assert (bad_default_value)
if (!ok) unreachable; // assertion failure
^
/home/ci/actions-runner/_work/zig-bootstrap/zig/docgen_tmp/bad_default_value.zig:8:15: 0x1033a89 in categorize (bad_default_value)
assert(t.maximum >= t.minimum);
^
/home/ci/actions-runner/_work/zig-bootstrap/zig/docgen_tmp/bad_default_value.zig:19:42: 0x10339ba in main (bad_default_value)
const category = threshold.categorize(0.90);
^
/home/ci/actions-runner/_work/zig-bootstrap/out/host/lib/zig/std/start.zig:511:37: 0x10338d5 in posixCallMainAndExit (bad_default_value)
const result = root.main() catch |err| {
^
/home/ci/actions-runner/_work/zig-bootstrap/out/host/lib/zig/std/start.zig:253:5: 0x10333f1 in _start (bad_default_value)
asm volatile (switch (native_arch) {
^
???:?:?: 0x0 in ??? (???)
(process terminated by signal)
Above you can see the danger of ignoring this principle. The default field values caused the data invariant to be violated, causing illegal behavior.
To fix this, remove the default values from all the struct fields, and provide a named default value:
struct_default_value.zig
const Threshold = struct {
minimum: f32,
maximum: f32,
const default: Threshold = .{
.minimum = 0.25,
.maximum = 0.75,
};
};
If a struct value requires a runtime-known value in order to be initialized without violating data invariants, then use an initialization method that accepts those runtime values, and populates the remaining fields.
extern struct
An extern struct
has in-memory layout matching the C ABI for the target.
If well-defined in-memory layout is not required, struct is a better choice because it places fewer restrictions on the compiler.
See packed struct for a struct that has the ABI of its backing integer, which can be useful for modeling flags.
See also:
packed struct
Unlike normal structs, packed
structs have guaranteed in-memory layout:
- Fields remain in the order declared, least to most significant.
- There is no padding between fields.
- Zig supports arbitrary width 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.
bool
fields use exactly 1 bit.- An enum field uses exactly the bit width of its integer tag type.
- A packed union field uses exactly the bit width of the union field with the largest bit width.
This means that a packed struct
can participate in a @bitCast or a @ptrCast to reinterpret memory. This even works at comptime:
test_packed_structs.zig
const std = @import("std");
const native_endian = @import("builtin").target.cpu.arch.endian();
const expect = std.testing.expect;
const Full = packed struct {
number: u16,
};
const Divided = packed struct {
half1: u8,
quarter3: u4,
quarter4: u4,
};
test "@bitCast between packed structs" {
try doTheTest();
try comptime doTheTest();
}
fn doTheTest() !void {
try expect(@sizeOf(Full) == 2);
try expect(@sizeOf(Divided) == 2);
const full = Full{ .number = 0x1234 };
const divided: Divided = @bitCast(full);
try expect(divided.half1 == 0x34);
try expect(divided.quarter3 == 0x2);
try expect(divided.quarter4 == 0x1);
const ordered: [2]u8 = @bitCast(full);
switch (native_endian) {
.big => {
try expect(ordered[0] == 0x12);
try expect(ordered[1] == 0x34);
},
.little => {
try expect(ordered[0] == 0x34);
try expect(ordered[1] == 0x12);
},
}
}
Shell
$ zig test test_packed_structs.zig
1/1 test_packed_structs.test.@bitCast between packed structs... OK
All 1 tests passed.
The backing integer is inferred from the fields’ total bit width. Optionally, it can be explicitly provided and enforced at compile time:
test_missized_packed_struct.zig
test "missized packed struct" {
const S = packed struct(u32) { a: u16, b: u8 };
_ = S{ .a = 4, .b = 2 };
}
Shell
$ zig test test_missized_packed_struct.zig
docgen_tmp/test_missized_packed_struct.zig:2:29: error: backing integer type 'u32' has bit size 32 but the struct fields have a total bit size of 24
const S = packed struct(u32) { a: u16, b: u8 };
^~~
Zig allows the address to be taken of a non-byte-aligned field:
test_pointer_to_non-byte_aligned_field.zig
const std = @import("std");
const expect = std.testing.expect;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var foo = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointer to non-byte-aligned field" {
const ptr = &foo.b;
try expect(ptr.* == 2);
}
Shell
$ zig test test_pointer_to_non-byte_aligned_field.zig
1/1 test_pointer_to_non-byte_aligned_field.test.pointer to non-byte-aligned field... OK
All 1 tests passed.
However, the pointer to a non-byte-aligned field has special properties and cannot be passed when a normal pointer is expected:
test_misaligned_pointer.zig
const std = @import("std");
const expect = std.testing.expect;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var bit_field = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointer to non-byte-aligned field" {
try expect(bar(&bit_field.b) == 2);
}
fn bar(x: *const u3) u3 {
return x.*;
}
Shell
$ zig test test_misaligned_pointer.zig
docgen_tmp/test_misaligned_pointer.zig:17:20: error: expected type '*const u3', found '*align(1:3:1) u3'
try expect(bar(&bit_field.b) == 2);
^~~~~~~~~~~~
docgen_tmp/test_misaligned_pointer.zig:17:20: note: pointer host size '1' cannot cast into pointer host size '0'
docgen_tmp/test_misaligned_pointer.zig:17:20: note: pointer bit offset '3' cannot cast into pointer bit offset '0'
docgen_tmp/test_misaligned_pointer.zig:20:11: note: parameter type declared here
fn bar(x: *const u3) u3 {
^~~~~~~~~
In this case, the function bar
cannot be called because the pointer to the non-ABI-aligned field mentions the bit offset, but the function expects an ABI-aligned pointer.
Pointers to non-ABI-aligned fields share the same address as the other fields within their host integer:
test_packed_struct_field_address.zig
const std = @import("std");
const expect = std.testing.expect;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var bit_field = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointers of sub-byte-aligned fields share addresses" {
try expect(@intFromPtr(&bit_field.a) == @intFromPtr(&bit_field.b));
try expect(@intFromPtr(&bit_field.a) == @intFromPtr(&bit_field.c));
}
Shell
$ zig test test_packed_struct_field_address.zig
1/1 test_packed_struct_field_address.test.pointers of sub-byte-aligned fields share addresses... OK
All 1 tests passed.
This can be observed with @bitOffsetOf and offsetOf:
test_bitOffsetOf_offsetOf.zig
const std = @import("std");
const expect = std.testing.expect;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
test "offsets of non-byte-aligned fields" {
comptime {
try expect(@bitOffsetOf(BitField, "a") == 0);
try expect(@bitOffsetOf(BitField, "b") == 3);
try expect(@bitOffsetOf(BitField, "c") == 6);
try expect(@offsetOf(BitField, "a") == 0);
try expect(@offsetOf(BitField, "b") == 0);
try expect(@offsetOf(BitField, "c") == 0);
}
}
Shell
$ zig test test_bitOffsetOf_offsetOf.zig
1/1 test_bitOffsetOf_offsetOf.test.offsets of non-byte-aligned fields... OK
All 1 tests passed.
Packed structs have the same alignment as their backing integer, however, overaligned pointers to packed structs can override this:
test_overaligned_packed_struct.zig
const std = @import("std");
const expect = std.testing.expect;
const S = packed struct {
a: u32,
b: u32,
};
test "overaligned pointer to packed struct" {
var foo: S align(4) = .{ .a = 1, .b = 2 };
const ptr: *align(4) S = &foo;
const ptr_to_b: *u32 = &ptr.b;
try expect(ptr_to_b.* == 2);
}
Shell
$ zig test test_overaligned_packed_struct.zig
1/1 test_overaligned_packed_struct.test.overaligned pointer to packed struct... OK
All 1 tests passed.
It’s also possible to set alignment of struct fields:
test_aligned_struct_fields.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "aligned struct fields" {
const S = struct {
a: u32 align(2),
b: u32 align(64),
};
var foo = S{ .a = 1, .b = 2 };
try expectEqual(64, @alignOf(S));
try expectEqual(*align(2) u32, @TypeOf(&foo.a));
try expectEqual(*align(64) u32, @TypeOf(&foo.b));
}
Shell
$ zig test test_aligned_struct_fields.zig
1/1 test_aligned_struct_fields.test.aligned struct fields... OK
All 1 tests passed.
Using packed structs with volatile is problematic, and may be a compile error in the future. For details on this subscribe to this issue. 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.
Struct Naming
Since all structs are anonymous, Zig infers the type name based on a few rules.
- If the struct is in the initialization expression of a variable, it gets named after that variable.
- If the struct is in the
return
expression, it gets named after the function it is returning from, with the parameter values serialized. - Otherwise, the struct gets a name such as
(filename.funcname.__struct_ID)
. - If the struct is declared inside another struct, it gets named after both the parent struct and the name inferred by the previous rules, separated by a dot.
struct_name.zig
const std = @import("std");
pub fn main() void {
const Foo = struct {};
std.debug.print("variable: {s}\n", .{@typeName(Foo)});
std.debug.print("anonymous: {s}\n", .{@typeName(struct {})});
std.debug.print("function: {s}\n", .{@typeName(List(i32))});
}
fn List(comptime T: type) type {
return struct {
x: T,
};
}
Shell
$ zig build-exe struct_name.zig
$ ./struct_name
variable: struct_name.main.Foo
anonymous: struct_name.main__struct_3389
function: struct_name.List(i32)
Anonymous Struct Literals
Zig allows omitting the struct type of a literal. When the result is coerced, the struct literal will directly instantiate the result location, with no copy:
test_struct_result.zig
const std = @import("std");
const expect = std.testing.expect;
const Point = struct {x: i32, y: i32};
test "anonymous struct literal" {
const pt: Point = .{
.x = 13,
.y = 67,
};
try expect(pt.x == 13);
try expect(pt.y == 67);
}
Shell
$ zig test test_struct_result.zig
1/1 test_struct_result.test.anonymous struct literal... OK
All 1 tests passed.
The struct type can be inferred. Here the result location does not include a type, and so Zig infers the type:
test_anonymous_struct.zig
const std = @import("std");
const expect = std.testing.expect;
test "fully anonymous struct" {
try check(.{
.int = @as(u32, 1234),
.float = @as(f64, 12.34),
.b = true,
.s = "hi",
});
}
fn check(args: anytype) !void {
try expect(args.int == 1234);
try expect(args.float == 12.34);
try expect(args.b);
try expect(args.s[0] == 'h');
try expect(args.s[1] == 'i');
}
Shell
$ zig test test_anonymous_struct.zig
1/1 test_anonymous_struct.test.fully anonymous struct... OK
All 1 tests passed.
Tuples
Anonymous structs can be created without specifying field names, and are referred to as “tuples”.
The fields are implicitly named using numbers starting from 0. Because their names are integers, they cannot be accessed with .
syntax without also wrapping them in @""
. Names inside @""
are always recognised as identifiers.
Like arrays, tuples have a .len field, can be indexed (provided the index is comptime-known) and work with the ++ and ** operators. They can also be iterated over with inline for.
test_tuples.zig
const std = @import("std");
const expect = std.testing.expect;
test "tuple" {
const values = .{
@as(u32, 1234),
@as(f64, 12.34),
true,
"hi",
} ++ .{false} ** 2;
try expect(values[0] == 1234);
try expect(values[4] == false);
inline for (values, 0..) |v, i| {
if (i != 2) continue;
try expect(v);
}
try expect(values.len == 6);
try expect(values.@"3"[0] == 'h');
}
Shell
$ zig test test_tuples.zig
1/1 test_tuples.test.tuple... OK
All 1 tests passed.
See also: