about things
1# structs
2
3designing structs that behave correctly when copied.
4
5## slices vs arrays in struct fields
6
7when you assign a struct, zig copies all fields by value. this has different implications depending on field type:
8
9**arrays** are value types - the data lives inside the struct:
10```zig
11const Foo = struct {
12 buffer: [64]u8, // 64 bytes stored in the struct
13};
14
15var a = Foo{ .buffer = "hello".* ++ .{0} ** 59 };
16var b = a; // copies all 64 bytes - b has its own data
17```
18
19**slices** are fat pointers - they store a pointer and length, not data:
20```zig
21const Bar = struct {
22 data: []const u8, // 16 bytes: pointer + length
23};
24
25var a = Bar{ .data = "hello" };
26var b = a; // copies the pointer - b.data points to same memory as a.data
27```
28
29this is documented in [zig.guide/slices](https://zig.guide/language-basics/slices/): "slices can be thought of as many-item pointers with a length... the validity and lifetime of the backing memory is in the hands of the programmer."
30
31## the copy problem
32
33if you build a struct, store a slice pointing to temporary data, then copy that struct - the copy's slice becomes dangling:
34
35```zig
36fn makeAttribute(temp_string: []const u8) Attribute {
37 return .{ .value = temp_string }; // slice points to temp_string
38}
39
40// later, after temp_string goes out of scope:
41const attr = makeAttribute(some_temp);
42const copy = attr; // copy.value is now a dangling pointer
43```
44
45this happened multiple times in logfire-zig when attributes were queued for batch export - by the time they were serialized, the original strings were gone.
46
47## copy-safe pattern: internal storage
48
49if a struct must survive being copied and needs string data, store the data internally:
50
51```zig
52pub const Attribute = struct {
53 key: []const u8,
54 value: Value,
55 _string_storage: [max_len]u8 = undefined,
56 _string_len: usize = 0,
57
58 pub const max_len = 512;
59
60 pub const Value = union(enum) {
61 string, // data in _string_storage[0.._string_len]
62 int: i64,
63 float: f64,
64 bool_val: bool,
65 };
66
67 pub fn getString(self: *const Attribute) ?[]const u8 {
68 return switch (self.value) {
69 .string => self._string_storage[0..self._string_len],
70 else => null,
71 };
72 }
73
74 fn setString(self: *Attribute, str: []const u8) void {
75 const len = @min(str.len, max_len);
76 @memcpy(self._string_storage[0..len], str[0..len]);
77 self._string_len = len;
78 self.value = .string;
79 }
80};
81```
82
83key points:
84- `_string_storage` is a fixed array, not a slice - data is copied with the struct
85- `_string_len` tracks how much of the buffer is used
86- `getString()` reconstructs the slice from internal storage
87- the slice returned by `getString()` points into `self`, so copies get slices into their own storage
88
89see: [logfire-zig/attribute.zig](https://tangled.sh/@zzstoatzz.io/logfire-zig/tree/main/src/attribute.zig)
90
91## @constCast for inline struct literal slices
92
93when you need to pass an inline struct literal as a slice parameter, zig may need `@constCast` because the literal is `const`:
94
95```zig
96// this builds an array of MapEntry inline and passes it as a slice
97try op_values.append(allocator, .{ .map = @constCast(&[_]cbor.Value.MapEntry{
98 .{ .key = "action", .value = .{ .text = action_str } },
99 .{ .key = "path", .value = .{ .text = path } },
100}) });
101```
102
103without `@constCast`, you get a type mismatch — the literal produces `*const [N]MapEntry` but the field expects `[]const MapEntry` through a mutable pointer. this is safe because the data is embedded in the struct value being appended.
104
105see: [zat/firehose.zig encodeCommitPayload](https://tangled.sh/@zzstoatzz.io/zat/tree/main/src/internal/firehose.zig)
106
107## std.math.cast for safe integer narrowing
108
109prefer `std.math.cast` over `@intCast` when narrowing integers from untrusted input. `@intCast` panics on overflow, `std.math.cast` returns `null`:
110
111```zig
112// dangerous — panics on 32-bit if header_len > maxInt(usize)
113const len: usize = @intCast(varint_u64);
114
115// safe — returns an error instead of panicking
116const len = std.math.cast(usize, varint_u64) orelse return error.InvalidHeader;
117```
118
119use `@intCast` for values you've already bounds-checked or that come from trusted internal code. use `std.math.cast` at system boundaries (parsing wire formats, external input).
120
121see: [zat/car.zig](https://tangled.sh/@zzstoatzz.io/zat/tree/main/src/internal/car.zig)
122
123## when to use this pattern
124
125use internal storage when:
126- structs are queued, batched, or stored for later processing
127- structs are copied into collections (ArrayList, hashmap values)
128- ownership of string data is unclear or temporary
129
130use slices when:
131- struct lifetime is shorter than the data it references
132- data is compile-time constant (string literals)
133- you explicitly manage the backing memory