A while ago, we looked at ArenaAllocators and why you should view their free as a noop. The short version is that, in order for a free or destroy to actually return memory to the arena, two conditions need to be met: The freed memory must have been the last allocated memory, and The freed memory must have been allocated on the Arena's current node (which is an internal detail of how the Arena…
If I told you that the following prints 1665 : var b: std.ArrayList(u8) = try .initCapacity(init.gpa, 1024); try b.appendSlice(init.gpa, "a" ** 1025); std.debug.print("{d} n", .{b.capacity}); Would you be able to guess what this prints? var w: Io.Writer.Allocating = try .initCapacity(init.gpa, 1024); try w.writer.writeAll("a" ** 1025); std.debug.print("{d} n", .{w.writer.buffer.len}); Like me, you…
I use AI (Claude Code). A lot. I think it's a useful tool and like all tools, its usefulness will vary based on how you use it and what you're trying to do with it. Considering where things stand now, I have a hard time imagining a case where the value doesn't outweigh the cost. I think there's one major factor that will determine how useful these tools are for you: do you enjoy doing code…
If we wanted to write a function that takes one of Zig's new *std.Io.Reader and write it to stdout, we might start with something like: fn output(r: *std.Io.Reader) !void { const stdout = std.fs.File.stdout(); var buffer: [???]u8 = undefined; var writer = stdout.writer(&buffer); _ = try r.stream(&writer.interface, .unlimited); try writer.interface.flush(); } But what should the size of buffer be?…
If you're coming to Zig from a more hand-holding language, one of the things worth exploring is the relationship between the compiler and memory. I think code is the best way to do that, but briefly put into words: the memory that your program uses is all just bytes; it is only the compile-time information (the type system) that gives meaning to and dictates how that memory is used and…
You might have heard that Zig 0.15 introduces a new IO interface, with the focus for this release being the new std.Io.Reader and std.Io.Writer types. The old "interfaces" had problems. Like this performance issue that I opened. And it relied on a mix of types , which always confused me, and a lot of anytype - which is generally great, but a poor foundation to build an interface on. I've been…
As you might have heard, Zig's Io namespace is being reworked. Eventually, this will mean the re-introduction of async. As a first step though, the Writer and Reader interfaces and some of the related code have been revamped. This post is written based on a mid-July 2025 development release of Zig. It doesn't apply to Zig 0.14.x (or any previous version) and is likely to be outdated as more of the…
In a recent, post-Zig 0.14 commit, Zig's SinglyLinkedList and DoublyLinkedList saw significant changes . The previous version was a generic and, with all the methods removed, looked like: pub fn SinglyLinkedList(comptime T: type) type { return struct { first: ?*Node = null, pub const Node = struct { next: ?*Node = null, data: T, }; }; } The new version isn't generic. Rather, you embed the linked…
There are four important methods on Zig's std.mem.Allocator interface that Zig developers must be comfortable with: alloc(T, n) - which creates an array of n items of type T , free(ptr) - which frees memory allocate with alloc (although, this is implementation specific ), create(T) - which creates a single item of type T , and destroy(ptr) - which destroys an item created with create While you…
What happens when you free with an ArenaAllocator? You might be tempted to look at the documentation for std.mem.Allocator.free which says "Free an array allocated with alloc". But this is the one thing we're sure it won't do. In its current implementation, calling free usually does nothing: the freed memory isn't made available for subsequent allocations by the arena, and it certainly isn't…
Maybe I'm the only one, but it always takes my little brain a split second to understand what's happening whenever I see, or have to write, something like value.* = .{...} . If we take a step back, a variable is just a convenient name for an address on the stack. When this function executes: fn isOver9000(power: i64) bool { return power > 9000; } Say, with a power of 593, we could visualize its…
I've previously blogged about how much I like Zig's getOrPut hashmap method. As a brief recap, we can visualize Zig's hashmap as two arrays: keys: values: -------- -------- | Paul | | 1234 | @mod(hash("Paul"), 5) == 0 -------- -------- | | | | -------- -------- | | | | -------- -------- | Goku | | 9001 | @mod(hash("Goku"), 5) == 3 -------- -------- | | | | -------- -------- When we call…
In the last blog posts, we looked at different ways to compare strings in Zig . A few posts back, we introduced Zig's @bitCast . As a quick recap, @bitCast lets us force a specific type onto a value. For example, the following prints 1067282596: const std = @import("std"); pub fn main() !void { const f: f32 = 1.23; const n: u32 = @bitCast(f); std.debug.print("{d} n", .{n}); } What's happening here…
Newcomers to Zig will quickly learn that you can't switch on a string (i.e. []const u8 ). The following code gives us the unambiguous error message cannot switch on strings : switch (color) { "red" => {}, "blue" => {}, "green" => {}, "pink" => {}, else => {}, } I've seen two explanations for why this isn't supported. The first is that there's ambiguity around string identity. Are two strings only…
I have an unapologetic and, I'd like to think, pragmatic take on testing. I want tests to run fast, not be flaky and not be brittle. By "flaky", I mean tests that randomly fail. By "brittle", I mean tests that break due to seemingly unrelated changes in the code. I'm happy that the definition of "unit tests" has broadened over the years: it's now acceptable to have "unit tests" do more, i.e.…
As of mid-July 2025, development on a new Io namespace is underway. If you're using Zig 0.14.1 or earlier, this post is still relevant. I've written a bit about Zig's new Writer . I find Zig's idea of a "writer" confusing. This is probably because there are three different types, each trying to compensate for compromises made by the others. Let's try to understand what each is and how it fits into…
One of the first Zig-related blog posts I wrote was an overview of SIMD with Zig . I recently needed to revisit this topic when enhancing my smtp client library . Specifically, SMTP mostly expects printable ASCII characters. Almost all other characters, including UTF-8 text, must be encoded. I found the various SMTP and MIME RFCs confusing. So I settled on a simple approach: if the high bit of a…
Zig doesn't have an interface keyword or some simple way to create interfaces. Nonetheless, types like std.mem.Allocator and std.Random are often cited as examples that, with a bit of elbow grease, Zig offers every thing needed to create them. We've looked at Zig Interfaces in the past. If you want to understand how they work and how to write your own, I recommend you read that post first. But I…
If you look at my httpz library, you'll notice that httpz.Server(T) is a generic. The type passed to Server serves two purposes. The first is to support an application-specific context - whatever instance of T passed into Server(T).init gets passed back to your custom HTTP handlers. But the other purpose of T is to act as a configuration object. For example, if you want to circumvent most of…
In an older post, we explored Zig's @ptrCast and then looked at a concrete usage in the shape of std.heap.MemoryPool . @ptrCast As a brief recap, when we use @ptrCast , we're telling the compiler to treat a pointer as a given type. For example, this code runs fine: const std = @import("std"); pub fn main() !void { var user = User{.power = 9001, .name = "Goku", .active = true}; const cat: *Cat =…