I am currently on a side quest to write some Zig code, or more specifically: take some C++ code that is written in a I-can’t-believe-it’s-not-C style and turn it into Zig.
This entire adventure started with a curiosity for using Zig’s build system. (Verdict so far: I like it!) I have always been Zig-curious in general, so it also seemed like a good opportunity to actually get my hands dirty.
What I realized throughout this (admittedly brief) journey is that while I despise header files as much as you do, I also miss them. Using Zig allows me to pin-point what I also dislike about C#: If you want to export something, you just mark it pub (or public) in its definition (instead of requiring a separate declaration somewhere).
What this means in practice is that if someone wants to understand what your module exports, they need to go through the entire file and figure out what you marked pub. It’s a lot of noise. It’s “reading 10000 lines when you actually only care about 50.” It’s the opposite of being able to point you to a header and say “that’s where we define the data for this interface.”
You might argue that documentation is the right solution to this. However, going through an HTML page is a detour when I am already in the code anyway. For Zig specifically, running a build with -femit-docs emits documentation, though I can’t exactly judge how useful it is because the generated index.html does not seem to work locally (some WASM failure, on Zig 0.15.2).
It is worth noting that C++ has the reverse problem (but to a smaller degree): private members bleed into headers. I personally do not care about this, because my C++ code usually does not have private members. C itself suffers from “single header” libraries, which are well-intended though I personally prefer “header + compilation unit.”
My first conclusion from this was that one of the reasons why C proper libraries are nice to consume is because header files make it easy to reason about their surface. From that perspective, the idea of marking things pub inline is a choice to make maintainers’ lives easier at the cost of consumers’. I am not sure whether this conclusion holds water, given that I am writing this post while I am writing a library, not while consuming one, and I still long for an overview of what API there is.
Luckily, Zig has stolen the excellent “ship a parser for the language in your stdlib” approach from Go. This makes it very simple to roll-your-own API extractor, and then you get nice output like this:
//! AUTO-GENERATED API SURFACE for library 'Test'.
//! This file is for API inspection and is not intended to compile as-is.
//! Generated by tools/api_generator.zig.
// ===== api.zig =====
pub const log = @import("log.zig");
pub fn add(lhs: i32, rhs: i32) i32;
pub fn name() [*:0]const u8;
We get to have our cake and eat it.