← Zurück zu Lernen

Übersicht

Funktionshighlights

Kleine, einfache Sprache

Der Fokus liegt darauf die Anwendung zu debuggen, anstatt seine Kenntnisse der Programmiersprache zu debuggen.

Die gesamte Syntax von Zig wird mit einer 580-zeiligen PEG-Grammatikdatei angegeben.

Es gibt keinen versteckten Kontrollfluss, keine versteckten Speicherzuweisungen, keinen Präprozessor und keine Makros. Wenn es nicht so aussieht, als würde der Zig-Code verzweigen, um eine Funktion aufzurufen, dann tut er das auch nicht. Das bedeutet, dass man sicher sein kann, dass der folgende Code nur foo() und dann bar() aufruft, und das ist garantiert, ohne dass man die Typen von irgendetwas kennen müsste:

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

Beispiele versteckten Kontrollflusses:

Zig fördert die Code-Wartung und Lesbarkeit, indem der gesamte Kontrollfluss ausschließlich mit Sprachschlüsselwörtern und Funktionsaufrufen verwaltet wird.

Leistung und Sicherheit: Wähle zwei

Zig hat vier Build-Modi, und sie können alle bis zur Granularität des Geltungsbereichs gemischt und angepasst werden.

Parameter Debug ReleaseSafe ReleaseFast ReleaseSmall
Optimierungen – verbessern die Geschwindigkeit, beeinträchtigen das Debugging, beeinträchtigen die Kompilierzeit -O3 -O3 -Os
Laufzeitsicherheitsprüfungen – beeinträchtigen die Geschwindigkeit, beeinträchtigen die Größe, führen zu Abstürzen statt undefiniertem Verhalten On On

So sieht Integerüberlauf zur Kompilierungszeit aus, unabhängig vom Build-Modus:

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

So sieht es zur Laufzeit in sicherheitsgeprüften Builds aus:

2-integer-overflow-runtime.zig
test "integer overflow at runtime" {
    var x: u8 = 255;
    x += 1;
}
Shell
$ zig test 2-integer-overflow-runtime.zig
1/1 2-integer-overflow-runtime.test.integer overflow at runtime...thread 3030394 panic: integer overflow
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/2-integer-overflow-runtime.zig:3:7: 0x103cc2e in test.integer overflow at runtime (test)
    x += 1;
      ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/compiler/test_runner.zig:157:25: 0x1047f99 in mainTerminal (test)
        if (test_fn.func()) |_| {
                        ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/compiler/test_runner.zig:37:28: 0x103e01b in main (test)
        return mainTerminal();
                           ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:22: 0x103d159 in posixCallMainAndExit (test)
            root.main();
                     ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:266:5: 0x103ccc1 in _start (test)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/.zig-cache/o/9e3bfcb661c1ce3251fa44aa9886f390/test

Diese Stacktraces funktionieren auf allen Zielplatformen, einschließlich freistehender Systeme.

Mit Zig kann man sich auf einen sicherheitsaktivierten Build-Modus verlassen und die Sicherheit an Leistungsengpässen selektiv deaktivieren. Das vorherige Beispiel könnte beispielsweise wie folgt geändert werden:

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

Zig verwendet undefiniertes Verhalten als messerscharfes Werkzeug sowohl zur Fehlerprävention als auch zur Leistungssteigerung.

Apropos Leistung: Zig ist schneller als C.

– Die Referenzimplementierung verwendet LLVM als Backend für modernste Optimierungen.

Es gilt zu beachten, dass Zig keine völlig sichere Sprache ist. Wer die Sicherheitsgeschichte von Zig verfolgen möchte, kann diese Ausgaben abonnieren:

Zig konkurriert mit C, anstatt davon abhängig zu sein

Die Zig-Standardbibliothek ist in libc integriert, aber nicht davon abhängig. Hier ist "Hello World":

4-hello.zig
const std = @import("std");

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

Bei der Kompilierung mit -O ReleaseSmall, ohne Debugsymbole und im Single-Thread-Modus entsteht eine statische ausführbare Datei mit 9,8 KiB für das Ziel x86_64-linux:

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

Ein Windows-Build ist sogar noch kleiner und umfasst 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

Reihenfolgeunabhängige Top-Level-Deklarationen

Deklarationen auf oberster Ebene wie globale Variablen sind reihenfolgeunabhängig und werden verzögert analysiert. Die Initialisierungswerte globaler Variablen werden zur Kompilierzeit ausgewertet.

5-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;
Shell
$ zig test 5-global-variables.zig
1/1 5-global-variables.test.global variables...OK
All 1 tests passed.

Optionaler Typ anstelle von Nullzeigern

In anderen Programmiersprachen sind Nullzeiger (null pointer) die Quelle vieler Laufzeitausnahmen und werden sogar als der schlimmste Fehler der Informatik bezeichnet.

Unmodifizierte Zig-Zeiger können nicht NULL sein:

6-null-to-ptr.zig
test "null @intToPtr" {
    const foo: *i32 = @ptrFromInt(0x0);
    _ = foo;
}
Shell
$ zig test 6-null-to-ptr.zig
assets/zig-code/features/6-null-to-ptr.zig:2:35: error: pointer type '*i32' does not allow address zero

Allerdings kann jeder Typ zu einem optionalen Typ gemacht werden, indem man ihm ein ? voranstellt:

7-optional-syntax.zig
const std = @import("std");
const assert = std.debug.assert;

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

Um auf einen optionalen Wert zuzugreifen, kann man orelse verwenden, um einen Standardwert zu liefern:

8-optional-orelse.zig
// malloc prototype included for reference
extern fn malloc(size: size_t) ?*u8;

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

Eine weitere Möglichkeit ist die Verwendung von if:

9-optional-if.zig
fn doAThing(optional_foo: ?*Foo) void {
    // do some stuff

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

    // do some stuff
}

Der gleiche Ausdruck funktioniert mit while:

10-optional-while.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});
    }
}
Shell
$ zig build-exe 10-optional-while.zig
$ ./10-optional-while
hello
this
is
dog

Manuelle Speicherverwaltung

Eine in Zig geschriebene Bibliothek kann überall verwendet werden:

Um dies zu erreichen, müssen Zig-Programmierer ihren eigenen Speicher verwalten und Fehler bei der Speicherzuweisung beheben.

Das gilt auch für die Zig-Standardbibliothek. Alle Funktionen, die Speicher zuweisen müssen, akzeptieren einen Allocator-Parameter. Daher kann die Zig-Standardbibliothek auch für ein "freistehende Ziel" verwendet werden.

Zusätzlich zu Einem neuen Ansatz zur Fehlerbehandlung bietet Zig defer und errdefer, um die gesamte Ressourcenverwaltung – nicht nur die des Speichers – einfach und leicht überprüfbar zu machen.

Ein Beispiel für „defer“ findet man unter Integration mit C-Bibliotheken ohne FFI/Bindings. Hier ist ein Beispiel für die Verwendung von errdefer:

11-errdefer.zig
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 dürfen nicht ignoriert werden:

12-errors-as-values.zig
const std = @import("std");

pub fn main() void {
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
}
Shell
$ zig build-exe 12-errors-as-values.zig
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/12-errors-as-values.zig:4:30: error: error union is discarded
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/12-errors-as-values.zig:4:30: note: consider using 'try', 'catch', or 'if'
referenced by:
    callMain: /home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:17
    callMainWithArgs: /home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:482:12
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

Fehler können mit catch behandelt werden:

13-errors-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;
}
Shell
$ zig build-exe 13-errors-catch.zig
$ ./13-errors-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:

14-errors-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");
}
Shell
$ zig build-exe 14-errors-try.zig
$ ./14-errors-try
error: FileNotFound
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/posix.zig:1768:23: 0x1066a70 in openatZ (14-errors-try)
            .NOENT => return error.FileNotFound,
                      ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/fs/Dir.zig:880:16: 0x1038294 in openFileZ (14-errors-try)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/fs/Dir.zig:827:5: 0x1034d9e in openFile (14-errors-try)
    return self.openFileZ(&path_c, flags);
    ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/14-errors-try.zig:4:18: 0x1034be8 in main (14-errors-try)
    const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
                 ^

Es gilt zu beachten, dass es sich um einen Error-Return-Trace und nicht um einen Stack-Trace handelt. Der Code hat nicht den Preis für das Abwickeln des Stacks bezahlt, um diesen Trace zu erstellen.

Das bei einem Fehler verwendete Schlüsselwort switch stellt sicher, dass alle möglichen Fehler behandelt werden:

15-errors-switch.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;
}
Shell
$ zig build-exe 15-errors-switch.zig
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:509:45: error: root struct of file '15-errors-switch' has no member named 'main'
    switch (@typeInfo(@typeInfo(@TypeOf(root.main)).Fn.return_type.?)) {
                                        ~~~~^~~~~
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/15-errors-switch.zig:1:1: note: struct declared here
const std = @import("std");
^~~~~
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:482:20: note: called from here
    return callMain();
           ~~~~~~~~^~
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:438:36: note: called from here
    std.posix.exit(callMainWithArgs(argc, argv, envp));
                   ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~
referenced by:
    _start: /home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:351:40
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

Das Schlüsselwort unreachable wird verwendet, um sicherzustellen, dass keine Fehler auftreten:

16-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;
}
Shell
$ zig build-exe 16-unreachable.zig
$ ./16-unreachable
thread 3030318 panic: attempt to unwrap error: FileNotFound
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/posix.zig:1768:23: 0x10699b0 in openatZ (16-unreachable)
            .NOENT => return error.FileNotFound,
                      ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/fs/Dir.zig:880:16: 0x1039a94 in openFileZ (16-unreachable)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/fs/Dir.zig:827:5: 0x103723e in openFile (16-unreachable)
    return self.openFileZ(&path_c, flags);
    ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/16-unreachable.zig:4:77: 0x1034fcf in main (16-unreachable)
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable;
                                                                            ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:22: 0x10347d9 in posixCallMainAndExit (16-unreachable)
            root.main();
                     ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:266:5: 0x1034341 in _start (16-unreachable)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
(process terminated by signal)

Dies ruft undefiniertes Verhalten in den unsicheren Build-Modi auf. Man sollte es daher nur verwenden, wenn der Erfolg garantiert ist.

Stacktraces auf allen Zielen

Die auf dieser Seite angezeigten Stacktraces und Error-Return-Traces funktionieren auf allen Tier-1-Support- und einigen Tier-2-Support-Zielen. Sogar auf freistehenden Zielen!

Darüber hinaus verfügt die Standardbibliothek über die Möglichkeit, an jedem beliebigen Punkt einen Stacktrace zu erfassen und ihn später in die Standardfehlerausgabe zu übertragen:

17-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);
}
Shell
$ zig build-exe 17-stack-traces.zig
$ ./17-stack-traces
first one:
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/debug.zig:365:29: 0x10396e7 in captureStackTrace (17-stack-traces)
            addr.* = it.next() orelse {
                            ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/17-stack-traces.zig:26:32: 0x103717c in foo (17-stack-traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/17-stack-traces.zig:16:8: 0x10350c8 in main (17-stack-traces)
    foo();
       ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:22: 0x1034979 in posixCallMainAndExit (17-stack-traces)
            root.main();
                     ^


second one:
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/debug.zig:365:29: 0x10396e7 in captureStackTrace (17-stack-traces)
            addr.* = it.next() orelse {
                            ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/17-stack-traces.zig:30:32: 0x103719c in bar (17-stack-traces)
    std.debug.captureStackTrace(null, &trace2);
                               ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/17-stack-traces.zig:17:8: 0x10350cd in main (17-stack-traces)
    bar();
       ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:22: 0x1034979 in posixCallMainAndExit (17-stack-traces)
            root.main();
                     ^

Hier kann man sehen, wie diese Technik im laufenden GeneralPurposeDebugAllocator-Projekt verwendet wird.

Generische Datenstrukturen und Funktionen

Typen sind Werte, die zur Kompilierzeit bekannt sein müssen (comptime known):

18-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);
}
Shell
$ zig test 18-types.zig
1/1 18-types.test.types are values...OK
All 1 tests passed.

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

19-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});
}
Shell
$ zig build-exe 19-generics.zig
$ ./19-generics
10

Reflexion zur Kompilierungszeit und Codeausführung zur Kompilierungszeit

Die integrierte Funktion @typeInfo bietet Reflexion:

20-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),
            },
        );
    }
}
Shell
$ zig build-exe 20-reflection.zig
$ ./20-reflection
20-reflection.Header has a field called magic with type u32
20-reflection.Header has a field called name with type []const u8

Die Zig-Standardbibliothek verwendet diese Technik, um formatierte Ausgabe zu implementieren. Obwohl es sich um eine kleine, einfache Sprache handelt, ist die formatierte Ausgabe von Zig vollständig in Zig implementiert. In C hingegen sind Kompilierungsfehler für 'printf' fest im Compiler codiert. In Rust ist das Makro für formatierte Ausgabe ebenfalls fest im Compiler codiert.

Zig kann auch Funktionen und Codeblöcke zur Kompilierzeit auswerten. In einigen Kontexten, wie z. B. bei der Initialisierung globaler Variablen, wird der Ausdruck zur Kompilierzeit implizit ausgewertet. Andernfalls kann man Code zur Kompilierzeit explizit mit dem Schlüsselwort comptime auswerten. Das kann besonders leistungsfähig sein, wenn es mit 'Assertions' (Programmprüfungen) kombiniert wird:

21-comptime.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);
    }
}
Shell
$ zig test 21-comptime.zig
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/debug.zig:412:14: error: reached unreachable code
assets/zig-code/features/21-comptime.zig:15:15: note: called from here

Integration mit C-Bibliotheken ohne FFI/Bindings

@cImport importiert direkt Typen, Variablen, Funktionen und einfache Makros zur Verwendung in Zig. Es übersetzt sogar Inline-Funktionen von C in Zig.

Hier ist ein Beispiel für die Ausgabe einer Sinuswelle mit libsoundio:

sine.zig

22-sine-wave.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 wesentlich einfacher als der entsprechende C-Code und verfügt zudem über mehr Sicherheitsvorkehrungen. Dies alles wird durch den direkten Import der C-Headerdatei erreicht – keine API-Bindungen.

Zig kann C-Bibliotheken besser verwenden als C.

Zig ist auch ein C-Compiler

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

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

Mittels --verbose-cc kann man sehen, welcher C-Compilerbefehl ausgeführt wurde:

$ 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

Es gilt zu beachten, dass bei einer erneuten Ausführung des Befehls keine Ausgabe erfolgt und der Vorgang sofort beendet wird:

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

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

Das ist dem Build-Artifact-Caching zu verdanken. Zig analysiert die .d-Datei automatisch und verwendet ein robustes Caching-System, um doppelte Arbeit zu vermeiden.

Zig kann nicht nur C-Code kompilieren, sondern es gibt auch einen sehr guten Grund, Zig als C-Compiler zu verwenden: Zig wird mit libc ausgeliefert.

Export von Funktionen, Variablen und Typen, von denen C-Code abhängt

Einer der wichtigsten Anwendungsfälle für Zig ist das Exportieren einer Bibliothek mit der C-ABI, die von anderen Programmiersprachen aufgerufen werden kann. Das Schlüsselwort export vor Funktionen, Variablen und Typen bewirkt, dass diese Teil der Bibliotheks-API sind:

mathtest.zig

23-math-test.zig
export fn add(a: i32, b: i32) i32 {
    return a + b;
}

Um eine statische Bibliothek zu erstellen:

$ zig build-lib mathtest.zig

Um eine dynamische Bibliothek zu erstellen:

$ zig build-lib mathtest.zig -dynamic

Hier ein Beispiel mit dem 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

24-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

Cross-Kompilierung ist ein erstklassiger Anwendungsfall

Zig kann für alle Ziele aus der Support-Tabelle (see latest release notes) mit Tier-3-Support oder besser bauen. Es muss keine "Cross-Toolchain" oder ähnliches installiert werden. Hier ist ein natives "Hello World":

4-hello.zig
const std = @import("std");

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

Jetzt wird es für x86_64-Windows, x86_64-MacOS und aarch64-Linux erstellt:

$ 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

Dies funktioniert auf jedem Tier-3+-Ziel, für jedes Tier-3+-Ziel.

Zig wird mit libc ausgeliefert

Alle verfügbaren libc-Ziele kann man mit zig targets finden:

...
 "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 Ziele nicht von irgendwelchen Systemdateien abhängig ist!

Schauen wir uns das C-Hello-World-Beispiel noch einmal 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)

glibc unterstützt den statischen Build nicht, musl hingegen schon:

$ 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 die 'musl libc' aus dem Quellcode erstellt und dann damit verknüpft. Der Build der 'musl libc' für x86_64-Linux bleibt dank des Caching-Systems verfügbar, sodass diese 'libc' jederzeit sofort verfügbar ist, wenn sie erneut benötigt wird.

Das bedeutet, dass diese Funktionalität auf jeder Plattform verfügbar ist. Windows- und macOS-Benutzer können Zig- und C-Code erstellen und ihn mit 'libc' verknüpfen, für jedes der oben aufgeführten Ziele. Ebenso kann Code für andere Architekturen plattformübergreifend kompiliert 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 ist Zig ein besserer C-Compiler als andere C-Compiler!

Diese Funktionalität ist mehr als das Bündeln einer Cross-Compilation-Toolchain zusammen mit Zig. Beispielsweise beträgt die Gesamtgröße der von Zig mitgelieferten libc-Header unkomprimiert 22 MiB. Die Header für 'musl libc' + Linux-Header auf x86_64 allein sind dagegen 8 MiB groß und für 'glibc' 3,1 MiB (glibc fehlen die Linux-Header), obwohl Zig derzeit mit 40 'libcs' ausgeliefert wird. Bei einer einfachen Bündelung wären das 444 MiB. Dank des process-headers-Tools und einiger guter alter Handarbeit bleiben die Zig-Binär-Tarballs insgesamt etwa 30 MiB groß, obwohl sie 'libc' für alle diese Ziele sowie 'compiler-rt', 'libunwind' und 'libcxx' unterstützen und obwohl es sich um einen C-Compiler handelt, der mit Clang kompatibel ist. Zum Vergleich: Der Windows-Binär-Build von 'clang 8.0.0' selbst von llvm.org ist 132 MiB groß.

Es gilt zu beachten, dass nur die Tier-1 Support-Ziele gründlich getestet wurden. Es ist geplant, weitere 'libcs' hinzuzufügen (auch für Windows) und Testabdeckung für den Build mit allen 'libcs' hinzuzufügen.

Zig Build-System

Zig verfügt über ein Build-System, sodass man weder 'make' noch 'cmake' oder ähnliches benötigt.

$ zig init-exe
Created build.zig
Created src/main.zig

Next, try `zig build --help` or `zig build run`

src/main.zig

25-all-bases.zig
const std = @import("std");

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

build.zig

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

Werfen wir einen Blick auf das Menü mit --help.

$ 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

Man kann sehen, dass einer der verfügbaren Schritte ausgeführt wird.

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

Hier sind einige Beispiele für Build-Skripte:

Parallelität über asynchrone Funktionen

Der folgende Abschnitt wurde aus der Übersetzung entfernt, da Zig derzeit keine asynchrone Funktionen mehr unterstützt und es auch nicht klar ist, ob und wann es diese wieder geben wird, siehe: async/await/suspend/resume

Unterstützung einer großen Auswahl an Zielen

Zig verwendet ein "Support-Tier"-System, um den Grad der Unterstützung für verschiedene Ziele anzugeben.

Support Table as of Zig 0.11.0

Einfach für Paketbetreuer

Der Zig-Compiler ist vollständig selbstgehostet.

Nicht-Debug-Build-Modi sind reproduzierbar/deterministisch.

Es gibt eine JSON-Version der Download-Seite.

Mehrere Mitglieder des Zig-Teams haben Erfahrung in der Paketwartung.