Zig Translate-C + NixOS
Writing a post to help my past self.
Zig’s C interop is quite great: historically it’s just been something like:
const c = @cImport({
@cInclude("some-system-header.h");
});
And the above would Just Work, even on NixOS, which doesn’t use the standard FHS. How does that work, you might be wondering? The answer is: zig has special support for NixOS.
How nix communicates include directories
When you use mkDerivation
or mkShell
, nix will take the
buildInputs
packages and pull out their include directories,
and throw them into an var called NIX_CFLAGS_COMPILE:
NIX_CFLAGS_COMPILE= -frandom-seed=w1kcmyba4m -isystem /nix/store/d92pdqjvpzz6nmxnaaazjxwdrg1mivjx-glfw-3.4/include -isystem /nix/store/8xbbdx4ckcdj76ldb0cbym965whipq72-libglvnd-1.7.0-dev/include -isystem /nix/store/9bbiy9b9j5gr4aih6glx2zsdavl75w33-libxkbcommon-1.10.0-dev/include -isystem /nix/store/g2q1fzy63ykhfb97wkicdpnj6a0zqlyf-wayland-protocols-1.45/include -isystem /nix/store/halasn45d3kybxczpr025vnn0wy7ksiv-wayland-1.23.1-dev/include -isystem /nix/store/vm10zh43xgfxvgrqs8brz6v6xyrq9qin-glibc-2.40-66-dev/include -isystem /nix/store/jwcsqd9xwmhhxjq90h1inxlawjh033sm-gdb-16.3/include -isystem /nix/store/4zwiw8lvh2yks6fil274ywrjcfpjf0i2-lldb-20.1.6-dev/include -isystem /nix/store/sgv2i8cvlpi8ly21xwdl8dvgia50x2mc-vulkan-headers-1.4.313.0/include -isystem /nix/store/d92pdqjvpzz6nmxnaaazjxwdrg1mivjx-glfw-3.4/include -isystem /nix/store/8xbbdx4ckcdj76ldb0cbym965whipq72-libglvnd-1.7.0-dev/include -isystem /nix/store/9bbiy9b9j5gr4aih6glx2zsdavl75w33-libxkbcommon-1.10.0-dev/include -isystem /nix/store/g2q1fzy63ykhfb97wkicdpnj6a0zqlyf-wayland-protocols-1.45/include -isystem /nix/store/halasn45d3kybxczpr025vnn0wy7ksiv-wayland-1.23.1-dev/include -isystem /nix/store/vm10zh43xgfxvgrqs8brz6v6xyrq9qin-glibc-2.40-66-dev/include -isystem /nix/store/jwcsqd9xwmhhxjq90h1inxlawjh033sm-gdb-16.3/include -isystem /nix/store/4zwiw8lvh2yks6fil274ywrjcfpjf0i2-lldb-20.1.6-dev/include -isystem /nix/store/sgv2i8cvlpi8ly21xwdl8dvgia50x2mc-vulkan-headers-1.4.313.0/include
and then for normal C projects, you’re meant to include that in your invocation to gcc:
$ gcc $NIX_CFLAGS_COMPILE main.c
Zig special cases this:
It took me embarassingly long to try and figure out how Zig was doing it’s thing here:
ultimately, I desperately grepped for NIX
in the zig codebase, expecting nothing to turn up, and…
Translate-C
So zig is able to do this, and it’s quite magical,
but my understanding is that this @cImport
is going to go away,
and be replaced by doing the C translation as part of the zig build system.
I don’t have have a decent reference for this claim, all I know is that
this repo is meant to be The Future.
Translate-C doesn’t special case NixOS (yet?)
NOOOOOOOOOOOOOO HOW COULD THEY???? THE BETRAYAL!! MY PRECIOUS NIXOS!!
(the more likely story here is just that translate-c is in it’s early days and they haven’t gotten around to implementing it. Maybe I should have contributed instead of writing this blog post HMMMM).
There’s a pretty simple workaround though: special-case nixos in your build.zig
:
const Translator = @import("translate_c").Translator;
var cSource = b.addWriteFiles();
const xdg_shell_translate: Translator = .init(tc_dep, .{
.name = "xdg_shell",
.c_source_file = cSource.add("c.c",
\\ #include <system-include-path.h>
),
.target = target,
.optimize = optimize,
.link_libc = true,
});
// The important bit:
const nixCFlags: []const u8 = std.process.getEnvVarOwned(b.allocator, "NIX_CFLAGS_COMPILE") catch "";
var iter = std.mem.tokenizeScalar(u8, std.mem.trim(u8, nixCFlags, " \n\t"), ' ');
while (iter.next()) |arg| {
xdg_shell_translate.run.addArg(std.mem.trim(u8, arg, "\n"));
}
Those std.mem.trim
s are load-bearing by the way.
How does translate-c work?
Translate-C is using arocc under the hood to do things like macro expansion and parsing.
Arocc is meant to be a drop-in replacement for gcc/clang, so it supports the standard argument format for specifying include directories and libraries.
Translate-C conveniently forwards arguments straight to arocc,
so by providing the arguments to the run
step of translate-c
, you provide them to arocc
.
This will all probably change soon.
I’ve been using the nightly zig toolchain (0.15.0-dev.1564+2761cc8be) for a few months now, and things are changing pretty fast - I would expect this blog post to be out-of-date by early 2026, but if you’ve stumbled upon this, hopefully it gives you some pointers of where to poke at next. Good luck!