Developing portable Zig is easier than C

“Preposterous” one may say, but in my eyes, the ability to write portable code doesn’t only mean the ability to compile on multiple platforms, but also the functions you use have the same behavior on each platform, error handling is also important as each platform has it either similar or a bit different. Let’s explore the differences between this kind of portability between C and Zig!

OS & CPU Architecture support

C very much shines here, a new CPU arch comes out? It already runs C, new OS? It has C too! The language is simple enough to implement and will allow you to port much more software to your needs. Zig on the other hand has to wait for LLVM to support it first and then they can possibly start porting the standard library to said target. Zig (as of now) has limited Operating System support and support for their respective architectures, these are split into 4 tiers ranging in support and availability.

Tier 1

Tier 2

Tier 3

Tier 4

You can find table of all supported and known targets by Zig here and the Tier system is more deeply described here.

Cross-Compilation

C

C has a lot longer list of cross-compilers for different targets, however it’s not that simple to configure a cross-compiler correctly, which makes this process a bit annoying and painful. I was developing OpenPNGStudio on Linux, but decided to add support for Windows (as it would be benefitial for the project as most users use Windows), okay great … now what? First, I needed to install a C cross-compiler targeting x86_64 version of Windows, so I have installed mingw-w64. As I use Meson as my build system, I needed to setup cross-compilation definition file. Now all that remained to do was to run meson setup --cross-file x86_64-w64-mingw32.txt build-mingw, simple right? Not exactly. My project was using some dependencies that were using the CMake build system … for which Meson was not overriding CMake’s toolchain files. So I have decided to make a simple shell script and just override CC, CXX and LD environment variables (Very bad).

This wasn’t the only trouble I met, since Raylib does not do any prefixing (C doesn’t have namespaces) it clashed with windows.h header file included by Libuv. There luckily was a helping hand - raylib_win32, so I have added it to already messy meson.build file.

You’d think MinGW is at least a capable toolchain for cross compiling for Windows, but no. It doesn’t implement many POSIX functions, like mmap for example, which means another dependency yaaaaaaaaaaaaaaay!! Last time I was working on upcomming release and added support for lua scripting, I encountered yet another missing dependency … libld.

Oh and one thing, errno is not being set, so nicely call that GetLastError(). When I was testing the app using Wine, I have encountered random crashes, given enough debugging and searching, I figured out that I had a file containing a character that was not allowed by “NT”FS. Well, you see … wine doesn’t mind those. With a bit of help from someone, I figured out that using MinGW shipped by distro maintainers is a big NO.

Another problem with OpenPNGStudio was the codebase was growing quickly and the amount of refactoring I would have to do made me start rewriting the project in Zig.

Last but not least, #ifdef here, #ifdef there! I’m definitely not going insane or anything haha!

This_is_Fine
Cross Compiling C to Windows

Zig

Now let’s shift gears and focus on Zig and its cross-compilation! Let’s say you want to cross compile your Zig app to Windows, do you have to install something? No! How so? Zig already comes with MinGW pre-installed! If you use build.zig, it’s very simple, just run zig build -Dtarget=x86_64-windows and Zig will compile a Windows executable! Want to do the same for MacOS? Set the target to -macos, it’s that simple! However for Apple M chips, you may want to use aarch64-macos. For more targets run zig targets and you’ll get a nice list of available CPU architectures, Operating Systems and ABIs.

Now that’s something, isn’t it? Not to mention, if something were to fail, it won’t silently fail and you will know as Zig has errors as values! While this still won’t solve mmap issues, but it should help you a lot during the development. Thanks to Zig’s comptime, you can nicely branch out code what it should do on said platform like so:

const builtin = @import("builtin");

switch (builtin.os.tag) {
    .linux => {},
    .windows => {},
    .macos, .ios, .watchos => {}, // Pretty sure there are many more Apple products
    else => @compileError("Unsupported platform " ++ @tagName(builtin.os.tag)),
}

And as an added bonus, you can use Zig to cross compile C and C++ with zig cc and zig c++ respectively!

Standard libraries

On the C side, there are many libc implementations, each one may have minor behavior difference which again results in a lot of #ifdefs, you can take a look at comparison of some popular Linux libc implementations here. POSIX will save you for the most part, but not always and definitely not on Windows and not fully with MinGW.

With Zig, the story is a bit different, Zig maintainers focus on writing code for its standard library in a way, the function behaves exactly like on any other platform, which reduces the need for more compilation logic, unless you use platform specific APIs, then yes.

Summary

C is still a very portable programming language, but requires a lot more effort than mostly writing code that’s both fast and safe (pokes at undefined behavior) and ensuring proper behavior on targetted platform. This is also why you write unit tests! I know writing them is boring, but this way you can easily ensure a lot of things. Zig is still an alpha software, but is getting better day by day, release by release and it’s so interesting to see Zig grow.

Final note

This blog post is still not 100% done, I’m sure I’ll continue expanding on this topic the more wiser I get and fix possible mistakes I have made.

Hope you had fun time reading, until next time!

Rewriting OpenPNGStudio to Zig - Part 1 >>>