Annotated Guide For using Zig Compiler for compiling Nim programs
What is ZigCC
I probably should start this guide with explanation of what is Zig. Zig is a modern systems programming language. With that out of the way, one of the nice features of Zig is it's ability to easily compile and cross-compile any C code. Zig achieves it by bundling a clang compiler with many different headers and libc implementations neatly compressed into one, not very large (around 300 MB uncompressed), standalone package.
This is undeniably an impressive feat and a really useful tool for many developers.
And it's possible to use it with C and Nim, even if you, like me, never touched a single line of Zig code.
You can read more about zig cc on Andrew Kelley's blog
Why use ZigCC with Nim
Because, it gives Nim super-power of cross-compiling all Nim code with almost zero setup. To many weird targets. Without sandboxing and running gigabytes of Docker containers under emulation.
It's small, convenient and almost magic.
Motivating example:
This is all you need to cross-compile pure Nim program to Windows ARM on any machine with Nim and Zig compiler:
nimble install zigcc
nim c --cc:clang --clang.exe:zigcc --clang.linkerexe:zigcc \
--passC:"-target aarch64-windows-gnu" \
--passL:"-target aarch64-windows-gnu" \
--os:windows --cpu:arm64 main.nim
Guide
Nim supports many compilers, but it doesn't support ZigCC natively (yet?). The reason for this is that Zig is very young language, not stable and ZigCC in 99.99% is just an interface to Clang. Fortunately, Nim has first-class support for Clang compiler. And if we fix few caveats - compiling Nim programs with ZigCC is easy and pretty straightforward.
We will start with the default commands to compile our Nim program and gradually add flags until cross-compilation works:
nim c main.nim
Choosing Nim's Backend Compiler
Nim's primary way to change supported compilers is with a flag --cc,
e.g. --cc:gcc for Gnu Compiler Collection or --cc:tcc for Tiny C Compiler.
As previosuly stated, ZigCC is just a clever Clang wrapper.
We can add the flag --cc:clang right away and deal with few problems later:
nim c --cc:clang main.nim
Problem #1: Nim looks for Clang, not Zig
We asked Nim to use clang and it will try to find and use clang binary.
Nim has a way to override the backend compiler name that it will be searching for.
It looks like this --<compiler>.exe:<path> for compiler executable and there is a similar flag --<compiler>.linkerexe:<path> for linker executable.
The compiler is same as we've specified before - clang, and the path, for now, is just zigcc.
Let's add these flags to our command:
nim c --cc:clang --clang.exe:zigcc --clang.linkerexe:zigcc main.nim
Note: We use zigcc for both flags, because Clang (and thus ZigCC) use the same interface for compiling and linking C programs.
Problem #2: Zig cli is slightly different from Clang
Nim doesn't know about ZigCC, so it still think we're using Clang compiler. There's a small mismatch between them:
clang: clang -O2 main.c
zig: zig cc -O2 main.c
Zig requires an additional parameter cc passed first before all other C flags.
This can be solved by installing zigcc wrapper from nimble:
nimble install zigcc
Alternatively, you can create a tiny shell/batch wrapper script:
Linux/MacOS:
#!/bin/sh
/path/to/zig cc $@
# save this script as 'zigcc' in directory available in your PATH, or reference with full path
# also make it executable with `chmod +x zigcc`
Windows:
@echo off
C:\path\to\zig.exe cc %*
:: save this script as 'zigcc.bat' in directory available in your PATH, or reference with full path
Note: if you choose to use batch script, change zigcc path in all commands to zigcc.bat
Our command is currently:
nim c --cc:clang --clang.exe:zigcc --clang.linkerexe:zigcc main.nim
And if you've properly installed the zigcc wrapper, that's already enough to use ZigCC for native compilation!
Go and try it. I'll wait.
Cross-compilation
For cross-compilation we still need a few more steps.
Choosing Nim target
First, we have to tell Nim that we want it to generate code for Windows OS on ARM cpu.
That's very easy and thoroughly documented, just use flags --os:windows and --cpu:arm64.
We will add them to our command:
nim c --cc:clang --clang.exe:zigcc --clang.linkerexe:zigcc \
--os:windows --cpu:arm64 main.nim
Choosing ZigCC target
Yes, ZigCC also needs to know it's target. And there's no standard way for Nim
compiler to share it's own flags with Zig. It's our job to tell ZigCC the proper targets.
Problem #3: ZigCC targets
Where you can find list of ZigCC supported build targets?
- In the blog post for Zig 0.15 there's a detailed table of first and second class support with detailed info.
- Run
zig targetscommand and inspect output (many thousands of lines of non-standard undocumented NIH json clone (zon))
Note: Before Zig 0.14 zig targets was returning json and was parseable with jq, but alas.
At least it's pretty straightforward to use them targets with ZigCC cli.
Example:
zig cc -target aarch64-windows-gnu ...
zig cc -target x86_64-linux-musl ...
It's pretty much a GNU target triplet - first part is a cpu architecture, operating system
is in the middle and the last part is the flavor of libc that we want to target.
With small addition, because we can also choose different versions of glibc and OS.
Advanced Example:
zig cc -target aarch64-windows.xp-gnu.2.36 ...
zig cc -target x86_64-linux.6.12-gnu.2.42 ...
TODO: add more examples how to extract data from Zig targets (and make it less brittle).
For now, you can list all targets for which Zig has built-in libc using sed:
zig targets | sed -n '/\.libc/,/},/p'
This is a list of targets for which you don't have to install anything except Zig and Nim for cross-compilation.
Problem #4: Zig Targets don't match with Nim targets
Unfortunately, Zig and Nim don't agree on all OS and CPU names.
Here is a helpful table with all fields that differ (as of Nim 2.2.8 and Zig 0.15.2):
| Nim CPU | Zig Equivalent |
|---|---|
| i386 | x86 |
| amd64 | x86_64 |
| arm64 | aarch64 |
| Nim OS | Zig Equivalent |
|---|---|
| macosx | macos |
Make sure you're using the right Cpu/OS for each flag.
Passing targets to Zig
Nim can pass options directly to backend compiler and backend linker with flags --passC and --passL respectively.
And, finally, we are ready to add the last flags to our command:
nim c --cc:clang --clang.exe:zigcc --clang.linkerexe:zigcc \
--passC:"-target aarch64-windows-gnu" \
--passL:"-target aarch64-windows-gnu" \
--os:windows --cpu:arm64 main.nim
Cross-compiling for MacOS
Is against Apple SDK license (unless you're github).
But, what if you've had an access to MacOS SDK. How could you do this, in theory?
You'd put this into your config.nims:
when defined(macosx):
const
macos_sdk {.strdefine.} = "./assets/zig-build-macos-sdk"
macos_lib = &"{macos_sdk}/lib"
macos_include = &"{macos_sdk}/include"
macos_frameworks = &"{macos_sdk}/Frameworks"
switch("passC", &"-I{macos_include} -F{macos_frameworks} -L{macos_lib}")
switch("passL", &"-I{macos_include} -F{macos_frameworks} -L{macos_lib}")
And you'd run nim c -d:macos_sdk="path/to/MacOS/SDK" ...
But that's just a theory! (please read a license agreement)
Additional Knowledge and Troubleshooting
OOM
If you're compiling on VPS, Raspberry Pi, or other device with constrained memory, Clang in release mode can easily use more than 4GB of memory even with relatively small programs.
I noticed that adding a flag --opt:size to optimize program for size instead of speed uses significantly less RAM.
Clang's DWARF debug symbols
When compiling for Windows targets, by default ZigCC dumps debug symbols into a separate file with .pdb extension. If you don't need them, you can disable creation of this file by passing an additional flag to linker --passL:"-Wl,--strip-debug".
Sanitizers
Another little difference of ZigCC from default Clang is that Zig enables all sanitizers by default in debug builds. if you want behavior closer to default Clang compiler - add this flag --passC:"-fno-sanitize=all".
This guide was written completely without AI and pretty much in one sitting.