struct

structs.zig

  1. // Declare a struct.
  2. // Zig gives no guarantees about the order of fields and the size of
  3. // the struct but the fields are guaranteed to be ABI-aligned.
  4. const Point = struct {
  5. x: f32,
  6. y: f32,
  7. };
  8. // Maybe we want to pass it to OpenGL so we want to be particular about
  9. // how the bytes are arranged.
  10. const Point2 = packed struct {
  11. x: f32,
  12. y: f32,
  13. };
  14. // Declare an instance of a struct.
  15. const p = Point {
  16. .x = 0.12,
  17. .y = 0.34,
  18. };
  19. // Maybe we're not ready to fill out some of the fields.
  20. var p2 = Point {
  21. .x = 0.12,
  22. .y = undefined,
  23. };
  24. // Structs can have methods
  25. // Struct methods are not special, they are only namespaced
  26. // functions that you can call with dot syntax.
  27. const Vec3 = struct {
  28. x: f32,
  29. y: f32,
  30. z: f32,
  31. pub fn init(x: f32, y: f32, z: f32) Vec3 {
  32. return Vec3 {
  33. .x = x,
  34. .y = y,
  35. .z = z,
  36. };
  37. }
  38. pub fn dot(self: Vec3, other: Vec3) f32 {
  39. return self.x * other.x + self.y * other.y + self.z * other.z;
  40. }
  41. };
  42. const expect = @import("std").testing.expect;
  43. test "dot product" {
  44. const v1 = Vec3.init(1.0, 0.0, 0.0);
  45. const v2 = Vec3.init(0.0, 1.0, 0.0);
  46. try expect(v1.dot(v2) == 0.0);
  47. // Other than being available to call with dot syntax, struct methods are
  48. // not special. You can reference them as any other declaration inside
  49. // the struct:
  50. try expect(Vec3.dot(v1, v2) == 0.0);
  51. }
  52. // Structs can have declarations.
  53. // Structs can have 0 fields.
  54. const Empty = struct {
  55. pub const PI = 3.14;
  56. };
  57. test "struct namespaced variable" {
  58. try expect(Empty.PI == 3.14);
  59. try expect(@sizeOf(Empty) == 0);
  60. // you can still instantiate an empty struct
  61. const does_nothing = Empty {};
  62. _ = does_nothing;
  63. }
  64. // struct field order is determined by the compiler for optimal performance.
  65. // however, you can still calculate a struct base pointer given a field pointer:
  66. fn setYBasedOnX(x: *f32, y: f32) void {
  67. const point = @fieldParentPtr(Point, "x", x);
  68. point.y = y;
  69. }
  70. test "field parent pointer" {
  71. var point = Point {
  72. .x = 0.1234,
  73. .y = 0.5678,
  74. };
  75. setYBasedOnX(&point.x, 0.9);
  76. try expect(point.y == 0.9);
  77. }
  78. // You can return a struct from a function. This is how we do generics
  79. // in Zig:
  80. fn LinkedList(comptime T: type) type {
  81. return struct {
  82. pub const Node = struct {
  83. prev: ?*Node,
  84. next: ?*Node,
  85. data: T,
  86. };
  87. first: ?*Node,
  88. last: ?*Node,
  89. len: usize,
  90. };
  91. }
  92. test "linked list" {
  93. // Functions called at compile-time are memoized. This means you can
  94. // do this:
  95. try expect(LinkedList(i32) == LinkedList(i32));
  96. var list = LinkedList(i32) {
  97. .first = null,
  98. .last = null,
  99. .len = 0,
  100. };
  101. try expect(list.len == 0);
  102. // Since types are first class values you can instantiate the type
  103. // by assigning it to a variable:
  104. const ListOfInts = LinkedList(i32);
  105. try expect(ListOfInts == LinkedList(i32));
  106. var node = ListOfInts.Node {
  107. .prev = null,
  108. .next = null,
  109. .data = 1234,
  110. };
  111. var list2 = LinkedList(i32) {
  112. .first = &node,
  113. .last = &node,
  114. .len = 1,
  115. };
  116. // When using a pointer to a struct, fields can be accessed directly,
  117. // without explicitly dereferencing the pointer.
  118. // So you can do
  119. try expect(list2.first.?.data == 1234);
  120. // instead of try expect(list2.first.?.*.data == 1234);
  121. }

Shell

  1. $ zig test structs.zig
  2. 1/4 test.dot product... OK
  3. 2/4 test.struct namespaced variable... OK
  4. 3/4 test.field parent pointer... OK
  5. 4/4 test.linked list... OK
  6. 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:

default_field_values.zig

  1. const Foo = struct {
  2. a: i32 = 1234,
  3. b: i32,
  4. };
  5. test "default struct initialization fields" {
  6. const x = Foo{
  7. .b = 5,
  8. };
  9. if (x.a + x.b != 1239) {
  10. @compileError("it's even comptime known!");
  11. }
  12. }

Shell

  1. $ zig test default_field_values.zig
  2. 1/1 test.default struct initialization fields... OK
  3. All 1 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, 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.
  • 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:

packed_structs.zig

  1. const std = @import("std");
  2. const native_endian = @import("builtin").target.cpu.arch.endian();
  3. const expect = std.testing.expect;
  4. const Full = packed struct {
  5. number: u16,
  6. };
  7. const Divided = packed struct {
  8. half1: u8,
  9. quarter3: u4,
  10. quarter4: u4,
  11. };
  12. test "@bitCast between packed structs" {
  13. try doTheTest();
  14. comptime try doTheTest();
  15. }
  16. fn doTheTest() !void {
  17. try expect(@sizeOf(Full) == 2);
  18. try expect(@sizeOf(Divided) == 2);
  19. var full = Full{ .number = 0x1234 };
  20. var divided = @bitCast(Divided, full);
  21. try expect(divided.half1 == 0x34);
  22. try expect(divided.quarter3 == 0x2);
  23. try expect(divided.quarter4 == 0x1);
  24. var ordered = @bitCast([2]u8, full);
  25. switch (native_endian) {
  26. .Big => {
  27. try expect(ordered[0] == 0x12);
  28. try expect(ordered[1] == 0x34);
  29. },
  30. .Little => {
  31. try expect(ordered[0] == 0x34);
  32. try expect(ordered[1] == 0x12);
  33. },
  34. }
  35. }

Shell

  1. $ zig test packed_structs.zig
  2. 1/1 test.@bitCast between packed structs... OK
  3. All 1 tests passed.

Zig allows the address to be taken of a non-byte-aligned field:

pointer_to_non-byte_aligned_field.zig

  1. const std = @import("std");
  2. const expect = std.testing.expect;
  3. const BitField = packed struct {
  4. a: u3,
  5. b: u3,
  6. c: u2,
  7. };
  8. var foo = BitField{
  9. .a = 1,
  10. .b = 2,
  11. .c = 3,
  12. };
  13. test "pointer to non-byte-aligned field" {
  14. const ptr = &foo.b;
  15. try expect(ptr.* == 2);
  16. }

Shell

  1. $ zig test pointer_to_non-byte_aligned_field.zig
  2. 1/1 test.pointer to non-byte-aligned field... OK
  3. 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.zig

  1. const std = @import("std");
  2. const expect = std.testing.expect;
  3. const BitField = packed struct {
  4. a: u3,
  5. b: u3,
  6. c: u2,
  7. };
  8. var bit_field = BitField{
  9. .a = 1,
  10. .b = 2,
  11. .c = 3,
  12. };
  13. test "pointer to non-bit-aligned field" {
  14. try expect(bar(&bit_field.b) == 2);
  15. }
  16. fn bar(x: *const u3) u3 {
  17. return x.*;
  18. }

Shell

  1. $ zig test test.zig
  2. docgen_tmp/test.zig:17:20: error: expected type '*const u3', found '*align(0:3:1) u3'
  3. try expect(bar(&bit_field.b) == 2);
  4. ^~~~~~~~~~~~
  5. docgen_tmp/test.zig:17:20: note: pointer host size '1' cannot cast into pointer host size '0'
  6. docgen_tmp/test.zig:17:20: note: pointer bit offset '3' cannot cast into pointer bit offset '0'
  7. docgen_tmp/test.zig:20:11: note: parameter type declared here
  8. fn bar(x: *const u3) u3 {
  9. ^~~~~~~~~

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:

packed_struct_field_addrs.zig

  1. const std = @import("std");
  2. const expect = std.testing.expect;
  3. const BitField = packed struct {
  4. a: u3,
  5. b: u3,
  6. c: u2,
  7. };
  8. var bit_field = BitField{
  9. .a = 1,
  10. .b = 2,
  11. .c = 3,
  12. };
  13. test "pointers of sub-byte-aligned fields share addresses" {
  14. try expect(@ptrToInt(&bit_field.a) == @ptrToInt(&bit_field.b));
  15. try expect(@ptrToInt(&bit_field.a) == @ptrToInt(&bit_field.c));
  16. }

Shell

  1. $ zig test packed_struct_field_addrs.zig
  2. 1/1 test.pointers of sub-byte-aligned fields share addresses... OK
  3. All 1 tests passed.

This can be observed with @bitOffsetOf and offsetOf:

test_bitOffsetOf_offsetOf.zig

  1. const std = @import("std");
  2. const expect = std.testing.expect;
  3. const BitField = packed struct {
  4. a: u3,
  5. b: u3,
  6. c: u2,
  7. };
  8. test "pointer to non-bit-aligned field" {
  9. comptime {
  10. try expect(@bitOffsetOf(BitField, "a") == 0);
  11. try expect(@bitOffsetOf(BitField, "b") == 3);
  12. try expect(@bitOffsetOf(BitField, "c") == 6);
  13. try expect(@offsetOf(BitField, "a") == 0);
  14. try expect(@offsetOf(BitField, "b") == 0);
  15. try expect(@offsetOf(BitField, "c") == 0);
  16. }
  17. }

Shell

  1. $ zig test test_bitOffsetOf_offsetOf.zig
  2. 1/1 test.pointer to non-bit-aligned field... OK
  3. All 1 tests passed.

Packed structs have the same alignment as their backing integer, however, overaligned pointers to packed structs can override this:

overaligned_packed_struct.zig

  1. const std = @import("std");
  2. const expect = std.testing.expect;
  3. const S = packed struct {
  4. a: u32,
  5. b: u32,
  6. };
  7. test "overaligned pointer to packed struct" {
  8. var foo: S align(4) = .{ .a = 1, .b = 2 };
  9. const ptr: *align(4) S = &foo;
  10. const ptr_to_b: *u32 = &ptr.b;
  11. try expect(ptr_to_b.* == 2);
  12. }

Shell

  1. $ zig test overaligned_packed_struct.zig
  2. 1/1 test.overaligned pointer to packed struct... OK
  3. All 1 tests passed.

It’s also possible to set alignment of struct fields:

test_aligned_struct_fields.zig

  1. const std = @import("std");
  2. const expectEqual = std.testing.expectEqual;
  3. test "aligned struct fields" {
  4. const S = struct {
  5. a: u32 align(2),
  6. b: u32 align(64),
  7. };
  8. var foo = S{ .a = 1, .b = 2 };
  9. try expectEqual(64, @alignOf(S));
  10. try expectEqual(*align(2) u32, @TypeOf(&foo.a));
  11. try expectEqual(*align(64) u32, @TypeOf(&foo.b));
  12. }

Shell

  1. $ zig test test_aligned_struct_fields.zig
  2. 1/1 test.aligned struct fields... OK
  3. 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 (anonymous struct at file.zig:7:38).
  • 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

  1. const std = @import("std");
  2. pub fn main() void {
  3. const Foo = struct {};
  4. std.debug.print("variable: {s}\n", .{@typeName(Foo)});
  5. std.debug.print("anonymous: {s}\n", .{@typeName(struct {})});
  6. std.debug.print("function: {s}\n", .{@typeName(List(i32))});
  7. }
  8. fn List(comptime T: type) type {
  9. return struct {
  10. x: T,
  11. };
  12. }

Shell

  1. $ zig build-exe struct_name.zig
  2. $ ./struct_name
  3. variable: struct_name.main.Foo
  4. anonymous: struct_name.main__struct_3896
  5. 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:

struct_result.zig

  1. const std = @import("std");
  2. const expect = std.testing.expect;
  3. const Point = struct {x: i32, y: i32};
  4. test "anonymous struct literal" {
  5. var pt: Point = .{
  6. .x = 13,
  7. .y = 67,
  8. };
  9. try expect(pt.x == 13);
  10. try expect(pt.y == 67);
  11. }

Shell

  1. $ zig test struct_result.zig
  2. 1/1 test.anonymous struct literal... OK
  3. 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:

struct_anon.zig

  1. const std = @import("std");
  2. const expect = std.testing.expect;
  3. test "fully anonymous struct" {
  4. try dump(.{
  5. .int = @as(u32, 1234),
  6. .float = @as(f64, 12.34),
  7. .b = true,
  8. .s = "hi",
  9. });
  10. }
  11. fn dump(args: anytype) !void {
  12. try expect(args.int == 1234);
  13. try expect(args.float == 12.34);
  14. try expect(args.b);
  15. try expect(args.s[0] == 'h');
  16. try expect(args.s[1] == 'i');
  17. }

Shell

  1. $ zig test struct_anon.zig
  2. 1/1 test.fully anonymous struct... OK
  3. All 1 tests passed.

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, the @"0" syntax must be used to access them. Names inside @"" are always recognised as identifiers.

Like arrays, tuples have a .len field, can be indexed and work with the ++ and ** operators. They can also be iterated over with inline for.

tuple.zig

  1. const std = @import("std");
  2. const expect = std.testing.expect;
  3. test "tuple" {
  4. const values = .{
  5. @as(u32, 1234),
  6. @as(f64, 12.34),
  7. true,
  8. "hi",
  9. } ++ .{false} ** 2;
  10. try expect(values[0] == 1234);
  11. try expect(values[4] == false);
  12. inline for (values) |v, i| {
  13. if (i != 2) continue;
  14. try expect(v);
  15. }
  16. try expect(values.len == 6);
  17. try expect(values.@"3"[0] == 'h');
  18. }

Shell

  1. $ zig test tuple.zig
  2. 1/1 test.tuple... OK
  3. All 1 tests passed.

See also: