Ausführliche Übersicht
Feature Highlights
Kleine, einfache Sprache
Debugge deine Anwendung, nicht deine Kenntnis der Programmiersprache.
Zig’s gesamte Syntax ist in 500 Zeilen PEG-Grammatik beschrieben.
Es gibt keinen versteckten Kontrollfluss, keine versteckten Speicherallokationen, keinen Präprozessor, und keine Makros. Wenn Zig Code nicht aussieht, als ober in einen Funktionsaufruf springt, dann tut er das nicht. Das bedeutet, dass du sicher sein kannst, dass der folgende Code nur foo()
und dann bar()
aufruft, und das ist garantiert, ohne die Typen von irgendwelchen Werten zu kennen:
var a = b + c.d;
foo();
bar();
Beispiele von verstecktem Kontrollfluss:
- D hat
@property
-Funktionen – Methoden, deren Aufruf wie ein Feldzugriff aussieht, also könnte im obigen Beispielc.d
eine Funktion aufrufen. - C++, D, und Rust haben Operatorenüberladung, also könnte der Operator
+
eine Funktion aufrufen. - C++, D, und Go haben throw/catch-Ausnahmen, also könnte
foo()
eine Ausnahme werfen und die Ausführung vonbar()
verhindern.
Zig fördert Codewartung und Lesbarkeit, indem Kontrollfluss ausschließlich mit Schlüsselwörtern und Funktionsaufrufen geschieht.
Performance und Sicherheit: wähle zwei
Zig hat vier Buildmodi, sie können Scope-weise kombiniert werden.
Parameter | Debug | ReleaseSafe | ReleaseFast | ReleaseSmall |
---|---|---|---|---|
Optimierungen - bessere Geschwindigkeit, schlechteres Debugging und Compilierdauer | -O3 | -O3 | -Os | |
Laufzeitsicherheitschecks - schlechtere Geschwindigkeit und Programmgröße, Crashes statt undefiniertem Verhalten | On | On |
So sieht Integer Overflow zur Compilezeit aus, in jedem Buildmodus:
test.zig
test "integer overflow at compile time" {
const x: u8 = 255;
_ = x + 1;
}
$ zig test test.zig
doctest-79764389/test.zig:3:11: error: overflow of integer type 'u8' with value '256'
_ = x + 1;
~~^~~
So sieht er zur Laufzeit aus, in safety-checked-Builds:
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 2821 panic: integer overflow
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-829c56f1/test.zig:3:7: 0x1038e2e 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: 0x104421c in mainTerminal (test)
if (test_fn.func()) |_| {
^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/compiler/test_runner.zig:35:28: 0x103a21b in main (test)
return mainTerminal();
^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x1039359 in posixCallMainAndExit (test)
root.main();
^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:253:5: 0x1038ec1 in _start (test)
asm volatile (switch (native_arch) {
^
???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/runner/.cache/zig/o/a32aa8be9084f599fee733bf715b04aa/test
Die Stacktraces funktionieren auf allen Targets, auch freestanding.
Zig erlaubt es, sich auf einen Buildmodus mit aktivierter Sicherheit zu verlassen, und die Sicherheit an Performanceengpässen selektiv zu deaktivieren. Das vorherige Beispiel könnte so verändert werden:
test "actually undefined behavior" {
@setRuntimeSafety(false);
var x: u8 = 255;
x += 1; // XXX undefined behavior!
}
Zig benutzt undefiniertes Verhalten als ein messerscharfes Instrument zur Vermeidung von Bugs und Performanceverbesserung.
Apropos Performance: Zig ist schneller als C.
- Die Referenzimplementierung nutzt LLVM als Backend für Optimierungen auf dem neuesten Stand der Technik.
- Was andere Sprachen “Link Time Optimization” nennen, tut Zig automatisch.
- Für native Targets sind moderne CPU-Features aktiviert (
-march=native
), denn Crosscompiling ist ein primärer Einsatzfall. - Sorgfältig ausgewähltes undefiniertes Verhalten. Zum Beispiel haben in Zig sowohl signed und unsigned Integers undefiniertes Verhalten beim Überlauf, was in C nur bei signed Integers der Fall ist. Das ermöglicht Optimierungen, die in C nicht möglich sind.
- Zig stellt einen Vektortyp für SIMD zur Verfügung, der es einfach macht, portablen vektorisierten Code zu schreiben.
Beachte bitte, dass Zig keine vollständig sichere Sprache ist. Interessierte an der Geschichte von Zigs Sicherheit können diese Issues abbonieren:
- enumerate all kinds of undefined behavior, even that which cannot be safety-checked
- make Debug and ReleaseSafe modes fully safe
Zig ist nicht abhängig von C, sondern konkurriert mit C
Zigs Standardbibliothek kann libc einbeziehen, aber ist nicht darauf angewiesen. Hier ist 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!
Mit -O ReleaseSmall
kompiliert, ohne Debugsymbole, im Single Thread-Modus, wird für das Target x86_64-linux eine 9.8 KiB große statische Programmdatei erzeugt:
$ zig build-exe hello.zig -O ReleaseSmall -fstrip -fsingle-threaded
$ wc -c hello
9944 hello
$ ldd hello
not a dynamic executable
Ein Build auf Windows ist noch kleiner, nur 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
Deklarationen in beliebiger Reihenfolge
Deklarationen im Toplevel, wie globale Variablen, sind reihenfolgenunabhängig und werden nur bei Bedarf ausgewertet. Die Initialisierungswerte globaler Variablen werden zur Compilezeit ausgewertet.
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.
Optionale Typen statt Nullpointer
In anderen Programmiersprachen sind Nullzeiger die Quelle vieler Laufzeitprobleme, und werden sogar beschuldigt, der schlimmste Fehler der Computerwissenwschaft zu sein.
Rohe Pointer können in Zig nicht Null sein:
test "null @intToPtr" {
const foo: *i32 = @ptrFromInt(0x0);
_ = foo;
}
$ zig test test.zig
doctest-215c0835/test.zig:2:35: error: pointer type '*i32' does not allow address zero
const foo: *i32 = @ptrFromInt(0x0);
^~~
Jedoch kann jeder Typ durch ein vorgestelltes ? in einen optionalen Typ verwandelt werden:
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.
Um einen optionalen Typ zu entpacken, kann ein orelse
verwendet werden, um einen Default-Wert anzugeben:
// malloc prototype included for reference
extern fn malloc(size: size_t) ?*u8;
fn doAThing() ?*Foo {
const ptr = malloc(1234) orelse return null;
// ...
}
Stattdessen kann auch ein if verwendet werden:
fn doAThing(optional_foo: ?*Foo) void {
// do some stuff
if (optional_foo) |foo| {
doSomethingWithFoo(foo);
}
// do some stuff
}
Dieselbe Syntax funktioniert mit 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
Manuelle Speicherverwaltung
Eine in Zig verfasste Bibliothek kann überall verwendet werden:
- Desktopanwendungen
- Server mit geringer Latenz
- Betriebssystemkernel
- Embedded-Geräte
- Echtzeitsoftware, z.B. Liveauftritte, Flugzeuge, Herzschrittmacher
- In Webbrowsern oder anderen Plugins mit WebAssembly
- Mit anderen Programmiersprachen, über die C ABI
Um das zu erreichen, müssen Zig-Programmierer ihren Speicher selbst verwalten und mit scheiternder Speicherallokation umgehen.
Das trifft auch auf die Standardbibliothek zu. Alle Funktionen, die Speicher allozieren müssen, nehmen einen Allocator
als Parameter an. Damit kann die Standardbibliothek sogar auf dem Freestanding-Target verwendet werden.
Außer einem neuen Ansatz zur Fehlerbehandlung, stellt Zig defer und errdefer zur Verfügung, um alle Ressourcenverwaltung – nicht nur Speicher – einfach und leicht verifizierbar zu machen.
Für Beispiele von defer
siehe Integration mit C-Bibliotheken ohne FFI/Bindings. Hier ist ein Beispiel von 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;
}
};
Ein neuer Ansatz zur Fehlerbehandlung
Fehler sind Werte, und können nicht ignoriert werden:
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-071e6909/discard.zig:4:30: error: error is discarded
_ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
doctest-071e6909/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
Fehler können mit catch verarbeitet werden:
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
Das Schlüsselwort try ist eine Abkürzung für 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: 0x10664d2 in openatZ (try)
.NOENT => return error.FileNotFound,
^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:846:16: 0x1036eb7 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:801:5: 0x1033a0e in openFile (try)
return self.openFileZ(&path_c, flags);
^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e57b440c/try.zig:4:18: 0x1033858 in main (try)
const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
^
Bemerke, dass das ein Error Return Trace ist, kein Stacktrace. Der Code hat nicht den Preis einer Stackabwicklung gezahlt, um den Trace zu erhalten.
Das switch-Schlüsselwort, benutzt mit einem Fehlerwert, stellt sicher, dass alle möglichen Fehler behandelt werden:
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-eee53895/test.zig:4:40: error: switch must handle all possibilities
_ = parseInt("hi", 10) catch |err| switch (err) {};
^~~~~~~~~~~~~~~
doctest-eee53895/test.zig:4:40: note: unhandled error value: 'error.InvalidCharacter'
doctest-eee53895/test.zig:4:40: note: unhandled error value: 'error.DigitExceedsRadix'
doctest-eee53895/test.zig:4:40: note: unhandled error value: 'error.Overflow'
Das Schlüsselwort unreachable kann genutzt werden, um zu versichern, dass kein Fehler auftreten kann:
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 3045 panic: attempt to unwrap error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/posix.zig:1764:23: 0x1069412 in openatZ (unreachable)
.NOENT => return error.FileNotFound,
^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:846:16: 0x1038697 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:801:5: 0x1035e6e in openFile (unreachable)
return self.openFileZ(&path_c, flags);
^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-1b0f2130/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)
Das führt in den ungesicherten Buildmodi zu undefiniertem Verhalten und sollte nur genutzt werden, wenn der Erfolg wirklich garantiert ist.
Stacktraces auf allen Targets
Die Stacktraces und Error Return Trace auf dieser Seite funktionieren für alle Targets mit Tier 1 Support und einige mit Tier 2 Support. Sogar Freestanding!
Außerdem kann die Standardbibliothek an jedem Punkt einen Stacktrace aufzeichnen und später ausgeben:
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: 0x1038327 in captureStackTrace (stack_traces)
addr.* = it.next() orelse {
^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e7eb0ab0/stack_traces.zig:26:32: 0x1035d8c in foo (stack_traces)
std.debug.captureStackTrace(null, &trace1);
^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e7eb0ab0/stack_traces.zig:16:8: 0x1033d08 in main (stack_traces)
foo();
^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x10335b9 in posixCallMainAndExit (stack_traces)
root.main();
^
second one:
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:356:29: 0x1038327 in captureStackTrace (stack_traces)
addr.* = it.next() orelse {
^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e7eb0ab0/stack_traces.zig:30:32: 0x1035dac in bar (stack_traces)
std.debug.captureStackTrace(null, &trace2);
^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-e7eb0ab0/stack_traces.zig:17:8: 0x1033d0d in main (stack_traces)
bar();
^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x10335b9 in posixCallMainAndExit (stack_traces)
root.main();
^
Diese Technik wird für den (in Entwicklung befindlichen) GeneralPurposeDebugAllocator verwendet.
Generische Datenstrukturen und Funktionen
Typen sind Werte, die zur Compilezeit bekannt sein müssen:
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.
Eine generische Datenstruktur ist einfach eine Funktion, die einen type
zurückgibt:
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
Introspektion und Codeausführung zur Compilezeit
Die Builtinfunktion @typeInfo erlaubt Introspektion:
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
Die Standardbibliothek benutzt diese Technik, um formatierte Ausgabe zu implementieren. Obwohl es eine kleine, einfache Sprache ist, wurde die formatierte Ausgabe vollständig in Zig programmiert. Währenddessen sind die Compilierfehler für printf
in den C-Compiler und das Formatierungsmakro in den Rust-Compiler hartkodiert.
Zig kann auch Funktionen und Codeblöcke zur Compilezeit auswerten. In einigen Kontexten, wie der Initialisierung von globalen Variablen, geschieht dies implizit. Andernfalls kann Code mit dem Schlüsselwort comptime explizit zur Compilezeit ausgewertet werden. In Kombination mit Assertions ist dies ein mächtiges Werkzeug:
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-a9224f98/test.zig:15:15: note: called from here
assert(array.len == 12345);
~~~~~~^~~~~~~~~~~~~~~~~~~~
Integration mit C-Bibliotheken ohne FFI/Bindings
@cImport importiert direkt Typen, Variablen, Funktionen und einfache Makros aus C-Bibliotheken und kann sogar Funktionen von C nach Zig übersetzen.
Hier ist ein Beispiel, das mit libsoundio ein Sinuswelle ausgibt:
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
Dieser Zig-Code ist signifikant einfacher als der äquivalente C-Code, hat mehr Sicherheitsvorkehrungen, und all das wird mit einem einfache @cImport
der C-Headerdateien erreicht – ohne API-Bindings.
Zig kann C-Bibliotheken besser nutzen als das C selbst kann.
Zig ist auch ein C-Compiler
Hier ist ein Beispiel dafür, wie Zig C-Code kompiliert:
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
Die Flag --verbose-cc
zeigt, welcher C-Compiler-Befehl ausgeführt wird:
$ 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
Wird der Befehl erneut ausgeführt, bricht er sofort ab, ohne den Compiler aufzurufen:
$ time zig build-exe hello.c --library c --verbose-cc
real 0m0.027s
user 0m0.018s
sys 0m0.009s
Das geschieht dank dem Build Artifact Caching. Zig parst automatisch die erzeugte Depfile und benutzt ein robustes Cachesystem, um redundante Arbeit zu vermeiden.
Nicht nur kann Zig C kompilieren, sondern es gibt auch einen sehr guten Grund, Zig als C-Compiler zu nutzen: Zig enthält libc.
Export von Funktionen, Variablen und Typen für C-Code
Ein primärer Einsatzfall für Zig ist es, eine Bibliothek mit C ABI zu exportieren, die von anderen Sprachen genutzt werden kann. Das Schlüsselwort export
vor Funktionen, Variablen und Typen macht sie zu einem Teil der API der Bibliothek:
mathtest.zig
export fn add(a: i32, b: i32) i32 {
return a + b;
}
Um eine statische Bibliothek zu erzeugen:
$ zig build-lib mathtest.zig
Um eine dynamische Bibliothek zu erzeugen:
$ zig build-lib mathtest.zig -dynamic
Hier ein Beispiel des Zig-Buildsystems:
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
Crosscompiling ist ein primärer Einsatzfall
Zig kann für jedes der Targets im Support Table mit Tier 3 Support oder besser kompilieren. Dazu muss keine “cross toolchain” oder Ähnliches muss installiert werden. Hier ist ein natives 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!
Und jetzt Builds für x86_64-windows, x86_64-macos und 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
Das funktioniert auf jedem Target mit Tier 3+, für jedes Target mit Tier 3+.
Zig enthält libc
zig targets
listet unter anderem die verfügbaren libc-Targets auf:
...
"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"
],
Das bedeutet, dass --library c
für diese Targets von keinerlei Systemdateien abhängt!
Sehen wir uns wieder das Hello World-Beispiel in C an:
$ 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)
Im Gegensatz zu glibc unterstützt musl statische Kompilierung:
$ zig build-exe hello.c --library c -target x86_64-linux-musl
$ ./hello
Hello world
$ ldd hello
not a dynamic executable
In diesem Beispiel hat Zig musls Quellcode kompiliert und verlinkt. Dank dem Cachesystem bleibt das Build von musl-libc für x86_64-linux
verfügbar und kann bei Bedarf sofort genutzt werden.
Das bedeutet, dass die Funktionalität auf jeder Plattform verfügbar ist. Nutzer von Windows und macOS können Zig- und C-Code für jedes der obigen Targets kompilieren und gegen libc linken. Auf ähnliche Art und Weise kann Code für andere Prozessorarchitekturen crosskompiliert werden:
$ 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 mancher Hinsicht kann Zig besser C kompilieren als C-Compiler!
Diese Funktionalität ist mehr als eine mit Zig gebündelte Cross-Toolchain. Zum Beispiel sind die libc-Header, die Zig mitbringt, unkomprimiert 22 MiB groß. Dabei kommen die Header für musl libc + Linux auf x86_64 alleine auf 8 MiB, und die Header für glibc machen 3.1 MiB aus (glibc fehlen die Linux-Header), aber Zig enthält momentan 40 libcs. Ohne Bündelung wären das 444 MiB. Dank meines Tools process_headers jedoch, und ein wenig guter alter manueller Arbeit, bleiben Zigs binäre Tarballs bei insgesamt rund 30 MiB, trotz Unterstützung für libc auf all diesen Targets, sowie compiler-rt, libunwind und libcxx, und dem Clang-kompatiblen C-Compiler. Zum Vergleich: das Build von clang 8.0.0 von llvm.org für Windows ist 132 MiB groß.
Beachte, dass nur die Targets im Tier 1 Support ausführlich getestet wurden. Es ist geplant, mehr libcs hinzuzufügen (auch für Windows), und Testabdeckung für Builds gegen alle libcs zu erreichen.
Ein Paketmanager für Zig ist geplant, aber noch nicht fertig. Damit soll es möglich werden, Pakete für C-Bibliotheken zu erstellen. Dies würde das Zigs Buildsystem für Zig- und C-Programmierer gleichermaßen attraktiv machen.
Zigs Buildsystem
Zig enthält ein Buildsystem, das make, cmake oder Ähnliches ersetzen kann.
$ 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);
}
Schauen wir uns das Menü --help
an:
$ 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
Einer der verfügbaren Steps ist es, den Code direkt auszuführen (run
):
$ zig build run
All your base are belong to us.
Hier sind einige Buildscripts als Beispiel:
- Build script of OpenGL Tetris game
- Build script of bare metal Raspberry Pi 3 arcade game
- Build script of self-hosted Zig compiler
Parallelität mit async-Funktionen
Zig 0.5.0 hat async-Funktionen eingeführt. Dieses Feature ist nicht vom Hostsystem oder Heapspeicher abhängig. Das bedeutet, dass async-Funktionen auch auf dem Freestanding-Target verfügbar sind.
Zig leitet automatisch ab, ob eine Funktion async ist, und erlaubt async
/await
auf nicht-async-Funktionen, was Zig-Bibliotheken agnostisch gegenüber blockierendem oder asynchronen I/O macht. Zig vermeidet Funktionenfarben.
Die Standardbibliothek implementiert eine Eventschleife, die async-Funktionen über einen Threadpool verteilt und damit N:M-Concurrency erlaubt. An den Bereichen der Multithreading-Sicherheit und Erkennung von Race-Conditions wird aktiv gearbeitet.
Breite Menge an unterstützten Targets
Zig kommuniziert die Unterstützung von verschiedenen Targets mit “Support Tiers”. Beachte, dass die Anforderungen für Tier 1 Support hoch sind – Tier 2 Support ist immer noch ziemlich nützlich.
Support Table
WebAssembly Support Table
Freestanding | emscripten | WASI | |
---|---|---|---|
wasm32 | Tier 1 | Tier 3 | Tier 1 |
wasm64 | Tier 4 | Tier 4 | Tier 4 |
Tier-System
Tier 1 Support
- Zig kann für diese Targets nicht nur Maschinencode erzeugen, auch die plattformunabhängigen Abstraktionen der Standardbibliothek wurden jeweils vollständig implementiert.
- Der CI-Server testet diese Targets automatisch bei jedem Commit zum Master-Branch, und aktualisiert die Downloadseite mit Links zu den vorkompilierten Binärdateien.
- Diese Targets haben vollständige Debuginformations-Fähigkeiten und erzeugen bei fehlgeschlagenen Assertions Stacktraces.
- die libc ist auch beim Crosskompilieren verfügbar.
- Alle Verhaltenstests und anwendbaren Standardbibliothekstests werden für diese Targets bestanden. Alle Features der Sprache funktionieren korrekt.
Tier 2 Support
- Die Standardbibliothek unterstützt diese Targets, kann aber teilweise “Unsupported OS”-Kompilezeitfehler erzeugen. Um die Lücken in der Standardbibliothek auszufüllen, kann gegen libc oder andere Bibliotheken verlinkt werden.
- Diese Targets funktionieren, aber werden möglicherweise nicht automatisch getestet, können also gelegentlich rückfällig werden.
- Einige Tests werden für diese Targets möglicherweise deaktiviert, während wir an Tier 1 Support arbeiten.
Tier 3 Support
- Die Standardbibliothek weiß wenig bis gar nichts von diesen Targets.
- Weil Zig auf LLVM basiert, kann es momentan für diese Targets kompilieren, und in LLVM sind sie standardmäßig aktiviert.
- Diese Targets sind nicht standardmäßig getestet; um zu ihnen zu kompilieren, muss man wahrscheinlich zu Zig beitragen.
- Der Compiler muss möglicherweise aktualisiert werden, mit Informationen zu
- Aufrufkonvention der C ABI für das jeweilige Target
- Den Größen der Integertypen in C
- Bootstrapcode und Standard-Panikhandler
zig targets
enthält diese Targets auf jeden Fall
Tier 4 Support
- Unterstützung für diese Targets ist rein experimentell.
- LLVM enthält dies möglicherweise als experimentelles Target; damit es verfügbar ist, sind Zig-seitige Dateien oder ein speziell konfiguriertes LLVM-Build notwendig.
zig targets
zeigt das Target an, wenn es verfügbar ist. - Dieses Target wird möglicherweise von einer offiziellen Seite als veraltet eingestuft, wie zum Beispiel macos/i386; in dem Fall wird es Tier 4 nie verlassen.
- Dieses Target unterstützt möglicherweise nur
--emit asm
und kann keine Objektdateien ausgeben.
Einfache Unterstützung von Paketen
Der Compiler ist noch nicht vollständig selbst-gehostet, aber es bleiben unter allen Umständen höchstens drei Schritte, um von einem System-C++-Compiler aus einen eigenständigen Zig-Compiler für jedes Target zu erreichen. Wie Maya Rashish anmerkt, lässt sich Zig angenehm und schnell auf andere Plattformen portieren.
Nicht-Debug-Buildmodi sind reproduzierbar/deterministisch.
Es gibt eine JSON-Version der Downloadseite.
Einige Mitglieder des Teams haben Erfahrung im Verwalten von Paketen.
- Daurnimator unterstützt ein Paket für Arch Linux
- Marc Tiehuis unterstützt das Paket für Visual Studio Code.
- Andrew Kelley hat etwa ein Jahr lang Debian- und Ubuntupakete verwaltet und trägt gelegentlich zu nixpkgs bei.
- Jeff Fowler verwaltet das Homebrew-Paket und begann das Paket für Sublime (jetzt verwaltet von emekoi).