Tuesday, October 5, 2021

Need a New Programming Language? Try Zig

Maybe you’ve heard of it, maybe you haven’t. Zig is a new programming language that seems to be growing in popularity. Let’s do a quick dive into what it is, why it’s unique, and what sort of things you would use it for. (Ed Note: Other than “for great justice“, naturally.)

What Is It?

You’ve likely heard of Rust as it has made significant inroads in critical low-level infrastructures such as operating systems and embedded microcontrollers. As a gross oversimplification, it offers memory safety and many traditional runtime checks pushed to compile time. It has been the darling of many posts here at Hackaday as it offers some unique advantages. With Rust on the rise, it makes sense that there might be some space for some new players. Languages like Julia, Go, Swift, and even Racket are all relative newcomers vying for the highly coveted mindshare of software engineers everywhere.

So let’s talk Zig. In a broad sense, Zig is really trying to provide some of the safety of Rust with the simplicity and ease of C. It touts a few core features such as:

  • No hidden control flow
  • No hidden memory allocations
  • No preprocessor, no macros
  • First-class support for optional standard library
  • Interoperable by design
  • Adjustable Runtime Safety
  • Compile-time code-execution

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

#include <iostream>
#include <fstream>
#include <string>

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

  char buffer[1024];
  file.read(buffer, sizeof(buffer));

  cout << buffer << endl;

  file.close();
  return 0;
}

Now let’s look at some comparable Zig code:

const std = @import("std");

using namespace std.fs;

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    const file = try cwd().openFile(
        "nonexistingfile.txt",
        .{ .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 doesn’t exist. The C++ code doesn’t explicitly check for any errors and in this scenario, it is perfectly valid code that displays no indication that anything failed. Zig, on the other hand, we have to do a try since that file could fail. When it does fail, 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 the try results in a compilation error. The backtrace here is especially impressive because this is a relatively simple language without a garbage collector, runtime, or virtual machine.

Let’s talk about some of Zig’s other features: interoperable by design, adjustable 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 marshall data for C and C++ interoperability. Zig can include C files directly in the main code by virtue of the built-in Clang compiler.  The output of Zig libraries is a .o file that can be fed right into GCC. Functions can be used by C code by just prepending export to the beginning of function definitions. Structs and datatypes have similar ease.

“Adjustable runtime safety” means that many of the runtime checks that Zig has can be turned on or off depending on the application. Things like integer overflow, bounds checking, unreachable code, and others.

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

What Would You Use It For?

Since Zig is LLVM-based, the targets for Zig include:

  • x86_64
  • ARM/ARM64
  • MIPS
  • PowerPC
  • WASM32
  • RISCV64
  • Sparc v9
  • Linux
  • MacOS
  • Windows
  • FreeBSD
  • DragonFly
  • UEFI

Given that it interoperates with C so smoothly, it’s quite simple to swap out small chunks or libraries for Zig equivalents.

Additionally, Zig can be used on microcontrollers. As a bit of a cherry-picked 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 key presses. In Zig, these are replaced by inline for, a for loop that is unrolled at compile time, and some clever use of comptime. In particular [Kevin] points out the consistency of the language and how it is a language that he feels like he could master.

If you’re looking for inspiration, there’s a Github repo 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 Get Started?

There’s a learning section on Zig’s homepage as well the site ziglearn.org that is chock-full of great resources. Ziglings is a Github project that has small broken programs that need small tweaks to get working again, allow you to get a feel for Zig. Maybe just dipping your toes in the water isn’t enough, and you want to dive into the deep end of the language implementation itself.


No comments:

Post a Comment