DEV Community

Hamish Milne
Hamish Milne

Posted on

Zig + CMake

I love Zig. I'm a big fan of many (though not quite all!) of the design choices and philosophies behind the language.

One particular capability I love is how easy it makes cross-compiling - both for Zig and C/C++ code. I have vivid memories of the pain involved in setting up cross-compilation toolchains, or just giving up and directly compiling directly on whatever Raspberry Pi I had to hand.

Zig makes this as easy as -DTarget=arm-linux-musleabihf, even from the locked-down Windows laptop I'm forced to use in my day job.

Clang, which zig cc wraps, has the same -target option - but Zig goes a step further and includes dependencies for all the LibC variants it supports, removing the need for any kind of cross-toolchain, headers, or system configuration.

It's magical. I genuinely believe that this will completely change developers' perspectives on cross-compilation. The "Cross compiling" section in every project's documentation will go from a multi-page guide to "here's a list of targets we've tested - go nuts".

To illustrate this, let's take a use case that doesn't involve Zig-the-language at all. Say we're building a pre-existing app for some non-native platform - maybe changing some configuration, adding a patch or two, you know the drill.

In many - dare I say most - cases, projects that proport to support multiple architectures, operating systems, and compiler toolchains will use CMake as a build system.

(CMake has many, many faults. It is not 'good'. But it is currently the best 'mature', 'general-purpose' C/C++ build system by a long, long way.)

In this case, you might define some top-level CMakeLists.txt and add your dependencies as Git submodules:

cmake_minimum_required(VERSION 3.22)
project(my-project)
# Adjust any configuration...
add_subdirectory(my-dependency)
Enter fullscreen mode Exit fullscreen mode

Normally, you'd now need to figure out how to get CMake to interact with your compiler toolchain of choice. This might involve running from a special command shell (i.e. Visual Studio), setting environment variables, relying on the built-in auto-detection, 'kits', et-cetera. It's a mess.

However, with Zig available on the PATH, we can create a file we'll call toolchain.cmake:

set(CMAKE_C_COMPILER zig cc)
set(CMAKE_CXX_COMPILER zig c++)
set(CMAKE_C_COMPILER_TARGET ${TARGET})
set(CMAKE_CXX_COMPILER_TARGET ${TARGET})
Enter fullscreen mode Exit fullscreen mode

And then add a single line to our main project file, just above the project() statement:

set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/toolchain.cmake")
Enter fullscreen mode Exit fullscreen mode

This tells CMake: don't rely on any system configuration. Use the compiler I've specified, and nothing else.

We can then build the project in the usual way, but specifying whatever target we like:

cmake -S . -B build -G Ninja -DTARGET=arm-linux-musleabihf
cmake --build build
Enter fullscreen mode Exit fullscreen mode

And that's all there is to it. zig cc ensures that LibC headers are present if needed, and since it's just Clang underneath, all the options that CMake passes in are well understood. We've just taken hours of awkward setup and debugging and replaced it with 5 lines of configuration.

Just to make this really clear, this applies not just to cross-compiling, but native compiling as well. As in, your development system setup for any and all targets is now just:

sudo apt install cmake ninja-build
sudo snap install zig --classic --beta
Enter fullscreen mode Exit fullscreen mode

Likewise for Windows, you only need to download binaries of Zig, CMake, and Ninja, and put them on the PATH. No installers, no Visual Studio, no eternally-cursed Developer Command Prompt.

If that's not magical, I don't know what is.

Top comments (0)