Vue d'ensemble du projet Zig

Fonctionnalités

Un langage simple

L’effort doit se faire sur la correction de votre application plutôt que sur la connaissance du langage.

La syntaxe de Zig est entièrement spécifiée dans 500 lignes de grammaire PEG (EN).

Rien n’est caché : ni flots de contrôle, ni allocations de mémoire. Zig n’a pas de préprocesseur ni de macros. Si un code en Zig ne semble pas faire appel à une fonction, c’est qu’il ne le fait pas. Vous pouvez donc être sûr que le code suivant ne fait appel qu’à foo() puis bar(), sans même connaître les types impliqués :

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

Exemples de flots de contrôle cachés :

Zig promeut la maintenance et la lisibilité du code : tout flot de contrôle est géré exclusivement avec des mots clés du langage et des appels de fonction.

Performance ET sécurité

Zig a quatre modes de compilation (EN), et ils peuvent être combinés jusqu’à une granularité aussi fine que le bloc de code (EN).

Mode de compilationDebugReleaseSafeReleaseFastReleaseSmall
Optimisations : + vitesse d’exécution, - détection d’erreurs, - durée de compilation-O3-O3-Os
Vérifications à l’exécution : - vitesse d’exécution, - taille, + plantage si comportement indéfiniOnOn

(Note : '+' indique un avantage, '-' indique un inconvénient.)

Voici ce à quoi ressemble un dépassement d’entier (EN) à la compilation, peu importe le mode de compilation :

test.zig

test "integer overflow at compile time" {
    const x: u8 = 255;
    _ = x + 1;
}
$ zig test test.zig
./doctest-14ad65ca/test.zig:3:11: error: operation caused overflow
    _ = x + 1;
          ^

Voici ce à quoi ressemble l’exécution avec une compilation avec des vérifications de sécurité :

test.zig

test "integer overflow at runtime" {
    var x: u8 = 255;
    x += 1;
}
$ zig test test.zig
Test [1/1] test "integer overflow at runtime"... thread 3336 panic: integer overflow
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-8fcba8d8/test.zig:3:7: 0x20683f in test "integer overflow at runtime" (test)
    x += 1;
      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/special/test_runner.zig:77:28: 0x22ccf2 in std.special.main (test)
        } else test_fn.func();
                           ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:517:22: 0x22522c in std.start.callMain (test)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:469:12: 0x2092de in std.start.callMainWithArgs (test)
    return @call(.{ .modifier = .always_inline }, callMain, .{});
           ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:383:17: 0x208366 in std.start.posixCallMainAndExit (test)
    std.os.exit(@call(.{ .modifier = .always_inline }, callMainWithArgs, .{ argc, argv, envp }));
                ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:296:5: 0x208172 in std.start._start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^
error: the following test command crashed:
doctest-8fcba8d8/zig-cache/o/9638dfc29dc05b30774601ed885b4c9f/test /home/runner/work/www.ziglang.org/www.ziglang.org/zig/zig

Ces traces d’appels fonctionnent sur toutes les cibles, et également en « freestanding » (binaire autonome, sans système d’exploitation) (EN).

Zig permet de compiler son programme avec des vérifications à l’exécution, tout en les désactivant seulement où les performances sont trop impactées. L’exemple précédent pourrait être modifié comme cela :

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

Zig détecte les comportements indéfinis (EN) à la compilation pour la prévention d’erreurs et l’amélioration des performances.

En parlant de performances, Zig est plus rapide que le C.

Important à noter, Zig n’est pas à considérer comme un langage sécurisé. Pour ceux qui sont intéressés par suivre les histoires de sécurité dans Zig, abonnez-vous à ces fils de discussion :

Zig est en compétition avec le C, il n’en dépend pas

La bibliothèque standard de Zig intègre la libc, mais n’en dépend pas. Voici un 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!

Quand ce code est compilé avec -O ReleaseSmall, les symboles de debug retirés, sur un seul fil d’exécution, cela produit un binaire statique de 9.8 KiB pour la cible x86_64 :

$ zig build-exe hello.zig --release-small --strip --single-threaded
$ wc -c hello
9944 hello
$ ldd hello
  not a dynamic executable

Le binaire produit pour Windows est encore plus petit, seulement 4096 octets :

$ 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

Déclarations de premier niveau indépendantes de l’ordre

Les déclarations de premier niveau, comme les variables globales, sont indépendantes de l’ordre dans lequel elles sont écrites et leur analyse est paresseuse. Les valeurs d’initialisation des variables globales sont évaluées à la compilation.

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
Test [1/1] test "global variables"... 
All 1 tests passed.

Type optionnel plutôt que des pointeurs null

Dans d’autres langages de programmation, les références null sont sources d’erreurs à l’exécution, et sont même soupçonnées être la pire erreur en informatique (EN).

Les pointeurs en Zig ne peuvent pas être null :

test "null @intToPtr" {
    _ = @intToPtr(*i32, 0x0);
}
$ zig test test.zig
./doctest-eb2f229b/test.zig:2:9: error: pointer type '*i32' does not allow address zero
    _ = @intToPtr(*i32, 0x0);
        ^

Cependant, tout type peut devenir un type optionnel (EN) en le préfixant par ?:

optional_syntax.zig

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

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

Pour récupérer la valeur d’un type optionnel, nous devons utiliser orelse pour fournir une valeur par défaut :

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

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

Une autre option est d’utiliser un if :

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

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

    // do some stuff
}

Cette syntaxe fonctionne également avec while (EN):

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

Gestion manuelle de la mémoire

Une bibliothèque écrite en Zig peut être utilisée n’importe où :

Pour accomplir tout cela, les développeurs de Zig doivent gérer la mémoire et les erreurs d’allocation.

Cela est vrai également pour la bibliothèque standard de Zig. Chaque fonction nécessitant d’allouer de la mémoire accepte un allocateur en paramètre. Par conséquent, la bibliothèque standard de Zig peut être utilisée même pour un binaire « freestanding » (application autonome, sans système d’exploitation).

En plus d’apporter un point de vue nouveau sur la gestion d’erreurs, Zig fournit defer (EN) et errdefer (EN) pour rendre la gestion de toutes les ressources plus simple et facilement vérifiable (pas seulement la mémoire).

Pour un exemple de defer, voir l’intégration des bibliothèques C sans FFI/bindings. Voici un exemple de code utilisant 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;
    }
};

Une nouvelle manière de gérer les erreurs

Les erreurs sont des valeurs, et ne peuvent pas être ignorées :

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-8708ba50/discard.zig:4:30: error: error is discarded. consider using `try`, `catch`, or `if`
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
                             ^

Les erreurs peuvent être gérées avec catch (EN):

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: {e}\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

Le mot clé try (EN) est un raccourci pour 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/os.zig:1384:23: 0x224efd in std.os.openatZ (try)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs.zig:999:13: 0x20c67b in std.fs.Dir.openFileZ (try)
            try os.openatZ(self.fd, sub_path, os_flags, 0);
            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs.zig:930:9: 0x20a511 in std.fs.Dir.openFile (try)
        return self.openFileZ(&path_c, flags);
        ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-294604f1/try.zig:4:18: 0x229a55 in main (try)
    const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
                 ^

À noter que ceci est une trace de retour d’erreur (EN), pas une trace d’une pile d’exécution. Le code n’a pas à payer le coût du déroulement d’une pile d’exécution pour arriver à cette trace.

Le mot clé switch (EN) utilisé sur une erreur assure que toutes les erreurs possibles sont gérées :

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-e1899c0a/test.zig:4:40: error: error.Overflow not handled in switch
    _ = parseInt("hi", 10) catch |err| switch (err) {};
                                       ^
./doctest-e1899c0a/test.zig:3:24: note: referenced here
test "switch on error" {
                       ^
./doctest-e1899c0a/test.zig:4:40: error: error.DigitExceedsRadix not handled in switch
    _ = parseInt("hi", 10) catch |err| switch (err) {};
                                       ^
./doctest-e1899c0a/test.zig:4:40: error: error.InvalidCharacter not handled in switch
    _ = parseInt("hi", 10) catch |err| switch (err) {};
                                       ^

Le mot clé unreachable (EN) est utilisé pour affirmer qu’aucune erreur ne peut survenir :

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 3427 panic: attempt to unwrap error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/os.zig:1384:23: 0x224d4d in std.os.openatZ (unreachable)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs.zig:999:13: 0x20c5eb in std.fs.Dir.openFileZ (unreachable)
            try os.openatZ(self.fd, sub_path, os_flags, 0);
            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs.zig:930:9: 0x20a481 in std.fs.Dir.openFile (unreachable)
        return self.openFileZ(&path_c, flags);
        ^
???:?:?: 0x20bde2 in ??? (???)
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-adc9fa03/unreachable.zig:4:71: 0x2298e2 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:517:22: 0x22210c in std.start.callMain (unreachable)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:469:12: 0x20619e in std.start.callMainWithArgs (unreachable)
    return @call(.{ .modifier = .always_inline }, callMain, .{});
           ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:383:17: 0x205226 in std.start.posixCallMainAndExit (unreachable)
    std.os.exit(@call(.{ .modifier = .always_inline }, callMainWithArgs, .{ argc, argv, envp }));
                ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:296:5: 0x205032 in std.start._start (unreachable)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^
(process terminated by signal)

Cela implique un comportement indéfini dans les modes de compilation non sûrs, donc assurez-vous de l’utiliser uniquement quand le succès est garanti.

Traces de pile d’exécution sur toutes les cibles

Les traces de piles d’exécution et les traces de retour d’erreurs (EN) montrées sur cette page fonctionnent sur toutes les cibles ayant une prise en charge de niveau 1 et certaines cibles de niveau 2. Y compris freestanding (EN) !

De plus, la bibliothèque standard a la possibilité de capturer une trace d’exécution et de l’afficher plus tard :

stack_traces.zig

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

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:186:29: 0x2322cc in std.debug.captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-d2d25fcf/stack_traces.zig:27:32: 0x2308ac in foo (stack_traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-d2d25fcf/stack_traces.zig:17:8: 0x229878 in main (stack_traces)
    foo();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:517:22: 0x22212c in std.start.callMain (stack_traces)
            root.main();
                     ^


second one:
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:186:29: 0x2322cc in std.debug.captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-d2d25fcf/stack_traces.zig:31:32: 0x2308cc in bar (stack_traces)
    std.debug.captureStackTrace(null, &trace2);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-d2d25fcf/stack_traces.zig:18:8: 0x22987d in main (stack_traces)
    bar();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:517:22: 0x22212c in std.start.callMain (stack_traces)
            root.main();
                     ^

Vous pouvez voir cette technique utilisée dans le projet actuel d'allocateur générique (EN).

Structures de données et fonctions génériques

Les types sont des valeurs qui doivent être connues à la compilation :

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
Test [1/1] test "types are values"... 
All 1 tests passed.

Une structure de données générique est simplement une fonction qui retourne un 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.print("{d}\n", .{list.items.len});
}
$ zig build-exe generics.zig
$ ./generics
10

Réflexivité et exécution de code à la compilation

La fonction intégrée @typeInfo (EN) fournit de la réflexivité :

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

La bibliothèque standard de Zig utilise cette technique pour implémenter l’affichage formaté. Malgré la volonté de faire de Zig un langage simple, l’affichage formaté est entièrement implémentée en Zig. Pendant ce temps, en C, les erreurs de printf à la compilation sont codées en dur dans le compilateur. De façon similaire, en Rust, les macros de formatage sont codées en dur dans le compilateur.

Zig peut également évaluer les fonctions et les blocs de code à la compilation. Dans certains contextes, comme l’initialisation des variables globales, l’expression est implicitement évaluée à la compilation. Autrement, il est également possible d’évaluer explicitement le code à la compilation en utilisant le mot clé comptime (EN). Cela est particulièrement utile une fois combiné avec des 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
./zig/lib/std/debug.zig:226:14: error: reached unreachable code
    if (!ok) unreachable; // assertion failure
             ^
./doctest-2dbfc381/test.zig:13:15: note: called from here
        assert(array.len == 12345);
              ^
./doctest-2dbfc381/test.zig:9:32: note: called from here
test "compile-time evaluation" {
                               ^
./doctest-2dbfc381/test.zig:13:15: note: referenced here
        assert(array.len == 12345);
              ^

Intégration avec les bibliothèques C sans FFI/bindings

@cImport (EN) importe directement les types, variables, fonctions et les macros simples en Zig. Ce mot clé peut même traduire les fonctions inline du C à Zig.

Voici un exemple de diffusion d’une onde sinusoïdale via la bibliothèque libsoundio :

sine.zig

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

fn sio_err(err: c_int) !void {
    switch (@intToEnum(c.SoundIoError, err)) {
        .None => {},
        .NoMem => return error.NoMem,
        .InitAudioBackend => return error.InitAudioBackend,
        .SystemResources => return error.SystemResources,
        .OpeningDevice => return error.OpeningDevice,
        .NoSuchDevice => return error.NoSuchDevice,
        .Invalid => return error.Invalid,
        .BackendUnavailable => return error.BackendUnavailable,
        .Streaming => return error.Streaming,
        .IncompatibleDevice => return error.IncompatibleDevice,
        .NoSuchClient => return error.NoSuchClient,
        .IncompatibleBackend => return error.IncompatibleBackend,
        .BackendDisconnected => return error.BackendDisconnected,
        .Interrupted => return error.Interrupted,
        .Underflow => return error.Underflow,
        .EncodingString => 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 {
    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| 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 sample = std.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| 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 = @intToEnum(c.SoundIoFormat, 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

Ce code Zig est bien plus simple que son équivalent en C et est également plus sécurisé. Tout ceci est accompli en important le fichier d’en-tête C - aucune API n’est utilisée.

Zig est meilleur que le C à utiliser des bibliothèques… C.

Zig est également un compilateur C

Voici un exemple de code C compilé avec Zig :

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

Nous pouvons utiliser --verbose-cc pour voir la commande de compilation :

$ 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

À noter que si nous relançons la commande à nouveau, rien n’est affiché et la commande se termine instantanément :

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

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

Ceci est rendu possible grâce au cache de compilation (EN). Zig analyse les fichiers .d (générés grâce à Clang) et utilise un système de cache robuste pour recompiler seulement le nécessaire.

Zig peut compiler du code C, mais il y a une vraie bonne raison de l’utiliser comme tel : Zig est livré avec la libc.

Export de fonctions, variables, et de types pour du code C

Un des usages de Zig est d’exporter une bibliothèque avec l’ABI C pour d’autres langages de programmation. Le mot clé export devant des fonctions, des variables ou des types les intègre à l’API de la bibliothèque :

mathtest.zig

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

Pour créer une bibliothèque statique :

$ zig build-lib mathtest.zig

Pour créer une bibliothèque partagée :

$ zig build-lib mathtest.zig -dynamic

Voici un exemple avec le système de construction de Zig :

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

La cross-compilation est un usage de première importance

Zig peut compiler pour n’importe quelle cible du tableau prise en charge avec un niveau 3 ou mieux. Pas besoin d’installer une chaîne de compilation. Voici un simple 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!

Maintenant voici comment le compiler pour x86_64-windows, x86_64-macosx, et 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

Cela fonctionne sur toutes les cibles de niveau 3 ou plus, pour n’importe quelle cible de niveau 3 ou plus.

Zig fournit la libc

Vous pouvez trouver les cibles disponibles avec zig targets :

...
 "libc": [
  "aarch64_be-linux-gnu",
  "aarch64_be-linux-musl",
  "aarch64_be-windows-gnu",
  "aarch64-linux-gnu",
  "aarch64-linux-musl",
  "aarch64-windows-gnu",
  "armeb-linux-gnueabi",
  "armeb-linux-gnueabihf",
  "armeb-linux-musleabi",
  "armeb-linux-musleabihf",
  "armeb-windows-gnu",
  "arm-linux-gnueabi",
  "arm-linux-gnueabihf",
  "arm-linux-musleabi",
  "arm-linux-musleabihf",
  "arm-windows-gnu",
  "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"
 ],

Cela signifie que --library c pour ces cibles ne dépend d’aucun fichier du système !

Revoyons l'exemple de Hello World en C :

$ 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 (EN) ne prend pas en charge la compilation statique, mais musl (EN) oui :

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

Dans cet exemple, Zig compile la libc musl depuis les sources puis l’utilise pour notre application. Les fichiers de construction de cette bibliothèque restent disponibles pour de futures compilations (elle n’aura pas à être recompilée) grâce au système de cache (EN).

Cette fonctionnalité est disponible pour toutes les plateformes. Les utilisateurs de Windows et macOS peuvent compiler du code C et Zig, les lier à la libc, pour toutes les cibles listées au-dessus. De même, le code peut être compilé pour d’autres 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

D’une certaine façon, Zig est un meilleur compilateur C que les compilateurs C !

Cette fonctionnalité est plus qu’un outil pour créer une chaîne de compilation croisée. Par exemple, la taille totale des en-têtes de libc que Zig fournit est de 22 MiB sans compression. Pendant ce temps, seulement les en-têtes pour musl et linux pour x86_64 font déjà 8 MiB, et glibc fait déjà 3.1 MiB à lui seul (sans les en-têtes de linux). Pourtant, Zig est actuellement fourni avec 40 libc. Avec un paquetage naïf cela voudrait dire 444 MiB. Cependant, grâce à un outil de gestion d’en-têtes (EN), et à un travail manuel minutieux (EN), les archives de Zig restent autour de 30 MiB, malgré le support de toutes ces cibles, en plus des bibliothèques compiler-rt, libunwind et libcxx, et malgré le fait d’être un compilateur C compatible Clang. En comparaison, clang 8.0.0 seul pour Windows pèse 132 MiB.

À noter que seules les cibles de niveau 1 ont été testées en détail. Il est prévu d'ajouter d’autres libc (EN) (en incluant Windows), et d’ajouter des tests de couverture pour compiler vers toutes les architectures (EN).

Un gestionnaire de paquets Zig (EN) est prévu, mais il n’est pas encore là. Cela permettra de créer des paquets pour des bibliothèques C et rendra le système de construction de Zig attractif à la fois pour les développeurs C et Zig.

Système de construction de Zig

Zig est fourni avec un système de construction, rendant inutile make, cmake et les autres.

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

Regardons le menu --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

Comme nous pouvons le voir, une des commandes disponibles est run.

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

Voici quelques exemples de scripts de construction :

Concurrence avec les fonctions Async

Zig 0.5.0 a introduit les fonctions async (EN). Cette fonctionnalité n’a pas de dépendance au système d’exploitation hôte ou même à l’allocation de mémoire dans le tas. Cela veut dire que les fonctions async sont disponibles pour la cible « freestanding » (sans système d’exploitation).

Zig déduit si une fonction est async, et permet async/await sur des fonctions non async. Les bibliothèques Zig sont donc les mêmes avec des appels bloquants ou des entrées et sorties asynchrones. Zig évite la coloration des fonctions (EN).

La bibliothèque standard Zig implémente une boucle d’événements qui multiplexe les fonctions asynchrones dans un pool de fils d’exécution pour une concurrence de type M:N. La sécurité de multiples fils d’exécution et la détection de race condition sont des domaines de recherche actifs.

Une large variété de cibles est disponible

Zig a un système de « niveaux de prise en charge » pour communiquer autour des différentes cibles. À noter que la barre est haute pour atteindre le niveau 1 - la prise en charge de niveau 2 est déjà intéressante.

Prise en charge des systèmes

free standingLinux 3.16+macOS 10.13+Windows 8.1+FreeBSD 12.0+NetBSD 8.0+DragonFly​BSD 5.8+UEFI
x86_64Niveau 1Niveau 1Niveau 1Niveau 2Niveau 2Niveau 2Niveau 2Niveau 2
arm64Niveau 1Niveau 2Niveau 2Niveau 3Niveau 3Niveau 3N/ANiveau 3
arm32Niveau 1Niveau 2N/ANiveau 3Niveau 3Niveau 3N/ANiveau 3
mips32 LENiveau 1Niveau 2N/AN/ANiveau 3Niveau 3N/AN/A
i386Niveau 1Niveau 2Niveau 4Niveau 2Niveau 3Niveau 3N/ANiveau 2
riscv64Niveau 1Niveau 2N/AN/ANiveau 3Niveau 3N/ANiveau 3
bpfNiveau 3Niveau 3N/AN/ANiveau 3Niveau 3N/AN/A
hexagonNiveau 3Niveau 3N/AN/ANiveau 3Niveau 3N/AN/A
mips32 BENiveau 3Niveau 3N/AN/ANiveau 3Niveau 3N/AN/A
mips64Niveau 3Niveau 3N/AN/ANiveau 3Niveau 3N/AN/A
amdgcnNiveau 3Niveau 3N/AN/ANiveau 3Niveau 3N/AN/A
sparcNiveau 3Niveau 3N/AN/ANiveau 3Niveau 3N/AN/A
s390xNiveau 3Niveau 3N/AN/ANiveau 3Niveau 3N/AN/A
lanaiNiveau 3Niveau 3N/AN/ANiveau 3Niveau 3N/AN/A
powerpc32Niveau 3Niveau 3Niveau 4N/ANiveau 3Niveau 3N/AN/A
powerpc64Niveau 3Niveau 3Niveau 4N/ANiveau 3Niveau 3N/AN/A
avrNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
riscv32Niveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/ANiveau 4
xcoreNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
nvptxNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
msp430Niveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
r600Niveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
arcNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
tceNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
leNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
amdilNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
hsailNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
spirNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
kalimbaNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
shaveNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A
renderscriptNiveau 4Niveau 4N/AN/ANiveau 4Niveau 4N/AN/A

Prise en charge de WebAssembly

free standingemscriptenWASI
wasm32Niveau 1Niveau 3Niveau 1
wasm64Niveau 4Niveau 4Niveau 4

Niveaux de prise en charge

Prise en charge niveau 1

Prise en charge niveau 2

Prise en charge niveau 3

Prise en charge niveau 4

Agréable pour les mainteneurs de paquets

Le compilateur Zig de référence n’est pas complètement autonome pour le moment (il ne se compile pas lui-même). Mais peu importe, il ne reste exactement que 3 étapes (EN) pour avoir un système autonome pouvant compiler pour n’importe quelle cible et se débarrasser de la dépendance à un compilateur C++. Pour citer Maya Rashish : porter Zig sur d’autres plateformes est fun et rapide (EN).

Les modes de compilation sans debug (EN) sont reproductibles, déterministes.

Il y a une version JSON de la page de téléchargements.

Plusieurs membres de l’équipe derrière Zig ont de l’expérience dans le maintien de paquets.