Suffering: The First Two Weeks Of Zig
āš”

Suffering: The First Two Weeks Of Zig

Tags
ZigCDev Log
Owner
Justin Nearing
ā€¼ļø
Yo this is still WIP as of @April 15, 2024 Iā€™ve figured out what a string is (5 days) and wrote a detailed explanation of *const [5:0]u8 means. I put that all in
šŸ§µ
Strings, Actually, Do Not Exist
Reddit had no chill in on the comments on that one. Iā€™ve struggled with the package manager, then abandoned using external packages to and ā€œjust use stdā€. Currently Iā€™m struggling with std.fs.dir The problem Iā€™ve run into now is that Iā€™ve kinda lost the plot for this article. The original intent was to be a first impressions kind of thing, but those impressions are old now. This is mainly due to having a bunch of tech interviews running deep, soaking up the mental bandwidth to work on this project. So I think I will finish the project, with this article, but not publish it out like I had originally intended to.

šŸŽ¶

Using Zig for the Memes and other bad choices.

In interviews, I usually joke that I'm equally bad coding in all languages.

It sometimes lands.

Most languages are pretty easy to grok.

Once you learn one loosely typed language, the others are just syntactic differences and particular idiosyncrasies with the language/dev ecosystem.

Same is true for strongly typed languages, C# and Java are actually the same.

It was this line of thinking where I thought writing a thing in Zig would be fast, and good for a laugh.

It has been good for a laugh.

It has not been fast.

šŸ”‘
The mistake I made was equating higher level languages with lower level ones. Learning each ā€œlevelā€ the first time is the hard part. Learning a loosely typed language like JS the first time is hard. Learning a strongly typed language like Java the first time is hard. Learning a memory constrained language like C the first time is hard.

So the jokes on me, because Iā€™m too stubborn to give up on learning Zig for the lulz, which means going through the painful process of learning memory management for the first time.

Some of that means teaching myself CompSci 101 level concepts like

Courses not required to get my certificate in ā€œGame Art & Designā€ from a for-profit college over 15 years ago.

Iā€™ve made great choices in life.

Building World of Warcraft in Zig

Have you ever wanted to build a game?

Thought incessantly about that grand MMORPG that would fundamentally change the nature of the games industry?

Yeah, thatā€™s not going to be your first game.

This is incredibly difficult to pull off as your first game, let alone WoW2: MechaDragonBoogaloo
This is incredibly difficult to pull off as your first game, let alone WoW2: MechaDragonBoogaloo

Defining the appropriate scope for a project is so freaking important.

I want to say something outrageous, like 80% of a projects success is determined by whether its scoped appropriately.

So instead of using Zig to build World of Warcraft, my first project using low level programming will be a bit more realistic.

I wanted a script to delete four directories from an Unreal game project.

But then I wanted a ā€œrealā€ executable to do it using a ā€œrealā€ programming language.

I decided to use Zig because I thought itā€™d be funny.

Spec:

  • Take an directory as an argument
  • Shallow walk the dir, match any subdir with an array of dirs to delete
  • Delete those dirs

Easy right?

ā€£

Getting bamboozled by the Package Manager

So lets start with the first thing: Take a directory as an argument

Iā€™ll admit I wasnā€™t thinking critically enough, because I decided to go with the top websearch answer for zig parse arg

Itā€™s 2024, one does not simply assume the top ten results for any given websearch are correct.

Anyways, it took me here:

Already Iā€™m shook:

const is_zig_11 = @import("builtin").zig_version.minor == 11;

pub fn main() !void {
    if (is_zig_11) return;

    // This example can't be compiled with zig 0.11,
    // so we put module import here.
    const simargs = @import("simargs");
}

Cool. The package in question, simargs , doesnā€™t compile with Zig 0.11.

Im pretty sure Iā€™m on 12 soooo, this will work?

0.11 and only 0.11 is broken?

Sus. Real sus.

Yeah we shook.

We could always try it.

Whereā€™s my npm install simargs?

Ok, thereā€™s zig fetch --save foo

Just need to solve for foo.

Probably github url?

simargs seems to point to a github project zigcli

zigcli is a good sounding name, surely there will be nothing malicious.

šŸ« 
Zigzon is something I just made up, the final nail in the coffin of a mind fully slipping into the sweet oblivion of madness. But, I ask you, what better term would you use to add items to the build.zig.zon file? Ur zigzonninā€™, son.

Although, we getting some real greasy install instructions on the official README.md :

.hashĀ field is difficult to get now, you can fill in a fake one likeĀ 1220b5dafa54081ce54f108d0b7fa9451149bde7dc7c20f4e14e322fdeeb7a1dfd9d, then runĀ zig build, it should throw following similar error:
Fetch Packages [1/1] zigcli... ~/demo/build.zig.zon:7:21: error: hash mismatch: expected: 
found:            
.hash =

TheĀ found:Ā section output the real hash, copy that toĀ build.zig.zon.

Here is live video of installing a package in Zig:

Celebrating independent Canadian media.

āš”
Iā€™m going to assume improved package management is coming, and that zigzon (as a noun) will incentivize:
  • A semantically versioned ecosystem
  • Provide a central hub for OSS module discovery/maintainability
  • Basically leverage the approach that made JS such a dominant language in the modern programming landscape.

User-friendly package management is table stakes at this point. Zig is a toy until this is literally a joy to use.

So I grab the latest commit from main

zig fetch --save https://github.com/jiacai2050/zigcli/archive/1e83260a431f696378ebad4bf72d25128fd26706.tar.gz
error: unable to unpack tarball
note: unable to create symlink from 'docs/content/_index.org' to '../../README.org': AccessDenied

Cool.

That URL is valid, successfully downloading a tar that unzips properly when done manually.

That note is utter nonsense.

WTF is this symlink? It appears to be linking nonsense to nonsense. And the error(?) is an Access Denied.

Iā€™m going to assume thereā€™s some nonsense with their package, nothing to do with me.

ā€£

Simple problems require standard solutions

const std = @import("std");

pub fn main() !void {
    // Get allocator
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer _ = gpa.deinit();

    // Parse args into string array (error union needs 'try')
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    // Get and print them!
    std.debug.print("There are {d} args:\n", .{args.len});
    for (args) |arg| {
        std.debug.print("  {s}\n", .{arg});
    }
}

//There are 1 args:
//  C:\path\to\CleanBuild\zig-out\bin\CleanBuild.exe

I shouldā€™ve known.

In retrospect, itā€™s bad practice to just default looking for 3rd party solutions.

Itā€™s lazy programming, and a pretty big downside of having an ā€œawesomeā€ package manager like NPM.

The kind of lazy programming that gives you the left-pad incident, and makes backdoors like xz util harder to spot.

šŸ“¦
So Iā€™m the idiot. I take back everything I said about Zig being a toy. An obtuse package manager makes for safer code and better programmers. Grow a beard and embrace arch, dorks.

ā€£

Stacked Heaps and Fat Allocators

šŸ“
What follows is dive into the basics of memory allocation, heap vs. stack, etc. If that sounds boring, I made this section collapsible.

Funnily enough, this is the first time in my life Iā€™ve had to use an allocator.

I know of them, mainly that they cause memory leaks because you forget to close them.

And that Zig is actually awesome because they have defer, which lets you forget to close them.

But itā€™s worth taking the time to dive a bit deeper since weā€™re here.

const is good, because the compiler can reserve the memory required for the item in advance.

Computers have two memory buckets, the stack and the heap.

Constants are stored on the stack, giving them scope (only available in the function that has the declared variable (or lower)).

Stack memory doesnā€™t really require you to do anything.

Things get more complicated when you have some dynamic variable, where you donā€™t know the size of in advance.

In those cases, things are stored on the heap.

Heap doesnā€™t have the same scope constraints, because memory is manually managed.

That memory management uses an allocator.

An allocator allocates memory on the heap, and the items assigned to the variable are stored on the heap.

I think.

You donā€™t interact with the heap directly, you get a pointer that references the location in memory where the object lives.

Pointers live on the stack, which means they have scope, which means if they go out of scope, but the items they pointed to still exist on the heap, the memory is essentially trapped.

Or maybe a better way to say it is the memory leaks out of your program, never getting deallocated.

So thatā€™s all cool.

In practice Zig has a GeneralPurposeAllocator , which seems like something I should use unless I have a good reason not to.

And has a defer keyword that, I guess, frees heap memory if things go out of scope?

So hereā€™s what we got:

    // Get allocator
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer _ = gpa.deinit();

    // Parse args into string array (error union needs 'try')
    // args[0] contains path\to\executable
    // args[1] would be first passed argument
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len != 2) {
        std.debug.print("Program expected to be run with 1 argument given, got {d}\n", .{args.len - 1});
        std.process.exit(1);
    }

The allocator marshals arguments into an args constant, with me deferring things twice.

I have no idea why Iā€™m doing that. I think its copypasta.

Ok letā€™s figure it out.

This is std.process.argsFree (I think Iā€™m on Zig 0.12)

pub fn argsFree(allocator: Allocator, args_alloc: []const [:0]u8) void {
    var total_bytes: usize = 0;
    for (args_alloc) |arg| {
        total_bytes += @sizeOf([]u8) + arg.len + 1;
    }
    const unaligned_allocated_buf = @as([*]const u8, @ptrCast(args_alloc.ptr))[0..total_bytes];
    const aligned_allocated_buf: []align(@alignOf([]u8)) const u8 = @alignCast(unaligned_allocated_buf);
    return allocator.free(aligned_allocated_buf);
}

Itā€™s returning the result of allocator.free

Which is taking an aligned_allocated_buf

So some kind of buffer.

Each type has anĀ alignmentĀ - a number of bytes such that, when a value of the type is loaded from or stored to memory, the memory address must be evenly divisible by this number

So, itā€™s like, a buffer the size of a number that is evenly divisible by the size of the type? or something.

It is taking an unaligned buffer, which appears to be an array of byte pointers.

I think this is getting the size of the pointer of the args, that size representing the amount of space that needs to be freed from allocator.free?

Imma tell you right now, this is exhausting to reason about.

Funny thing is none of this really actually matters for the task at hand.

I mean, really all I actually care about is if thing compiles, and then if thing actually accomplishes the goal.

But I believe that going a bit deeper pays dividends.

But itā€™s exhausting.

ā€£

The Zig Standard Library is a legitimate joy

My initial foray into Zig has been as a maximally dumb human.

That is to say, I havenā€™t been using Copilot, or even a Zig language server.

This is infuriating, because it is slow as hell to make any progress.

Itā€™s a good thing progress doesnā€™t matter.

šŸ“
One of the values Iā€™ve really been trying to focus on is Donā€™t Worship Progress. Our culture incentivizes relentless optimization. Curiosity and the ability to surprise yourself can only thrive when you force yourself to slow down.

The good thing about this approach is that it forces me to really go line by line, character by character in some cases.

Itā€™s also forced me to look at the standard library a hell of a lot.

Zig has an excellent standard library.

My experience in the past has been immediately overwhelmed, but Zig is actually quite easy to follow.