Gotchas when Doing Zig Programming (v0.7)

A report back from the land of Zig and the dangers that lurk there

So I have been spending some time familiarizing myself with the Zig programming language.

Since we are still not in version 1.0, there are bound to be some bumps in the road. So this is me documenting some of the challenges I faced.

On of the first things I naturally do in my Zig programs is to create an allocator to pass to all the functions which need to allocate memory. I use the GeneralPurposeAllocator which is good for debugging. Perhaps too good, I found its complaints a bit annoying for small test programs where I was totally fine with leaking memory all over the place. But I digress.

One thing I initially felt very uncertain about was when I would seek to obtain an address. If you look at the example below we simply get the value for the gpa and dir variable:

const std = @import("std");

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var allocator: *std.mem.Allocator = &gpa.allocator;

const dir: Dir = std.fs.cwd();

However the allocator is a pointer. How do you know when to get a pointer? Here are some simple rules to keep in mind:

  1. If you are not providing an allocator to a function, then it also will not be allocating anything. Meaning it will return whole values and not pointers in most cases.
  2. If you don’t use some accessor function but actually obtain field of a struct, then you would generally want a pointer to this field.

gpa.allocator is a field in the gpa struct. If you did not take a pointer to this field, then you would be modifying a copy of this field. That is fine if the field is just primitive data such as an integer. But when you have more complex data structures which you will be mutating, then would want a pointer.

std.fs.cwd() e.g. is not part of an object, so whatever is returned cannot be a field of say std.fs, hence you should get a whole struct value to work with and not use a pointer.

It might be worth mentioning as a side note of how interfaces and inheritance works in Zig. Interfaces and inheritance is not formally part of the language. Instead it is done much like C, where one of the member variables of a struct basically represents the super class / base class.

E.g. you could think of std.mem.Allocator as being the base class or interface of std.heap.GeneralPurposeAllocator. The base class functionality is accessible through the .allocator member. It contains function pointers taking std.mem.Allocator as argument.

But it actually depends on the surrounding struct. Here is a simplified and edited version of how the alloc function on the `std.mem.Allocator struct works:

fn alloc(allocator: *Allocator, ...) ![*]u8 {
var self: GeneralPurposeAllocator = undefined;
self = @fieldParentPtr(GeneralPurposeAllocator,
// code ...

This function is actually first is given as a function pointer to a field on std.mem.Allocator. Every specific kind of allocator will populate these function pointer fields with functions specific to that particular allocator.

So this function uses the @fieldParentPtr to obtain a pointer to the surrounding GeneralPurposeAllocator struct. But if allocator is a copy of the allocator object inside a GeneralPurposeAllocator struct, instead of inside it, then this will blow up.

@fieldParentPtr is a function which runs at compilation time, to calculate the base pointer for the struct. It can do that because the compiler naturally knows what memory offset the allocator field is within the GeneralPurposeAllocator. However the compiler cannot know whether the allocator object is actually inside such a struct, only that if it was, it can give you the base pointer. If this explanation doesn't make sense, then I advice you to read this more detailed explanation of Zig interfaces.

You may ask, why all this complication? Why not just do like C++, Go and others and provide interfaces or classes at a language level?

The point is for Zig to be fairly low-level. An advantage of creating interface like this is that there is a lot of flexibility in how it is done. E.g. multiple inheritance is easy to add if desired.

You could add a field to your struct for every interface / base class you are implementing e.g.

In my other life I would usually be using Julia and Python where one simple calls open to open a file. Zig here has a somewhat peculiar interface. It makes some sense, but I am not sure where this was inspired by.

In Zig when you want to open a file, you typically have to start with a directory. Then you ask this directory to open a file relative to it.

If you want to find out what your current working directory is according to Zig you can to that with these lines of code:

const path = try fs.realpathAlloc(allocator, ".");
try stdout.print("{}\n", .{path});

You don’t strictly need to deallocate the path but that is good habit.

If the current directory is where you think it should be we can try to open a file in that directory. First we get the current working directory:

const fs = std.fs;
const dir: fs.Dir = fs.cwd();

From that working directory we open the file foobar.txt in the current directory:

const file: fs.File = try dir.openFile(
.{ .read = true },
defer file.close();

The second argument .{ .read = true } needs some explanation for Zig beginners. What we are doing here is actually providing a struct where we set the read field to true. The struct is defined roughly as:

pub const OpenFlags = struct {
read: bool = true,
write: bool = false,
lock: Lock = .None,

Please note I removed some fields for clarity.

By just writing dots . we rely on the Zig compiler to automatically infer the type. If we did not want to do that we could instead write:

var flags = fs.File.OpenFlags{}; = true;

const file: File = try dir.openFile(

Which would be tedious. The reason we see this pattern so often in Zig code, is because Zig does not have named arguments or variable number of arguments. But by using structs in this way we get many of the same advantages.

As we talked about with allocators, using a base interface often involves getting a field. The file object itself does not offer generic reader functions. We get that by calling reader().

const io =;
const reader: io.Reader = file.reader();

If you read older Zig code examples you will see file.inStream() used instead, but this is deprecated so forget you ever saw it.

io.Reader is an object you can pass to other functions so they don't have to concern themselves with exactly what you are reading, whether it is a file, pipe or socket.

Finally we get to the part which inspired me to write this story. I wanted to read one line at a time through the io.Reader interface.

Several functions allow you to do that:

  • readUntilDelimiterArrayList
  • readUntilDelimiterAlloc
  • readUntilDelimiterOrEof

I choose the later as it seems smart to avoid allocating memory for a line over and over again, and instead reuse a buffer:

var buffer: [500]u8 = undefined;
while (try reader.readUntilDelimiterOrEof(buffer[0..], '\n')) |line| {
try stdout.print("Line: {}\n", .{line});

Using this correctly however proved tricky. The reason can be found in the function signature:

fn readUntilDelimiterOrEof(self: Self, buf: []u8, delimiter: u8) !?[]u8

The ! means it returns either and error set or a value. However the ? means this value could be 8-bit unsigned integer array ([]u8) or a null pointer. Having both a null and potentially an error code is a headache, and honestly here I think they made a mistake in the interface design.

The error is to report e.g. that the buffer is too small. While the optional value is to indicate that we have read until the end, and there is no more data.

However dealing with both an optional value and an error set in a while-loop in Zig is not something it was designed for. This line means we return the optional value or exit the function with an error:

try reader.readUntilDelimiterOrEof(buffer[0..], '\n')

The while look is setup so that you can do this:

while (next()) |item| {

Here the loop continues while element returned from next() is not null. This gives desired behavior in our case.

But what we we decide that we want to actually handle the potential error code returned locally? Then we might be tempted to think we could write:

while (reader.readUntilDelimiterOrEof(buffer[0..], '\n')) |line| {
try stdout.print("Line: {}\n", .{line});
} else |err| {
try stdout.print("Error: {}\n", .{err});

But DON’T do this! You will get an infinite loop. If you look in the manual this looks tempting because adding an else to the while-loop allows you to catch errors in a while loop.

Except this construct does not deal with options. So in this case when line becomes null the loop doesn't actually terminate, which means it continues indefinitely.

So there is no simple way of handling both issue at the same time. The error is better to propagate out of the function. Alternatively we create an infinite while loop which we deliberately break out of when needed. The implementation actually use this tactic:

pub fn readUntilDelimiterOrEof(self: Self, buf: []u8, delimiter: u8) !?[]u8 {
var index: usize = 0;
while (true) {
const byte = self.readByte() catch |err| switch (err) {
error.EndOfStream => {
if (index == 0) {
return null;
} else {
return buf[0..index];
else => |e| return e,

if (byte == delimiter) return buf[0..index];
if (index >= buf.len) return error.StreamTooLong;

buf[index] = byte;
index += 1;

Written by

Geek dad, living in Oslo, Norway with passion for UX, Julia programming, science, teaching, reading and writing.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store