Ausführliche Übersicht

Feature Highlights

Kleine, einfache Sprache

Debugge deine Anwendung, nicht deine Kenntnis der Programmiersprache.

Zig’s gesamte Syntax ist in 500 Zeilen PEG-Grammatik beschrieben.

Es gibt keinen versteckten Kontrollfluss, keine versteckten Speicherallokationen, keinen Präprozessor, und keine Makros. Wenn Zig Code nicht aussieht, als ober in einen Funktionsaufruf springt, dann tut er das nicht. Das bedeutet, dass du sicher sein kannst, dass der folgende Code nur foo() und dann bar() aufruft, und das ist garantiert, ohne die Typen von irgendwelchen Werten zu kennen:

var a = b + c.d;
foo();
bar();

Beispiele von verstecktem Kontrollfluss:

Zig fördert Codewartung und Lesbarkeit, indem Kontrollfluss ausschließlich mit Schlüsselwörtern und Funktionsaufrufen geschieht.

Performance und Sicherheit: wähle zwei

Zig hat vier Buildmodi, sie können Scope-weise kombiniert werden.

ParameterDebugReleaseSafeReleaseFastReleaseSmall
Optimierungen - bessere Geschwindigkeit,
schlechteres Debugging und Compilierdauer
-O3-O3-Os
Laufzeitsicherheitschecks - schlechtere Geschwindigkeit
und Programmgröße, Crashes statt undefiniertem Verhalten
OnOn

So sieht Integer Overflow zur Compilezeit aus, in jedem Buildmodus:

test.zig

test "integer overflow at compile time" {
    const x: u8 = 255;
    _ = x + 1;
}
$ zig test test.zig
doctest-79764389/test.zig:3:11: error: overflow of integer type 'u8' with value '256'
    _ = x + 1;
        ~~^~~

So sieht er zur Laufzeit aus, in safety-checked-Builds:

test.zig

test "integer overflow at runtime" {
    var x: u8 = 255;
    x += 1;
}
$ zig test test.zig
1/1 test.test.integer overflow at runtime... thread 2821 panic: integer overflow
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-829c56f1/test.zig:3:7: 0x1038e2e in test.integer overflow at runtime (test)
    x += 1;
      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/compiler/test_runner.zig:158:25: 0x104421c in mainTerminal (test)
        if (test_fn.func()) |_| {
                        ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/compiler/test_runner.zig:35:28: 0x103a21b in main (test)
        return mainTerminal();
                           ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x1039359 in posixCallMainAndExit (test)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:253:5: 0x1038ec1 in _start (test)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/runner/.cache/zig/o/a32aa8be9084f599fee733bf715b04aa/test

Die Stacktraces funktionieren auf allen Targets, auch freestanding.

Zig erlaubt es, sich auf einen Buildmodus mit aktivierter Sicherheit zu verlassen, und die Sicherheit an Performanceengpässen selektiv zu deaktivieren. Das vorherige Beispiel könnte so verändert werden:

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

Zig benutzt undefiniertes Verhalten als ein messerscharfes Instrument zur Vermeidung von Bugs und Performanceverbesserung.

Apropos Performance: Zig ist schneller als C.

Beachte bitte, dass Zig keine vollständig sichere Sprache ist. Interessierte an der Geschichte von Zigs Sicherheit können diese Issues abbonieren:

Zig ist nicht abhängig von C, sondern konkurriert mit C

Zigs Standardbibliothek kann libc einbeziehen, aber ist nicht darauf angewiesen. Hier ist Hello World:

hello.zig

const std = @import("std");

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

Mit -O ReleaseSmall kompiliert, ohne Debugsymbole, im Single Thread-Modus, wird für das Target x86_64-linux eine 9.8 KiB große statische Programmdatei erzeugt:

$ zig build-exe hello.zig -O ReleaseSmall -fstrip -fsingle-threaded
$ wc -c hello
9944 hello
$ ldd hello
  not a dynamic executable

Ein Build auf Windows ist noch kleiner, nur 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

Deklarationen in beliebiger Reihenfolge

Deklarationen im Toplevel, wie globale Variablen, sind reihenfolgenunabhängig und werden nur bei Bedarf ausgewertet. Die Initialisierungswerte globaler Variablen werden zur Compilezeit ausgewertet.

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 global_variables.test.global variables... OK
All 1 tests passed.

Optionale Typen statt Nullpointer

In anderen Programmiersprachen sind Nullzeiger die Quelle vieler Laufzeitprobleme, und werden sogar beschuldigt, der schlimmste Fehler der Computerwissenwschaft zu sein.

Rohe Pointer können in Zig nicht Null sein:

test "null @intToPtr" {
    const foo: *i32 = @ptrFromInt(0x0);
    _ = foo;
}
$ zig test test.zig
doctest-215c0835/test.zig:2:35: error: pointer type '*i32' does not allow address zero
    const foo: *i32 = @ptrFromInt(0x0);
                                  ^~~

Jedoch kann jeder Typ durch ein vorgestelltes ? in einen optionalen Typ verwandelt werden:

optional_syntax.zig

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

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

Um einen optionalen Typ zu entpacken, kann ein orelse verwendet werden, um einen Default-Wert anzugeben:

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

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

Stattdessen kann auch ein if verwendet werden:

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

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

    // do some stuff
}

Dieselbe Syntax funktioniert mit while:

iterator.zig

const std = @import("std");

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

Manuelle Speicherverwaltung

Eine in Zig verfasste Bibliothek kann überall verwendet werden:

Um das zu erreichen, müssen Zig-Programmierer ihren Speicher selbst verwalten und mit scheiternder Speicherallokation umgehen.

Das trifft auch auf die Standardbibliothek zu. Alle Funktionen, die Speicher allozieren müssen, nehmen einen Allocator als Parameter an. Damit kann die Standardbibliothek sogar auf dem Freestanding-Target verwendet werden.

Außer einem neuen Ansatz zur Fehlerbehandlung, stellt Zig defer und errdefer zur Verfügung, um alle Ressourcenverwaltung – nicht nur Speicher – einfach und leicht verifizierbar zu machen.

Für Beispiele von defer siehe Integration mit C-Bibliotheken ohne FFI/Bindings. Hier ist ein Beispiel von 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={d})", id);
        errdefer allocator.free(device.name);

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

        return device;
    }
};

Ein neuer Ansatz zur Fehlerbehandlung

Fehler sind Werte, und können nicht ignoriert werden:

discard.zig

const std = @import("std");

pub fn main() void {
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
}
$ zig build-exe discard.zig
doctest-071e6909/discard.zig:4:30: error: error is discarded
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
doctest-071e6909/discard.zig:4:30: note: consider using 'try', 'catch', or 'if'
referenced by:
    callMain: zig/lib/std/start.zig:501:17
    callMainWithArgs: zig/lib/std/start.zig:469:12
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

Fehler können mit catch verarbeitet werden:

catch.zig

const std = @import("std");

pub fn main() void {
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch |err| label: {
        std.debug.print("unable to open file: {}\n", .{err});
        const stderr = std.io.getStdErr();
        break :label stderr;
    };
    file.writeAll("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

Das Schlüsselwort try ist eine Abkürzung für catch |err| return err:

try.zig

const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
    defer file.close();
    try file.writeAll("all your codebase are belong to us\n");
}
$ zig build-exe try.zig
$ ./try
error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/posix.zig:1764:23: 0x10664d2 in openatZ (try)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:846:16: 0x1036eb7 in openFileZ (try)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:801:5: 0x1033a0e in openFile (try)
    return self.openFileZ(&path_c, flags);
    ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e57b440c/try.zig:4:18: 0x1033858 in main (try)
    const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
                 ^

Bemerke, dass das ein Error Return Trace ist, kein Stacktrace. Der Code hat nicht den Preis einer Stackabwicklung gezahlt, um den Trace zu erhalten.

Das switch-Schlüsselwort, benutzt mit einem Fehlerwert, stellt sicher, dass alle möglichen Fehler behandelt werden:

test.zig

const std = @import("std");

test "switch on error" {
    _ = 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
doctest-eee53895/test.zig:4:40: error: switch must handle all possibilities
    _ = parseInt("hi", 10) catch |err| switch (err) {};
                                       ^~~~~~~~~~~~~~~
doctest-eee53895/test.zig:4:40: note: unhandled error value: 'error.InvalidCharacter'
doctest-eee53895/test.zig:4:40: note: unhandled error value: 'error.DigitExceedsRadix'
doctest-eee53895/test.zig:4:40: note: unhandled error value: 'error.Overflow'

Das Schlüsselwort unreachable kann genutzt werden, um zu versichern, dass kein Fehler auftreten kann:

unreachable.zig

const std = @import("std");

pub fn main() void {
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable;
    file.writeAll("all your codebase are belong to us\n") catch unreachable;
}
$ zig build-exe unreachable.zig
$ ./unreachable
thread 3045 panic: attempt to unwrap error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/posix.zig:1764:23: 0x1069412 in openatZ (unreachable)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:846:16: 0x1038697 in openFileZ (unreachable)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:801:5: 0x1035e6e in openFile (unreachable)
    return self.openFileZ(&path_c, flags);
    ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-1b0f2130/unreachable.zig:4:77: 0x1033c2f in main (unreachable)
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable;
                                                                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x1033439 in posixCallMainAndExit (unreachable)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:253:5: 0x1032fa1 in _start (unreachable)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
(process terminated by signal)

Das führt in den ungesicherten Buildmodi zu undefiniertem Verhalten und sollte nur genutzt werden, wenn der Erfolg wirklich garantiert ist.

Stacktraces auf allen Targets

Die Stacktraces und Error Return Trace auf dieser Seite funktionieren für alle Targets mit Tier 1 Support und einige mit Tier 2 Support. Sogar Freestanding!

Außerdem kann die Standardbibliothek an jedem Punkt einen Stacktrace aufzeichnen und später ausgeben:

stack_traces.zig

const std = @import("std");

var address_buffer: [8]usize = undefined;

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

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

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

    std.debug.print("first one:\n", .{});
    std.debug.dumpStackTrace(trace1);
    std.debug.print("\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/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:356:29: 0x1038327 in captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e7eb0ab0/stack_traces.zig:26:32: 0x1035d8c in foo (stack_traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e7eb0ab0/stack_traces.zig:16:8: 0x1033d08 in main (stack_traces)
    foo();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x10335b9 in posixCallMainAndExit (stack_traces)
            root.main();
                     ^


second one:
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:356:29: 0x1038327 in captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e7eb0ab0/stack_traces.zig:30:32: 0x1035dac in bar (stack_traces)
    std.debug.captureStackTrace(null, &trace2);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e7eb0ab0/stack_traces.zig:17:8: 0x1033d0d in main (stack_traces)
    bar();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x10335b9 in posixCallMainAndExit (stack_traces)
            root.main();
                     ^

Diese Technik wird für den (in Entwicklung befindlichen) GeneralPurposeDebugAllocator verwendet.

Generische Datenstrukturen und Funktionen

Typen sind Werte, die zur Compilezeit bekannt sein müssen:

types.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 types.zig
1/1 types.test.types are values... OK
All 1 tests passed.

Eine generische Datenstruktur ist einfach eine Funktion, die einen type zurückgibt:

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,
    };
    list.items[0] = 1234;
    list.len += 1;

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

Introspektion und Codeausführung zur Compilezeit

Die Builtinfunktion @typeInfo erlaubt Introspektion:

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(T);
    inline for (info.Struct.fields) |field| {
        std.debug.print(
            "{s} has a field called {s} with type {s}\n",
            .{
                @typeName(T),
                field.name,
                @typeName(field.type),
            },
        );
    }
}
$ zig build-exe reflection.zig
$ ./reflection
reflection.Header has a field called magic with type u32
reflection.Header has a field called name with type []const u8

Die Standardbibliothek benutzt diese Technik, um formatierte Ausgabe zu implementieren. Obwohl es eine kleine, einfache Sprache ist, wurde die formatierte Ausgabe vollständig in Zig programmiert. Währenddessen sind die Compilierfehler für printf in den C-Compiler und das Formatierungsmakro in den Rust-Compiler hartkodiert.

Zig kann auch Funktionen und Codeblöcke zur Compilezeit auswerten. In einigen Kontexten, wie der Initialisierung von globalen Variablen, geschieht dies implizit. Andernfalls kann Code mit dem Schlüsselwort comptime explizit zur Compilezeit ausgewertet werden. In Kombination mit Assertions ist dies ein mächtiges Werkzeug:

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;

    @memset(&array, 42);

    comptime {
        assert(array.len == 12345);
    }
}
$ zig test test.zig
zig/lib/std/debug.zig:403:14: error: reached unreachable code
    if (!ok) unreachable; // assertion failure
             ^~~~~~~~~~~
doctest-a9224f98/test.zig:15:15: note: called from here
        assert(array.len == 12345);
        ~~~~~~^~~~~~~~~~~~~~~~~~~~

Integration mit C-Bibliotheken ohne FFI/Bindings

@cImport importiert direkt Typen, Variablen, Funktionen und einfache Makros aus C-Bibliotheken und kann sogar Funktionen von C nach Zig übersetzen.

Hier ist ein Beispiel, das mit libsoundio ein Sinuswelle ausgibt:

sine.zig

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

fn sio_err(err: c_int) !void {
    switch (err) {
        c.SoundIoErrorNone => {},
        c.SoundIoErrorNoMem => return error.NoMem,
        c.SoundIoErrorInitAudioBackend => return error.InitAudioBackend,
        c.SoundIoErrorSystemResources => return error.SystemResources,
        c.SoundIoErrorOpeningDevice => return error.OpeningDevice,
        c.SoundIoErrorNoSuchDevice => return error.NoSuchDevice,
        c.SoundIoErrorInvalid => return error.Invalid,
        c.SoundIoErrorBackendUnavailable => return error.BackendUnavailable,
        c.SoundIoErrorStreaming => return error.Streaming,
        c.SoundIoErrorIncompatibleDevice => return error.IncompatibleDevice,
        c.SoundIoErrorNoSuchClient => return error.NoSuchClient,
        c.SoundIoErrorIncompatibleBackend => return error.IncompatibleBackend,
        c.SoundIoErrorBackendDisconnected => return error.BackendDisconnected,
        c.SoundIoErrorInterrupted => return error.Interrupted,
        c.SoundIoErrorUnderflow => return error.Underflow,
        c.SoundIoErrorEncodingString => return error.EncodingString,
        else => return error.Unknown,
    }
}

var seconds_offset: f32 = 0;

fn write_callback(
    maybe_outstream: ?[*]c.SoundIoOutStream,
    frame_count_min: c_int,
    frame_count_max: c_int,
) callconv(.C) void {
    _ = frame_count_min;
    const outstream: *c.SoundIoOutStream = &maybe_outstream.?[0];
    const layout = &outstream.layout;
    const float_sample_rate: f32 = @floatFromInt(outstream.sample_rate);
    const seconds_per_frame = 1.0 / 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(&areas),
            &frame_count,
        )) catch |err| std.debug.panic("write failed: {s}", .{@errorName(err)});

        if (frame_count == 0) break;

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

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

        frames_left -= frame_count;
    }
}

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

    try sio_err(c.soundio_connect(soundio));

    c.soundio_flush_events(soundio);

    const default_output_index = c.soundio_default_output_device_index(soundio);
    if (default_output_index < 0) return error.NoOutputDeviceFound;

    const device = c.soundio_get_output_device(soundio, default_output_index) orelse return error.OutOfMemory;
    defer c.soundio_device_unref(device);

    std.debug.print("Output device: {s}\n", .{device.*.name});

    const outstream = c.soundio_outstream_create(device) orelse return error.OutOfMemory;
    defer c.soundio_outstream_destroy(outstream);

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

    try sio_err(c.soundio_outstream_open(outstream));

    try sio_err(c.soundio_outstream_start(outstream));

    while (true) c.soundio_wait_events(soundio);
}

$ zig build-exe sine.zig -lsoundio -lc
$ ./sine
Output device: Built-in Audio Analog Stereo
^C

Dieser Zig-Code ist signifikant einfacher als der äquivalente C-Code, hat mehr Sicherheitsvorkehrungen, und all das wird mit einem einfache @cImport der C-Headerdateien erreicht – ohne API-Bindings.

Zig kann C-Bibliotheken besser nutzen als das C selbst kann.

Zig ist auch ein C-Compiler

Hier ist ein Beispiel dafür, wie Zig C-Code kompiliert:

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

Die Flag --verbose-cc zeigt, welcher C-Compiler-Befehl ausgeführt wird:

$ 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

Wird der Befehl erneut ausgeführt, bricht er sofort ab, ohne den Compiler aufzurufen:

$ time zig build-exe hello.c --library c --verbose-cc

real	0m0.027s
user	0m0.018s
sys	0m0.009s

Das geschieht dank dem Build Artifact Caching. Zig parst automatisch die erzeugte Depfile und benutzt ein robustes Cachesystem, um redundante Arbeit zu vermeiden.

Nicht nur kann Zig C kompilieren, sondern es gibt auch einen sehr guten Grund, Zig als C-Compiler zu nutzen: Zig enthält libc.

Export von Funktionen, Variablen und Typen für C-Code

Ein primärer Einsatzfall für Zig ist es, eine Bibliothek mit C ABI zu exportieren, die von anderen Sprachen genutzt werden kann. Das Schlüsselwort export vor Funktionen, Variablen und Typen macht sie zu einem Teil der API der Bibliothek:

mathtest.zig

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

Um eine statische Bibliothek zu erzeugen:

$ zig build-lib mathtest.zig

Um eine dynamische Bibliothek zu erzeugen:

$ zig build-lib mathtest.zig -dynamic

Hier ein Beispiel des Zig-Buildsystems:

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

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);
}

$ zig build test
1379

Crosscompiling ist ein primärer Einsatzfall

Zig kann für jedes der Targets im Support Table mit Tier 3 Support oder besser kompilieren. Dazu muss keine “cross toolchain” oder Ähnliches muss installiert werden. Hier ist ein natives Hello World:

hello.zig

const std = @import("std");

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

Und jetzt Builds für x86_64-windows, x86_64-macos und 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

Das funktioniert auf jedem Target mit Tier 3+, für jedes Target mit Tier 3+.

Zig enthält libc

zig targets listet unter anderem die verfügbaren libc-Targets auf:

...
 "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",
  "i386-linux-gnu",
  "i386-linux-musl",
  "i386-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_64-linux-gnu",
  "x86_64-linux-gnux32",
  "x86_64-linux-musl",
  "x86_64-windows-gnu"
 ],

Das bedeutet, dass --library c für diese Targets von keinerlei Systemdateien abhängt!

Sehen wir uns wieder das Hello World-Beispiel in C an:

$ 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)

Im Gegensatz zu glibc unterstützt musl statische Kompilierung:

$ zig build-exe hello.c --library c -target x86_64-linux-musl
$ ./hello
Hello world
$ ldd hello
  not a dynamic executable

In diesem Beispiel hat Zig musls Quellcode kompiliert und verlinkt. Dank dem Cachesystem bleibt das Build von musl-libc für x86_64-linux verfügbar und kann bei Bedarf sofort genutzt werden.

Das bedeutet, dass die Funktionalität auf jeder Plattform verfügbar ist. Nutzer von Windows und macOS können Zig- und C-Code für jedes der obigen Targets kompilieren und gegen libc linken. Auf ähnliche Art und Weise kann Code für andere Prozessorarchitekturen crosskompiliert werden:

$ 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 mancher Hinsicht kann Zig besser C kompilieren als C-Compiler!

Diese Funktionalität ist mehr als eine mit Zig gebündelte Cross-Toolchain. Zum Beispiel sind die libc-Header, die Zig mitbringt, unkomprimiert 22 MiB groß. Dabei kommen die Header für musl libc + Linux auf x86_64 alleine auf 8 MiB, und die Header für glibc machen 3.1 MiB aus (glibc fehlen die Linux-Header), aber Zig enthält momentan 40 libcs. Ohne Bündelung wären das 444 MiB. Dank meines Tools process_headers jedoch, und ein wenig guter alter manueller Arbeit, bleiben Zigs binäre Tarballs bei insgesamt rund 30 MiB, trotz Unterstützung für libc auf all diesen Targets, sowie compiler-rt, libunwind und libcxx, und dem Clang-kompatiblen C-Compiler. Zum Vergleich: das Build von clang 8.0.0 von llvm.org für Windows ist 132 MiB groß.

Beachte, dass nur die Targets im Tier 1 Support ausführlich getestet wurden. Es ist geplant, mehr libcs hinzuzufügen (auch für Windows), und Testabdeckung für Builds gegen alle libcs zu erreichen.

Ein Paketmanager für Zig ist geplant, aber noch nicht fertig. Damit soll es möglich werden, Pakete für C-Bibliotheken zu erstellen. Dies würde das Zigs Buildsystem für Zig- und C-Programmierer gleichermaßen attraktiv machen.

Zigs Buildsystem

Zig enthält ein Buildsystem, das make, cmake oder Ähnliches ersetzen kann.

$ 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.print("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);
}

Schauen wir uns das Menü --help an:

$ 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

Einer der verfügbaren Steps ist es, den Code direkt auszuführen (run):

$ zig build run
All your base are belong to us.

Hier sind einige Buildscripts als Beispiel:

Parallelität mit async-Funktionen

Zig 0.5.0 hat async-Funktionen eingeführt. Dieses Feature ist nicht vom Hostsystem oder Heapspeicher abhängig. Das bedeutet, dass async-Funktionen auch auf dem Freestanding-Target verfügbar sind.

Zig leitet automatisch ab, ob eine Funktion async ist, und erlaubt async/await auf nicht-async-Funktionen, was Zig-Bibliotheken agnostisch gegenüber blockierendem oder asynchronen I/O macht. Zig vermeidet Funktionenfarben.

Die Standardbibliothek implementiert eine Eventschleife, die async-Funktionen über einen Threadpool verteilt und damit N:M-Concurrency erlaubt. An den Bereichen der Multithreading-Sicherheit und Erkennung von Race-Conditions wird aktiv gearbeitet.

Breite Menge an unterstützten Targets

Zig kommuniziert die Unterstützung von verschiedenen Targets mit “Support Tiers”. Beachte, dass die Anforderungen für Tier 1 Support hoch sind – Tier 2 Support ist immer noch ziemlich nützlich.

Support Table

FreestandingLinux 3.16+macOS 10.13+Windows 8.1+FreeBSD 12.0+NetBSD 8.0+DragonFly​BSD 5.8+UEFI
x86_64Tier 1Tier 1Tier 1Tier 2Tier 2Tier 2Tier 2Tier 2
arm64Tier 1Tier 2Tier 2Tier 3Tier 3Tier 3N/ATier 3
arm32Tier 1Tier 2N/ATier 3Tier 3Tier 3N/ATier 3
mips32 LETier 1Tier 2N/AN/ATier 3Tier 3N/AN/A
i386Tier 1Tier 2Tier 4Tier 2Tier 3Tier 3N/ATier 2
riscv64Tier 1Tier 2N/AN/ATier 3Tier 3N/ATier 3
bpfTier 3Tier 3N/AN/ATier 3Tier 3N/AN/A
hexagonTier 3Tier 3N/AN/ATier 3Tier 3N/AN/A
mips32 BETier 3Tier 3N/AN/ATier 3Tier 3N/AN/A
mips64Tier 3Tier 3N/AN/ATier 3Tier 3N/AN/A
amdgcnTier 3Tier 3N/AN/ATier 3Tier 3N/AN/A
sparcTier 3Tier 3N/AN/ATier 3Tier 3N/AN/A
s390xTier 3Tier 3N/AN/ATier 3Tier 3N/AN/A
lanaiTier 3Tier 3N/AN/ATier 3Tier 3N/AN/A
powerpc32Tier 3Tier 3Tier 4N/ATier 3Tier 3N/AN/A
powerpc64Tier 3Tier 3Tier 4N/ATier 3Tier 3N/AN/A
avrTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
riscv32Tier 4Tier 4N/AN/ATier 4Tier 4N/ATier 4
xcoreTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
nvptxTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
msp430Tier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
r600Tier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
arcTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
tceTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
leTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
amdilTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
hsailTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
spirTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
kalimbaTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
shaveTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A
renderscriptTier 4Tier 4N/AN/ATier 4Tier 4N/AN/A

WebAssembly Support Table

FreestandingemscriptenWASI
wasm32Tier 1Tier 3Tier 1
wasm64Tier 4Tier 4Tier 4

Tier-System

Tier 1 Support

Tier 2 Support

Tier 3 Support

Tier 4 Support

Einfache Unterstützung von Paketen

Der Compiler ist noch nicht vollständig selbst-gehostet, aber es bleiben unter allen Umständen höchstens drei Schritte, um von einem System-C++-Compiler aus einen eigenständigen Zig-Compiler für jedes Target zu erreichen. Wie Maya Rashish anmerkt, lässt sich Zig angenehm und schnell auf andere Plattformen portieren.

Nicht-Debug-Buildmodi sind reproduzierbar/deterministisch.

Es gibt eine JSON-Version der Downloadseite.

Einige Mitglieder des Teams haben Erfahrung im Verwalten von Paketen.