Zig sparks absolutely no interest in me as a language, but it does seem like it could be a compelling build tool for existing C and C++ projects due to its integrations with clang and musl that makes it easy to cross compile and even set specific versions of libraries like glibc. I have little experience with the language, but I wanted to see whether I could build a C++ project with dependencies successfully, meaning all the necessary libraries would be linked and it would conveniently output header files in a reasonable location for any editor to find (I use Neovim btw.).
The Project
The project I wanted to build is a small C++ game which depends on raylib, raylib-cpp, and googletest. Raylib is basically an abstraction layer over different platform graphics systems, with some convenient APIs for 2D and 3D game development. It’s written in C. The raylib-cpp dependency is a header-only C++ library which provides an object-oriented wrapper around the core raylib APIs. GoogleTest is a C++ xUnit testing framework, and building it will be an interesting challenge since it’s not a header-only library.
The structure of the project is pretty straightforward.
Add the following code to their respective files. It just sets up a game class with a run method, as well as a single test.
Now for the fun stuff.
Specifying Our Dependencies
Zig is not stable. APIs are subject to breakage with each release, and the tooling, while impressive for a pre-1.0 language, has many rough edges. An example of this is the lack of an ergonomic package manager. Coming from the Rust and Go worlds, build.zig.zon is far cry from cargo add or go get, but it is all we have to work with. The build.zig.zon file specifies dependencies in Zig Object Notation, which looks pretty similar to JSON (with more punctuation). The possible top level keys are specified in the documentation (there aren’t very many). We can start off our build.zig.zon with the following:
Each dependency is a struct which may include the following keys:
url
hash
path
You must either provide a url and a hash, or a path. We will be using some C and C++ libraries from GitHub, and won’t have any actual zig code in our project at all (although there are zig bindings for raylib)1. The url of a GitHub dependency in build.zig.zon is not the GitHub URL for the project, but rather a link to the zipped (.tar.gz, zip) project. To retrieve this for a branch, go where you normally would to clone the project and copy the “Download ZIP” link. You can also do this for tags by copying the tar.gz or zip URL for the tag. This sounds confusing, but really isn’t difficult in practice. Let’s add raylib as a dependency.
We also need to specify a hash. To get this hash, you can run zig fetch <url>. Update your build.zig.zon with the hash printed to the console like so:
It’s important to note that since we’re tracking raylib’s master branch, we will need to update the hash when new commits are made. Raylib’s build.zig in its last release (5.0.0) doesn’t work with the most recent version of zig so we have to track master. If this is an issue, you can fork raylib and point your .url at the forked version of master. It also means you shouldn’t just copy this hash if you’re following along with me–generate your own. It’ll probably be different.
The second dependency we need for our project is the C++ bindings for raylib. We can add another dependency to our project like so:
This dependency is pinned to the 5.0.2 (most recent as of September 13, 2024) tag on the repository. If you need a newer version, just zig fetch <url> for the version you want.
Start with the following in your build.zig. This just sets up an executable with the source files we created (main.cc and src/game.cc), with default target and optimization options. It also links libc and libcpp so the standard libraries for C and C++ are accessible.
Now we can add our dependencies.
Since raylib has a build.zig file, we have a relatively easy time installing it. We can reference the dependency by its name in build.zig.zon, create an artifact, link the artifact to the executable, and install the headers alongside the executable. The headers will show up under /zig-out/include.
The C++ bindings to raylib are a little more complicated to add as a dependency, but it’s still not too difficult–you just can’t make it an artifact the way you can with raylib2.
These additions add raylib-cpp as a dependency, adds the include path of raylib-cpp, and installs the include directory into the header directory of /zig-out, which is called include.
The last thing we can do is add a run command (kind of like a cmake target). This is easy. Just add the following lines at the bottom.
Now if you run zig build, your project will be built and linked properly, and if you run zig build run, your application will also run after its built.
Building a Test Binary
Building GoogleTest is a lot more involved than adding Raylib since it doesn’t have a zig build system configured. We’ll have to do that ourselves. Luckily, that’s not too difficult. First, let’s create a new executable and load the dependency.
Now we need to specify how to build and link GoogleTest. This process involves inspecting the structure of the GoogleTest library on GitHub and telling zig to create a static library with that information.
Notice how gtest-internals-inl.h is commented out. Do not make the same mistake I did and include that file. Inline headers are not part of the C++ source files, and you will get a scary error if you include it.
For GoogleMock, the process is similar:
Now we just need to link the static libraries we’ve built to test_exe and tell zig to output the C++ headers in a convenient location. We can also add another run target at the bottom so we can conveniently run all the tests.
Running zig build should build GoogleTest and add the headers to /zig-out/include/. Running zig build test will run the tests.
Editor Support
The header files are located under /zig-out/include. You’ll need to add this to your IDE search path in order to make the header not found errors go away. I use neovim with clangd as my C++ LSP, so I just add the following to a .clangd file and restart my LSP to get IDE functionality.
For VSCode, I’m pretty sure you just have to modify a .vscode/settings.json file to override/append to your include path.
Anyway, hope this was informative. It was interesting working with zig, and while I don’t really see its value proposition as a language, it seems like a very promising build tool for C and C++ projects.
Addendums
There are zig bindings to raylib, but I really wouldn’t recommend using zig for actual production code outside of small, hobby projects. It’s fine as a build system because of its cross compile capabilities and abstractions over clang, but, at the end of the day, its unstable. Use it for larger projects at your own risk. In my opinion, its LSP/editor support is lacking, its documentation is anemic, and you are expected to regularly read the source code of the standard library to keep up with breaking API changes which leads to a poor developer experience. I just wanted to include a link to the zig bindings for completeness.