Meet Zig: The modern alternative to C

Meet Zig: The modern alternative to C

Programmers are never satisfied. As soon as we’ve accomplished one near-impossible goal, we start working toward a new one. A case in point is systems-oriented development, where we already have a complement of strong languages to work with: C, C++, Rust, and Go. Now we also have Zig, a newer language that seeks to absorb what’s best about these languages and offer comparable performance with a better, more reliable developer experience. 

Zig is a very active project. It was started by Andrew Kelley in 2015 and now seems to be reaching critical mass. Zig’s ambition is rather momentous in software history: to become the heir to C’s longstanding reign as both the go-to portable low-level language and as a standard to which other languages are compared.

Before we dive into the specifics of programming with Zig, let’s consider where it fits within the larger programming language ecosystem. 

A replacement for C?

Zig is described as a “low-level systems language,” but what is low-level? Systems language is also fairly ambiguous. I asked Zig project developer Karsten Schmidt how he describes Zig, and he said “I define Zig as a general-purpose programming language because, while it’s an obvious good fit for systems programming, it’s also suited for programming embedded devices, targeting WebAssembly, writing games, and even most tasks that would normally be handled by higher-level languages.”

Zig is perhaps easiest to understand in relation to C, as a general purpose, non-garbage collected, and portable language with pointers. Today, virtually all programming infrastructure rests on C in various ways, including being the foundation of other programming languages like Java, JavaScript, and Python. Imagine the ripple effect of evolving to a language that is like C, but safer, less buggy, and easier to maintain. If Zig were to be adopted broadly as an archetypal replacement for C, it could have enormous systemic benefits. 

Karsten told me that, while Zig does compete with C, “we don’t expect it to supplant C without a very long stretch of time where both languages have to coexist.”

Zig’s design goals and syntax

Zig is a “close to the metal” language in that it allows developers to work directly with system memory, a requirement for writing code that can be maximally optimized to its task. Direct memory allocation is a characteristic shared by the C family, Rust, and other low-level systems languages. Zig offers similar capabilities but aims to improve on them in several ways.

Zig seeks to be a simpler systems-oriented language than its predecessors and make it easier to write safe, correct code. It also aims for a better developer experience by reducing the sharp edges found in writing C-like software. On the first review, Zig’s features might not come across as earth-shattering, but the overall effect is of a platform that developers are finding easier to master and use.

Currently, Zig is being used to implement the Bun.js JavaScript runtime as an alternative to Node.js. Bun’s creator Jarred Sumner told me “Zig is sort of similar to writing C, but with better memory safety features in debug mode and modern features like defer (sort of similar to Go’s) and arbitrary code can be executed at compile-time via comptime. It has very few keywords so it’s a lot easier to learn than C++ or Rust.”

Zig differs from most other languages in its small feature footprint, which is the outcome of an explicit design goal: Only one obvious way to do things. Zig’s developers take this goal so much to heart that for a time, Zig had no for loop, which was deemed an unnecessary syntactic elaboration upon the already adequate while loop.

Kevin Lynagh, coming from a Rust background, wrote, “The language is so small and consistent that after a few hours of study I was able to load enough of it into my head to just do my work.” Nathan Craddock, a C developer, echoed the sentiment. Programmers seem to really like the focused quality of Zig’s syntax.

How Zig handles memory

A distinctive feature of Zig is that it does not deal with memory allocation directly in the language. There is no malloc keyword like in C/C++. Instead, access to the heap is handled explicitly in the standard library. When you need such a feature, you pass in an Allocator object. This has the effect of clearly denoting when memory is being engaged by libraries while abstracting how it should be addressed. Instead, your client code determines what kind of allocator is appropriate.

Making memory access an obvious library characteristic is meant to avoid hidden allocations, which is a boon to resource-limited and real-time environments. Memory is lifted out of the language syntax, where it can appear anywhere, and its handling is made more explicit.

Allowing client code to specify what type of allocator it passes into an API means the code gets to choose based on the environment it is targeting. That means library code becomes more obvious and reusable. An application can determine when exactly a library it is using will access memory, and hand it the type of allocator—embedded, server, WASM, etc.—that is most appropriate for the runtime.

As an example, the Zig standard library ships with a basic allocator called a page allocator, which requests memory from the operating system by issuing: const allocator = std.heap.page_allocator;. See the Zig documentation for more about available allocators.

Zig also includes safety features for avoiding buffer overflows, and it ships with a debug allocator that detects memory leaks.

Conditional compilation

Zig uses conditional compilation, which eliminates the need for a preprocessor as found in C. Therefore, Zig does not have macros like C/C++. From a design standpoint, Zig’s development team views the need for a preprocessor as indicative of a language limitation that has been crudely patched over.

Instead of macros, Zig’s compiler determines what code can be evaluated at compilation time. For example, an if statement will actually eliminate its dead branch at compile-time if possible. Instead of using #define to create a compile-time constant, Zig will determine if the const value can be treated that way and just do it. This not only makes code easier to read, write, and think about, but also opens up the opportunity for optimization. 

As Erik Engheim writes, Zig makes compile-time computing a central feature instead of an afterthought. This allows Zig developers “to write generic code and do meta programming without having any explicit support for generics or templates.”

A distinctive Zig feature is the comptime keyword. This allows for executing code at compile time, which lets developers enforce types against generics, among other things. 

Interoperability with C/C++

Zig sports a high degree of interoperability with C and C++. As the Zig docs acknowledge, “currently it is pragmatically true that C is the most versatile and portable language. Any language that does not have the ability to interact with C code risks obscurity.” 

Zig can can compile C and C++. It also ships with libc libraries for many platforms. It is able to build these without linking to external libc libraries. (For a detailed discussion of Zig’s relationship to libc, see this Reddit thread.)

Here is Zig’s creator discussing the C compiler capability in depth, including a sample of Zig compiling the GCC LuaJIT compiler. The bottom line is that Zig attempts to not only supercede C with its own syntax, but actually absorb C into itself as much as possible. 

Karsten told me that “Zig is a better C/C++ compiler than other C/C++ compilers since it supports cross-compilation out of the box, among other things. Zig can also trivially interoperate with C (you can import C header files directly) and it’s overall better than C at using C libraries, thanks to a stronger type system and language features like defer.”

Error handling in Zig

Zig has a unique error-handling system. As part of its “avoid hidden control flow” design philosophy, Zig doesn’t use throw to raise exceptions. The throw function can branch execution in ways that are hard to follow. Instead, if necessary, statements and functions can return an error type, as part of a union type with whatever is returned on the happy path. Code can use the error object to respond accordingly or use the try keyword to pass up the error.

An error union type has the syntax <error set type> ! <primitive type>. You can see this in action with the simple “Hello, world” example (from the Zig docs) in Listing 1.

Listing 1. Helloworld.zig

 const std = @import("std");  pub fn main() !void {     const stdout = std.io.getStdOut().writer();     try stdout.print("Hello, {s}!n", .{"world"}); } 

Most of Listing 1 is self explanatory. The !void syntax is interesting. It says the function can return either void or an error. This means if the main() function runs without error, it’ll return nothing; but if it does error out, it’ll return an error object describing the error condition.

You can see how client code can use an error object in the line where the try keyword appears. Since stdout.print can return an error, the try expression here means the error will be passed up to the main() function’s return value.

Toolchain and testing

Zig also includes a build tool. As an example, we could build and run the program in Listing 1 with the commands in Listing 2 (this is again from the Zig docs).

Listing 2. Build and run Helloworld.zig

 $ zig build-exe hello.zig $ ./hello Hello, world! 

Zig’s build tool works in a cross-platform way and replaces tools like make and cmake.

A package manager is in the works, and testing support is built directly into the language and runner.

State of Zig

Zig has an active Discord community and a lively GitHub ecosystem. The in-house documentation is pretty thorough, and Zig users have produced a good amount of third-party material, as well. 

Zig is not yet in 1.0 release, but its creators say it is approaching production ready. On the topic of readiness, Karsten said, “Zig is not yet at v1.0, so things like webdev are still in their infancy, but the only usage that I would consider not recommending Zig for is data wrangling, for which I think a dynamic language like Python or Julia would be more practical.”

For now, the Zig team appears to be taking its time with the 1.0 release, which may drop in 2025 or later—but none of that stops us from building all sorts of things with the language today.

Zig’s activity, goals, and uptake by the developer community make it an interesting project to watch.

Learn more about Zig

Here are a few articles where you can learn more about Zig and how it is shaking up the world of systems-oriented programming:

Add a Comment