Revisión a fondo

Características

Lenguaje simple y compacto

Concéntrate en depurar tu aplicación y no tus conocimientos del lenguaje.

La sintaxis completa de Zig se encuentra especificada en un archivo de 500 lineas de gramática PEG.

En Zig, no hay control de flujo oculto, ni asignaciones de memoria ocultas, ni preprocesador, ni macros. Si un fragmento de código Zig no parece estar saltando a hacer una llamada a una función, es por que no lo está haciendo. Esto significa que podemos estar seguros de que el siguiente código llama solo a foo() y después a bar() y esto esta garantizado sin tener que conocer tipo de dato alguno:

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

Ejemplos de control de flujo oculto:

Zig promueve la facilidad de mantenimiento y legibilidad haciendo que todo control de flujo sea manejado exclusivamente con palabras reservadas del lenguaje y con llamadas a funciones.

Rendimiento y Seguridad: Elije dos

Zig tiene cuatro modos de compilación, los cuales pueden ser usados en forma individual o combinada en un alcance granular.

ParámetroDebugReleaseSafeReleaseFastReleaseSmall
Optimizaciones - ejecutable rápido, depuración lenta, compilación lenta-O3-O3-Os
Chequeo de seguridad - ejecutable lento, ejecutable grande, finaliza en vez de ‘undefined behavior’OnOn

Así es como se ve el desbordamiento de entero en tiempo de compilación, sin importar el modo de compilación:

test.zig

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

Así es como se ve en tiempo de ejecución con chequeos de seguridad:

test.zig

test "integer overflow at runtime" {
    var x: u8 = 255;
    x += 1;
}
$ zig test test.zig
1/1 test.test.integer overflow at runtime... thread 2721 panic: integer overflow
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-05de9f7c/test.zig:3:7: 0x1038e4e in test.integer overflow at runtime (test)
    x += 1;
      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/compiler/test_runner.zig:158:25: 0x1044252 in mainTerminal (test)
        if (test_fn.func()) |_| {
                        ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/compiler/test_runner.zig:35:28: 0x103a23b in main (test)
        return mainTerminal();
                           ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x1039379 in posixCallMainAndExit (test)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:253:5: 0x1038ee1 in _start (test)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/runner/.cache/zig/o/743f1a954ab405a6c33b490d9add149c/test

Estos ‘stack traces’ funcionan en todos los ‘targets’, incluyendo freestanding.

Con Zig podemos confiar en un modo de compilación seguro y podemos deshabilitar selectivamente esos chequeos de seguridad cuando sea necesario exprimir al máximo el rendimiento. Por ejemplo, el fragmento de código anterior podría ser modificado así:

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

Zig utiliza ‘undefined behavior’ como una herramienta para prevención de bugs y mejoras de desempeño.

Hablando de desempeño, Zig es mas rápido que C.

Toma en cuenta que Zig no es un lenguaje completamente seguro. Si estas interesado en seguir la historia relacionada con la seguridad de Zig, suscríbete a los siguientes issues:

Zig compite con C en vez de depender de el

La biblioteca estándar de Zig se integra con libc, pero no depende de ella. Aquí está 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!

Al compilar con -O ReleaseSmall, lo cual remueve símbolos de depuración y es un modo single-threaded, se produce un ejecutable estático de 9.8 KiB para la arquitectura x86_64-linux:

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

En Windows se produce una compilación aún mas pequeña, llegando a 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

Declaraciones de nivel superior independientes de orden

Las declaraciones de de nivel superior, como son las variables globales, son independientes de orden y analizadas en forma tardía. La inicialización de valores de variables globales es evaluada en tiempo de compilación.

global_variables.zig

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

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

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

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

Tipo de dato Optional en vez de punteros a null

En otros lenguajes de programación, las referencias a null son una fuente excepciones en tiempo de ejecución, incluso son acusadas de ser el peor error en la ciencia de la computación.

Los punteros Zig (sin adornos) no pueden ser null:

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

No obstante, cualquier tipo pude ser un tipo opcional utilizando el prefijo ‘?':

optional_syntax.zig

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

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

Para resolver un valor opcional, se puede usar orelse para proveer un valor por defecto:

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

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

Otra opción es usar if:

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

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

    // do some stuff
}

La misma sintaxis funciona con while:

iterator.zig

const std = @import("std");

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

Manejo manual de memoria

Una biblioteca escrita en Zig es elegible para ser usada en cualquier lugar:

Para lograr esto, los programadores de Zig deben hacer un manejo de memoria manual y deben poder resolver fallas de asignación de memoria.

Esto también aplica para la biblioteca estándar de Zig. Cualquier función que requiera asignaciones de memoria acepta un parámetro ‘allocator’ (asignador de memoria). Como resultado, la biblioteca estándar de Zig pude ser usada para cualquier arquitectura objetivo.

Adicionalmente de lo dicho en Una visión fresca sobre manejo de errores, Zig provee defer y errdefer para lograr que todo manejo de recursos, no solo de memoria, sea simple y fácilmente verificable.

Como ejemplo de defer, ve Integración con bibliotecas de C sin FFI/bindings. Este es un ejemplo del uso de 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;
    }
};

Una visión fresca sobre manejo de errores

Los errores son valores y no deben ser ignorados:

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-a5c5b013/discard.zig:4:30: error: error is discarded
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
doctest-a5c5b013/discard.zig:4:30: note: consider using 'try', 'catch', or 'if'
referenced by:
    callMain: zig/lib/std/start.zig:501:17
    callMainWithArgs: zig/lib/std/start.zig:469:12
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

Los errores pueden ser manejados con catch:

catch.zig

const std = @import("std");

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

La palabra reservada try es un atajo para catch |err| return err:

try.zig

const std = @import("std");

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

Notese que se trata de una traza de error: Error Return Trace, y no de una traza de pila: stack trace. Este código no incurrió en el costo de deshilar el stack (pila) para arrojar esa traza.

La plabra reservada switch, usada para evaluar un error asegura que todos los posibles errores sean manejados.

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

La palabra reservada unreachable es usada para asumir que no ocurrirán errores:

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 2942 panic: attempt to unwrap error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/posix.zig:1764:23: 0x1069422 in openatZ (unreachable)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:872:16: 0x10386a7 in openFileZ (unreachable)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:827:5: 0x1035e7e in openFile (unreachable)
    return self.openFileZ(&path_c, flags);
    ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-25380235/unreachable.zig:4:77: 0x1033c2f in main (unreachable)
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable;
                                                                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x1033439 in posixCallMainAndExit (unreachable)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:253:5: 0x1032fa1 in _start (unreachable)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
(process terminated by signal)

Esto invoca undefined behavior (comportamiento indefinido) en modos de compilación no segura, asegúrate de solo utilizarla cuando esté garantizado que no habrá errores.

Stack traces (trazas de pila) en todas las arquitecturas objetivo

Las stack traces (trazas de pila) y las error return traces (trazas de error) mostradas en esta página funcionan para targets (arquitecturas objetivo) de Soporte nivel 1 y algunas de Soporte nivel 2. Incluso freestanding!

Adicionalmente, la biblioteca estándar es capaz de capturar trazas de pila en cualquier punto para posteriormente arrojarlas a la salida de error estándar:

stack_traces.zig

const std = @import("std");

var address_buffer: [8]usize = undefined;

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

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

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

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

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

fn bar() void {
    std.debug.captureStackTrace(null, &trace2);
}
$ zig build-exe stack_traces.zig
$ ./stack_traces
first one:
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:356:29: 0x1038367 in captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-37e2a31f/stack_traces.zig:26:32: 0x1035dcc in foo (stack_traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-37e2a31f/stack_traces.zig:16:8: 0x1033d38 in main (stack_traces)
    foo();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x10335e9 in posixCallMainAndExit (stack_traces)
            root.main();
                     ^


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

Puedes ver como es empleada esta técnica en el proyecto GeneralPurposeDebugAllocator.

Estructuras genéricas de datos y funciones

Los tipos de datos son valores y esto debe ser conocido en tiempo de compilacion:

types.zig

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

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

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

Una estructura genérica de datos es simplemente una función que retorna un tipo:

generics.zig

const std = @import("std");

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

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

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

Introspección y ejecución de código en tiempo de compilación

La función @typeInfo provee introspección (reflection):

reflection.zig

const std = @import("std");

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

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

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

La biblioteca estándar de Zig utiliza esta técnica para implementar salida formateada. No obstante de ser un lenguaje compacto y simple, el formateo de salida de Zig está implementado completamente con el mismo lenguaje Zig. Mientras tanto en C, los errores de compilación para printf están codificados manualmente dentro del compilador. De forma similar, en Rust, el macro para salida formateada esta codificado dentro del compilador.

Zig es capaz de evaluar funciones y bloques de código en tiempo de compilación. En algunos contextos tales como inicialización de variables globales, la expresión es evaluada implícitamente en tiempo de compilación. Además de esto, es posible evaluar código en tiempo de compilación con la palabra reservada comptime. Esta característica, combinada con ‘assertions’ de pruebas aporta gran poder:

test.zig

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

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

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

    @memset(&array, 42);

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

Integración con bibliotecas de C sin FFI/bindings

@cImport importa directamente tipos, variables, funciones y macros simples para ser usados en Zig. Incluso traduce funciones ‘inline’ de C a Zig.

Este es un ejemplo de como emitir una onda sinusoidal usando libsoundio:

sine.zig

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

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

var seconds_offset: f32 = 0;

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

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

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

        if (frame_count == 0) break;

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

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

        frames_left -= frame_count;
    }
}

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

    try sio_err(c.soundio_connect(soundio));

    c.soundio_flush_events(soundio);

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

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

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

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

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

    try sio_err(c.soundio_outstream_open(outstream));

    try sio_err(c.soundio_outstream_start(outstream));

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

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

Este código Zig es significativamente mas simple que su equivalente en C, además de contar con mayores protecciones de seguridad y todo esto se logra importando directamente archivos de encabezado de C y no ‘bindings’ de API.

Zig es mejor usando bibliotecas de C que el mismo C.

Zig es también un compilador de C

Este es un ejemplo de Zig compilado algo de código 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

Puedes usar la opción --verbose-cc para ver que comando del compilador de C fue ejecutado:

$ 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

Observa que si se ejecuta el comando de nuevo, no habrá salida y el comando termina al instante:

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

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

Esto ocurre gracias a Build Artifact Caching. Zig, automaticamente interpreta el archivo .d utilizando un robusto sistema de cache para evitar duplicar el trabajo.

Existe un buen motivo para usar Zig como compilador de C: Zig ships with libc.

Funciones, variables y tipos de exportación como dependencias de código C

Uno de los principales casos de uso de Zig es exportar una biblioteca con el ABI de C para ser llamada desde otros lenguajes de programación. La palabra reservada export antes del nombre de una función, variable o definición de tipo, hace que dichos elementos sean parte de la API de la biblioteca:

mathtest.zig

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

Para crear una biblioteca estática:

$ zig build-lib mathtest.zig

Para crear una biblioteca compartida:

$ zig build-lib mathtest.zig -dynamic

Este es un ejemplo con el Zig Build System (sistema de compilación 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

Cross-compiling (compilación para arquitectura objetivo distinta) es un caso de uso de primera clase

Zig puede compilar para cualquiera de los objetivos en Support Table con Tier 3 Support (nivel 3 de soporte) o mayor. No se necesitan herramientas adicionales. Este es un ejemplo nativo de 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!

Para compilar este ejemplo específicamente para arquitecturas x86_64-windows, x86_64-macos, y 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

Esto aplica en cualquier objetivo Tier 3+, para cualquier objetivo Tier 3+.

libc viene incluida en Zig

Puedes encontrar las arquitecturas objetivo libc con 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"
 ],

Esto significa que --library c para estos objetivos no depende de ningun archivo del sistema!

Demos de nuevo un vistazo al ejemplo de hello world en 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 no soporta compilar en forma estática, pero musl si:

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

En este ejemplo, Zig compiló musl libc desde los fuentes para luego proceder a hacer el ‘link’. La compilación de musl libc para x86_64-linux se mantiene disponible gracias al sistema de cache, de manera que esta libc estará disponible cuando sea necesaria.

Esta funcionalidad está disponible en cualquier plataforma. Los usuarios de Windows y macOs pueden compilar código Zig y C y efectuar link con libc para cualquiera de los objetivos listados arriba. De igual forma, el código puede ser compilado entre otras arquitecturas:

$ 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

De alguna forma, Zig es un mejor compilador de C que el mismo C!

Esta funcionalidad es mas que construir una cadena de herramientas de inter-compilación con Zig. Por ejemplo, el tamaño total los encabezados de libc que vienen con Zig es de 22 MiB sin comprimir. Mientras tanto, los encabezados de musl libc + encabezados de linux en solamente x86_64 son 8 MiB y para glibc 3.1 MiB (glibc no incluye los encabezados de linux), aun así, Zig se distribuye con 40 diferentes libc. Con una compilación típica, esto resultaría en 444 MiB. Grácias al process_headers tool que desarrollé y un poco de trabajo manual, Los tarballs (archivos tar comprimidos) de binarios de Zig se mantienen en aproximadamente 30 MiB en total a pesar de soportar libc para todos estas arquitecturas y soportar compiler-rt, libunwind y libcxx y a pesar de ser un compilador compatible con C. Como comparación, el binario de Windows para clang 8.0.0 de llvm.org es de 132 MiB.

Toma en cuenta que solo las arquitecturas de Tire 1 Support han sido probadas exhaustivamente. Hay planes para añadir más libcs (incluyendo Windows) y para añadir cobertura de pruebas para compilar hacia todas las libc.

Está planeado tener un manejador de paquetes para Zig, pero aun no esta listo. Una de las cosas que serán posibles es la capacidad de crear un paquete para bibliotecas de C. Esto hará el sistema de compilación de Zig atractivo tanto para programadores de Zig como de C.

El sistema de compilación de Zig

Zig viene con un sistema de compilación, de tal forma que no necesitas make, cmake o nada parecido.

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

Veamos el menú --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

Podemos ver que uno de los pasos es ‘run’.

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

Aquí, algunos ejemplos de scripts de compilación:

Concurrencia vía funciones asíncronas

Zig 0.5.0 introdujo funciones asincronas. Esta característica no tiene dependencia con un sistema operativo o memoria asignada. Las funciones asíncronas están disponibles independientemente de la arquitectura.

Zig puede inferir si una función es asíncrona o no y permite async/await en funciones no asíncronas, lo que significa que las bibliotecas de Zig son agnósticas con respecto a I/O (entrada/salida) blocking vs. async. Zig avoids function colors.

La biblioteca estándar de Zig implementa un loop de evento que multiplexa funciones ‘async’ en un pool de hilos para concurrencia M:N. Seguridad multi-hilo y detección de ‘race conditions’ son áreas en continua investigación.

Amplia gama de arquitecturas soportadas

Zig utilíza un sistema de “support tier” (nivel de soporte) para comunicar el nivel de soporte de diferentes arquitecturas. Toma en cuenta que los requerimientos para Tier 1 Support son altos pero Tier 2 Support es bastante usable.

Tabla de soporte

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

Tabla de soporte para WebAssembly

free standingemscriptenWASI
wasm32Tier 1Tier 3Tier 1
wasm64Tier 4Tier 4Tier 4

Tier System (sistema de niveles de soporte)

Tier 1 Support (soporte nivel 1)

Tier 2 Support (soporte nivel 2)

Tier 3 Support (soporte nivel 3)

Tier 4 Support (soporte nivel 4)

Amigable con los desarrolladores de paquetes

El compilador estándar de Zig no está completamente auto-contenido aún, no obstante permanecerá a exactamente 3 pasos de ser un compilador de C++ a ser un compilador totalmente autocontenido para cualquier plataforma. Como lo menciona Maya Rashish portar Zig a otras plataformas es divertido y rápido.

Los modos de compilación sin depuración son reproducibles y deterministicos.

Existe una versión JSON de la página de descargas.

Muchos miembros del equipo de Zig tienen experiencia manteniendo paquetes.