← Torna a Impara

Panoramica

Caratteristiche di rilievo

Un linguaggio piccolo, semplice

Concentrati sul debuggare la tua applicazione, invece di debuggare la tua conoscenza del linguaggio di programmazione.

L'intera sintassi di Zig è specificata con un file di grammatica PEG di 580 righe.

Non c'è nessun controllo nascosto del flusso di esecuzione, nessuna allocazione dinamica nascosta, nessun preprocessore e nessuna macro. Se vedi del codice Zig e non sembra che stia chiamando una funzione, allora non lo sta facendo. Questo significa che il seguente codice chiamerà sicuramente solo foo() e poi bar(), e questo è garantito anche senza conoscere i tipi di dato utilizzati:

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

Esempi di controllo nascosto del flusso di esecuzione:

Zig promuove la manutenibilità e leggibilità del codice permettendo di gestire il flusso di esecuzione esclusivamente con le parole chiave del linguaggio e le chiamate di funzioni.

Prestazioni e sicurezza: scegline due

Zig ha quattro modalità di build, che possono essere mescolate tra di loro e specificare la modalità di qualunque blocco o ambito di visibilità.

Parametro Debug ReleaseSafe ReleaseFast ReleaseSmall
Ottimizzazioni - migliora prestazioni, peggiora debug e tempo di compilazione -O3 -O3 -Os
Controlli di sicurezza a runtime - peggiora prestazioni e dimensioni, crash invece di comportamenti non definiti (UB) On On

Ecco come l'overflow di interi appare in fase di compilazione, a prescindere dalla modalità di build:

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'

Ecco come appare durante l'esecuzione, nelle build con controlli di sicurezza attivi:

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

Questi stack trace funzionano su tutte le piattaforme, incluse quelle freestanding.

Con Zig puoi fare affidamento sulla modalità di build sicura, e disattivare selettivamente alcuni dei controlli di sicurezza se e dove sono necessarie maggiori prestazioni. Per esempio, il codice mostrato nell'esempio precedente può essere modificato in questo modo:

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

Zig usa i comportamenti non definiti come strumento estremamente preciso per prevenire bug e migliorare le prestazioni.

A proposito di prestazioni, Zig è più veloce rispetto a C.

Zig non è un linguaggio completamente sicuro. Per chi fosse interessato ad approfondire il tema della sicurezza in Zig, tieni d'occhio le seguenti discussioni:

Zig compete con C invece di dipendere da C

La libreria standard di Zig comunica con libc, ma può farne a meno. Ecco un "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!

Quando compilato con -O ReleaseSmall, senza simboli di debug, in modalità single-thread, il seguente codice produce un eseguibile statico di 9.8 KiB per la piattaforma x86_64-linux:

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

Una build per Windows è persino più piccola, occupando solo 4096 byte:

$ 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

Dichiarazioni di alto livello indipendenti dall'ordine

Le dichiarazioni di alto livello come le variabili globali sono visibili indipendentemente dall'ordine con cui appaiono nel codice, e sono analizzate in modo lazy. I valori usati per inizializzare le variabili globali sono valutati in fase di compilazione.

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.

Tipi opzionali invece di puntatori nulli

In altri linguaggi di programmazione, i riferimenti nulli sono la causa di molte eccezioni a runtime, e sono persino accusati di essere il più grave errore delle scienze informatiche.

In Zig, di base i puntatori non possono essere nulli:

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

Ciononostante, ogni tipo di dato può essere reso un tipo opzionale anteponendo un ?:

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.

Per estrarre il valore da un tipo opzionale (unwrapping), si può usare orelse per fornire un valore di default:

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;
    // ...
}

In alternativa si può usare un if:

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

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

    // do some stuff
}

La stessa sintassi funziona con 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

Gestione manuale della memoria

Una libreria scritta in Zig è adatta a qualunque contesto:

Per permettere questo risultato, i programmatori Zig devono gestire manualmente la memoria, e devono gestire gli errori di allocazione.

Questo vale anche per la libreria standard di Zig. Ogni funzione che necessita di allocazioni dinamiche accetta come parametro un allocatore. Per questo motivo, la libreria standard di Zig può essere utilizzata anche su piattaforme hardware.

In aggiunta a un nuovo approccio alla gestione degli errori, Zig fornisce defer e errdefer per rendere tutta la gestione di risorse - non solo la memoria - semplice e facilmente verificabile.

Per un esempio di defer, vedi la sezione Integrazione con librerie C senza FFI/binding. Ecco un esempio di utilizzo di 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;
    }
};

Un nuovo approccio alla gestione degli errori

Gli errori sono valori, e non possono essere ignorati:

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

Gli errori possono essere gestiti con catch:

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

La parola chiave try è un'abbreviazione di 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", .{});
                 ^

Nota che questa è un Error Return Trace, non uno stack trace. Il codice non ha dovuto pagare il prezzo dell'unwinding dello stack per ottenere queste informazioni.

La parola chiave switch usata su un errore garantisce che tutti i possibili errori siano gestiti:

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

La parola chiave unreachable viene usata per dichiarare che non si verificherà alcun errore:

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)

Nelle build senza controlli di sicurezza, questo risulterà un comportamento non definito, quindi assicurati di usare unreachable solo quando hai la certezza che un'operazione avrà successo.

Stack trace su ogni piattaforma

I stack trace e gli error return trace mostrati in questa pagina funzionano su tutte le piattaforme con supporto Tier 1 e alcune con supporto Tier 2. Persino piattaforme hardware!

Inoltre, la libreria standard permette di catturare uno stack trace in qualunque punto del programma e inviarlo al canale standard error in un secondo momento:

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();
                     ^

Puoi vedere un utilizzo di questa tecnica nel progetto GeneralPurposeDebugAllocator.

Strutture dati generiche e funzioni

I tipi di dato sono valori che devono essere conosciuti in fase di compilazione.

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.

Una struttura dati generica è semplicemente una funzione che restituisce un valore di tipo type:

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

Riflessione ed esecuzione in fase di compilazione

La funzione builtin @typeInfo permette la riflessione:

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

La libreria standard di Zig usa questa tecnica per implementare l'output di stringhe formattate. Nonostante il linguaggio sia piccolo e semplice, questa funzione è implementata interamente in Zig. Nel frattempo, in C, gli errori di compilazione per printf sono hard-coded, scritti direttamente nel compilatore. Similmente, in Rust, la funzione equivalente è una macro implementata direttamente nel compilatore.

Zig può anche valutare funzioni e blocchi di codice in fase di compilazione. In alcuni contesti, come l'inizializzazione di variabili globali, le espressioni sono implicitamente valutate in fase di compilazione. In alternativa, la parola chiave comptime permette di eseguire esplicitamente una porzione di codice in fase di compilazione. Può essere estremamente utile se combinato con le asserzioni:

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

Integrazione con librerie C senza FFI/bindings

@cImport importa direttamente tipi, variabili, funzioni e macro semplici, permettendone l'utilizzo in Zig. Traduce persino le funzioni inline da C a Zig.

Il seguente esempio emette un'onda sonora sinusoidale utilizzando 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

Questo codice Zig è molto più semplice dell'equivalente C, oltre ad avere migliori controlli sugli errori, e tutto questo è stato ottenuto importando direttamente un file di intestazione C - senza binding delle API.

Zig usa le librerie C meglio di quanto C usa le librerie C.

Zig è anche un compilatore C

Ecco un esempio di come Zig compila del codice C:

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

Puoi usare --verbose-cc per vedere il comando eseguito dal compilatore C:

$ 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

Nota che eseguendo di nuovo il comando, non c'è alcun output, e la compilazione è istantanea:

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

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

Questo è possibile grazie al caching dei risultati di compilazione. Zig legge automaticamente il file .d e usa un robusto sistema di caching per evitare di rifare lavoro non necessario.

Non solo Zig può compilare codice C, ma ci sono anche ottime ragioni per usare Zig come compilatore C: Zig integra libc.

Esporta funzioni, variabili e tipi per permetterne l'uso a C

Uno dei casi d'uso primari di Zig è esportare una libreria con l'ABI di C che possa essere richiamata da altri linguaggi di programmazione. La parola chiave export anteposta a funzioni, variabili e tipi di dato, li rende parte dell'API della libreria:

mathtest.zig

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

Per creare una libreria statica:

$ zig build-lib mathtest.zig

Per creare una libreria dinamica/condivisa:

$ zig build-lib mathtest.zig -dynamic

Un esempio con il sistema di build di 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

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

la cross-compilazione è un caso d'uso primario

Zig può compilare per ognuna delle piattaforme indicate nella tabella di supporto (vedi le note di rilacio dell'ultima versione di Zig) come Tier 3 o superiore. Non è necessario installare alcuna "cross toolchain" o niente di simile. Ecco un Hello World nativo:

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!

Ora lo compiliamo per x86_64-windows, x86_64-macos e 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

Questo funziona da qualunque piattaforma Tier 3+, verso qualunque piattaforma Tier 3+.

Zig integra libc

Puoi vedere la lista delle piattaforme supportate da libc con il comando zig targets:

...
 "libc": [
  "aarch64_be-linux-gnu",
  "aarch64_be-linux-musl",
  "aarch64_be-windows-gnu",
  "aarch64-linux-gnu",
  "aarch64-linux-musl",
  "aarch64-windows-gnu",
  "armeb-linux-gnueabi",
  "armeb-linux-gnueabihf",
  "armeb-linux-musleabi",
  "armeb-linux-musleabihf",
  "armeb-windows-gnu",
  "arm-linux-gnueabi",
  "arm-linux-gnueabihf",
  "arm-linux-musleabi",
  "arm-linux-musleabihf",
  "arm-windows-gnu",
  "mips64el-linux-gnuabi64",
  "mips64el-linux-gnuabin32",
  "mips64el-linux-musl",
  "mips64-linux-gnuabi64",
  "mips64-linux-gnuabin32",
  "mips64-linux-musl",
  "mipsel-linux-gnu",
  "mipsel-linux-musl",
  "mips-linux-gnu",
  "mips-linux-musl",
  "powerpc64le-linux-gnu",
  "powerpc64le-linux-musl",
  "powerpc64-linux-gnu",
  "powerpc64-linux-musl",
  "powerpc-linux-gnu",
  "powerpc-linux-musl",
  "riscv64-linux-gnu",
  "riscv64-linux-musl",
  "s390x-linux-gnu",
  "s390x-linux-musl",
  "sparc-linux-gnu",
  "sparcv9-linux-gnu",
  "wasm32-freestanding-musl",
  "x86-linux-gnu",
  "x86-linux-musl",
  "x86-windows-gnu",
  "x86_64-linux-gnu",
  "x86_64-linux-gnux32",
  "x86_64-linux-musl",
  "x86_64-windows-gnu"
 ],

Questo significa che --library c su queste piattaforme non dipende da alcun file di sistema!

Diamo nuovamente un'occhiata a quel Hello World in C:

$ 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 non supporta la creazione di binari statici, ma musl lo può fare:

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

In questo esempio, Zig ha compilato musl libc da sorgente e poi ha linkato ad esso. La build di musl libc per x86_64-linux rimane disponibile grazie al sistema di cache, quindi ogni volta che ci sarà nuovamente bisogno di questo libc sarà disponibile istantaneamente.

Questo significa che questa funzionalità è disponibile su ogni piattaforma. Gli utenti Windows e macOS possono compilare codice Zig e C, e linkare libc, per ognuno dei target elencati sopra. Allo stesso modo, è possibile cross-compilare del codice per altre architetture:

$ 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 un certo senso, Zig compila C meglio dei compilatori C!

Questa funzionalità è più avanzata rispetto alla sola integrazione di uno strumento di cross-compilazione in Zig. Per esempio, la dimensione totale non compressa delle intestazioni libc incluse in Zig è 22 MiB. Allo stesso tempo, gli header di musl libc + header di linux per la sola architettura x86_64 sono in totale 8 MiB, e per glibc sono 3.1 MiB (a glibc mancano gli header linux), eppure Zig al momento include 40 versioni di libc. Includendoli così come sono, sarebbero 444 MiB. Però, grazie a questo strumento di elaborazione header, e del buon vecchio olio di gomito, gli archivi scaricabili di Zig rimangono a circa 50 Mib totali, nonostante supportino libc su tutti questi target, oltre a compiler-rt, libunwind e libcxx, e nonostante sia un compilator compatibile con Clang. Per fare un confronto, la build per Windows dello stesso Clang (v8.0.0) presa da llvm.org occupa 132 MiB.

Nota che solo i target con supporto Tier 1 sono stati testati in modo esaustivo. Abbiamo già in programma di aggiungere altri libc (anche per Windows), e di aggiungere copertura dei test per la compilazione con tutte le versioni di libc.

It's planned to have a Zig Package Manager, but it's not done yet. One of the things that will be possible is to create a package for C libraries. This will make the Zig Build System attractive for Zig programmers and C programmers alike.

Il build system di Zig

Zig ha un proprio sistema di build, quindi non servono make, cmake o simili.

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

Ora prova `zig build --help` oppure `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);
}

Diamo un'occhiata a quel 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

Come vedi uno degli step diponibili è run:

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

Ecco alcuni esempi di build script:

Concurrency via Async Functions

Zig 0.5.0 introduced async functions. This feature has no dependency on a host operating system or even heap-allocated memory. That means async functions are available for the freestanding target.

Zig infers whether a function is async, and allows async/await on non-async functions, which means that Zig libraries are agnostic of blocking vs async I/O. Zig avoids function colors.

The Zig Standard Library implements an event loop that multiplexes async functions onto a thread pool for M:N concurrency. Multithreading safety and race detection are areas of active research.

Vasta gamma di piattaforme supportate

Zig usa un sistema "grado di supporto" per indicare fino a che punto Zig è supportato su una certa piattaforma.

Tabella di supporto aggiornata a Zig 0.11.0

Semplifica la vita ai mantenitori di pacchetti

The reference Zig compiler is not completely self-hosted yet, but no matter what, it will remain exactly 3 steps to go from having a system C++ compiler to having a fully self-hosted Zig compiler for any target. Come fa notare Maya Rashish, rendere Zig disponibile su altre piattaforme è divertente e richiede poco tempo.

Ad eccezione della modalità debug, le modalità di build sono riproducibili/deterministiche.

C'è una versione JSON della pagina dei download.

Diversi membri del team di Zig hanno esperienza nel mentenimento di pacchetti.