Programmers are never satisfied. As soon as we achieve an almost impossible goal, we start working towards a new one. An example of this is systems-oriented development, where we already have a complement of powerful languages to work with: C, C++, Rust, and Go. Now we also have Zig, a newer language that seeks to absorb the best of these languages and offer comparable performance with a better and 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 quite momentous in software history: to become the heir to C’s long-standing reign as the benchmark portable low-level language and as a standard against which other languages are compared.
Before we dive into the details of programming with Zig, let’s consider where it fits within the broader ecosystem of the programming language.
A replacement for C?
Zig is described as a “low-level systems language”, but what it is low level? systems language it is also quite 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 choice for systems programming, it’s also well-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 easier to understand in relation to C, as a general-purpose, non-garbage-collecting, portable language with pointers. Today, virtually the entire programming infrastructure is based on C in various ways, including the foundation of other programming languages such as Java, JavaScript, and Python. Imagine the ripple effect of evolving to a language that’s like C, but more secure, less buggy, and easier to maintain. If Zig were to be widely adopted as an archetypal replacement for C, it could have huge systemic benefits.
Karsten told me that while Zig competes with C, “we don’t expect it to supplant C without a very long period of time in which both languages have to coexist.”
Zig Syntax and Design Goals
Zig is a “close to metal” language in the sense that it allows developers to work directly with system memory, a requirement for writing code that can be fully optimized for their task. Direct memory allocation is a feature shared by the C family, Rust, and other low-level system languages. Zig offers similar capabilities but aims to improve on them in a number of ways.
Zig seeks to be a simpler systems-oriented language than its predecessors and to make it easier to write correct and safe code. It also aims for a better experience for developers by reducing the sharp edges found when writing C-like software. On first review, Zig’s features might not seem all that impressive, but the overall effect is that of a platform. that developers find easier to master and use.
Zig is currently used to implement the Bun.js JavaScript runtime as an alternative to Node.js. bun maker jarred sumner told me “Zig is similar to writing C, but with better memory safety features in debug mode and modern features like defer
(more or less similar to Go) and arbitrary code can be executed at compile time via comptime
. It has very few keywords, so it’s much easier to learn than C++ or Rust.”
Zig differs from most other languages in its small feature footprint, which is the result of an explicit design goal: Just an obvious way to do things. Zig’s developers take this goal so seriously that for a while Zig had no for loop, which was considered an unnecessary syntactical elaboration on the already adequate while
loop.
Kevin Lynagh, with a background in Rust, wrote: “The language is so small and consistent that after a few hours of study I was able to load enough into my head to do my job.” 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 doesn’t deal with memory allocation directly in the language. There is no malloc
keyword as in C/C++. Instead, heap access is handled explicitly in the standard library. When you need such a feature, you pass in a Allocator
object. This has the effect of clearly denoting when the libraries are using memory while abstracting away how it should be addressed. Instead, your client code determines what type of allocator is appropriate.
Making memory access an obvious feature of the library is intended to prevent hidden allocations, which is a boon for resource-constrained and real-time environments. The memory is taken out of the syntax of the language, where it can appear anywhere, and its handling is made more explicit.
Allowing client code to specify what kind of mapper to pass to an API means that code can choose based on the environment it is targeting. That means the library code becomes more obvious and reusable. An application can determine exactly when a library it is using will access memory and assign 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 information on the available mappers.
Zig also includes security features to prevent buffer overflows and 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, the Zig development team sees the need for a preprocessor as indicative of a language limitation that has been crudely patched.
Instead of macros, the Zig compiler determines what code can be evaluated at compile time. for example, a if
The statement will actually remove your dead branch at compile time if possible. instead of using #define
to create a compile-time constant, Zig will determine if the const
the value can be treated that way and just do it. This not only makes the 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 core feature rather than an afterthought. This allows Zig developers to “write generic code and do metaprogramming without having any explicit support for generics or templates.”
A distinctive feature of Zig is the comptime keyword. This allows code to be executed at compile time, allowing developers to enforce types versus generics, among other things.
Interoperability with C/C++
Zig has a high degree of interoperability with C and C++. As the Zig papers acknowledge, “it is currently pragmatically true that C is the most versatile and portable language. Any language that does not have the ability to interact with C code runs the risk of being left in the dark.”
Zig can compile C and C++. It also ships with libc libraries for many platforms. It is capable of compiling them without linking to external libc libraries. (For a detailed discussion of Zig’s relationship with libc, see this Reddit thread.)
Here is the creator of Zig discussing the capabilities of the C compiler in depth, including a sample of Zig compiling the GCC LuaJIT compiler. The bottom line is that Zig tries not just to replace C with its own syntax, but to absorb C into itself as much as possible.
Karsten told me that “Zig is a better C/C++ compiler than other C/C++ compilers because it supports cross-compiling out of the box, among other things. Zig can also trivially interoperate with C (it can import C header files directly), and is generally better than C at using C libraries, thanks to a stronger type system and language features like deferring.
Error Handling in Zig
Zig has a unique error handling system. As part of its “avoid hidden control flow” design philosophy, Zig does not use throw
to raise exceptions. He throw
The function can branch execution in ways that are difficult to follow. Instead, if necessary, declarations and functions can return an error type, as part of a union type with whatever is returned in the happy path. The code can use the error object to respond accordingly or use the try
keyword to pass the error.
One type of error union 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. He !void
The syntax is interesting. It says that the function can return null or an error. This means that if the main()
the function executes without error, it will not return anything; but if it fails, it will return an error object that describes the error condition.
You can see how the client code can use an error object on the line where the try
the keyword appears. From stdout.print
can return an error try
expression here means that the error will be passed to the main()
return value of the function.
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. Create and run Helloworld.zig
$ zig build-exe hello.zig
$ ./hello
Hello, world!
Zig’s build tool works cross-platform and replaces tools like make
and cmake
.
A package manager is in the works, and testing support is built right into the language and runner.
zig status
Zig has an active Discord community and a lively GitHub ecosystem. The internal documentation is quite comprehensive, and Zig users have also produced a fair amount of third-party material.
Zig is not yet at version 1.0, but its creators say it is nearing production. On the subject of readiness, Karsten said, “Zig isn’t at version 1.0 yet, so things like webdev are still in their infancy, but the only use I wouldn’t recommend 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 seems to be taking their time with version 1.0, which may be released in 2025 or later, but none of that is stopping us from building all sorts of things with the language today.
The activity, goals, and adoption of Zig by the developer community make it an interesting project to watch.
More information about Zig
Here are some articles where you can learn more about Zig and how it’s revolutionizing the world of Systems Oriented Programming:
Copyright © 2023 IDG Communications, Inc.
Be First to Comment