about things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 133 lines 4.8 kB view raw view rendered
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