Need a new programming language? Try Zig


Maybe you’ve heard of it, maybe not. Zig is a new programming language which seems to be growing in popularity. Let’s take a quick dive into what it is, why it’s unique, and what sort of things you’d use it for. (Editor’s note: Other than “for great justice“, of course.)

What is that?

You’ve probably heard of Rust because it made significant inroads into low-level critical infrastructure such as operating systems and embedded microcontrollers. As a gross simplification, it offers memory safety and many traditional runtime checks pushed at compile time. It has been a darling of many posts here at Hackaday as it offers unique benefits. With Rust on the rise, it makes sense that there is room for new players. languages ​​like Julia, Go, Fastand even Racket are all relative newcomers vying for the highly coveted mindshare of software engineers around the world.

So let’s talk Zig. In a broad sense, Zig really tries to deliver some of Rust’s security with the simplicity and ease of C. It boasts a few core features such as:

  • No hidden control flow
  • No cache memory allocation
  • No preprocessor, no macros
  • First-class optional standard library support
  • Interoperable by design
  • Adjustable execution security
  • Executing code at compile time

The last one, in particular, is perhaps the most interesting, but we’ll get to that. Let’s look at some code, but skipping hello world and going straight to opening a file. Here is the C++ code:


using namespace std;
int main (int argc, char const *argv[]) {
  ifstream file("nonexistingfile.txt");

  char buffer[1024];, sizeof(buffer));

  cout << buffer << endl;

  return 0;

Now let’s look at a comparable Zig code:

const std = @import("std");

using namespace std.fs;

pub fn main() !void {
    const stdout =;

    const file = try cwd().openFile(
        .{ .read = true },
    defer file.close();

    var buffer: [1024]u8 = undefined;
    const size = try file.readAll(buffer[0..]);

    try stdout.writeAll(buffer[0..size]);

(Thanks to Erik Engheim for the C++ and Zig sample code.)

As you might have guessed from the file name, the file does not exist. The C++ code does not explicitly check for errors and in this scenario it is perfectly valid code that shows no indication that anything has failed. Zig, on the other hand, we have to give it a try because this file might fail. If it fails, you get a nice stack trace:

error: FileNotFound
/usr/local/Cellar/zig/0.7.0/lib/zig/std/os.zig:1196:23: 0x10b3ba52e in std.os.openatZ (fileopen)
            ENOENT => return error.FileNotFound,
/usr/local/Cellar/zig/0.7.0/lib/zig/std/fs.zig:754:13: 0x10b3b857e in std.fs.Dir.openFileZ (fileopen)
            try os.openatZ(self.fd, sub_path, os_flags, 0);
/usr/local/Cellar/zig/0.7.0/lib/zig/std/fs.zig:687:9: 0x10b3b6c4b in std.fs.Dir.openFile (fileopen)
        return self.openFileZ(&path_c, flags);
~/Development/Zig/fileopen.zig:8:18: 0x10b3b6810 in main (fileopen)
    const file = try cwd().openFile(

Removing try results in a compilation error. The backtrace here is particularly impressive because it’s a relatively simple language with no garbage collectors, runtimes, or virtual machines.

Let’s talk about some of Zig’s other features: interoperable by design, tunable runtime safety, and compile-time code execution.

“Interoperable by design” means that ordinary Zig is easily consumed by C and in turn consumes C. In many other languages, such as Python, you need to specifically gather the data for C and C++ interoperability. Zig can embed C files directly into the main code thanks to the built-in Clang compiler. The output of Zig Libraries is a .o file that can be introduced directly into GCC. Functions can be used by C code by simply adding export at the beginning of function definitions. Structures and data types have the same facility.

“Adjustable runtime security” means that many of Zig’s runtime checks can be enabled or disabled depending on the application. Things like integer overflow, bounds checking, inaccessible code, etc.

You may notice in some code you have seen that there is a data type in Zig called comptime. You can use it in function arguments and in the program itself. This means that the value must be computable at compile time. It can be used to implement some form of generics or templates. This is quite a powerful feature that can be used in interesting ways.

What would you use it for?

Since Zig is based on LLVM, Zig’s targets include:

  • x86_64
  • ARM/ARM64
  • MIPS
  • Power PC
  • WASM32
  • RISCV64
  • Sparc v9
  • linux
  • macOS
  • the Windows
  • FreeBSD
  • Dragonfly
  • UEFI

Since it interoperates with C so easily, it’s pretty straightforward to swap out small chunks or libraries for Zig equivalents.

Additionally, Zig can be used on microcontrollers. As a selected example, [Kevin Lynagh] recently went through the journey of converting his keyboard firmware from Rust to Zig. Many of Rust’s well-known language features such as features, macros, and pattern matching are used to initialize and scan ports for keypresses. In Zig, these are replaced by inline fora for loop that is unrolled at compile time and a clever use of comptime. Specifically [Kevin] emphasizes the consistency of the language and how it’s a language he feels he can master.

If you are looking for inspiration, there is a Github repository with hundreds of excellent examples written in Zig. There are Gameboy emulators, HTTP/DNS servers, ray tracers, several kernels and booters, databases and compilers.

How can I start?

There is a learning section on Zig homepage as well as the website which abounds in great resources. Ziglings is a Github project which has small broken programs that need small tweaks to work again, let you get a feel for Zig. Maybe just dipping your toes in water isn’t enough, and you want to dive into the depths of the language implementation itself.


About Author

Comments are closed.