about things
0
fork

Configure Feed

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

structs#

designing structs that behave correctly when copied.

slices vs arrays in struct fields#

when you assign a struct, zig copies all fields by value. this has different implications depending on field type:

arrays are value types - the data lives inside the struct:

const Foo = struct {
    buffer: [64]u8,  // 64 bytes stored in the struct
};

var a = Foo{ .buffer = "hello".* ++ .{0} ** 59 };
var b = a;  // copies all 64 bytes - b has its own data

slices are fat pointers - they store a pointer and length, not data:

const Bar = struct {
    data: []const u8,  // 16 bytes: pointer + length
};

var a = Bar{ .data = "hello" };
var b = a;  // copies the pointer - b.data points to same memory as a.data

this is documented in zig.guide/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."

the copy problem#

if you build a struct, store a slice pointing to temporary data, then copy that struct - the copy's slice becomes dangling:

fn makeAttribute(temp_string: []const u8) Attribute {
    return .{ .value = temp_string };  // slice points to temp_string
}

// later, after temp_string goes out of scope:
const attr = makeAttribute(some_temp);
const copy = attr;  // copy.value is now a dangling pointer

this happened multiple times in logfire-zig when attributes were queued for batch export - by the time they were serialized, the original strings were gone.

copy-safe pattern: internal storage#

if a struct must survive being copied and needs string data, store the data internally:

pub const Attribute = struct {
    key: []const u8,
    value: Value,
    _string_storage: [max_len]u8 = undefined,
    _string_len: usize = 0,

    pub const max_len = 512;

    pub const Value = union(enum) {
        string,  // data in _string_storage[0.._string_len]
        int: i64,
        float: f64,
        bool_val: bool,
    };

    pub fn getString(self: *const Attribute) ?[]const u8 {
        return switch (self.value) {
            .string => self._string_storage[0..self._string_len],
            else => null,
        };
    }

    fn setString(self: *Attribute, str: []const u8) void {
        const len = @min(str.len, max_len);
        @memcpy(self._string_storage[0..len], str[0..len]);
        self._string_len = len;
        self.value = .string;
    }
};

key points:

  • _string_storage is a fixed array, not a slice - data is copied with the struct
  • _string_len tracks how much of the buffer is used
  • getString() reconstructs the slice from internal storage
  • the slice returned by getString() points into self, so copies get slices into their own storage

see: logfire-zig/attribute.zig

@constCast for inline struct literal slices#

when you need to pass an inline struct literal as a slice parameter, zig may need @constCast because the literal is const:

// this builds an array of MapEntry inline and passes it as a slice
try op_values.append(allocator, .{ .map = @constCast(&[_]cbor.Value.MapEntry{
    .{ .key = "action", .value = .{ .text = action_str } },
    .{ .key = "path", .value = .{ .text = path } },
}) });

without @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.

see: zat/firehose.zig encodeCommitPayload

std.math.cast for safe integer narrowing#

prefer std.math.cast over @intCast when narrowing integers from untrusted input. @intCast panics on overflow, std.math.cast returns null:

// dangerous — panics on 32-bit if header_len > maxInt(usize)
const len: usize = @intCast(varint_u64);

// safe — returns an error instead of panicking
const len = std.math.cast(usize, varint_u64) orelse return error.InvalidHeader;

use @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).

see: zat/car.zig

when to use this pattern#

use internal storage when:

  • structs are queued, batched, or stored for later processing
  • structs are copied into collections (ArrayList, hashmap values)
  • ownership of string data is unclear or temporary

use slices when:

  • struct lifetime is shorter than the data it references
  • data is compile-time constant (string literals)
  • you explicitly manage the backing memory