ZIG

Zig is a general-purpose programming language designed for robustness, optimality, and maintainability.

Introduction §

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 500-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:

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:

test.zig

test "integer overflow at compile time" {
    const x: u8 = 255;
    const y = x + 1;
}
$ zig test test.zig
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:3:17: error: operation caused overflow
    const y = x + 1;
                ^

Here is what it looks like at runtime, in safety-checked builds:

test.zig

test "integer overflow at runtime" {
    var x: u8 = 255;
    x += 1;
}
$ zig test test.zig
1/1 test "integer overflow at runtime"...integer overflow
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:3:7: 0x2045f0 in test "integer overflow at runtime" (test)
    x += 1;
      ^
/home/andy/downloads/zig/build/lib/zig/std/special/test_runner.zig:13:25: 0x224ddd in std.special.main (test)
        if (test_fn.func()) |_| {
                        ^
/home/andy/downloads/zig/build/lib/zig/std/special/start.zig:146:22: 0x223c46 in std.special.posixCallMainAndExit (test)
            root.main() catch |err| {
                     ^
/home/andy/downloads/zig/build/lib/zig/std/special/start.zig:66:5: 0x223ac0 in std.special._start (test)
    @noInlineCall(posixCallMainAndExit);
    ^

Tests failed. Use the following command to reproduce the failure:
/home/andy/dev/www.ziglang.org/docgen_tmp/test

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:

test "actually undefined behavior" {
    @setRuntimeSafety(false);
    var x: u8 = 255;
    x += 1; // XXX undefined behavior!
}

Zig uses undefined behavior as a razor sharp tool for both bug prevention and performance enhancement.

Speaking of performance, Zig is faster than C.

Please note that Zig is not a fully safe language. For those interested in following Zig's safety story, subscribe to these issues:

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:

hello.zig

const std = @import("std");

pub fn main() void {
    std.debug.warn("Hello, world!\n");
}
$ zig build-exe hello.zig
$ ./hello
Hello, world!

When compiled with --release-small, 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 --release-small --strip --single-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 --release-small --strip --single-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 value of global variables is evaluated at compile-time.

global_variables.zig

var y: i32 = add(10, x);
const x: i32 = add(12, 34);

test "global variables" {
    assert(x == 46);
    assert(y == 56);
}

fn add(a: i32, b: i32) i32 {
    return a + b;
}

const std = @import("std");
const assert = std.debug.assert;
$ zig test global_variables.zig
1/1 test "global variables"...OK
All tests passed.

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:

test.zig

test "null @intToPtr" {
    const ptr = @intToPtr(*i32, 0x0);
}
$ zig test test.zig
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:2:17: error: pointer type '*i32' does not allow address zero
    const ptr = @intToPtr(*i32, 0x0);
                ^

However any type can be made into an optional type by prefixing it with ?:

test.zig

const std = @import("std");
const assert = std.debug.assert;

test "null @intToPtr" {
    const ptr = @intToPtr(?*i32, 0x0);
    assert(ptr == null);
}
$ zig test test.zig
1/1 test "null @intToPtr"...OK
All tests passed.

To unwrap an optional value, one can use orelse to provide a default value:

// malloc prototype included for reference
extern fn malloc(size: size_t) ?*u8;

fn doAThing() ?*Foo {
    const ptr = malloc(1234) orelse return null;
    // ...
}

Another option is to use if:

fn doAThing(optional_foo: ?*Foo) void {
    // do some stuff

    if (optional_foo) |foo| {
      doSomethingWithFoo(foo);
    }

    // do some stuff
}

The same syntax works with while:

iterator.zig

const std = @import("std");

pub fn main() void {
    const msg = "hello this is dog";
    var it = std.mem.tokenize(msg, " ");
    while (it.next()) |item| {
        std.debug.warn("{}\n", item);
    }
}
$ zig build-exe iterator.zig
$ ./iterator
hello
this
is
dog

Manual memory management §

A library written in Zig is eligible to be used anywhere:

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:

const Device = struct {
    name: []u8,

    fn create(allocator: *Allocator, id: u32) !Device {
        const device = try allocator.create(Device);
        errdefer allocator.destroy(device);

        device.name = try std.fmt.allocPrint(allocator, "Device(id={})", id);
        errdefer allocator.free(device.name);

        if (id == 0) return error.ReservedDeviceId;

        return device;
    }
};

A fresh take on error handling §

Errors are values, and may not be ignored:

discard.zig

const std = @import("std");
const File = std.fs.File;

pub fn main() void {
    _ = File.openRead("does_not_exist/foo.txt");
}
$ zig build-exe discard.zig
/home/andy/dev/www.ziglang.org/docgen_tmp/discard.zig:5:7: error: error is discarded
    _ = File.openRead("does_not_exist/foo.txt");
      ^

Errors can be handled with catch:

catch.zig

const std = @import("std");
const File = std.fs.File;

pub fn main() void {
    const file = File.openRead("does_not_exist/foo.txt") catch |err| label: {
        std.debug.warn("unable to open file: {}\n", err);
        const stderr = std.io.getStdErr() catch @panic("unable to get stderr");
        break :label stderr;
    };
    file.write("all your codebase are belong to us\n") catch return;
}
$ zig build-exe catch.zig
$ ./catch
unable to open file: error.FileNotFound
all your codebase are belong to us

The keyword try is a shortcut for catch |err| return err:

try.zig

const std = @import("std");
const File = std.fs.File;

pub fn main() !void {
    const file = try File.openRead("does_not_exist/foo.txt");
    defer file.close();
    try file.write("all your codebase are belong to us\n");
}
$ zig build-exe try.zig
$ ./try
error: FileNotFound
/home/andy/downloads/zig/build/lib/zig/std/os.zig:537:23: 0x21c59f in std.os.openC (try)
            ENOENT => return error.FileNotFound,
                      ^
/home/andy/downloads/zig/build/lib/zig/std/fs/file.zig:45:20: 0x206c2a in std.fs.file.File.openReadC (try)
        const fd = try os.openC(path, flags, 0);
                   ^
/home/andy/downloads/zig/build/lib/zig/std/fs/file.zig:35:9: 0x21a6d0 in std.fs.file.File.openRead (try)
        return openReadC(&path_c);
        ^
/home/andy/dev/www.ziglang.org/docgen_tmp/try.zig:5:18: 0x224c8d in main (try)
    const file = try File.openRead("does_not_exist/foo.txt");
                 ^

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:

test.zig

const std = @import("std");

test "switch on error" {
    const result = parseInt("hi", 10) catch |err| switch (err) {};
}

fn parseInt(buf: []const u8, radix: u8) !u64 {
    var x: u64 = 0;

    for (buf) |c| {
        const digit = try charToDigit(c);

        if (digit >= radix) {
            return error.DigitExceedsRadix;
        }

        x = try std.math.mul(u64, x, radix);
        x = try std.math.add(u64, x, digit);
    }

    return x;
}

fn charToDigit(c: u8) !u8 {
    const value = switch (c) {
        '0'...'9' => c - '0',
        'A'...'Z' => c - 'A' + 10,
        'a'...'z' => c - 'a' + 10,
        else => return error.InvalidCharacter,
    };

    return value;
}
$ zig test test.zig
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:4:51: error: error.Overflow not handled in switch
    const result = parseInt("hi", 10) catch |err| switch (err) {};
                                                  ^
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:4:51: error: error.DigitExceedsRadix not handled in switch
    const result = parseInt("hi", 10) catch |err| switch (err) {};
                                                  ^
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:4:51: error: error.InvalidCharacter not handled in switch
    const result = parseInt("hi", 10) catch |err| switch (err) {};
                                                  ^

The keyword unreachable is used to assert that no errors will occur:

unreachable.zig

const std = @import("std");
const File = std.fs.File;

pub fn main() void {
    const file = File.openRead("does_not_exist/foo.txt") catch unreachable;
    file.write("all your codebase are belong to us\n") catch unreachable;
}
$ zig build-exe unreachable.zig
$ ./unreachable
attempt to unwrap error: FileNotFound
/home/andy/downloads/zig/build/lib/zig/std/os.zig:537:23: 0x21c59f in std.os.openC (unreachable)
            ENOENT => return error.FileNotFound,
                      ^
/home/andy/downloads/zig/build/lib/zig/std/fs/file.zig:45:20: 0x206c2a in std.fs.file.File.openReadC (unreachable)
        const fd = try os.openC(path, flags, 0);
                   ^
/home/andy/downloads/zig/build/lib/zig/std/fs/file.zig:35:9: 0x21a6d0 in std.fs.file.File.openRead (unreachable)
        return openReadC(&path_c);
        ^
???:?:?: 0x224e86 in ??? (???)


/home/andy/dev/www.ziglang.org/docgen_tmp/unreachable.zig:5:58: 0x224b75 in main (unreachable)
    const file = File.openRead("does_not_exist/foo.txt") catch unreachable;
                                                         ^
/home/andy/downloads/zig/build/lib/zig/std/special/start.zig:136:22: 0x223b89 in std.special.posixCallMainAndExit (unreachable)
            root.main();
                     ^
/home/andy/downloads/zig/build/lib/zig/std/special/start.zig:66:5: 0x223a70 in std.special._start (unreachable)
    @noInlineCall(posixCallMainAndExit);
    ^

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:

stack_traces.zig

const std = @import("std");
const builtin = @import("builtin");

var address_buffer: [8]usize = undefined;

var trace1 = builtin.StackTrace{
    .instruction_addresses = address_buffer[0..4],
    .index = 0,
};

var trace2 = builtin.StackTrace{
    .instruction_addresses = address_buffer[4..],
    .index = 0,
};

pub fn main() void {
    foo();
    bar();

    std.debug.warn("first one:\n");
    std.debug.dumpStackTrace(trace1);
    std.debug.warn("\n\nsecond one:\n");
    std.debug.dumpStackTrace(trace2);
}

fn foo() void {
    std.debug.captureStackTrace(null, &trace1);
}

fn bar() void {
    std.debug.captureStackTrace(null, &trace2);
}
$ zig build-exe stack_traces.zig
$ ./stack_traces
first one:
/home/andy/dev/www.ziglang.org/docgen_tmp/stack_traces.zig:27:32: 0x224d6c in foo (stack_traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/andy/dev/www.ziglang.org/docgen_tmp/stack_traces.zig:17:8: 0x224b08 in main (stack_traces)
    foo();
       ^
/home/andy/downloads/zig/build/lib/zig/std/special/start.zig:136:22: 0x223b89 in std.special.posixCallMainAndExit (stack_traces)
            root.main();
                     ^
/home/andy/downloads/zig/build/lib/zig/std/special/start.zig:66:5: 0x223a70 in std.special._start (stack_traces)
    @noInlineCall(posixCallMainAndExit);
    ^


second one:
/home/andy/dev/www.ziglang.org/docgen_tmp/stack_traces.zig:31:32: 0x224d8c in bar (stack_traces)
    std.debug.captureStackTrace(null, &trace2);
                               ^
/home/andy/dev/www.ziglang.org/docgen_tmp/stack_traces.zig:18:8: 0x224b0d in main (stack_traces)
    bar();
       ^
/home/andy/downloads/zig/build/lib/zig/std/special/start.zig:136:22: 0x223b89 in std.special.posixCallMainAndExit (stack_traces)
            root.main();
                     ^
/home/andy/downloads/zig/build/lib/zig/std/special/start.zig:66:5: 0x223a70 in std.special._start (stack_traces)
    @noInlineCall(posixCallMainAndExit);
    ^

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:

test.zig

const std = @import("std");
const assert = std.debug.assert;

test "types are values" {
    const T1 = u8;
    const T2 = bool;
    assert(T1 != T2);

    const x: T2 = true;
    assert(x);
}
$ zig test test.zig
1/1 test "types are values"...OK
All tests passed.

A generic data structure is simply a function that returns a type:

generics.zig

const std = @import("std");

fn List(comptime T: type) type {
    return struct {
        items: []T,
        len: usize,
    };
}

pub fn main() void {
    var buffer: [10]i32 = undefined;
    var list = List(i32){
        .items = &buffer,
        .len = 0,
    };

    std.debug.warn("{}\n", list.items.len);
}
$ zig build-exe generics.zig
$ ./generics
10

Compile-time reflection and compile-time code execution §

The @typeInfo builtin function provides reflection:

reflection.zig

const std = @import("std");

const Header = struct {
    magic: u32,
    name: []const u8,
};

pub fn main() void {
    printInfoAboutStruct(Header);
}

fn printInfoAboutStruct(comptime T: type) void {
    const info = @typeInfo(Header);
    inline for (info.Struct.fields) |field| {
        std.debug.warn(
            "{} has a field called {} with type {}\n",
            @typeName(T),
            field.name,
            @typeName(field.field_type),
        );
    }
}
$ zig build-exe reflection.zig
$ ./reflection
Header has a field called magic with type u32
Header has a field called name with type []const u8

The Zig Standard Library uses this techinque 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:

test.zig

const std = @import("std");
const assert = std.debug.assert;

fn fibonacci(x: u32) u32 {
    if (x <= 1) return x;
    return fibonacci(x - 1) + fibonacci(x - 2);
}

test "compile-time evaluation" {
    var array: [fibonacci(6)]i32 = undefined;

    comptime {
        assert(array.len == 12345);
    }
}
$ zig test test.zig
/home/andy/downloads/zig/build/lib/zig/std/debug.zig:205:14: error: unable to evaluate constant expression
    if (!ok) unreachable; // assertion failure
             ^
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:13:15: note: called from here
        assert(array.len == 12345);
              ^

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

const c = @cImport(@cInclude("soundio/soundio.h"));
const std = @import("std");
const panic = std.debug.panic;
const math = std.math;

fn sio_err(err: c_int) !void {
    switch (@intToEnum(c.SoundIoError, err)) {
        c.SoundIoError.None => {},
        c.SoundIoError.NoMem => return error.NoMem,
        c.SoundIoError.InitAudioBackend => return error.InitAudioBackend,
        c.SoundIoError.SystemResources => return error.SystemResources,
        c.SoundIoError.OpeningDevice => return error.OpeningDevice,
        c.SoundIoError.NoSuchDevice => return error.NoSuchDevice,
        c.SoundIoError.Invalid => return error.Invalid,
        c.SoundIoError.BackendUnavailable => return error.BackendUnavailable,
        c.SoundIoError.Streaming => return error.Streaming,
        c.SoundIoError.IncompatibleDevice => return error.IncompatibleDevice,
        c.SoundIoError.NoSuchClient => return error.NoSuchClient,
        c.SoundIoError.IncompatibleBackend => return error.IncompatibleBackend,
        c.SoundIoError.BackendDisconnected => return error.BackendDisconnected,
        c.SoundIoError.Interrupted => return error.Interrupted,
        c.SoundIoError.Underflow => return error.Underflow,
        c.SoundIoError.EncodingString => return error.EncodingString,
    }
}

var seconds_offset: f32 = 0;

extern fn write_callback(
    maybe_outstream: ?[*]c.SoundIoOutStream,
    frame_count_min: c_int,
    frame_count_max: c_int,
) void {
    const outstream = @ptrCast(*c.SoundIoOutStream, maybe_outstream);
    const layout = &outstream.layout;
    const float_sample_rate = outstream.sample_rate;
    const seconds_per_frame = 1.0 / @intToFloat(f32, float_sample_rate);
    var frames_left = frame_count_max;

    while (frames_left > 0) {
        var frame_count = frames_left;

        var areas: [*]c.SoundIoChannelArea = undefined;
        sio_err(c.soundio_outstream_begin_write(
            maybe_outstream,
            @ptrCast([*]?[*]c.SoundIoChannelArea, &areas),
            &frame_count,
        )) catch |err| panic("write failed: {}", @errorName(err));

        if (frame_count == 0) break;

        const pitch = 440.0;
        const radians_per_second = pitch * 2.0 * math.pi;
        var frame: c_int = 0;
        while (frame < frame_count) : (frame += 1) {
            const sample = math.sin((seconds_offset + @intToFloat(f32, frame) *
                seconds_per_frame) * radians_per_second);
            {
                var channel: usize = 0;
                while (channel < @intCast(usize, layout.channel_count)) : (channel += 1) {
                    const channel_ptr = areas[channel].ptr;
                    const sample_ptr = &channel_ptr[@intCast(usize, areas[channel].step * frame)];
                    @ptrCast(*f32, @alignCast(@alignOf(f32), sample_ptr)).* = sample;
                }
            }
        }
        seconds_offset += seconds_per_frame * @intToFloat(f32, frame_count);

        sio_err(c.soundio_outstream_end_write(maybe_outstream)) catch |err| panic("end write failed: {}", @errorName(err));

        frames_left -= frame_count;
    }
}

pub fn main() void {
    const soundio = c.soundio_create();
    defer c.soundio_destroy(soundio);

    sio_err(c.soundio_connect(soundio)) catch |err| panic("unable to connect: {}", @errorName(err));

    c.soundio_flush_events(soundio);

    const default_output_index = c.soundio_default_output_device_index(soundio);
    if (default_output_index < 0) panic("no output device found");

    const device = c.soundio_get_output_device(soundio, default_output_index);
    defer c.soundio_device_unref(device);

    std.debug.warn("Output device: {s}\n", ([*]const u8)(device.*.name));

    const outstream = c.soundio_outstream_create(device);
    defer c.soundio_outstream_destroy(outstream);

    outstream.*.format = c.SoundIoFormatFloat32NE;
    outstream.*.write_callback = write_callback;

    sio_err(c.soundio_outstream_open(outstream)) catch |err| panic("unable to open stream: {}", @errorName(err));

    sio_err(c.soundio_outstream_start(outstream)) catch |err| panic("unable to start stream: {}", @errorName(err));

    while (true) c.soundio_wait_events(soundio);
}
$ zig build-exe sine.zig --library soundio --library c
$ ./sine
Output device: Built-in Audio Analog Stereo
^C

There are a few open issues which will clean up some of this code, such as supporting if, orelse, null, and .? for C pointers. But even as is, 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 --c-source hello.c --library c
$ ./hello
Hello world

You can use --verbose-cc to see what C compiler command this executed:

$ zig build-exe --c-source 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 I run the command again, there is no output, and it finishes instantly:

$ time zig build-exe --c-source 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

export fn add(a: i32, b: i32) i32 {
    return a + b;
}

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

// This header is generated by zig from mathtest.zig
#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

const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    const lib = b.addSharedLibrary("mathtest", "mathtest.zig", b.version(1, 0, 0));

    const exe = b.addExecutable("test", null);
    exe.addCSourceFile("test.c", [][]const u8{"-std=c99"});
    exe.linkLibrary(lib);
    exe.linkSystemLibrary("c");

    b.default_step.dependOn(&exe.step);

    const run_cmd = exe.run();

    const test_step = b.step("test", "Test the program");
    test_step.dependOn(&run_cmd.step);
}

terminal

$ zig build test
1379

Automatically generate C header (.h) files §

In the Export functions, variables, and types for C code to depend on section, the file mathtest.h was generated by Zig:

mathtest.h

#ifndef MATHTEST_H
#define MATHTEST_H

#include <stdint.h>

#ifdef __cplusplus
#define MATHTEST_EXTERN_C extern "C"
#else
#define MATHTEST_EXTERN_C
#endif

#if defined(_WIN32)
#define MATHTEST_EXPORT MATHTEST_EXTERN_C __declspec(dllimport)
#else
#define MATHTEST_EXPORT MATHTEST_EXTERN_C __attribute__((visibility ("default")))
#endif

MATHTEST_EXPORT int32_t add(int32_t a, int32_t b);

#endif

Cross-compiling is a first-class use case §

Zig can build for any of the targets from the Support Table with Tier 3 Support or better. No "cross toolchain" needs to be installed or anything like that. Here's a native Hello World:

hello.zig

const std = @import("std");

pub fn main() void {
    std.debug.warn("Hello, world!\n");
}
$ zig build-exe hello.zig
$ ./hello
Hello, world!

Now to build it for x86_64-windows, x86_64-macosx, and aarch64v8-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-macosx
$ file hello
hello: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>
$ zig build-exe hello.zig -target aarch64v8-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:

...
Available libcs:
  aarch64_be-linux-gnu
  aarch64_be-linux-musl
  aarch64-linux-gnu
  aarch64-linux-musleabi
  armeb-linux-gnueabi
  armeb-linux-gnueabihf
  armeb-linux-musleabi
  armeb-linux-musleabihf
  arm-linux-gnueabi
  arm-linux-gnueabihf
  arm-linux-musleabi
  arm-linux-musleabihf
  i386-linux-gnu
  i386-linux-musl
  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
  riscv32-linux-musl
  riscv64-linux-gnu
  riscv64-linux-musl
  s390x-linux-gnu
  s390x-linux-musl
  sparc-linux-gnu
  sparcv9-linux-gnu
  x86_64-linux-gnu
  x86_64-linux-gnux32
  x86_64-linux-musl

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 --c-source 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 --c-source 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 --c-source hello.c --library c -target aarch64v8-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 that I made, 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

const std = @import("std");

pub fn main() anyerror!void {
    std.debug.warn("All your base are belong to us.\n");
}

build.zig

const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    const mode = b.standardReleaseOptions();
    const exe = b.addExecutable("example", "src/main.zig");
    exe.setBuildMode(mode);

    const run_cmd = exe.run();

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    b.default_step.dependOn(&exe.step);
    b.installArtifact(exe);
}

Let's have a look at that --help menu.

$ zig build --help
Usage: zig build [steps] [options]

Steps:
  default            Build the project
  run                Run the app
  install            Copy build artifacts to prefix path
  uninstall          Remove build artifacts from prefix path

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:
  -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-std-dir [arg] Override path to Zig standard library
  --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

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:

Concurrency via stackless coroutines §

This is an active area of development.

Read about Zig's concurrency story in the 0.3.0 release notes.

Zig's status quo coroutines are about to go through a design iteration.

One thing is certain though: no syscalls are made directly by the language, and therefore these features are available in freestanding mode.

The Zig Standard Library implements an event loop that multiplexes coroutines onto a thread pool for M:N concurrency. Multithreading safety and race detection are areas of active research, as well as ability to write code that is agnostic of blocking vs async I/O.

Wide range of targets supported §

Zig uses a "support tier" system to communicate the level of support for different targets.

Support Table §

free standing Linux 3.16+ macOS 10.13+ Windows 7+ FreeBSD 12.0+ NetBSD 8.0+ UEFI WASI other
x86_64 Tier 2 Tier 1 Tier 1 Tier 1 Tier 2 Tier 2 Tier 2 N/A Tier 3
arm 64-bit Tier 2 Tier 2 Tier 3 Tier 3 Tier 3 Tier 3 Tier 3 N/A Tier 3
wasm32 Tier 2 N/A N/A N/A N/A N/A N/A Tier 2 N/A
arm 32-bit Tier 2 Tier 3 Tier 3 Tier 3 Tier 3 Tier 3 Tier 3 N/A Tier 3
i386 Tier 2 Tier 2 Tier 4 Tier 2 Tier 3 Tier 3 Tier 3 N/A Tier 3
bpf Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A Tier 3
hexagon Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A Tier 3
mips 32-bit Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A Tier 3
mips 64-bit Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A Tier 3
amdgcn Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A Tier 3
sparc Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A Tier 3
s390x Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A Tier 3
lanai Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A Tier 3
powerpc32 Tier 3 Tier 3 Tier 4 N/A Tier 3 Tier 3 N/A N/A Tier 3
powerpc64 Tier 3 Tier 3 Tier 4 N/A Tier 3 Tier 3 N/A N/A Tier 3
wasm64 Tier 4 N/A N/A N/A N/A N/A N/A Tier 4 N/A
avr Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
riscv32 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 Tier 4 N/A Tier 4
riscv64 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 Tier 4 N/A Tier 4
xcore Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
nvptx Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
msp430 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
r600 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
arc Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
tce Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
le Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
amdil Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
hsail Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
spir Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
kalimba Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
shave Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4
renderscript Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A Tier 4

Tier System §

Tier 1 Support §

Tier 2 Support §

Tier 3 Support §

Tier 4 Support §

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.

Reading and Viewing Material §

Zig in the Wild §

Sponsors §

This section is updated at the beginning of each month.