Revisión a fondo
Características destacadas
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:
- D tiene funciones
@property
, las cuales son métodos que puedes llamar con algo que se percibe como acceso a propiedades, de tal forma que el ejemplo de código anterior,c.d
, podría estar llamando a una función. - C++, D y Rust tienen sobrecarga de operadores, de tal forma que el operador
+
podría llamar a una función. - C++, D y Go tienen manejo de excepciones con throw/catch, de tal manera que
foo()
podría arrojar una excepción y prevenir quebar()
sea llamada.
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ámetro | Debug | ReleaseSafe | ReleaseFast | ReleaseSmall |
---|---|---|---|---|
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' | On | On |
Así es como se ve el desbordamiento de entero en tiempo de compilación, sin importar el modo de compilación:
test "integer overflow at compile time" {
const x: u8 = 255;
_ = x + 1;
}
$ 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'
Así es como se ve en tiempo de ejecución con chequeos de seguridad:
test "integer overflow at runtime" {
var x: u8 = 255;
x += 1;
}
$ 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
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.
- La implementación de referencia usa LLVM como backend para optimizaciones avanzadas.
- Zig hace automáticamente lo que otros proyectos llaman "Link Time Optimization"
- Zig tiene habilitadas algunas características avanzadas de CPU para 'targets' nativos (-march=native), gracias al hecho de que el 'cross-compiling' es un caso de uso de primera clase.
- 'undefined behavior' ha sido seleccionado cuidadosamente. En Zig, tipos enteros con o sin signo (signed/unsigned) presentan comportamiento indefinido (undefined behavior) al momento de desborde (overflow), en contraste a C que solo presenta esta característica en enteros con signo. Esto facilita optimizaciones que no están disponibles en C.
- Zig expone directamente un tipo vector SIMD, haciendo más simple escribir código portable con vectores.
Ten en cuenta que Zig no es un lenguaje completamente seguro. Si tienes interés en seguir la historia relacionada con la seguridad de Zig, suscríbete a los siguientes issues:
- Enumeración de todos los tipos de 'undefined behavior', incluyendo aquellos que no pueden ser verificados por los chequeos de seguridad
- Hacer los modos Debug y ReleaseSafe modos completamente seguros
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:
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, world!\n", .{});
}
$ zig build-exe 4-hello.zig
$ ./4-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.
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 5-global-variables.zig
1/1 5-global-variables.test.global variables...OK
All 1 tests passed.
Tipo de dato Opcional en vez de punteros a null
En otros lenguajes de programación, las referencias a null son una fuente de 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 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
No obstante, cualquier tipo pude ser un tipo opcional utilizando el prefijo ?:
const std = @import("std");
const assert = std.debug.assert;
test "null @intToPtr" {
const ptr: ?*i32 = @ptrFromInt(0x0);
assert(ptr == null);
}
$ zig test 7-optional-syntax.zig
1/1 7-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:
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 10-optional-while.zig
$ ./10-optional-while
hello
this
is
dog
Manejo manual de memoria
Una biblioteca escrita en Zig es elegible para ser usada en cualquier lugar:
- Aplicaciones de escritorio
- Servidores de baja latencia
- Kernels de sistemas operativos
- Dispositivos embebidos
- Software en tiempo real, ejemplos: actuaciones en vivo, aviones, marcapasos
- En web browsers o plugins con WebAssembly
- Desde otros lenguajes de programación, usando el ABI de C
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:
const std = @import("std");
pub fn main() void {
_ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
}
$ 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
Los errores pueden ser manejados con catch:
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 13-errors-catch.zig
$ ./13-errors-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
:
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 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 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 palabra reservada switch, usada para evaluar un error asegura que todos los posibles errores sean manejados.
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 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 palabra reservada unreachable es usada para asumir que no ocurrirán errores:
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 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)
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:
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 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();
^
Puedes ver como se emplea 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 compilación:
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 18-types.zig
1/1 18-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
:
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 19-generics.zig
$ ./19-generics
10
Introspección y ejecución de código en tiempo de compilación
La función @typeInfo provee introspección (reflection):
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 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 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 se evalúa 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:
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 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
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, automáticamente 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:
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(ver las release notes más recientes) con Tier 3 Support o mayor. No se necesitan herramientas adicionales. Este es un ejemplo nativo de Hello World:
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, world!\n", .{});
}
$ zig build-exe 4-hello.zig
$ ./4-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",
"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"
],
Esto significa que --library c
para estos objetivos no depende de ningún 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 inmediatamente.
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(cross-compiled):
$ 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. Gracias al process_headers tool 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.
Ten 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 está 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:
- Script de compilación del juego Tetris en OpenGL
- Script de compilación de juego de arcade nativo en Raspberry Pi
- Script de compilación del compilador Zig auto hosteado
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 de investigación continua.
Amplia gama de arquitecturas soportadas
Zig utiliza un sistema de "support tier" (nivel de soporte) para comunicar el nivel de soporte de diferentes arquitecturas.
Tabla de soporte en la versión 0.11.0 de Zig
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 determinísticos.
Existe una versión JSON de la página de descargas.
Muchos miembros del equipo de Zig tienen experiencia manteniendo paquetes.
- Daurnimator mantiene el paquete para Arch Linux
- Marc Tiehuis mantiene el paquete de Visual Studio Code.
- Andrew Kelley trabajó por un año desarrollando paquetes para Debian y Ubuntu, y ocasionalmente contribuye con nixpkgs.
- Jeff Fowler mantiene el paquete Homebrew e inició el paquete Sublime (hoy mantenido por emekoi).