Zig Translate-C + NixOS

Posted on Aug 19, 2025

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…

oh hey, nice

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.trims 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!