Overview
Feature Highlights
Small, simple language
Focus on debugging your application rather than debugging your programming language knowledge.
Zig's entire syntax is specified with a 580-line PEG grammar file.
There is no hidden control flow, no hidden memory allocations, no preprocessor, and no macros. If Zig code doesn't look like it's jumping away to call a function, then it isn't. This means you can be sure that the following code calls only foo()
and then bar()
, and this is guaranteed without needing to know the types of anything:
var a = b + c.d;
foo();
bar();
Examples of hidden control flow:
- D has
@property
functions, which are methods that you call with what looks like field access, so in the above example,c.d
might call a function. - C++, D, and Rust have operator overloading, so the
+
operator might call a function. - C++, D, and Go have throw/catch exceptions, so
foo()
might throw an exception, and preventbar()
from being called.
Zig promotes code maintenance and readability by making all control flow managed exclusively with language keywords and function calls.
Performance and Safety: Choose Two
Zig has four build modes, and they can all be mixed and matched all the way down to scope granularity.
Parameter | Debug | ReleaseSafe | ReleaseFast | ReleaseSmall |
---|---|---|---|---|
Optimizations - improve speed, harm debugging, harm compile time | -O3 | -O3 | -Os | |
Runtime Safety Checks - harm speed, harm size, crash instead of undefined behavior | On | On |
Here is what Integer Overflow looks like at compile time, regardless of the build mode:
Here is what it looks like at runtime, in safety-checked builds:
Those stack traces work on all targets, including freestanding.
With Zig one can rely on a safety-enabled build mode, and selectively disable safety at the performance bottlenecks. For example the previous example could be modified like this:
Zig uses undefined behavior as a razor sharp tool for both bug prevention and performance enhancement.
Speaking of performance, Zig is faster than C.
- The reference implementation uses LLVM as a backend for state of the art optimizations.
- What other projects call "Link Time Optimization" Zig does automatically.
- For native targets, advanced CPU features are enabled (-march=native), thanks to the fact that Cross-compiling is a first-class use case.
- Carefully chosen undefined behavior. For example, in Zig both signed and unsigned integers have undefined behavior on overflow, contrasted to only signed integers in C. This facilitates optimizations that are not available in C.
- Zig directly exposes a SIMD vector type, making it easy to write portable vectorized code.
Please note that Zig is not a fully safe language. For those interested in following Zig's safety story, subscribe to these issues:
- enumerate all kinds of undefined behavior, even that which cannot be safety-checked
- make Debug and ReleaseSafe modes fully safe
Zig competes with C instead of depending on it
The Zig Standard Library integrates with libc, but does not depend on it. Here's Hello World:
When compiled with -O ReleaseSmall
, debug symbols stripped, single-threaded mode, this produces a 9.8 KiB static executable for the x86_64-linux target:
$ zig build-exe hello.zig -O ReleaseSmall -fstrip -fsingle-threaded
$ wc -c hello
9944 hello
$ ldd hello
not a dynamic executable
A Windows build is even smaller, coming out to 4096 bytes:
$ zig build-exe hello.zig -O ReleaseSmall -fstrip -fsingle-threaded -target x86_64-windows
$ wc -c hello.exe
4096 hello.exe
$ file hello.exe
hello.exe: PE32+ executable (console) x86-64, for MS Windows
Order independent top level declarations
Top level declarations such as global variables are order-independent and lazily analyzed. The initialization values of global variables are evaluated at compile-time.
Optional type instead of null pointers
In other programming languages, null references are the source of many runtime exceptions, and even stand accused of being the worst mistake of computer science.
Unadorned Zig pointers cannot be null:
However any type can be made into an optional type by prefixing it with ?:
To unwrap an optional value, one can use orelse
to provide a default value:
Another option is to use if:
The same syntax works with while:
Manual memory management
A library written in Zig is eligible to be used anywhere:
- Desktop applications
- Low latency servers
- Operating System kernels
- Embedded devices
- Real-time software, e.g. live performances, airplanes, pacemakers
- In web browsers or other plugins with WebAssembly
- By other programming languages, using the C ABI
In order to accomplish this, Zig programmers must manage their own memory, and must handle memory allocation failure.
This is true of the Zig Standard Library as well. Any functions that need to allocate memory accept an allocator parameter. As a result, the Zig Standard Library can be used even for the freestanding target.
In addition to A fresh take on error handling, Zig provides defer and errdefer to make all resource management - not only memory - simple and easily verifiable.
For an example of defer
, see Integration with C libraries without FFI/bindings. Here is an example of using errdefer
:
A fresh take on error handling
Errors are values, and may not be ignored:
Errors can be handled with catch:
The keyword try is a shortcut for catch |err| return err
:
Note that is an Error Return Trace, not a stack trace. The code did not pay the price of unwinding the stack to come up with that trace.
The switch keyword used on an error ensures that all possible errors are handled:
The keyword unreachable is used to assert that no errors will occur:
This invokes undefined behavior in the unsafe build modes, so be sure to use it only when success is guaranteed.
Stack traces on all targets
The stack traces and error return traces shown on this page work on all Tier 1 Support and some Tier 2 Support targets. Even freestanding!
In addition, the standard library has the ability to capture a stack trace at any point and then dump it to standard error later:
You can see this technique being used in the ongoing GeneralPurposeDebugAllocator project.
Generic data structures and functions
Types are values that must be known at compile-time:
A generic data structure is simply a function that returns a type
:
Compile-time reflection and compile-time code execution
The @typeInfo builtin function provides reflection:
The Zig Standard Library uses this technique to implement formatted printing. Despite being a Small, simple language, Zig's formatted printing is implemented entirely in Zig. Meanwhile, in C, compile errors for printf are hard-coded into the compiler. Similarly, in Rust, the formatted printing macro is hard-coded into the compiler.
Zig can also evaluate functions and blocks of code at compile-time. In some contexts, such as global variable initializations, the expression is implicitly evaluated at compile-time. Otherwise, one can explicitly evaluate code at compile-time with the comptime keyword. This can be especially powerful when combined with assertions:
Integration with C libraries without FFI/bindings
@cImport directly imports types, variables, functions, and simple macros for use in Zig. It even translates inline functions from C into Zig.
Here is an example of emitting a sine wave using libsoundio:
sine.zig
$ zig build-exe sine.zig -lsoundio -lc
$ ./sine
Output device: Built-in Audio Analog Stereo
^C
This Zig code is significantly simpler than the equivalent C code, as well as having more safety protections, and all this is accomplished by directly importing the C header file - no API bindings.
Zig is better at using C libraries than C is at using C libraries.
Zig is also a C compiler
Here's an example of Zig building some C code:
hello.c
#include <stdio.h>
int main(int argc, char **argv) {
printf("Hello world\n");
return 0;
}
$ zig build-exe hello.c --library c
$ ./hello
Hello world
You can use --verbose-cc
to see what C compiler command this executed:
$ zig build-exe hello.c --library c --verbose-cc
zig cc -MD -MV -MF .zig-cache/tmp/42zL6fBH8fSo-hello.o.d -nostdinc -fno-spell-checking -isystem /home/andy/dev/zig/build/lib/zig/include -isystem /home/andy/dev/zig/build/lib/zig/libc/include/x86_64-linux-gnu -isystem /home/andy/dev/zig/build/lib/zig/libc/include/generic-glibc -isystem /home/andy/dev/zig/build/lib/zig/libc/include/x86_64-linux-any -isystem /home/andy/dev/zig/build/lib/zig/libc/include/any-linux-any -march=native -g -fstack-protector-strong --param ssp-buffer-size=4 -fno-omit-frame-pointer -o .zig-cache/tmp/42zL6fBH8fSo-hello.o -c hello.c -fPIC
Note that if you run the command again, there is no output, and it finishes instantly:
$ time zig build-exe hello.c --library c --verbose-cc
real 0m0.027s
user 0m0.018s
sys 0m0.009s
This is thanks to Build Artifact Caching. Zig automatically parses the .d file uses a robust caching system to avoid duplicating work.
Not only can Zig compile C code, but there is a very good reason to use Zig as a C compiler: Zig ships with libc.
Export functions, variables, and types for C code to depend on
One of the primary use cases for Zig is exporting a library with the C ABI for other programming languages to call into. The export
keyword in front of functions, variables, and types causes them to be part of the library API:
mathtest.zig
To make a static library:
$ zig build-lib mathtest.zig
To make a shared library:
$ zig build-lib mathtest.zig -dynamic
Here is an example with the Zig Build System:
test.c
#include "mathtest.h"
#include <stdio.h>
int main(int argc, char **argv) {
int32_t result = add(42, 1337);
printf("%d\n", result);
return 0;
}
build.zig
$ zig build test
1379
Cross-compiling is a first-class use case
Zig can build for any of the targets from the Support Table (see latest release notes) with Tier 3 Support or better. No "cross toolchain" needs to be installed or anything like that. Here's a native Hello World:
Now to build it for x86_64-windows, x86_64-macos, and aarch64-linux:
$ zig build-exe hello.zig -target x86_64-windows
$ file hello.exe
hello.exe: PE32+ executable (console) x86-64, for MS Windows
$ zig build-exe hello.zig -target x86_64-macos
$ file hello
hello: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>
$ zig build-exe hello.zig -target aarch64-linux
$ file hello
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped
This works on any Tier 3+ target, for any Tier 3+ target.
Zig ships with libc
You can find the available libc targets with zig targets
:
...
"libc": [
"aarch64_be-linux-gnu",
"aarch64_be-linux-musl",
"aarch64_be-windows-gnu",
"aarch64-linux-gnu",
"aarch64-linux-musl",
"aarch64-windows-gnu",
"armeb-linux-gnueabi",
"armeb-linux-gnueabihf",
"armeb-linux-musleabi",
"armeb-linux-musleabihf",
"armeb-windows-gnu",
"arm-linux-gnueabi",
"arm-linux-gnueabihf",
"arm-linux-musleabi",
"arm-linux-musleabihf",
"arm-windows-gnu",
"mips64el-linux-gnuabi64",
"mips64el-linux-gnuabin32",
"mips64el-linux-musl",
"mips64-linux-gnuabi64",
"mips64-linux-gnuabin32",
"mips64-linux-musl",
"mipsel-linux-gnu",
"mipsel-linux-musl",
"mips-linux-gnu",
"mips-linux-musl",
"powerpc64le-linux-gnu",
"powerpc64le-linux-musl",
"powerpc64-linux-gnu",
"powerpc64-linux-musl",
"powerpc-linux-gnu",
"powerpc-linux-musl",
"riscv64-linux-gnu",
"riscv64-linux-musl",
"s390x-linux-gnu",
"s390x-linux-musl",
"sparc-linux-gnu",
"sparcv9-linux-gnu",
"wasm32-freestanding-musl",
"x86-linux-gnu",
"x86-linux-musl",
"x86-windows-gnu",
"x86_64-linux-gnu",
"x86_64-linux-gnux32",
"x86_64-linux-musl",
"x86_64-windows-gnu"
],
What this means is that --library c
for these targets does not depend on any system files!
Let's look at that C hello world example again:
$ zig build-exe hello.c --library c
$ ./hello
Hello world
$ ldd ./hello
linux-vdso.so.1 (0x00007ffd03dc9000)
libc.so.6 => /lib/libc.so.6 (0x00007fc4b62be000)
libm.so.6 => /lib/libm.so.6 (0x00007fc4b5f29000)
libpthread.so.0 => /lib/libpthread.so.0 (0x00007fc4b5d0a000)
libdl.so.2 => /lib/libdl.so.2 (0x00007fc4b5b06000)
librt.so.1 => /lib/librt.so.1 (0x00007fc4b58fe000)
/lib/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fc4b6672000)
glibc does not support building statically, but musl does:
$ zig build-exe hello.c --library c -target x86_64-linux-musl
$ ./hello
Hello world
$ ldd hello
not a dynamic executable
In this example, Zig built musl libc from source and then linked against it. The build of musl libc for x86_64-linux remains available thanks to the caching system, so any time this libc is needed again it will be available instantly.
This means that this functionality is available on any platform. Windows and macOS users can build Zig and C code, and link against libc, for any of the targets listed above. Similarly code can be cross compiled for other architectures:
$ zig build-exe hello.c --library c -target aarch64-linux-gnu
$ file hello
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped
In some ways, Zig is a better C compiler than C compilers!
This functionality is more than bundling a cross-compilation toolchain along with Zig. For example, the total size of libc headers that Zig ships is 22 MiB uncompressed. Meanwhile, the headers for musl libc + linux headers on x86_64 alone are 8 MiB, and for glibc are 3.1 MiB (glibc is missing the linux headers), yet Zig currently ships with 40 libcs. With a naive bundling that would be 444 MiB. However, thanks to this process_headers tool, and some good old manual labor, Zig binary tarballs remain roughly 30 MiB total, despite supporting libc for all these targets, as well as compiler-rt, libunwind, and libcxx, and despite being a clang-compatible C compiler. For comparison, the Windows binary build of clang 8.0.0 itself from llvm.org is 132 MiB.
Note that only the Tier 1 Support targets have been thoroughly tested. It is planned to add more libcs (including for Windows), and to add test coverage for building against all the libcs.
It's planned to have a Zig Package Manager, but it's not done yet. One of the things that will be possible is to create a package for C libraries. This will make the Zig Build System attractive for Zig programmers and C programmers alike.
Zig Build System
Zig comes with a build system, so you don't need make, cmake, or anything like that.
$ zig init-exe
Created build.zig
Created src/main.zig
Next, try `zig build --help` or `zig build run`
src/main.zig
build.zig
Let's have a look at that --help
menu.
$ zig build --help
Usage: zig build [steps] [options]
Steps:
install (default) Copy build artifacts to prefix path
uninstall Remove build artifacts from prefix path
run Run the app
General Options:
--help Print this help and exit
--verbose Print commands before executing them
--prefix [path] Override default install prefix
--search-prefix [path] Add a path to look for binaries, libraries, headers
Project-Specific Options:
-Dtarget=[string] The CPU architecture, OS, and ABI to build for.
-Drelease-safe=[bool] optimizations on and safety on
-Drelease-fast=[bool] optimizations on and safety off
-Drelease-small=[bool] size optimizations on and safety off
Advanced Options:
--build-file [file] Override path to build.zig
--cache-dir [path] Override path to zig cache directory
--override-lib-dir [arg] Override path to Zig lib directory
--verbose-tokenize Enable compiler debug output for tokenization
--verbose-ast Enable compiler debug output for parsing into an AST
--verbose-link Enable compiler debug output for linking
--verbose-ir Enable compiler debug output for Zig IR
--verbose-llvm-ir Enable compiler debug output for LLVM IR
--verbose-cimport Enable compiler debug output for C imports
--verbose-cc Enable compiler debug output for C compilation
--verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
You can see that one of the available steps is run.
$ zig build run
All your base are belong to us.
Here are some example build scripts:
- Build script of OpenGL Tetris game
- Build script of bare metal Raspberry Pi 3 arcade game
- Build script of self-hosted Zig compiler
Concurrency via Async Functions
Zig 0.5.0 introduced async functions. This feature has no dependency on a host operating system or even heap-allocated memory. That means async functions are available for the freestanding target.
Zig infers whether a function is async, and allows async
/await
on non-async functions, which means that Zig libraries are agnostic of blocking vs async I/O. Zig avoids function colors.
The Zig Standard Library implements an event loop that multiplexes async functions onto a thread pool for M:N concurrency. Multithreading safety and race detection are areas of active research.
Wide range of targets supported
Zig uses a "support tier" system to communicate the level of support for different targets.
Support Table as of Zig 0.11.0
Friendly toward package maintainers
The reference Zig compiler is not completely self-hosted yet, but no matter what, it will remain exactly 3 steps to go from having a system C++ compiler to having a fully self-hosted Zig compiler for any target. As Maya Rashish notes, porting Zig to other platforms is fun and speedy.
Non-debug build modes are reproducible/deterministic.
There is a JSON version of the download page.
Several members of the Zig team have experience maintaining packages.
- Daurnimator maintains the Arch Linux package
- Marc Tiehuis maintains the Visual Studio Code package.
- Andrew Kelley spent a year or so doing Debian and Ubuntu packaging, and casually contributes to nixpkgs.
- Jeff Fowler maintains the Homebrew package and started the Sublime package (now maintained by emekoi).