Adventures in Zig

Our Salamander Corn

Zig is a general purpose programming language, it is low level, type safe, and doesn’t allow values to be null, not unlike rust. Where zig differs from rust is mostly in what it lacks: it has no official package manager yet, functions are not first class citizens, and no traits or classes, making most of the code very C like and procedural.

These differences were expected when trying out zig, some others I was not prepared for.

If you are interested in learning zig, I highly recommend ziglearn.org or the official documentation.

No String type

Zig does not have a string type, similar to C, we can treat an array of characters or u8s as a string.

const str = "Hello there";

std.debug.print("{}\n", .{@TypeOf(arr)}); // *const [11:0]u8

Because the type inference of zig is pretty good, albiet not quite rust level, we can still treat them as strings in our head, except when needing to print them out.

const str = [_]u8{ 72, 79, 87, 68, 89 };

std.debug.print("{any}\n", .{str});
// { 72, 79, 87, 68, 89 }

std.debug.print("{s}\n", .{str});
// HOWDY

Specifying memory allocation

In rust, we are forced to think about memory allocation in an interesting way, by introducing the concept of borrowing and lifetimes, we now think about who owns what, in terms of functions are variables. In zig, you are forced to think about memory allocation explicitly, by providing what type of memory allocation you wish for a data structure to be initialized using.

var list = ArrayList(u8).init(std.heap.page_allocator);
defer list.deinit();
try list.append('H');
try list.append('e');
try list.append('l');
try list.append('l');
try list.append('o');
try list.appendSlice(" World!");

There are a few benefits for this, outside of forcing us to think about what happens when initializing variables. We can specify, for example, that we want to only allocate 8 bytes of memory for any given data structures, that would otherwise rely on dynamic memory allocation, giving us the ability to use the same API, but specify how the memory is treated.

pub fn main() void {

    // create allocator
    var buffer: [8]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const allocator = fba.allocator();

    // initialize ArrayList
    var list = ArrayList(u8).init(allocator);
    defer list.deinit();

    // Attempt to append 9 u8's
    for (1..10) |c| {
        const cu8 = @truncate(u8, c);
        list.append(cu8) catch printError(cu8);
    }

    // print final created list
    std.debug.print("arr items: {any}\n", .{list.items});
}

// print an exception
fn printError(c: u8) void {
    std.debug.print("Out of Memory! Cannot append {}\n", .{c});
}

Output

Out of Memory! Cannot append 9
arr items: { 1, 2, 3, 4, 5, 6, 7, 8 }

Yes, this is essentially a more complicated var list = [8]u8{}, but it is using the ArrayList API and can be done with hashmaps. I haven’t seen this done in any language before, but I’m not an expert on low level languages so maybe this is borrowed from elsewhere. Looking at this example leads into the next point.

Java-like errors

There is probably a better way to describe this, but as someone who also writes a fair amount of java this was the way that helped me understand it.

Rust’s Result type

In rust, you have what’s known as a Result<T, E> where it is unknown when writing the code if a value is an error or not, rust also uses this concept for getting around null with Option<T> but we will go over that later. An easy example of this is when attempting to parse a string into a number;

let four_hundred = "400".parse::<u32>();

four_hundred is a Result type, if we wanted to get access to the value we can do any of the following

print!(four_hundred.unrwap());

four_hundred.map(|four_hundred_u32| print!(four_hundred_u32));

match four_hundred {
  Ok(n) => print!(n),
  Err(..) => {}
}

// any many more

What we can do is pass the Result value around before we need to .unrwap() the value and use it, giving us the opportunity to write safe code we know will run without an exception being thrown.

Let’s try this in zig

const fourHundred = std.fmt.parseInt(u32, "400", 10);

Already this code throws an error, so we have to handle it here, sorta. Like java, if you have code that throws an exception, you can propagate the exception by adding throws Exception to the end of the function, allowing whoever runs your function to handle it. We can do this in zig by adding try before problem expression and then adding a ! to our function signature, even main.

pub fn main() !void {
    const fourHundred = try std.fmt.parseInt(u32, "400", 10);

    std.debug.print("{}\n", .{fourHundred});
}

What if we want to handle the exception ourselves?

We did this before with catch.

  • SIDE_NOTE: Try and catch typically in java and javascript would be used together, here they are used separately, where catch is taking the place for try {} catch (e) {} in other languages.

catch after an expression will call the following expression in place of the expression, it is here we can specify a default or fallback value, or tell the compiler this will never happen with the unreachable keyword.

pub fn main() void {
    const fourHundred =
      std.fmt.parseInt(u32, "400", 10) catch 0;
    const fourHundred =
      std.fmt.parseInt(u32, "400", 10) catch unreachable;

    std.debug.print("{}", .{fourHundred});

// or use an if statement

    if (std.fmt.parseInt(u32, "400", 10)) |fourHundred| {
      std.debug.print("{}", .{fourHundred});

    }
}

The reason to use try and ! over catch unreachable is the former will dump the specific error that happened in stderr, where unreachable will remove any information about that.


These are a few things that took me a minute to really grasp in zig, overall I enjoy the language.

I enjoy the javascript like syntax with typesafe and minimal footprint, it’s ability to interact with C libraries without the need for bindings is really attractive, especially for using in existing C codebases, I understand why it exists, it certainly fills a hole in between C and go in a way that isn’t rust. Overall I think I will continue to learn and stick with it.