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:
- D ha le funzioni
@property
, ovvero metodi che possono essere chiamati in un modo che sembra l'accesso a un attributo. Quindi, nell'esempio sopra,c.d
potrebbe chiamare una funzione. - C++, D e Rust supportano l'overload degli operatori, quindi l'operatore
+
potrebbe chiamare una funzione. - C++, D e Go hanno eccezioni throw/catch, quindi
foo()
potrebbe lanciare un'eccezione, e impedire l'esecuzione dibar()
.
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:
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'
Ecco come appare durante l'esecuzione, nelle build con controlli di sicurezza attivi:
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
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:
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.
- L'implementazione di riferimento usa LLVM come backend per applicare lo stato dell'arte delle ottimizzazioni.
- Ciò che altri progetti chiamano "Link Time Optimization", Zig lo fa in automatico.
- Per la piattaforma native vengono abilitate le funzioni avanzate della CPU (
-march=native
), grazie al fatto che la cross-compilazione è un caso d'uso primario. - Comportamenti non definiti (UB) accuratamente selezionati. Per esempio, in Zig sia gli interi con segno che quelli unsigned hanno comportamenti non definiti in caso di overflow, in contrasto a C dove ciò è vero solo per gli interi con segno. Questo facilita delle ottimizzazioni non disponibili in C.
- Zig espone direttamente un tipo di vettore SIMD, che rende semplice scrivere codice vettorizzato portabile.
Zig non è un linguaggio completamente sicuro. Per chi fosse interessato ad approfondire il tema della sicurezza in Zig, tieni d'occhio le seguenti discussioni:
- enumerate all kinds of undefined behavior, even that which cannot be safety-checked
- make Debug and ReleaseSafe modes fully safe
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":
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, world!\n", .{});
}
$ 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.
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.
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:
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
Ciononostante, ogni tipo di dato può essere reso un tipo opzionale anteponendo un ?
:
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.
Per estrarre il valore da un tipo opzionale (unwrapping), si può usare orelse
per fornire un valore di default:
// 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
:
fn doAThing(optional_foo: ?*Foo) void {
// do some stuff
if (optional_foo) |foo| {
doSomethingWithFoo(foo);
}
// do some stuff
}
La stessa sintassi funziona 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
Gestione manuale della memoria
Una libreria scritta in Zig è adatta a qualunque contesto:
- Applicazioni desktop
- Server a bassa latenza
- Kernel di sistemi operativi
- Sistemi embedded
- Software real-time, come performance live, aeroplani, pacemaker
- In browser web o plugin con WebAssembly
- Utilizzo da altri linguaggi di programmazione, mediante l'ABI di C
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
: 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:
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
Gli errori possono essere gestiti 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 parola chiave try è un'abbreviazione di 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 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:
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 parola chiave unreachable viene usata per dichiarare che non si verificherà alcun errore:
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)
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:
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();
^
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.
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 struttura dati generica è semplicemente una funzione che restituisce un valore di tipo type
:
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
Riflessione ed esecuzione in fase di compilazione
La funzione builtin @typeInfo permette la riflessione:
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 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:
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
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 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 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 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:
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, world!\n", .{});
}
$ 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 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);
}
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:
- Build script di un gioco Tetris in OpenGL
- Build script di un gioco arcade per Raspberry Pi 3 (solo hardware)
- Build script del compilatore self-hosted di Zig
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.
- Daurnimator mantiene il pacchetto per Arch Linux.
- Marc Tiehuis mantiene il pacchetto per Visual Studio Code.
- Andrew Kelley ha passato circa un anno gestendo pacchetti per Debian e Ubuntu, e ogni tanto contribuisce a nixpkgs.
- Jeff Fowler mantiene il pacchetto Homebrew e ha creato il pacchetto Sublime Text (ora mantenuto da emekoi).