Übersicht
Funktionshighlights
Kleine, einfache Sprache
Der Fokus liegt darauf die Anwendung zu debuggen, anstatt seine Kenntnisse der Programmiersprache zu debuggen.
Die gesamte Syntax von Zig wird mit einer 580-zeiligen PEG-Grammatikdatei angegeben.
Es gibt keinen versteckten Kontrollfluss, keine versteckten Speicherzuweisungen, keinen Präprozessor und keine Makros. Wenn es nicht so aussieht, als würde der Zig-Code verzweigen, um eine Funktion aufzurufen, dann tut er das auch nicht. Das bedeutet, dass man sicher sein kann, dass der folgende Code nur foo()
und dann bar()
aufruft, und das ist garantiert, ohne dass man die Typen von irgendetwas kennen müsste:
var a = b + c.d;
foo();
bar();
Beispiele versteckten Kontrollflusses:
- D hat
@property
-Funktionen, das sind Methoden, die man mit etwas aufruft, das wie ein Feldzugriff aussieht, im obigen Beispiel könnte alsoc.d
eine Funktion aufrufen. - C++, D und Rust haben Operatorüberladung, so dass der Operator „+“ eine Funktion aufrufen kann.
- C++, D und Go haben 'throw/catch'-Ausnahmen, also könnte
foo()
eine Ausnahme auslösen und verhindern, dassbar()
aufgerufen wird.
Zig fördert die Code-Wartung und Lesbarkeit, indem der gesamte Kontrollfluss ausschließlich mit Sprachschlüsselwörtern und Funktionsaufrufen verwaltet wird.
Leistung und Sicherheit: Wähle zwei
Zig hat vier Build-Modi, und sie können alle bis zur Granularität des Geltungsbereichs gemischt und angepasst werden.
Parameter | Debug | ReleaseSafe | ReleaseFast | ReleaseSmall |
---|---|---|---|---|
Optimierungen – verbessern die Geschwindigkeit, beeinträchtigen das Debugging, beeinträchtigen die Kompilierzeit | -O3 | -O3 | -Os | |
Laufzeitsicherheitsprüfungen – beeinträchtigen die Geschwindigkeit, beeinträchtigen die Größe, führen zu Abstürzen statt undefiniertem Verhalten | On | On |
So sieht Integerüberlauf zur Kompilierungszeit aus, unabhängig vom Build-Modus:
So sieht es zur Laufzeit in sicherheitsgeprüften Builds aus:
Diese Stacktraces funktionieren auf allen Zielplatformen, einschließlich freistehender Systeme.
Mit Zig kann man sich auf einen sicherheitsaktivierten Build-Modus verlassen und die Sicherheit an Leistungsengpässen selektiv deaktivieren. Das vorherige Beispiel könnte beispielsweise wie folgt geändert werden:
Zig verwendet undefiniertes Verhalten als messerscharfes Werkzeug sowohl zur Fehlerprävention als auch zur Leistungssteigerung.
Apropos Leistung: Zig ist schneller als C.
– Die Referenzimplementierung verwendet LLVM als Backend für modernste Optimierungen.
- Was andere Projekte "Link Time Optimization" nennen, macht Zig automatisch.
- Für native Ziele sind erweiterte CPU-Funktionen aktiviert (-march=native), dank der Tatsache, dass Cross-Compiling ein erstklassiger Anwendungsfall ist.
- Sorgfältig ausgewähltes undefiniertes Verhalten. Beispielsweise haben in Zig sowohl vorzeichenbehaftete als auch vorzeichenlose Ganzzahlen ein undefiniertes Verhalten bei Überlauf, im Gegensatz zu ausschließlich vorzeichenbehafteten Ganzzahlen in C. Dies erleichtert Optimierungen, die in C nicht verfügbar sind.
- Zig stellt direkt einen SIMD-Vektortyp bereit, wodurch das Schreiben von portablem vektorisiertem Code erleichtert wird.
Es gilt zu beachten, dass Zig keine völlig sichere Sprache ist. Wer die Sicherheitsgeschichte von Zig verfolgen möchte, kann diese Ausgaben abonnieren:
- enumerate all kinds of undefined behavior, even that which cannot be safety-checked
- make Debug and ReleaseSafe modes fully safe
Zig konkurriert mit C, anstatt davon abhängig zu sein
Die Zig-Standardbibliothek ist in libc integriert, aber nicht davon abhängig. Hier ist "Hello World":
Bei der Kompilierung mit -O ReleaseSmall
, ohne Debugsymbole und im Single-Thread-Modus entsteht eine statische ausführbare Datei mit 9,8 KiB für das Ziel x86_64-linux:
$ zig build-exe hello.zig -O ReleaseSmall -fstrip -fsingle-threaded
$ wc -c hello
9944 hello
$ ldd hello
not a dynamic executable
Ein Windows-Build ist sogar noch kleiner und umfasst 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
Reihenfolgeunabhängige Top-Level-Deklarationen
Deklarationen auf oberster Ebene wie globale Variablen sind reihenfolgeunabhängig und werden verzögert analysiert. Die Initialisierungswerte globaler Variablen werden zur Kompilierzeit ausgewertet.
Optionaler Typ anstelle von Nullzeigern
In anderen Programmiersprachen sind Nullzeiger (null pointer) die Quelle vieler Laufzeitausnahmen und werden sogar als der schlimmste Fehler der Informatik bezeichnet.
Unmodifizierte Zig-Zeiger können nicht NULL sein:
Allerdings kann jeder Typ zu einem optionalen Typ gemacht werden, indem man ihm ein ? voranstellt:
Um auf einen optionalen Wert zuzugreifen, kann man orelse
verwenden, um einen Standardwert zu liefern:
Eine weitere Möglichkeit ist die Verwendung von if
:
Der gleiche Ausdruck funktioniert mit while:
Manuelle Speicherverwaltung
Eine in Zig geschriebene Bibliothek kann überall verwendet werden:
- Desktopanwendungen
- Server mit geringer Latenz
- Betriebssystemkernel
- Embedded devices
- Echtzeitanwendungen, z. B. Live-Auftritte, Flugzeuge, Herzschrittmacher
- In Web-Browsern oder anderen Erweiterungen mit WebAssembly
- Durch andere Programmiersprachen, unter Verwendung des C-ABI
Um dies zu erreichen, müssen Zig-Programmierer ihren eigenen Speicher verwalten und Fehler bei der Speicherzuweisung beheben.
Das gilt auch für die Zig-Standardbibliothek. Alle Funktionen, die Speicher zuweisen müssen, akzeptieren einen Allocator-Parameter. Daher kann die Zig-Standardbibliothek auch für ein "freistehende Ziel" verwendet werden.
Zusätzlich zu Einem neuen Ansatz zur Fehlerbehandlung bietet Zig defer und errdefer, um die gesamte Ressourcenverwaltung – nicht nur die des Speichers – einfach und leicht überprüfbar zu machen.
Ein Beispiel für „defer“ findet man unter Integration mit C-Bibliotheken ohne FFI/Bindings. Hier ist ein Beispiel für die Verwendung von errdefer
:
Ein neuer Ansatz zur Fehlerbehandlung
Fehler sind Werte und dürfen nicht ignoriert werden:
Fehler können mit catch behandelt werden:
Das Schlüsselwort try ist eine Abkürzung für catch |err| return err
:
Es gilt zu beachten, dass es sich um einen Error-Return-Trace und nicht um einen Stack-Trace handelt. Der Code hat nicht den Preis für das Abwickeln des Stacks bezahlt, um diesen Trace zu erstellen.
Das bei einem Fehler verwendete Schlüsselwort switch stellt sicher, dass alle möglichen Fehler behandelt werden:
Das Schlüsselwort unreachable wird verwendet, um sicherzustellen, dass keine Fehler auftreten:
Dies ruft undefiniertes Verhalten in den unsicheren Build-Modi auf. Man sollte es daher nur verwenden, wenn der Erfolg garantiert ist.
Stacktraces auf allen Zielen
Die auf dieser Seite angezeigten Stacktraces und Error-Return-Traces funktionieren auf allen Tier-1-Support- und einigen Tier-2-Support-Zielen. Sogar auf freistehenden Zielen!
Darüber hinaus verfügt die Standardbibliothek über die Möglichkeit, an jedem beliebigen Punkt einen Stacktrace zu erfassen und ihn später in die Standardfehlerausgabe zu übertragen:
Hier kann man sehen, wie diese Technik im laufenden GeneralPurposeDebugAllocator-Projekt verwendet wird.
Generische Datenstrukturen und Funktionen
Typen sind Werte, die zur Kompilierzeit bekannt sein müssen (comptime known):
Eine generische Datenstruktur ist einfach eine Funktion, die einen type
zurückgibt:
Reflexion zur Kompilierungszeit und Codeausführung zur Kompilierungszeit
Die integrierte Funktion @typeInfo bietet Reflexion:
Die Zig-Standardbibliothek verwendet diese Technik, um formatierte Ausgabe zu implementieren. Obwohl es sich um eine kleine, einfache Sprache handelt, ist die formatierte Ausgabe von Zig vollständig in Zig implementiert. In C hingegen sind Kompilierungsfehler für 'printf' fest im Compiler codiert. In Rust ist das Makro für formatierte Ausgabe ebenfalls fest im Compiler codiert.
Zig kann auch Funktionen und Codeblöcke zur Kompilierzeit auswerten. In einigen Kontexten, wie z. B. bei der Initialisierung globaler Variablen, wird der Ausdruck zur Kompilierzeit implizit ausgewertet. Andernfalls kann man Code zur Kompilierzeit explizit mit dem Schlüsselwort comptime auswerten. Das kann besonders leistungsfähig sein, wenn es mit 'Assertions' (Programmprüfungen) kombiniert wird:
Integration mit C-Bibliotheken ohne FFI/Bindings
@cImport importiert direkt Typen, Variablen, Funktionen und einfache Makros zur Verwendung in Zig. Es übersetzt sogar Inline-Funktionen von C in Zig.
Hier ist ein Beispiel für die Ausgabe einer Sinuswelle mit libsoundio:
sine.zig
$ zig build-exe sine.zig -lsoundio -lc
$ ./sine
Output device: Built-in Audio Analog Stereo
^C
Dieser Zig-Code ist wesentlich einfacher als der entsprechende C-Code und verfügt zudem über mehr Sicherheitsvorkehrungen. Dies alles wird durch den direkten Import der C-Headerdatei erreicht – keine API-Bindungen.
Zig kann C-Bibliotheken besser verwenden als C.
Zig ist auch ein C-Compiler
Hier ist ein Beispiel dafür, wie Zig C-Code erstellt:
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
Mittels --verbose-cc
kann man sehen, welcher C-Compilerbefehl ausgeführt wurde:
$ 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
Es gilt zu beachten, dass bei einer erneuten Ausführung des Befehls keine Ausgabe erfolgt und der Vorgang sofort beendet wird:
$ time zig build-exe hello.c --library c --verbose-cc
real 0m0.027s
user 0m0.018s
sys 0m0.009s
Das ist dem Build-Artifact-Caching zu verdanken. Zig analysiert die .d-Datei automatisch und verwendet ein robustes Caching-System, um doppelte Arbeit zu vermeiden.
Zig kann nicht nur C-Code kompilieren, sondern es gibt auch einen sehr guten Grund, Zig als C-Compiler zu verwenden: Zig wird mit libc ausgeliefert.
Export von Funktionen, Variablen und Typen, von denen C-Code abhängt
Einer der wichtigsten Anwendungsfälle für Zig ist das Exportieren einer Bibliothek mit der C-ABI, die von anderen Programmiersprachen aufgerufen werden kann. Das Schlüsselwort export
vor Funktionen, Variablen und Typen bewirkt, dass diese Teil der Bibliotheks-API sind:
mathtest.zig
Um eine statische Bibliothek zu erstellen:
$ zig build-lib mathtest.zig
Um eine dynamische Bibliothek zu erstellen:
$ zig build-lib mathtest.zig -dynamic
Hier ein Beispiel mit dem 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
$ zig build test
1379
Cross-Kompilierung ist ein erstklassiger Anwendungsfall
Zig kann für alle Ziele aus der Support-Tabelle (see latest release notes) mit Tier-3-Support oder besser bauen. Es muss keine "Cross-Toolchain" oder ähnliches installiert werden. Hier ist ein natives "Hello World":
Jetzt wird es für x86_64-Windows, x86_64-MacOS und aarch64-Linux erstellt:
$ 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
Dies funktioniert auf jedem Tier-3+-Ziel, für jedes Tier-3+-Ziel.
Zig wird mit libc ausgeliefert
Alle verfügbaren libc-Ziele kann man mit zig targets
finden:
...
"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 Ziele nicht von irgendwelchen Systemdateien abhängig ist!
Schauen wir uns das C-Hello-World-Beispiel noch einmal 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)
glibc unterstützt den statischen Build nicht, musl hingegen schon:
$ 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 die 'musl libc' aus dem Quellcode erstellt und dann damit verknüpft. Der Build der 'musl libc' für x86_64-Linux bleibt dank des Caching-Systems verfügbar, sodass diese 'libc' jederzeit sofort verfügbar ist, wenn sie erneut benötigt wird.
Das bedeutet, dass diese Funktionalität auf jeder Plattform verfügbar ist. Windows- und macOS-Benutzer können Zig- und C-Code erstellen und ihn mit 'libc' verknüpfen, für jedes der oben aufgeführten Ziele. Ebenso kann Code für andere Architekturen plattformübergreifend kompiliert 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 ist Zig ein besserer C-Compiler als andere C-Compiler!
Diese Funktionalität ist mehr als das Bündeln einer Cross-Compilation-Toolchain zusammen mit Zig. Beispielsweise beträgt die Gesamtgröße der von Zig mitgelieferten libc-Header unkomprimiert 22 MiB. Die Header für 'musl libc' + Linux-Header auf x86_64 allein sind dagegen 8 MiB groß und für 'glibc' 3,1 MiB (glibc fehlen die Linux-Header), obwohl Zig derzeit mit 40 'libcs' ausgeliefert wird. Bei einer einfachen Bündelung wären das 444 MiB. Dank des process-headers-Tools und einiger guter alter Handarbeit bleiben die Zig-Binär-Tarballs insgesamt etwa 30 MiB groß, obwohl sie 'libc' für alle diese Ziele sowie 'compiler-rt', 'libunwind' und 'libcxx' unterstützen und obwohl es sich um einen C-Compiler handelt, der mit Clang kompatibel ist. Zum Vergleich: Der Windows-Binär-Build von 'clang 8.0.0' selbst von llvm.org ist 132 MiB groß.
Es gilt zu beachten, dass nur die Tier-1 Support-Ziele gründlich getestet wurden. Es ist geplant, weitere 'libcs' hinzuzufügen (auch für Windows) und Testabdeckung für den Build mit allen 'libcs' hinzuzufügen.
Zig Build-System
Zig verfügt über ein Build-System, sodass man weder 'make' noch 'cmake' oder ähnliches benötigt.
$ zig init-exe
Created build.zig
Created src/main.zig
Next, try `zig build --help` or `zig build run`
src/main.zig
build.zig
Werfen wir einen Blick auf das Menü mit --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
Man kann sehen, dass einer der verfügbaren Schritte ausgeführt wird.
$ zig build run
All your base are belong to us.
Hier sind einige Beispiele für Build-Skripte:
- 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 über asynchrone Funktionen
Der folgende Abschnitt wurde aus der Übersetzung entfernt, da Zig derzeit keine asynchrone Funktionen mehr unterstützt und es auch nicht klar ist, ob und wann es diese wieder geben wird, siehe: async/await/suspend/resume
Unterstützung einer großen Auswahl an Zielen
Zig verwendet ein "Support-Tier"-System, um den Grad der Unterstützung für verschiedene Ziele anzugeben.
Support Table as of Zig 0.11.0
Einfach für Paketbetreuer
Der Zig-Compiler ist vollständig selbstgehostet.
Nicht-Debug-Build-Modi sind reproduzierbar/deterministisch.
Es gibt eine JSON-Version der Download-Seite.
Mehrere Mitglieder des Zig-Teams haben Erfahrung in der Paketwartung.
- Daurnimator verwaltet das Arch Linux-Paket
- Marc Tiehuis verwaltet das Visual Studio Code-Paket.
- Andrew Kelley hat etwa ein Jahr mit der Paketverwaltung von Debian und Ubuntu verbracht und trägt gelegentlich zu nixpkgs bei.
- Jeff Fowler verwaltet das Homebrew-Paket und hat das Sublime-Paket gestartet (jetzt verwaltet von emekoi).