struct
structs.zig
// Declare a struct.
// Zig gives no guarantees about the order of fields and whether or
// not there will be padding.
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 assert = @import("std").debug.assert;
test "dot product" {
const v1 = Vec3.init(1.0, 0.0, 0.0);
const v2 = Vec3.init(0.0, 1.0, 0.0);
assert(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:
assert(Vec3.dot(v1, v2) == 0.0);
}
// Structs can have global declarations.
// Structs can have 0 fields.
const Empty = struct {
pub const PI = 3.14;
};
test "struct namespaced variable" {
assert(Empty.PI == 3.14);
assert(@sizeOf(Empty) == 0);
// you can still instantiate an empty struct
const does_nothing = Empty {};
}
// 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 = @fieldParentPtr(Point, "x", x);
point.y = y;
}
test "field parent pointer" {
var point = Point {
.x = 0.1234,
.y = 0.5678,
};
setYBasedOnX(&point.x, 0.9);
assert(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:
assert(LinkedList(i32) == LinkedList(i32));
var list = LinkedList(i32) {
.first = null,
.last = null,
.len = 0,
};
assert(list.len == 0);
// Since types are first class values you can instantiate the type
// by assigning it to a variable:
const ListOfInts = LinkedList(i32);
assert(ListOfInts == LinkedList(i32));
var node = ListOfInts.Node {
.prev = null,
.next = null,
.data = 1234,
};
var list2 = LinkedList(i32) {
.first = &node,
.last = &node,
.len = 1,
};
assert(list2.first.?.data == 1234);
}
$ zig test structs.zig
Test 1/4 dot product...OK
Test 2/4 struct namespaced variable...OK
Test 3/4 field parent pointer...OK
Test 4/4 linked list...OK
All tests passed.
extern struct
An extern struct
has in-memory layout guaranteed to match the C ABI for the target.
This kind of struct should only be used for compatibility with the C ABI. Every other use case should be solved with packed struct or normal struct.
See also:
packed struct
Unlike normal structs, packed
structs have guaranteed in-memory layout:
- Fields remain in the order declared.
- 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.- A packed 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.
- Non-ABI-aligned fields are packed into the smallest possible ABI-aligned integers in accordance with the target endianness.
This means that a packed struct
can participate in a @bitCast or a @ptrCast to reinterpret memory. This even works at comptime:
test.zig
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Full = packed struct {
number: u16,
};
const Divided = packed struct {
half1: u8,
quarter3: u4,
quarter4: u4,
};
test "@bitCast between packed structs" {
doTheTest();
comptime doTheTest();
}
fn doTheTest() void {
assert(@sizeOf(Full) == 2);
assert(@sizeOf(Divided) == 2);
var full = Full{ .number = 0x1234 };
var divided = @bitCast(Divided, full);
switch (builtin.endian) {
builtin.Endian.Big => {
assert(divided.half1 == 0x12);
assert(divided.quarter3 == 0x3);
assert(divided.quarter4 == 0x4);
},
builtin.Endian.Little => {
assert(divided.half1 == 0x34);
assert(divided.quarter3 == 0x2);
assert(divided.quarter4 == 0x1);
},
}
}
$ zig test test.zig
Test 1/1 @bitCast between packed structs...OK
All tests passed.
Zig allows the address to be taken of a non-byte-aligned field:
test.zig
const std = @import("std");
const assert = std.debug.assert;
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;
assert(ptr.* == 2);
}
$ zig test test.zig
Test 1/1 pointer to non-byte-aligned field...OK
All 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.zig
const std = @import("std");
const assert = std.debug.assert;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var bit_field = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointer to non-bit-aligned field" {
assert(bar(&bit_field.b) == 2);
}
fn bar(x: *const u3) u3 {
return x.*;
}
$ zig test test.zig
/home/andy/dev/zig/docgen_tmp/test.zig:17:26: error: expected type '*const u3', found '*align(:3:1) u3'
assert(bar(&bit_field.b) == 2);
^
In this case, the function bar
cannot be called becuse 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.zig
const std = @import("std");
const assert = std.debug.assert;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var bit_field = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointer to non-bit-aligned field" {
assert(@ptrToInt(&bit_field.a) == @ptrToInt(&bit_field.b));
assert(@ptrToInt(&bit_field.a) == @ptrToInt(&bit_field.c));
}
$ zig test test.zig
Test 1/1 pointer to non-bit-aligned field...OK
All tests passed.
This can be observed with @bitOffsetOf and byteOffsetOf:
test.zig
const std = @import("std");
const assert = std.debug.assert;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
test "pointer to non-bit-aligned field" {
comptime {
assert(@bitOffsetOf(BitField, "a") == 0);
assert(@bitOffsetOf(BitField, "b") == 3);
assert(@bitOffsetOf(BitField, "c") == 6);
assert(@byteOffsetOf(BitField, "a") == 0);
assert(@byteOffsetOf(BitField, "b") == 0);
assert(@byteOffsetOf(BitField, "c") == 0);
}
}
$ zig test test.zig
Test 1/1 pointer to non-bit-aligned field...OK
All tests passed.
Packed structs have 1-byte alignment. However if you have an overaligned pointer to a packed struct, Zig should correctly understand the alignment of fields. However there is a bug:
test.zig
const S = packed struct {
a: u32,
b: u32,
};
test "overaligned pointer to packed struct" {
var foo: S align(4) = undefined;
const ptr: *align(4) S = &foo;
const ptr_to_b: *u32 = &ptr.b;
}
$ zig test test.zig
/home/andy/dev/zig/docgen_tmp/test.zig:8:32: error: expected type '*u32', found '*align(1) u32'
const ptr_to_b: *u32 = &ptr.b;
^
When this bug is fixed, the above test in the documentation will unexpectedly pass, which will cause the test suite to fail, notifying the bug fixer to update these docs.
It's also planned to be able to set alignment of struct fields.
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
(anonymous struct at file.zig:7:38)
.
struct_name.zig
const std = @import("std");
pub fn main() void {
const Foo = struct {};
std.debug.warn("variable: {}\n", @typeName(Foo));
std.debug.warn("anonymous: {}\n", @typeName(struct {}));
std.debug.warn("function: {}\n", @typeName(List(i32)));
}
fn List(comptime T: type) type {
return struct {
x: T,
};
}
$ zig build-exe struct_name.zig
$ ./struct_name
variable: Foo
anonymous: struct:6:49
function: List(i32)
See also: