Main
Main
[Link]
1
[Link]
2
3 [Link]
4 [Link]
[Link]
Insbesondere bedeutet das: keine versteckten Allokationen, bei denen die Sprache ohne Zutun
des Entwicklers dynamisch Speicher alloziert. Alles was mit der Allokation von dynamischem
Speicher zu tun hat ist in Zig explizit!
Fun-Fact: Während der StackOverflow 2024 Developer Survey5 gaben 6.2% der Befragten an
„umfangreiche Entwicklungsarbeiten“ in Zig getätigt zu haben und 73.8% wollen die Sprache
im kommenden Jahr (2025) nutzen. Damit ist Zig trotz seines Alpha-Status eine gern genutzte
Programmiersprache und reiht sich von der Zahl der Anwender neben Sprachen wie Swift, Dart,
Elixir und Ruby ein.
Zielgruppe
Falls Sie bereits Erfahrung mit C oder einer anderen systemnahen Programmiersprache haben
und mehr über Zig erfahren wollen ist diese Buch für Sie. Wenn Sie Erfahrung mit einer höheren
Programmiersprache haben und mehr über Systemprogrammierung und Zig erfahren wollen ist
dieses Buch ebenfalls für Sie.
Grundsätzlich empfehle ich Ihnen parallel zum lesen dieses Buches eigene Programmierprojekte
zu realisieren um praktische Erfahrung mit der Sprache zu sammeln. Beginnen Sie mit etwas
einfachem, vertrauten und steigern Sie sich, sobald Sie ein Gefühl für die Sprache bekommen
haben. Sie werden merken, dass die Grundlagen in Zig schnell zu erlernen sind, es gibt jedoch
auch nach einiger Zeit viel zu entdecken. Sollten Sie etwas Inspiration benötigen, so kann Ihnen
Project Euler6 eventuell weiterhelfen.
Wichtig zu erwähnen ist, dass Zig derzeit noch nicht die Version 1.0 erreicht hat, d.h. die Sprache
und damit auch die Standardbibliothek werden sich in Zukunft noch ändern. Damit kann es sein,
dass bestimmte Beispiele mit einer zukünftigen Zig-Compiler-Version nicht mehr compilieren.
Sollte das für Sie ein Dealbreaker sein, so empfehle ich Ihnen die Finger von diesem Buch zu
lassen und zu warten bis Zig Version 1.0 veröffentlicht wurde.
Voraussetzungen
Die Zig-Version, die in diesem Buch verwendet wird ist 0.13.07. Je nachdem wann Sie dieses Buch
lesen kann es sein, dass diese Version nicht mehr aktuell ist. Bei Abweichungen von der angege-
benen Version ist nicht garantiert, dass die in diesem Buch abgebildeten Beispiele compilieren.
Zwar sind die meisten Konzepte und Beispiele in diesem Buch unabhängig von einem bestimm-
ten Betriebssystem und Architektur, jedoch geht das Buch grundsätzlich von einem x86_64 Linux
System aus. Dies wird relevant wenn auf Assembler, Calling-Conventions und ähnliche Konzepte
Bezug genommen wird, da diese immer sowohl von der Architektur als auch dem Betriebssystem
abhängen. Sollte Ihr Computer eine dieser Anforderungen nicht erfüllen, so empfiehlt es sich
ggf. ein virtuelle Maschine zu verwenden8.
5 [Link]
6 [Link]
7 [Link]
8 [Link]
overview
Struktur
Die ersten Kapitel beschäftigen sich mit den Grundlagen der Programmiersprache Zig. Das
erste Kapitel bietet anhand von Beispielen einen Überblick über die Sprache. Im zweiten Kapitel
werden die grundlegenden Datentypen der Programmiersprache näher beleuchtet. In Kapitel
drei wird der Leser in grundlegende Konzepte der Speicherverwaltung eingeführt, die für die
korrekte und sichere Entwicklung von Anwendungen unabdingbar sind. In Kapitel vier nutzen
wir das Gelernte und entwickeln wir einen kleinen, minimalistischen Taschenrechner. Kapitel
fünf geht näher auf If-Statements, Switch und Schleifen ein.
Diese Buch befindet sich noch im Entstehungsprozess, das heiß es wird nach und nach um
weitere Kapitel ergänzt. Weiterhin kann sich der Inhalt einzelner Kapitel ändern.
Zig bietet für jede Compiler-Version zusätzliche Ressourcen zum Lernen der Sprache und als
Referenz9, darunter die Language Reference und die Online-Dokumentation der Standardbiblio-
thek. Diese können beim Entwickeln eigener Projekte aber auch beim nachvollziehen der Code-
Beispiele eine große Hilfe darstellen.
Konventionen
Die folgenden Konventionen werden in diesem Buch eingehalten:
Italic: Markiert neue Begriffe, URLs, Email-Adressen, Dateinamen und -endungen.
Konstanter Abstand : Wird verwendet für Programmbeispiele, sowie zum benennen von Pro-
grammbausteinen, wie etwa Variablennamen oder Umgebungsvariablen.
Konstanter Abstand Fett : Zeigt Kommandos oder andern, vom Nutzer zu tippenden, Text.
Code Beispiele
Die in diesem Buch abgebildeten Code-Beispiele finden sich auf Github unter todo zum Down-
load.
Alle Beispiele können von Ihnen ohne Einschränkung verwendet werden. Sie brauchen die
Autoren nicht explizit um Genehmigung fragen. Am Schluss geht es darum Ihnen zu helfen und
nicht darum Ihnen Steine in den Weg zu legen.
Zitierungen würden uns freuen, sind jedoch keinesfalls notwendig. Ein Zitat umfasst gewöhnlich
Titel, Autor, Publizist und ISBN. In diesem Fall wäre dies: ,,Zig Basics by David Pierre Sugar’‚.
Sollten Sie Fehler im Buch oder Code finden, die nicht auf unterschiedliche Compiler-Versionen
zurückzuführen sind können Sie uns mit einem Verbesserungsvorschlag kontaktieren.
9 [Link]
Fragen, Anmerkungen und Verbesserungen
Ich habe mein Bestes getan dieses Buch so informativ und technisch korrekt wie möglich zu
gestalten. Ich bin mir jedoch auch sicher, dass dieses Buch besser sein könnte als es gerade ist.
Sollten Sie Fehler finden oder generell Feedback zu diesem Buch geben wollen, so können Sie
mich unter david@[Link] kontaktieren. Dies gibt mir die Möglichkeit dieses Buch über die
Zeit zu verbessern. Es ist mir jedoch nicht immer möglich zu antworten. Nehmen Sie es sich
deswegen nicht zu Herzen wenn Sie nichts von mir hören.
Danksagung
TDB
Zig Basics
Kapitel 1
In diesem Kapitel schauen wir uns einige kleine Zig Programme an, damit Sie ein Gespür für
die Programmiersprache bekommen. Machen Sie sich nicht zu viele Sorgen wenn Sie nicht alles
sofort verstehen, in den folgenden Kapiteln werden wir uns mit den hier vorkommenden Konzept
noch näher beschäftigen. Wichtig ist, dass Sie diese Kapitel nicht nur lesen sondern die Beispiel
auch ausführen, um das meiste aus diesem Kapitel herauszuholen.
Zig installieren
Um Zig zu installieren besuchen Sie die Seite [Link] und folgen den Instruktionen
unter „GET STARTED“10.
Die Installation ist unter allen Betriebssystemen relativ einfach durchzuführen. In der Download
Sektion11 finden Sie vorkompilierte Zig-Compiler für die gängigsten Betriebssysteme, darunter
Linux, macOS und Windows.
Linux
Unter Linux können Sie mit dem Befehl uname -a Ihre Architektur bestimmen. In meinem Fall
ist dies X86_64 .
$ uname -a
Linux ... x86_64 x86_64 x86_64 GNU/Linux
Die Beispiele in diesem Buch basieren auf der Zig-Version 0.13.0, d.h. um den entspre-
chenden Compiler auf meinem Linux system zu installieren würde ich die Datei zig-linux-
x86_64-[Link] aus der Download-Sektion herunterladen.
10 [Link]
11 [Link]
1
Linux
$ ls zig-linux-x86_64-0.13.0
doc lib LICENSE [Link] zig
• doc: Die Referenzdokumentation der Sprache. Diese ist auch online, unter [Link]
documentation/0.13.0/, zu finden und enthält einen Überblick über die gesamte Sprache. Ich
empfehle Ihnen ergänzend zu diesem Buch die Dokumentation zu Rate zu ziehen.
• lib: Enthält alle benötigten Bibliotheken, inklusive der Standardbibliothek. Die Standardbi-
bliothek enthält viel nützliche Programmbausteine, darunter geläufige Datenstrukturen, einen
JSON-Parser, Kompressionsalgorithmen, kryptographische Algorithmen und Protokolle und
vieles mehr. Eine Dokumentation der gesamten Standardbibliothek findet sich online unter
[Link]
• zig: Dies ist ein Kommandozeilenwerkzeug mit dem unter anderem Zig-Programme kompiliert
werden können.
2
Zig Basics
Um den Zig-Compiler nach dem Entpacken auf einem Linux System zu installieren, können wir
diesen nach /usr/local/bin verschieben.
Danach erweitern wir die $PATH Umgebungsvariable um den Pfad zu unserem Zig-Compiler.
Dies können wir in der Datei ~/.profile oder auch ~/.bashrc machen12.
# ...
export PATH="$PATH:/usr/local/bin/zig-linux-x86_64-0.13.0"
Nach Änderung der Konfigurationsdatei muss diese neu geladen werden. Dies kann entweder
durch das öffnen eines neuen Terminalfensters erfolgen oder wir führen im derzeitigen Terminal
das Kommando source .bashrc in unserem Home-Verzeichnis aus. Danach können wir zum
überprüfen, ob alles korrekt installiert wurde, das Zig-Zen auf der Kommandozeile ausgeben
lassen. Das Zig-Zen kann als die Kernprinzipien der Sprache und ihrer Community angesehen
werden, wobei man dazu sagen muss, dass es nicht „die eine“ Community gibt.
$ source ~/.bashrc
$ zig zen
Windows
Der einfachste Weg um den Zig-Compiler unter Windows zu installieren ist unter Verwendung
von Visual Studio Code. Hierzu besuchen Sie [Link] und laden dort
3
Windows
zuerst den Installer für Windows herunter. Öffnen Sie nach der Installation VS-Code, suchen Sie
nach der Zig Language Extension und installieren Sie diese13.
13Stellen Sie sicher, dass Sie die korrekte Extension installieren. Der Herausgeber der Extension ist ziglang ,
markiert mit einem blauen Haken.
14 [Link]
4
Zig Basics
C:\Users\Sugar\AppData\Roaming\Code\User\globalStorage\[Link]-
do zig\zig\windows-x86_64-0.13.0\[Link] init Sugar
durch Ihren Benutzernamen zu ersetzen.
5
Abbildung 4: Neuen Pfad unter Windows hinzufügen
Angenommen wir haben die Datei zig-windows-x86_64-[Link] heruntergeladen und in den
Documents Ordner entpackt (Bei mir wäre der vollständige Pfad in diesem Fall C:\Users\Sugar\Do-
cuments\zig-windows-x86_64-0.13.0). In diesem Fall können Sie die enthaltene [Link] zugänglich
machen, indem Sie, wie oben beschrieben, die Path umgebungsvariable auswählen und Bear
beiten beziehungsweise Edit auswählen. Danach sehen Sie eine Liste aller Pfade, auf die Path
verweist. Klicken Sie auf Neu / New und tragen Sie hier C:\Users\Sugar\Documents\zig-windows-
x86_64-0.13.0 ein, wobei Sie Sugar durch Ihren Benutzernamen ersetzen. Danach drücken Sie OK.
6
Zig Basics
Abbildung 6: Nach dem Hinzufügen des Zig-Ordners zu Path sollten [Link] von überall, inner-
halb der PowerShell, aufrufbar sein
Compiler Grundlagen
Mit dem Kommando zig help lässt sich ein Hilfetext auf der Kommandozeile anzeigen, der die
zu Verfügung stehenden Kommandos auflistet.
Praktisch ist, dass Zig für uns ein neues Projekt, inklusive Standardkonfiguration, anlegen kann.
Das Kommando zig init initialisiert den gegebenen Ordner mit Template-Dateien, durch die
sich sowohl eine Executable, als auch eine Bibliothek bauen lassen. Schaut man sich die erzeugten
Dateien an so sieht man, dass Zig eine Datei namens [Link] erzeugt hat. Bei dieser handelt
es sich um die Konfigurationsdatei des Projekts. Sie beschreibt aus welchen Dateien eine Execu-
table bzw. Bibliothek gebaut werden soll und welche Abhängigkeiten (zu anderen Bibliotheken)
diese besitzen. Ein bemerkenswertes Detail ist dabei, dass [Link] selbst ein Zig Programm ist,
welches kompiliert und ausgeführt wird um die eigentlichle Anwendung zu bauen.
Die Datei [Link] enthält weitere Informationen über das Projekt, darunter dessen Namen,
die Versionsnummer, sowie mögliche Dependencies. Dependencies können dabei lokal vorliegen
und über einen relativen Pfad angegeben oder von einer Online-Quelle, wie etwa Github,
bezogen werden. Die Endung der Datei steht im übrigen für Zig Object Notation (ZON), ein
Dateiformat, welches an die Zig-Basistypen angelehnt ist.
Schauen wir in src/[Link], so sehen wir das Zig für uns ein kleines Programm geschrieben hat.
7
Compiler Grundlagen
Der Code kann auf den ersten Blick überwältigend wirken, schauen wir ihn uns deswegen Stück
für Stück an.
Mit der @import() Funktion importieren wir die Standardbibliothek ( std ) und binden diese
an eine Konstante mit dem selben Namen. Die Standardbibliothek ist eine Ansammlung von
nützlichen Funktionen und Datentypen, die während der Entwicklung von Anwendungen häu-
figer zum Einsatz kommen und deswegen von Zig zur Verfügung gestellt werden. Die Funktion
@import() wird nicht nur zum importieren der Standardbibliothek verwendet, sondern auch
um auf Module und andere, zu einem Projekt gehörende, Quelldateien zuzugreifen.
Nach der Definition der Konstante std beginnt die main Funktion:
Unsere main Funktion beginnt, wie alle Funktionen, mit fn und dem Namen der Funktion. Sie
gibt keinen Wert zurück, aus diesem Grund folgt auf die leere Parameterliste () der Rückga-
betyp void . Das Ausrufezeichen ! weist darauf hin, das die Funktion einen Fehler zurückgeben
kann. Fehler in Zig sind eigenständige Werte, die von einer Funktion zurückgegeben werden
können und sich semantisch vom eigentlichen Rückgabewert unterscheiden.
Als erstes gibt die main Funktion einen String über die Debugausgabe auf der Kommandozeile
aus. Die Funktion print erwartet dabei einen Format-String, der mit Platzhaltern (z.B. {s} )
versehen werden kann, sowie eine Liste an Ausdrücken (z.B. .{"codebase"} ) deren Werte in
den String eingefügt werden sollen. Der Platzhalter {s} gibt z.B. an, dass an der gegebenen
8
Zig Basics
Stelle ein String eingefügt werden soll. Neben s gibt es unter anderem noch d für Ganzzahlen
und any für beliebige werte.
Via [Link] können wir mit getStdIn() , getStdOut() und getStdErr() auf stdin ,
stdout und stderr zugreifen. Alle drei Funktionen geben jeweils eine Objekt vom Typ File
zurück. Die Funktion writer() welche auf der stdout-Datei aufgerufen wird, gibt einen Writer
zurück. Ein Writer ist ein Wrapper um einen beliebiges Datenobjekt (z.B. eine offene Datei, ein
Array, …) und stellt eine standartisiertes Interface zur Verfügung um Daten zu serialisieren. In
unserem Fall wird der stdout_file Writer wiederum in einen BufferedWriter gewrapped,
welcher nicht bei jedem einzelnen Schreibvorgang auf die Datei stdout zugreift, sondern erst
wenn genug Daten geschrieben wurden bzw. wenn die Funktion flush() aufgerufen wird. Die
Konstante stdout ist also ein Writer der einen Writer umschließt, der eine Datei umschließt,
in die schlussendlich geschrieben werden soll.
Der BufferedWriter ( stdout ) wird verwendet um (indirekt) den String „Run zig build test to
run the tests.“ nach stdout (standardmäßig die Kommandozeile) zu schreiben. Da diese Schreib-
operation fehlschlagen kann wird vor den Ausdruck ein try gestellt. Damit wird ein potenzieller
Fehler „nach oben“ propagiert, was im gegebenen Fall zu einem Programmabsturz führen würde,
da main keine Funktion über sich besitzt. Als Alternative könnte mit einem catch Block der
Fehler explizit abgefangen werden.
try [Link]();
Um sicher zu gehen, dass auch alle Daten aus dem BufferedWriter tatsächlich geschrieben
wurden, muss schlussendlich flush() aufgerufen werden.
Das von Zig vorbereitete „Hello, World“-Programm kann mit zig build run , von einem belie-
bigen Ordner innerhalb des Zig-Projekts, ausgeführt werden.
Im gegebenen Beispiel wurden zwei Schritte ausgeführt. Zuerst wurde der Zig-Compiler aufge-
rufen um das Programm in src/[Link] zu kompilieren und im zweiten Schritt wurde das
Programm ausgeführt. Zig platziert dabei seine Kompilierten Anwendungen in zig-out/bin und
Bibliotheken in zig-out/lib.
9
Funktionen
Zig’s Grammatik ist sehr überschaubar und damit leicht zu erlernen. Diejenigen mit Erfahrung
in anderen C ähnlichen Programmiersprachen wie C, C++, Java oder Rust sollten sich direkt
Zuhause fühlen. Die unterhalb abgebildete Funktion berechnet den größten gemeinsamer Teiler
(greatest common divisor) zweier Zahlen.
chapter01/[Link]
fn gcd(n: u64, m: u64) u64 {
return if (n == 0)
m
else if (m == 0)
n
else if (n < m)
gcd(m, n)
else
gcd(m, n % m);
}
Das fn Schlüsselwort markiert den Beginn einer Funktion. Im gegebenen Beispiel definieren
wir eine Funktion mit dem Name gcd , welche zwei Argumente m und n , jeweils vom Typ
u64 , erwartet. Nach der Liste an Argumenten in runden Klammern folgt der Typ des erwarteten
Rückgabewertes. Da die Funktion den größten gemeinsamen Teiler zweier u64 Ganzzahlen be-
rechnet ist auch der Rückgabewert vom Typ u64 . Der Körper der Funktion wird in geschweifte
Klammern gefasst.
Zig unterscheidet zwischen zwei Variablen-Typen, Variablen und Konstanten. Konstanten kön-
nen nach ihrer Initialisierung nicht mehr verändert werden, während Variablen neu zugewiesen
werden können. Funktionsargumente zählen grundsätzlich zu Konstanten, d.h. sie können nicht
verändert werden. Der Zig-Compiler erzwingt die Nutzung von Konstanten, sollte eine Variable
nach ihrer Initialisierung nicht mehr verändert werden. Dies ist eine durchaus kontroverse De-
signentscheidung, welche aber auf das Zig-Zen zurückgeführt werden kann, das besagt: ,,Favor
reading code over writing code„. Sollten Sie also eine Variable in fremden Code sehen so können
Sie sicher sein, dass diese an einer anderen Stelle manipuliert bzw. neu zugewiesen wird.
Eine Besonderheit, die Zig von anderen Sprachen unterscheidet ist, dass Integer mit beliebiger
Präzision unterstützt werden. Im obigen Beispiel handelt es sich bei u64 um eine vorzeichenlose
Ganzzahl (unsigned integer) mit 64 Bits, d.h. es können alle Zahlen zwischen 0 und 264 − 1
dargestellt werden. Zig unterstützt jedoch nicht nur u8 , u16 , u32 oder u128 sondern alle
unsigned Typen zwischen u0 und u65535 .
10
Zig Basics
Alle Zig-Basistypen sind Teil des selben union : [Link]. Das union
beinhaltet den Int Typ welcher ein struct mit zwei Feldern ist, signedness
und bits , wobei bits vom Typ u16 ist, d.h. es können alle Integer-Typen
zwischen 0 und 216 − 1 Bits verwendet werden. Ja Sie hören richtig, der Zig-
Compiler ist seit Version 0.10.0 selbst in Zig geschrieben, d.h. er ist self-hosted.
Unit Tests
Wie von einer modernen Programmiersprache zu erwarten bietet Zig von Haus aus Unterstüt-
zung für Tests. Tests beginnen mit dem Schlüsselwort test , gefolgt von einem String der den
Test bezeichnet. In geschweiften Klammern folgt der Test-Block.
chapter01/[Link]
test "assert that the gcd of 21 and 4 is 1" {
try [Link](@as(u64, 1), gcd(21, 4));
}
Die Standardbibliothek bietet unter [Link] eine ganze Reihe an Testfunktionen für
verschiedene Datentypen und Situationen. Im obigen Beispiel verwenden wir ExpectEqual ,
welche als erstes Argument den erwarteten Wert erhält und als zweites Argument das Resultat
eines Aufrufs von gcd . Die Funktion überprüft beide Werte auf ihre Gleichheit und gibt im
Fehlerfall einen error zurück. Dieser Fehler kann mittels try propagiert werden, wodurch der
Testrunner im obigen Beispiel erkennt, dass der Test fehlgeschlagen ist.
11
Innerhalb einer Datei sind Definitionen auf oberster Ebene (top-level definitions) unabhängig
von ihrer Reihenfolge, was die Definition von Tests mit einschließt. Damit können Tests an einer
beliebigen Stelle definiert werden, darunter direkt neben der zu testenden Funktion oder am
Ende einer Datei. Der Zig-Test-Runner sammelt automatisch alle definierten Tests und führt dies
beim Aufruf von zig test aus. Worauf Sie jedoch achten müssen ist, dass Sie ausgehend von
der Wurzel-Datei (in den meisten Fällen src/[Link]), die konzeptionell den Eintritspunkt für
den Compiler in ihr Programm oder Ihre Bibliothek darstellt, Zig mitteilen müssen in welchen
Dateien zusätzlich nach Tests gesucht werden soll. Dies bewerkstelligen Sie, indem Sie die
entsprechende Datei innerhalb eines Tests importieren.
Comptime
Die meisten Sprachen erlauben eine Form von Metaprogrammierung, d.h. das Schreiben von
Code der wiederum Code generiert. In C können die gefürchteten Makros mit dem Präprozessor
verwendet werden und Rust bietet sogar zwei verschiedene Typen von Makros, jeweils mit einer
eigenen Syntax. Zig bietet mit comptime seine eigene Form der Metaprogrammierung. Was
Zig von anderen kompilierten Sprachen unterscheidet ist, dass die Metaprogrammierung in der
Sprache selber erfolgt, das heißt wer Zig programmieren kann, der hat das nötige Handwerks-
zeug um auch Metaprogrammierung in Zig zu betreiben.
Ein Aufgabe für die Metaprogrammierung sehr gut geeignet ist, ist die Implementierung von
Container-Typen wie etwa [Link] . Eine ArrayList ist ein Liste von Elementen eines
beliebigen Typen, die eine Menge an Standardfunktionen bereitstellt um die Liste zu manipu-
lieren. Nun wäre es sehr aufwändig die ArrayList für jeden Typen einzeln implementieren zu
müssen. Aus diesem Grund ist ArrayList als Funktion implementiert, welche zur Compilezeit
einen beliebigen Typen übergeben bekommt auf Basis dessen einen ArrayList -Typ generiert.
Der Funktionsaufruf ArrayList(u8) wird zur Compilezeit ausgewertet und gibt einen neuen
Listen-Typen zurück, mit dem sich eine Liste an u8 Objekten managen lassen. Auf diesem Typ
wird init() aufgerufen um eine neu Instanz des Listen-Typs zu erzeugen. Mit der Funktion
append() kann z.B., ein Element an das Ende der Liste angehängt werden. Eine stark simplifi-
zierte Version von ArrayList könnte wie folgt aussehen.
chapter01/[Link]
12
Zig Basics
// Füge da Element `e` vom Typ `T` ans ende der Liste.
pub fn append(self: *@This(), e: T) !void {
// `realloc()` kopiert die Daten bei Bedarf in den neuen
// Speicherbereich aber die Allokation kann auch
// fehlschlagen. An dieser Stelle verbleiben wir der
// Einfachheit halber bei einem `try`.
[Link] = try [Link]([Link], [Link]
+ 1);
[Link][[Link] - 1] = e;
}
};
}
13
try [Link](0xAF);
try [Link](0xFE);
[Link]("{s}", .{[Link]([Link][0..])});
}
Mit dem comptime Keyword sagen wir dem Compiler, dass das Argument T zur Compilezeit
erwartet wird. Beim Aufruf von MyArrayList(u8) wertet der Compiler die Funktion aus und
generiert dabei einen neuen Typen. Das praktische ist, dass wir MyArrayList nur einmal imple-
mentieren müssen und diese im Anschluss mit einem beliebigen Typen verwenden können.
Der comptime Typ T kann innerhalb und auch außerhalb des von der Fukntion MyArrayList
zurückgegebenen Structs, anstelle eines expliziten Typs, verwendet werden.
Structs die mit init() initialisiert und mit deinit() deinitialisiert werden sind ein wieder-
kehrendes Muster in Zig. Dabei erwartet init() meist einen [Link] der von der
erzeugten Instanz verwaltet wird.
Ein weiterer Anwendungsfall bei dem Comptime zum Einsatz kommen kann ist die Implemen-
tierung von Parsern. Ein Beispiel hierfür ist der Json-Parser der Standardbibliothek ( [Link] ),
welcher dazu verwendet werden kann um Zig-Typen als Json zu serialisieren und umgekehrt15.
Um ein Zig-Objekt zu de-/serialisieren werden Informationen über den Typ des Objekts und
dessen Beschaffenheit benötigt. Hierzu kommen die Funktionen @TypeOf() und @typeInfo()
zum Einsatz, wie das folgende Beispiel zeigt.
chapter01/[Link]
const std = @import("std");
Die JavaScript Object Notation (JSON) ist eines der gängigsten Datenformate und wird unter anderem zur
15
14
Zig Basics
[Link]) });
}
Anstelle eines Typen kann anytype für Parameter verwendet werden. In diesem Fall wird der
Typ des Parameters, beim Aufruf der Funktion, abgeleitet. Zig erlaubt Reflexion (type reflection).
Unter anderem erlaubt Zig die Abfrage von (Typ-)Informationen über ein Objekt. Funktionen
denen ein @ vorangestellt sind heißen Builtin-Function (eingebaute Funktion) und werden
direkt vom Compiler bereitgestellt, d.h., sie können überall in Programmen, ohne Einbindung
der Standardbibliothek, verwendet werden.
Die Funktion @TypeOf() ist insofern speziell, als dass sie eine beliebige Anzahl an Ausdrücken
als Argument annimmt und als Rückgabewert den Typ des Resultats zurückliefert. Die Ausdrü-
cke werden dementsprechend evaluiert. Im obigen Beispiel wird @TypeOf() genutzt um den
Typen des übergebenen Objekts zu bestimmen, da isStruct() aufgrund von anytype mit
einem Objekt beliebigen Typs aufgerufen werden kann.
Die eigentliche Reflexion kann mithilfe der Funktion @typeInfo() durchgeführt werden,
die zusätzliche Informationen über einen Typ zurückliefert. Felder sowie Deklarationen von
structs , unions , enums und error Sets kommen dabei in der selben Reihenfolge vor, wie
sie auch im Source Code zu sehen sind. Im obigen Beispiel testen wir mittels eines switch
Statements ob es sich um ein struct handelt oder nicht und geben dementsprechend entweder
true oder false zurück. Sollte es sich um ein struct handeln, so iterieren wir zusätzlich über
dessen Felder und geben den Namen des Felds, sowie dessen Wert aus. Den Wert des jeweiligen
Felds erhalten wir, indem wir mittels @field() darauf zugreifen. Die Funktion @field()
erwartet als erstes Argument ein Objekt (ein Struct) und als zweites Argument einen zu Compile-
Zeit bekannten String, der den Namen des Felds darstellt, auf das zugegriffen werden soll. Damit
ist @field(s, "b") das Äquivalent zu s.b .
Für jeden Typen, mit dem isStruct() aufgerufen wird, wird eine eigene Kopie der Funktion
(zur Compile Zeit) erstellt, die an den jeweiligen Typen angepasst ist. Das Iterieren über die
einzelnen Felder eines structs muss zur Compile Zeit erfolgen, aus diesem Grund nutzt die
15
obige Funktion inline um die For-Schleife zu entrollen, d.h., aus der Schleife eine lineare
Abfolge von Instruktionen zu machen.
Reflexion kann in vielen Situationen äußerst nützlich sein, darunter der Implementierung von
Parsern für Formate wie JSON oder CBOR16, da im Endeffekt nur zwei Funktionen implementiert
werden müssen, eine zum Serialisieren der Daten und eine zum Deserialisieren. Mithilfe von
Reflexion kann dann, vom Compiler, für jeden zu serialisierenden Datentyp eine Kopie der
Funktionen erzeugt werden, die auf den jeweiligen Typen zugeschnitten ist.
Kryptographie
Ein Großteil der Anwendungen, die Sie wahrscheinlich täglich verwenden, benutzt in irgend
einer Form Kryptographie. Dabei handelt es sich grob gesagt um mathematische Algorithmen,
mit denen vorwiegend die Vertraulichkeit (Confidentiality), Integrität (Integrity) und Authenti-
zität (Authenticity) von Daten gewährleistet werden kann. Typische Anwendungsbereiche die
Kryptographie verwenden sind Messenger, Video Chats, Networking (TLS), Passwortmanager
und Smart Cards. Zig bietet in seiner Standardbibliothek bereits jetzt eine Vielzahl and krypto-
graphischen Algorithmen und Protokollen, wobei ein Großteil davon von Frank Denis17, Online
auch bekannt als jedisct1, beigetragen wurde. Ohne groß ein Authoritätsargument aufmachen
zu wollen, ist Frank der Maintainer von libsodium18 und libhydrogen19, zwei viel genutzte,
kryptographische Bibliotheken.
Wir werden uns in einem späteren Kapitel noch genauer mit Kryptographie auseinandersetzen,
machen Sie sich deshalb keine Sorgen, wenn Sie nicht alles in diesem Abschnitt auf Anhieb
verstehen. Fürs erste schauen wir uns einen gängigen Anwendungsfall von Kryptographie an, die
Verschlüsselung einer Datei. Angenommen wir haben eine Datei deren Inhalt geheim bleiben soll
und wir wollen des weiteren überprüfen können, dass der Inhalt der Datei nicht verändert wurde.
In solch einem Fall bietet sich die Verwendung eines AEAD (Authenticated Encryption with
Associated Data) Ciphers an. Zig bietet unter [Link] verschiedene AEAD Cipher an.
Die Unterschiede zwischen den Ciphern ist für dieses Beispiel Out-of-Scope. Sie müssen sich
fürs erste damit begnügen mir zu glauben, dass XChaCha20Poly1305 20 für diese Art von Problem
16 [Link]
17 [Link]
18 [Link]
19 [Link]
20 [Link]
16
Zig Basics
eine gute Wahl ist. Der Name XChaCha20Poly1305 enthält dabei zwei Informationen, die uns
Aufschluss über die Zusammensetzung des Ciphers geben:
• XChaCha20: Zur Verschlüsselung der Daten wird die „Nonce-eXtended“ Version der
ChaCha20 Stromchiffre verwendet. XChaCha20 erwartet einen Schlüssel und eine Nonce
(Number used once: Eine Byte-Sequenz die nur einmal für eine Verschlüsselung verwendet
werden darf) und leitet daraus eine Schlüsselsequenz ab, die mit dem Klartext XORed wird.
Die eXtended Version verwendet dabei eine 192-Bit Nonce anstelle einer 96-Bit Nonce, was
es deutlich sicherer macht diese zufällig mittels eines (kryptographisch sicheren) Zufallszah-
lengenerators zu erzeugen. Dieser Teil des Algorithmus ist für die Vertraulichkeit der Daten
verantwortlich.
• Poly1305 : Poly1305 ist ein Hash, der zur Erzeugung von (one-time) Message Authentica-
tion Codes (MAC) verwendet werden kann. MACs sind sogenannte Keyed-Hashfunktionen,
bei denen in einen Hash (keine Sorge, wir werden uns noch näher damit beschäftigen) ein
geheimer Schlüssel integriert wird. Die Hashsumme wird dabei in unserem Beispiel über den
Ciphertext, d.h. den Verschlüsselten Text, gebildet21. Durch den Einbezug eines Schlüssels kann
nicht nur überprüft werden, dass die Integrität der Datei nicht verletzt wurde (sie wurde nicht
verändert), sondern es kann auch sichergestellt werden, dass die MAC von Ihnen generiert
wurde, da nur Sie als Nutzer der Anwendung den geheimen Schlüssel kennen.
chapter01/[Link]
const std = @import("std");
17
Kryptographie
defer [Link]();
// Als nächstes lesen wir die übergebenen Daten von `stdin` ein.
const stdin = [Link]();
const data = try [Link](allocator, 64_000);
defer {
// Wir überschreiben die Daten bevor wir den Speicher wieder freigeben.
@memset(data, 0);
[Link](data);
}
if (mode == .encrypt) {
// Bei der Verschlüsselung müssen wir eine Reihe an (öffentlichen)
// Parametern festlegen, die bei der Entschlüsselung wiederverwendet
// werden müssen.
// Als erstes müssen wir ein Schlüssel von unserem Passwort ableiten.
// Hierfür verwenden wir die Argon2id Key-Derivation-Function (KDF).
var salt: [32]u8 = undefined;
[Link](&salt);
18
Zig Basics
.t = 3,
.m = 4096,
.p = 1,
}, .argon2id);
// Der TAG wird von der encrypt() Funktion erzeugt und später
// von decrypt() überprüft.
var tag: [XChaCha20Poly1305.tag_length]u8 = undefined;
// Der Salt, Nonce und Tag müssen mit den verschlüsselten Daten
serialisiert werden,
// da wir diese später zur Entschlüsselung benötigen.
const stdout = [Link]();
try [Link]([Link](), "{s}:{s}:{s}:{s}", .{
// Wir serialisieren die Binärdaten in Hexadezimal.
[Link](salt[0..]),
[Link](nonce[0..]),
[Link](tag[0..]),
[Link](data),
});
} else {
// Da wir die Daten in Hexadezimal serialisiert haben, müssen wir diese
// wieder voneinander trennen und in Binärdaten umwandeln.
var si = [Link](u8, data, ":");
19
[Link]("invalid data (missing nonce)", .{});
return;
}
var nonce_: [XChaCha20Poly1305.nonce_length]u8 = undefined;
_ = try [Link](&nonce_, nonce.?);
const ct = [Link]();
if (ct == null) {
[Link]("invalid data (missing cipher text)", .{});
return;
}
In diesem Beispiel laufen eine Vielzahl von Konzepten zusammen, die sie im Laufen diese
Buches noch häufiger antreffen werden. Unsere Anwendung erwartet Daten, z.B. den Inhalt
einer Datei, über stdin , sowie zwei Kommandozeilenargumente: --password und --encrypt
20
Zig Basics
bzw. --decrypt . Basierend auf diesen Argumenten werden die übergebenen Daten entweder
verschlüsselt oder entschlüsselt und nach stdout geschrieben.
Wir beginnen mit einigen Top-Level-Deklarationen, damit wir den Pfad zu Datenstrukturen,
wie etwa XChaCha20Poly1305 , nicht immer ausschreiben müssen. Weiterhin definieren wir ein
Enum Mode welches zwei operationelle Zustände ausdrücken kann, Verschlüsselung ( encrypt )
und Entschlüsselung ( decrypt ).
Innerhalb von main parsen wir zuerst die übergebenen Argumente, indem wir durch die
Funktion argsWithAllocator() einen Iterator über die Kommandozeilenargumente beziehen
und mithilfe dessen über die einzelnen Argumente iterieren. Iteratoren sind ein häufig wieder-
zufindendes Konzept und lassen sich hervorragend mit while Schleifen kombinieren. Solange
[Link]() ein Element zurückliefert, wird diese an arg gebunden und die Schleife wird
fortgeführt. Liefert next() den Wert null zurück, so wird automatisch aus der Schleife ausge-
brochen.
Danach stellen wir sicher, dass sowohl ein Passwort als auch ein Modus vom Nutzer spezifiziert
wurden. Sollte eines der beiden Argumente fehlen, so wird ein entsprechender Fehler gelogged
und der Prozess vorzeitig beendet.
Als nächstes wird eine über stdin übergebene Datei eingelesen und an die Konstante data
gebunden. Da der für die Datei benötigte Speicher dynamisch alloziert wird muss dieser
wider freigegeben werden. Hierfür wird eine defer -Block verwendet, der vor Beendigung der
Anwendung ausgeführt wird. Innerhalb dieses Blocks wird zusätzlich der Speicherinhalt mittels
@memset überschrieben.
Sowohl für die Ver- als auch Entschlüsselung muss zuerst ein geheimer Schlüssel vom übergebe-
nen Passwort, mittels einer Key-Derivation-Funktion, abgeleitet werden. Für diese Beispiel wird
Argon2id22, der Gewinner der 2015 Password Hashing Competition, verwendet. Die Berechnung
eines Schlüssels durch Argon2 hängt von den Folgenden (öffentlichen) Parametern ab:
• Salt: eine zufällige Sequenz die in die Schlüsselberechnung einfließt.
• Time: Die Anzahl an Iterationen für die Berechnung.
• Memory: Die Speicher-Kosten für die Berechnung.
• Parallelismus: Die Anzahl an parallelen Berechnungen.
22 [Link]
21
Time, Memory und Parallelismus bestimmen wie aufwändig die Ableitung eines Schlüssels ist.
Grundsätzlich gilt: je aufwendiger desto besser, jedoch schlägt sich dies auch in einer längeren
Wartezeit nieder (spielen Sie deshalb gerne mit den Parametern). Alle Parameter werden bei
der Verschlüsselung festgelegt und müssen mit dem Ciphertext zusammen gespeichert werden,
da bei der Entschlüsselung die selben Parameter wieder in die KDF einfließen müssen um den
Selben Schlüssel vom Passwort abzuleiten.
Zur Verschlüsselung wird eine zufällige Nonce generiert welche zusammen mit den zu verschlüs-
selnden Daten, einem Zeiger auf ein Array für den Tag, zusätzliche Daten (in diesem Fall der
leere String "" ) und dem abgeleiteten Schlüssel an encrypt übergeben werden. Die Funktion
verschlüsselt daraufhin die Daten. Danach wird der Salt, die Nonce, der Tag, sowie die verschlüs-
selten Daten, getrennt durch ein : , in die Standardausgabe stdout geschrieben.
Für die Entschlüsselung wird dieser String anhand der : , mittels split , aufgeteilt. Sollten
die eingelesenen Daten nicht im erwarteten Format vorliegen, das heißt Salt, Nonce, Tag oder
Ciphertext fehlen, so wird ein Fehler ausgegeben und die Anwendung beendet. Andernfalls,
werden die eingelesenen Parameter verwendet um den Ciphertext, mittels decrypt , wieder zu
entschlüsseln.
Das kleine Verschlüsselungsprogramm kann wie folgt verwendet werden:
$ cat [Link]
Hello, World!
$ cat [Link] | ./encrypt --password=supersecret --encrypt > [Link]
$ cat [Link]
828dfa14efa4b1f8242a8258a411301bd79bc4b7528294500305a4e9baaecbba:
85e4593786697e4e49212131a8e6e6bb68d25f43613dd870:ec666e95ebe1fa4c
53a1183379ae0dbd:80a7fe0475834364229c15dfb96d
$ cat [Link] | ./encrypt --password=supersecret --decrypt
Hello, World!
Graphische Applikationen
Ein weiterer Anwendungsfall für Zig ist die Entwicklung graphischer Applikationen. Hierfür
existiert eine Vielzahl an Bibliotheken, darunter GTK und QT. Was beide Bibliotheken gemein-
sam haben ist, dass sie in C beziehungsweise C++ geschrieben sind. Normalerweise würde das
die Entwicklung von Bindings voraussetzen, um die Bibliotheken in anderen Sprachen nutzen
zu können. Zig integriert jedoch direkt C, wodurch C-Bibliotheken direkt verwendet werden
können23.
In diesem Abschnitt zeige ich Ihnen, wie sie eine simple GUI-Applikation mit GTK4 und Zig
schreiben können. Hierfür müssen Sie zuerst eine GTK4-Entwicklungsumgebung installieren.
23 Mit wenigen Einschränkungen. Zig scheitert zurzeit noch an der Übersetzung einer Makros.
22
Zig Basics
24 [Link]
23
Abbildung 8: Erweiterung der Umgebungsvariablen Path , LIB und INCLUDE .
Weitere Informationen finden Sie im angegebenen Github-Repository.
Achten Sie darauf, dass Sie den Rechner neu starten beziehungsweise ein neues PowerShell-
Fenster öffnen müssen, bevor die Änderungen wirksam werden.
Projekt anlegen
Nachdem GTK4 korrekt installiert wurde legen wir als erstes einen neuen Projektordner an und
initialisieren diesn.
$ mkdir gui
$ cd gui
$ zig init
info: created [Link]
info: created [Link]
info: created src/[Link]
info: created src/[Link]
info: see `zig build --help` for a menu of options
Fügen Sie danach gtk4 als Bibliothek zu Ihrer Anwendung hinzu. Hierfür öffnen Sie [Link]
mit einem Texteditor und erweitern die Datei um die folgenden Zeilen:
chapter01/gui/[Link]
//...
const exe = [Link](.{
//...
});
// Fügen Sie die folgenden beiden Zeilen hinzu
[Link]();
// Gegebenfalls müssen Sie "gtk4" durch "gtk-4" ersetzen!
[Link]("gtk4");
//...
24
Zig Basics
Mit linkSystemLibrary können Sie Systembibliotheken, in unserem Fall GTK4, verlinken. LibC
ist eine standart C-Bibliothek und wird, bis auf wenige Ausnahmen, von allen C-Anwendungen,
unter anderem GTK4, benötigt. Um LibC zu verlinken wird die Funktion linkLibC verwendet,
deren Aufruf äquivalent zu dem Aufruf linkSystemLibrary("c") ist. Grundsätzlich können
Sie sich merken, dass Sie bei der Verwendung einer C-Bibliothek mit Zig auch immer LibC
verlinken müssen25.
Führen Sie nach dem Hinzufügen der benötigten Bibliotheken zig build aus um zu überprüfen,
dass Zig die benötigte Bibliothek auf Ihrem System findet. An dieser Stelle kann es zu Problemen
kommen, die entweder auf eine falsche Installation von GTK4 oder auf einen falschen Bezeichner
(probieren Sie gegebenenfalls gtk-4 anstelle von gtk4 ) zurückzuführen sind. Stellen Sie bei
Problemen sicher, dass GTK4 auf Ihrem System vorliegt und dass die entsprechenden Umge-
bungsvariablen auf GTK4 verweisen.
War der Build-Prozess erfolgreich, steht der eigentlichen Anwendung nichts mehr im Wege.
Die Anwendung
Erzeugen Sie als nächstes die Datei src/[Link] und fügen Sie den Folgenden Code hinzu:
chapter01/gui/src/[Link]
pub usingnamespace @cImport({
@cInclude("gtk/gtk.h");
});
const c = @cImport({
@cInclude("gtk/gtk.h");
});
25
Die Anwendung
);
}
Zig ist zwar ziemlich gut darin mit C zu integrieren, jedoch werden Sie von Zeit zu Zeit noch
auf Probleme stoßen. In den meisten Fällen lässt sich dies jedoch relativ einfach lösen. Innerhalb
von src/[Link] inkludieren wir zuerst die GTK4 Header-Datei gtk.h . Wie Ihnen vielleicht
aufgefallen ist, haben wir an keiner Stelle innerhalb von [Link] auf diese Datei verwiesen.
Zig reicht es in den aller meisten Fällen aus, wenn Sie die Bibliothek benennen die Sie einbinden
möchten und fügt die benötigten Pfade automatisch hinzu.
Das Schlüsselwort usingnamespace sorgt dafür, dass wir auf alle in gtk.h deklarierten Objekte,
über [Link] , direkt zugreifen können.
Eine in gtk.h deklarierte Funktion, die wir später noch benötigen, ist g_signal_connect .
Diese lässt sich leider nicht ohne weiteres direkt verwenden (einer der seltenen Fälle bei denen
Zig derzeit noch versagt). Aus diesem Grund implementieren wir die Funktion selber und nennen
unsere Implementierung z_signal_connect .
Nun haben wir alles vorbereitet und können uns um die eigentliche Anwendung kümmern.
Ersetzen Sie den Code in src/[Link] mit dem folgenden Programm:
chapter01/gui/src/[Link]
const std = @import("std");
const gtk = @import("[Link]");
gtk.gtk_window_set_title(
@as(*[Link], @ptrCast(window)),
"Zig Basics",
);
gtk.gtk_window_set_default_size(
@as(*[Link], @ptrCast(window)),
920,
640,
);
gtk.gtk_window_present(@as(*[Link], @ptrCast(window)));
}
26
Zig Basics
);
_ = gtk.z_signal_connect(
application,
"activate",
@as([Link], @ptrCast(&onActivate)),
null,
);
_ = gtk.g_application_run(
@as(*[Link], @ptrCast(application)),
0,
null,
);
}
Ganz oben importieren wir die Standardbibliothek, als auch die Datei [Link] unter dem
Namen gtk . Danach folgt die Funktion onActivate , welche verwendet wird um ein GTK-
Fenster zu erzeugen. Schauen wir uns aber zuerst die main Funktion an.
Innerhalb von main wird als erstes, mithilfe von gtk_application_new , ein Anwendungsob-
jekt erzeugt, welches an die Konstante application gebunden wird. Als nächstes wird an
Callback registriert, der durch das Signal activate aufgerufen wird. Als Callback nutzen wir
die Funktion onActivate . Nachdem die Anwendung mittels g_application_run die GTK-An-
wendung gestartet hat, wird das activate Signal ausgelöst, wodurch onActivate aufgerufen
wird.
Die Funktion onActivate erzeugt als erstes ein neues Fenster für die Anwendung und weist
dem Fenster, mithilfe von get_window_set_title , den Titel Zig Basics zu. Danach wird eine
Fenstergröße von 920 x 640 Pixeln festgelegt, bevor das Fenster mit gtk_window_present
angezeigt wird.
Innerhalb des Root-Verzeichnisses des Projekts können Sie mit zig build run die Anwendung
starten. Nach dem Starten des Programms sollten Sie ein leeres Fenster sehen.
27
Abbildung 9: Leeres GKT4-Fenster
Nur ein leeres Fenster ist etwas langweilig, deshalb fügen wir als nächstes noch einen Button
hinzu, der den Text „Hello, World!“ auf der Kommandozeile ausgibt. Ich weis, ein Button ist nicht
viel spannender als ein leeres Fenster, er sollte jedoch als Beispiel genügen.
Zuerst muss ein Callback definiert werden, der aufgerufen wird sobald der Button vom Nutzer
gedrückt wird.
chapter01/gui/src/[Link]
fn onButtonClicked(_: *[Link], _: [Link]) void {
[Link]("Hello, World!", .{});
}
Callbacks in GTK erwarten zwei Argumente, einen Zeiger auf das Widget (z.B. der Button)
welches den Callback ausgelöst hat und optional einen Zeiger auf Daten, die an die Funktion
übergeben werden sollen. Da wir weder das Widget noch Daten benötigen, werden die Parame-
ternamen durch _ ersetzt. Damit stellen wir den Compiler zufrieden der erwartet, dass alle
deklarierten Variablen verwendet werden, Parameter eingeschlossen.
Allgemein setzt sich eine GTK Anwendung aus Widgets (Bausteinen) zusammen. Alles was in
einem Fenster angezeigt wird, wird intern als Baumstruktur, bestehend aus Objekten vom Typ
GtkWidget , abgebildet, wobei das Fenster selber die Wurzel des Baums ist. GtkWidget ist dabei
ein generischer Typ, das heist er umfasst Verhalten das von allen Bausteinen geteilt wird, egal
ob es sich dabei um einen Button, Text oder eine andere graphische Komponente handelt.
Fügen Sie den folgenden Code zwischen dem Aufruf von gtk_window_set_default_size und
z_signal_connect ein.
chapter01/gui/src/[Link]
28
Zig Basics
Da alle Bausteine als GtkWidget verwendet werden können ist es teilweise nötig einzelne
Zeiger auf den richtigen, von einer Funktion erwarteten, Parametertypen zu casten. Der Aus-
druck @as(*[Link], @ptrCast(window)) bedeutet zum Beispiel: betrachte den Zeiger
window als einen Zeiger zu einem GtkWindow .
Mit der Funktion gtk_window_set_child kann ein Widget als Kind des gegebenen Fensters
gesetzt werden. Danach registrieren wir noch einen Callback für den Button, der bei einem Click
(Signal „clicked“) ausgelöst wird. Als Callback verwenden wir die Funktion onButtonClicked ,
die wir zuvor definiert hatten.
Nachdem Sie das Programm mit zig build run gestartet haben sollten Sie innerhalb des
Fensters einen Button sehen, der das Fenster ausfüllt.
29
Die Anwendung
In diesem Beispiel möchte ich ihnen Zeigen, wie Sie eine kleine Bibliothek in C schreiben und
diese im Anschluss in einem weiteren Projekt verwenden können.
Bibliothek schreiben
Legen Sie zuerst einen Ordner math an und initialisieren Sie diesen mit cd math && zig init .
Entfernen Sie danach alle Dateien in src und fügen Sie die Datei math.c hinzu, welche den
folgenden Code enthält:
chapter01/math/src/math.c
#include "math.h"
chapter01/math/src/math.h
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
Unsere Bibliothek enthält genau eine Funktion add() , deren Prototyp in der Header-Datei
deklariert wird. Damit die Bibliothek auch mit C++ verwendet werden kann, prüfen wir ob
__cplusplus definiert ist und umschließen die Deklaration falls nötig mit extern "C" { } .
Durch extern "C" sagen wir dem C++-Compiler, dass er keine zusätzlichen Parameterinfor-
mationen dem Namen hinzufügen soll, der zum linken verwendet wird.
30
Zig Basics
Öffnen Sie danach [Link] und entfernen Sie alles bis auf den folgenden Code:
chapter01/math/[Link]
const std = @import("std");
[Link]([Link]("src"));
[Link]([Link]("src/math.h"), "mymath.h");
[Link]();
[Link](lib);
}
Mit addSharedLibrary() erzeugen Sie eine neue dynamische Bibliothek. Sollten Sie eine
statische Bibliothek benötigen, können Sie diesen Aufruf einfach durch addStaticLibrary()
austauschen. Die Funktion erwartet ein Argument vom Typ SharedLibraryOptions mit dem
die Eigenschaften der Bibliothek beeinflusst werden können. Dazu zählt unter anderem der Name
der Bibliothek, sowie die Zielarchitektur ( target ) und der Optimierungsgrad ( optimize ).
C-Quelldateien können der Bibliothek lib , mit der Methode addCSourceFiles() , hinzugefügt
werden. Zusätzlich zu den Quelldateien können auch Flags angegeben werden, die beim kompi-
lieren mit berücksichtigt werden sollen. addCSourceFiles() kann dabei beliebig oft aufgerufen
werden.
[Link](.{
.files = &.{"src/math.c"},
.flags = &.{"-std=gnu11"},
});
Zig sucht automatisch nach benötigten Headern im System (zum Beispiel in /usr/include),
jedoch kann es auch nötig sein weitere Pfade anzugeben, in denen für das Projekt benötigte
31
Bibliothek schreiben
Header liegen. Dies erfolgt durch die addIncludePath() Methode, welche auf dem mit
addStaticLibrary() erzeugten Objekt aufgerufen wird. Der Pfad kann relativ zum Root-
Verzeichnis des Projekts angegeben werden.
[Link]([Link]("src"));
Wenn die eigene Bibliothek Header exportieren soll, die für die Verwendung der Bibliothek
benötigt werden, so kann dies durch installHeader() erfolgen. Die Methode erwartet als
erstes Argument einen Pfad zu der zu exportierenden Header-Datei und als zweites den Namen
für die Header-Datei, unter welchem diese exportiert werden soll. Im gegebenen Fall exportieren
wir die Header Datei unter dem Namen mymath.h, damit diese sich nicht mit der math.h aus der
Standardbibliothek überschneidet.
[Link]([Link]("src/math.h"), "mymath.h");
Bei der Verwendung von C wird in den meisten Fällen LibC benötigt, die Standardbibliothek für
die Programmiersprache C. Diese kann mit linkLibC() gelinkt werden.
[Link]();
[Link](lib);
Nach dem Ausführen von zig build sollten Sie die folgenden Dateien in zig-out vorfinden:
$ ls -R zig-out/
zig-out/:
include lib
zig-out/include:
math.h
zig-out/lib:
libmath.a
Um die Bibliothek systemweit verwenden zu können, müssen die Dateien an die entsprechenden
Stellen im Dateisystem kopiert werden. Unter Linux ist dies zum Beispiel /usr/local/include für
math.h. Für dieses Beispiel wird dies jedoch nicht benötigt.
Bibliothek einbinden
32
Zig Basics
chapter01/user/src/[Link]
#include <iostream>
#include "mymath.h"
int main()
{
std::cout << "4 + 3" << add(4, 3) << "\n";
}
Fügen Sie als nächstes unter [Link] die math Bibliothek als Abhängigkeit hinzu:
chapter01/user/[Link]
//...
.dependencies = .{
.mymath = .{
.path = "../math",
},
},
//...
Entfernen Sie dann alles bis auf den folgenden Code aus [Link]:
chapter01/[Link]
const std = @import("std");
33
.target = target,
.optimize = optimize,
});
[Link](.{
.files = &.{"src/[Link]"},
.flags = &.{},
});
[Link](math_dep.artifact("mymath"));
[Link]();
[Link](exe);
Zwei Besonderheiten dieses Build-Skripts sind zum einen der Aufruf von [Link]() ,
sowie [Link]() .
Mit [Link]() können sie, über den Namen (in diesem Fall mymath ), auf die in
[Link] definierten Abhängigkeiten zugreifen. Als zusätzliches Argument erwartet die
Methode sowohl die Zielarchitektur als auch den Optimierungsgrad.
Die mymath Abhängigkeit enthält sowohl die dynamische Bibliothek, als auch die zugehörige
Header-Datei mymath.h (beziehungsweise die Bauanleitung für diese). Mit [Link]()
können wir die Bibliothek mit unserer Executable linken. Hierzu greifen wir mit
math_dep.artifact("mymath") auf die Bibliothek zu und übergeben diese an linkLibrary()
26.
[Link](math_dep.artifact("mymath"));
Mit zig build run können Sie die C++-Anwendung bauen und ausführen.
26 Eigentlich greifen wir mit artifact() auf den Compile-Step für mymath zu.
34
Zig Basics
35
36
Zig Basics
Kapitel 2
Grundlagen
Zig ist eine kompilierte Sprache, d.h. sie wird, bevor der Programmcode ausgeführt werden kann,
in eine Sprache übersetzt die vom Prozessor verstanden wird. Die Übersetzungsarbeit übernimmt
dabei ein Compiler.
Zig verfügt über viel Datentypen, darunter vorzeichenbehaftete und -unbehaftete Ganzzahlen
(Integer), Fließkommazahlen (Float), Booleans und Stirngs. Weiterhin besitzt Zig eine Vielzahl
an Collection-Typen, darunter Arrays, Tuples.
Zig unterscheidet bei Variablen zwischen Variablen und Konstanten, welche Werte speichern,
die über einen Namen referenziert werden. Der Name einer Variable beziehungsweise Konstante
wird auch als Identifier bezeichnet. Konstanten sind nach ihrer Initialisierung nicht mehr verän-
derbar, während Variablen neu zugewiesen werden können. Durch die Unterscheidung zwischen
Variablen und Konstanten kann die Absicht hinter einer Variablen-Deklaration eindeutiger
ausgedrückt werden.
Zusätzlich zu simplen Typen stellt Zig zusätzliche Collection-Typen, darunter Hash-Maps und
Array-Listen, über die Standardbibliothek bereit.
Weiterhin unterstützt Zig optionale Typen, welche die Abwesenheit eines Wertes ausdrücken.
Das heißt ein optionaler Typ kann entweder einen Wert besitzen oder keinen. Optionals ersetzen
unter anderem NULL-Pointer, wodurch vielen, aus C bekannten Speicherfehlern, vorgebeugt
werden kann.
In Zig sind Fehler ebenfalls Werte, das heißt anstatt eine Exception zu werfen können Funktionen
einen Fehler-Wert an die Aufrufende Funktion zurückgeben, welche potenzielle Fehler behan-
deln muss bevor auf den eigentlichen Rückgabewert zugegriffen werden kann.
Im laufe dieses Buches werden Sie häufiger auf die Wörter Ausdruck (engl.
Expression) und Statement stoßen. Ein Ausdruck ist ein „Stück-Code“, das evalu-
iert werden kann und einen Wert produziert. Statements dagegen, produzieren
keinen Wert, das heißt sie können unter anderem nicht als Funktionsargument
verwendet werden.
37
Konstanten und Variablen
Konstanten und Variablen bestehen aus einem Namen in Snake-Case ( buffer oder
private_key ) und einem Typen (zum Beispiel u8 oder []const u8 ). Sie werden verwendet
um Werte vom entsprechenden Typ zu binden (zum Beispiel 13 oder "Hello, World!" ).
Konstanten können nach ihrer Initialisierung nicht mehr neu zugewiesen werden.
Variablen-Deklarationen
Konstanten und Variablen müssen vor ihrer Verwendung deklariert und initialisiert werden.
Konstanten werden mit dem const Schlüsselwort deklariert, während für Variablen var
verwendet wird.
In diesem Beispiel wird ein Konstante mit dem Namen es256 deklariert und ihr wird der Wert
"ES256" zugewiesen. Danach wird eine Variable mit dem Namen retries deklariert und der
Wert 3 zugewiesen. Die Anzahl an Versuchen muss als Variable deklariert werden, da die Anzahl
dekrementiert wird.
Sollte eine Variable sich nach ihrer Initialisierung nicht mehr verändern muss diese immer als
Konstante deklariert werden! Dies wird vom Compiler sichergestellt.
Konstanten und Variablen müssen bei ihrer Deklarationen auch initialisiert werden. Alternativ
kann ihnen auch der Wert undefined zugewiesen werden, was so viel bedeutet wie „der Wert
der Variable ist zu diesem Zeitpunkt undefiniert“.
Wichtig zu betonen ist bei der Verwendung von undefined , dass Zig den Typ der Variable nicht
ableiten kann. Deshalb muss der Typ, bei der Deklaration der Variable, explizit mit angegeben
werden.
Typ-Annotationen
Durch Typ-Annotation kann der Typ einer Konstante oder Variable angegeben werden. Hierzu
wird hinter dem Variablen-Namen ein „ : “ angehängt, gefolgt vom Namen des Typen, der
verwendet werden soll.
38
Zig Basics
Im obigen Beispiel wird eine Konstante mit dem Namen hello vom Typ []const u8 (String)
deklariert.
In vielen Fällen kann Zig den Typ einer Variable ableiten, zum Beispiel durch den verwendeten
Initialisierungswert. Es gibt jedoch auch Situationen, bei denen der Typ einer Variable klar
ausgedrückt werden muss. Ein solcher Fall betrifft die Verwendung von undefined , bei dem
der Compiler keinen Typen für die Variable ableiten kann. Es gibt jedoch auch Situationen, bei
denen der Compiler den falschen Typen für eine Variable bestimmt.
var i = 0;
while (i < 10) : (i += 1) {}
Im obigen Beispiel wird der Variable i der Typ comptime_int zugewiesen, da Integer-Literale
ebenfalls vom Typ comptime_int sind. Variablen vom Typ comptime_int müssen jedoch zur
Kompilierzeit bekannt sein, was im gegebenen Fall nicht zutrifft, da i zur Laufzeit, innerhalb
der Schleife, inkrementiert wird.
Dies führt zu dem folgenden Fehler beim Kompilieren:
Um da Problem zu lösen muss der Variable i ein Integer-Typ mit einer bekannten Größe
zugewiesen werden. Für Zähler-Variablen ist dies oft usize .
var i: usize = 0;
while (i < 10) : (i += 1) {}
Variablen benennen
Die Namen von Konstanten und Variablen müssen mit einem Buchstaben oder Underscore ( _ )
beginnen, gefolgt einer beliebigen Anzahl an Buchstaben oder Ziffern. Dabei ist darauf zu achten,
dass der Name nicht mit dem Identifier eines Schlüsselworts überlappt. Zum Beispiel ist es nicht
erlaubt eine Konstante const zu nennen.
const pi = 3.14;
const private_key = "\x01\x02\x03\x04";
Es ist Konvention, die Namen von Variablen und Konstanten in Snake-Case zu schreiben, das
heißt Wörter werden in Kleinbuchstaben geschrieben, getrennt durch einen Unterstrich ( _ ).
39
Variablen benennen
Die Namen von Variablen dürfen niemals die Namen von Variablen aus einem umschließenden
Scope überschatten, das heißt sie dürfen nicht den selben Namen besitzen.
Sollte ein Name nicht die genannten Bedingungen erfüllen, so kann die @"" Syntax verwendet
werden.
Die @"" Syntax kann auch verwendet werden um Schlüsselwörter als Varia-
blen-Namen verwenden zu können. Dies sollte jedoch vermieden werden um
Verwirrung vorzubeugen.
Lokale Variablen
Lokale Variablen erscheinen innerhalb von Funktionen, Comptime-Blöcken und @cImport -
Blöcken.
Einer lokalen Variable kann das comptime Schlüsselwort vorangestellt werden. Dadurch ist der
Wert der Variable zur Kompilierzeit bekannt und das Laden und Speichern der Variable passiert
während der semantischen Analyse des Programms, anstatt zur Laufzeit.
x += 1;
y += 1;
Die Life-Time einer lokalen Variable, das heißt der Zeitraum in dem die Variable existiert, beginnt
und endet mit dem Block, indem sie deklariert wurde.
40
Zig Basics
Container-Level Variablen
Container-Level Variablen werden außerhalb einer Funktion, Comptime-Blocks oder @cImport
-Blocks deklariert und sind vergleichbar mit globalen Variablen in anderen Sprachen.
Jedes syntaktische Konstrukt in Zig, welches als Namensraum dient und Varia-
blen- oder Funktionsdeklaraionen umschließt, wird als Container bezeichnet.
Weiterhin können Container selbst Typdeklarationen sein, welche instantiiert
werden können. Dazu zählen struct s, enum s, union s und sogar Quellcode-
Dateien mit der Dateiendung .zig.
Der Initialisierungswert einer container-level Variable ist implizit comptime . Ist die deklarierte
Variable eine Konstante, so ist ihr Wert zur Kompilierzeit bekannt, andernfalls ihr Wert zur
Laufzeit bekannt.
Die Life-Time einer container-level Variable ist statisch, das heißt die Variable existiert während
der gesamten Laufzeit des Programms.
Statisch-lokale Variablen
Es ist möglich lokale Variablen mit einer statischen Life-Time zu deklarieren, indem ein Contai-
ner innerhalb einer Funktion verwendet wird.
chapter02/static_local_variable.zig
const std = @import("std");
fn next() i32 {
const S = struct {
var x: i32 = 0;
};
41
defer S.x += 1;
return S.x;
}
Kommentare
Kommentare können genutzt werden um die Funktionsweise von Programmabschnitten zu
Dokumentieren. Dabei unterscheidet Zig zwischen drei Arten von Kommentaren.
Normale Kommentare beginnen mit // und können an einer beliebigen Stelle im Code platziert
werden. Alles was auf // innerhalb einer Zeile folgt ist Teil des Kommentars.
Doc-Kommentare können für die Dokumentation einzelner Programmteile genutzt werden und
beginnen mit /// . Mehrere, hintereinander folgende Doc-Kommentare bilden einen zusam-
menhängenden Block und erlauben es Kommentare über mehrere Zeilen hinweg zu verfassen.
Doc-Kommentare sind kontextabhängig und dokumentieren was auch immer dem Kommentar
folgt.
chapter02/[Link]
//! Ein Modul bestehend aus einem Struct `Color` und
//! einer Funktion `add(u32, u32) u32`.
42
Zig Basics
Mit zig test -femit-docs <your-code>.zig können die Doc- und Top-
Level-Kommentare in eine HTML-Seite umgewandelt werden. Zig wird
hierfür einen neuen Ordner mit dem Namen docs anlegen. Mit
python3 -m [Link] kann ein HTTP-Server gestartet werden um die
Dokumentation anzuzeigen.
Zurzeit scheint es jedoch noch Probleme mit dem Erzeugen zu geben.
Ganzzahlen (Integer)
Integer sind Ganzzahlen, das heißt sie Besitzt keine Bruchkomponente und können entweder
vorzeichenbehaftet (signed) oder vorzeichenunbehaftet (unsigned) sein.
Zig unterstützt Ganzzahlen mit einer beliebigen Bitbreite. Der Bezeichner eines jeden Integer-
Typen beginnt mit einem Buchstaben i (signed) oder u (unsigned) gefolgt von einer oder
mehreren Ziffern, welche die Bitbreite in Dezimal darstellen. Als Beispiel, i7 ist eine vorzei-
chenbehaftete Ganzzahl der sieben Bit zur Kodierung der Zahl zur Verfügung stehen. Die
Aussage, dass die Bitbreite beliebig ist entspricht dabei nicht ganz der Wahrheit. Die maximal
erlaubte Bitbreite beträgt 216 − 1 = 65535. Beispiele für Integer sind:
Typ Wertebereich
i7 −26 bis 26 − 1
u8 0 bis 28 − 1
43
Typ Wertebereich
Integer-Literale
Zur Compile-Zeit bekannte Literale vom Typ comptime_int haben kein Limit was ihre Größe
(in Bezug auf die Bitbreite) und konvertieren zu anderen Integertypen, solange das Literal im
Wertebereich des Typen liegt.
27 [Link]
28 [Link]
44
Zig Basics
Optional können die Prefixe 0x , 0o und 0b an ein Literal angehängt werden um Literale in
Hexadezimal, Octal oder Binär anzugeben, z.B. 0xcafebabe .
Um größere Zahlen besser lesbar zu machen, kann ein Literal mit Hilfe von Unterstrichen
aufgeteilt werden, z.B. 0xcafe_babe .
Laufzeit-Variablen
Um die Variable zur Laufzeit modifizieren zu können, muss ihr eine expliziter Type mit fester
Bitbreite zugewiesen werden. Dies kann auf zwei weisen erfolgen.
1. Deklaration der Variable i mit explizitem Typ, z.B. var i: usize = 0 .
2. Verwendung der Funktion @as() , von welcher der Compiler den Type der Variable i
ableiten kann, z.B. var i = @as(usize, 0) .
Ein häufiger Fehler, der aber schnell behoben ist, ist die Verwendung einer Variable vom Typ
comptime_int in einer Schleife.
var i = 0;
while (i < 100) : (i += 1) {}
Der Zig-Compiler ist dabei hilfreich, indem er neben dem Fehler auch einen Lösungsansatz
bietet. Nachdem der Variable i ein expliziter Typ zugewiesen wird ( var i: usize ) compiliert
das Programm ohne weitere Fehler.
Integer-Operatoren
Zig unterstützt verschiedene Operator für das Rechnen mit Integern, darunter + (Addition), -
(Subtraktion), * (Multiplikation) und / (Division).
Die Verwendung dieser Operatoren führt bei einem Überlauf jedoch zu undefiniertem Verhalten
(engl. undefined behavior). Aus diesem Grund stellt Zig spezielle Versionen dieser Operatoren
zur Verfügung, darunter:
45
Integer-Operatoren
Integer-Bounds
Auf den Minimal- und Maximalwert eines Integers kann mit [Link] und
[Link] zugegriffen werden.
Beide Funktionen erwarten als Argument den Integer-Typ, für den das Minimum oder Maximum
bestimmt werden soll. Da beide Funktionen comptime sind wird der Rückgabewert zur Kompi-
lierzeit bestimmt.
Fließkommazahlen (Float)
Fließkommazahlen haben eine Bruchkomponente, wie etwa 3.14 oder -0.5 .
Im Gegensatz zu Integern erlaubt Zig keine beliebige Bitbreite für Fließkommazahlen. Zur
Verfügung stehen:
Typ Repräsentation
Der Typ f32 entspricht dem Typ float (single precision) in C, während f64 dem Typ double
(double precision) entspricht. Je nach Prozessortyp stehen dedizierte Maschineninstruktionen
46
Zig Basics
für zumindest einen Teil der Typen zur Verfügung, was eine effizientere Verwendung ermöglicht.
Auf x86_64 Prozessor stehen z.B. Instruktionen für single und double Precision zur Verfügung.
Float-Literale
Literale sind immer vom Typ comptime_float , welcher äquivalent zum größtmöglichen Fließ-
kommatypen ( f128 ) ist, und können zu jedem beliebigen Fließkommatypen konvertiert werden.
Enthält ein Literal keinen Bruchteil, so ist eine Konvertierung zu einem Integertyp ebenfalls
möglich.
Alle Float-Literale haben einen Dezimalpunkt ( . ). Sie können entweder als Dezimalzahl ange-
geben werden (ohne Präfix) oder als Hexadezimalzahl (mit dem Präfix 0x ). Optional kann ein
Exponent mit angegeben werden. Für Dezimalzahlen wird hierfür ein E oder e verwendet und
für Hexadezimalzahlen ein P oder p .
Für Dezimalzahlen mit einem Exponenten e wird die angegebene Fließkommazahl mit 10𝑒
multipliziert:
• 123.0E+77 = 123.0 ∗ 1077
Für Hexadezimalzahlen mit einem Exponenten p wird die Fließkommazahl mit 2𝑝 multipliziert:
• 0x103.70p-5 = 103.7016 ∗ 2−5
const fp = 123.0E+77;
const hfp = 0x103.70p-5;
31 30 … 23 22 … 0
s exponent (e) fraction (f)
Diese Darstellung entspricht der Gleichung (−1)𝑠 ∗ 1.𝑓 ∗ 2𝑒−127 . Der Bruch 𝑓 entspricht einer
normalisierten, binär kodierten Fließkommazahl, d.h. die Zahl wird um eine entsprechende
Anzahl an Stelle verschoben, sodass genau eine führende Eins vor dem Komma steht. Als Beispiel
entspricht die Fließkommazahl 3.25 in binär der Zahl 11.01 oder anders ausgedrückt 11.01 ∗
20 . Um die Zahl zu normalisieren wird diese nun um eine Stelle nach rechts verschoben 1.101 ∗
21 . Die Zahl nach der führenden Eins (101) entspricht 𝑓 und der Exponent 𝑒 ist die Summe des
Exponenten der normalisierten Darstellung und einem Bias (im Fall von f32 ist dieser 127), d.h.
𝑒 = 1 + 127 = 12816 = 100000002 . Damit wird 3.25 wie folgt kodiert:
47
Darstellung von Floats im Speicher
31 30 … 23 22 … 0
0 100000002 101000000000000000000002
Integer Konvertierung
Die Funktion @intCast(anytype) konvertiert einen Integer zu einem anderen Integer, wobei
der numerische Wert beibehalten wird. Der Typ des Rückgabewertes wird dabei vom Compiler
abgeleitet. Hierzu muss @as() in Kombination mit @intCast() verwendet werden. Alternativ
kann einer Variable auch explizit ein Typ zugewiesen werden, an den der konvertierte Wert
gebunden werden soll.
chapter02/[Link]
test "Konvertierungs-Test: pass" {
var a: u16 = 0x00ff; // runtime-known
_ = &a;
const b: u8 = @intCast(a);
_ = b;
const c = @as(u8, @intCast(a));
_ = c;
}
chapter02/[Link]
48
Zig Basics
Sollte es erlaubt sein einen Wert, bei einer Narrowing-Konvertierung, abzuschneiden, so kann
entweder @truncate oder alternativ der Und-Operator ( & ) in Kombination mit einer Bit-Maske
verwendet werden.
Widening-Konvertierungen wiederum sind unkritisch und damit immer erfolgreich.
Float Konvertierung
Die Funktion @floatCast(anytype) konvertiert einen Float zu einem anderen Float, wobei der
numerische Wert an Präzision verlieren kann. Die Konvertierung von Floats ist dabei sicher. Der
Typ des Rückgabewerts wird wie bei der Konvertierung von Integern abgeleitet.
chapter02/[Link]
49
test "Float Konvertierung" {
var a: f32 = 1234567.0; // runtime-known
_ = &a;
const b: f16 = @floatCast(a);
_ = b;
}
Typen Alias
Alle primitiven Typen in Zig haben type als ihren Meta-Typ und können selbst an Konstanten
gebunden werden. Damit erlaubt Zig die Definition eines Alias für einen bestehenden Typen.
Ein Alias ist nützlich, um einen Typen bei einem Namen zu referenzieren. Beispielsweise werden
Universally Unique Identifier (UUID)29 als 128-Bit-Zahl kodiert.
[Link]
/// Universally Unique IDentifier
///
/// A UUID is 128 bits long, and can guarantee uniqueness across space and
time (RFC4122).
pub const Uuid = u128;
Nachdem ein Alias definiert wurde kann dieser überall verwendet werden, wo auch der
ursprüngliche Bezeichner des Typen verwendet werden kann.
[Link]
/// Create a version 4 UUID using a user provided RNG
pub fn new2(r: [Link]) Uuid {
// Set all bits to pseudo-randomly chosen values.
var uuid: Uuid = [Link](Uuid);
// Set the two most significant bits of the
// clock_seq_hi_and_reserved to zero and one.
// Set the four most significant bits of the
// time_hi_and_version field to the 4-bit version number.
uuid &= 0xffffffffffffff3fff0fffffffffffff;
uuid |= 0x00000000000000800040000000000000;
return uuid;
}
Booleans
29 [Link]
50
Zig Basics
Zig besitzt einen primitiven Boolean Typ bool . Ein Boolean ist ein Daten-Typ der zwei mögliche
Werte annehmen kann true oder false . Er ist nach Georg Boole30 benannt, der die Boolsche-
Algebra definierte.
Booleans werden primär in Conditional-Satements und -Expressions (Kontrollstrukturen)
verwendet31, zum Beispiel in Kombination mit If-Then-Else Blöcken, um zu bestimmen, welcher
Block ausgeführt werden soll.
Die Funktion [Link] überprüft ob die zwei gegebenen Strings, bezogen auf ihren Inhalt,
gleich sind. Falls ja wird der Wert true zurückgegeben, andernfalls der Wert false .
Im Gegensatz zu C verhindert Zig, dass numerische Werte als Booleans verwendet werden32.
defer
Mit dem defer Schlüsselwort können Ausdrücke und Blöcke markiert werden, die beim
Verlassen eines Blocks ausgeführt werden sollen. Solche defer -Ausdrücke und -Blöcke werden
in der umgekehrten Reihenfolge ausgeführt, in welcher sie definiert wurden.
chapter02/[Link]
const std = @import("std");
const print = [Link];
fn myDefer() void {
defer {
print("Wird als zweites ausgeführt\n", .{});
}
if (false) {
defer print("Wird nie ausgeführt\n", .{});
}
30 [Link]
31 [Link]
32 In C ist der Wert 0 äquivalent zu False und alle verbleibenden Werte äquivalent zu True .
51
}
defer s werden dabei nur ausgeführt, wenn Sie beim Ausführen eines Blocks auch erreicht
wurden. Im obigen Beispiel kommt zuerst ein defer -Block vor, gefolgt von einem defer -
Ausdruck. Da defer s in umgekehrter Reihenfolge ausgeführt werden, wird beim Verlassen der
Funktion zuerst „Wird als erstes ausgeführt“ auf der Kommandozeile ausgegeben, gefolgt von
„Wird als zweites ausgeführt“. „Wird nie ausgeführt“ wird nicht ausgegeben, da die If-Bedingung
immer false ist und somit der If-Block nie ausgeführt wird.
defer s eignen sich besonders gut zum aufräumen von Ressourcen. Ein Beispiel hierfür ist die
Deallokation von dynamisch alloziertem Speicher. Es ist gängige Praxis, dass auf die dynamische
Allokation von Speicher ein defer folgt, welches den Speicher wieder frei gibt, sollte dieser
nach dem Verlassen des umschließenden Blocks nicht mehr benötigt werden.
Optionals
In Situationen bei denen eine Wert fehlen kann, können Optionals verwendet werden. Ein
Optional repräsentiert zwei mögliche Zustände: Entweder es ist ein Wert vorhanden oder es
ist kein Wert vorhanden. Dies ist nicht zu verwechseln mit undefinierten Werten, die bei der
Verwendung von undefined vorkommen!
Als ein Beispiel stellt die Zig Standardbibliothek die Funktion [Link] zur Verfügung.
Diese erlaubt das Konvertieren eines Integer in einen anderen Integer-Typen. Falls der gegebene
Wert nicht in den neuen Integer-Typen passt, so wird von der Funktion null zurückgegeben.
Ein optionaler Typ besteht aus einem beliebigen Typen, dem ein ? vorangestellt wird, zum
Beispiel ?u32 oder ?[]const u8 .
52
Zig Basics
null
Um einer optionalen Variable einen wertlosen Zustand zuzuweisen, wird der Wert null
verwendet.
Sollte eine optionale Variable einen Wert besitzen, so wird dieser als „ungleich zu null “
betrachtet. Dies kann mit dem Gleicheits- ( == ) beziehungsweise Ungelichheits-Operator ( != )
abgefragt werden.
chatper02/[Link]
test "Optional" {
const num: ?u8 = [Link](u8, @as(u32, 255));
if (num != null) {
try [Link](num.? == 255);
} else {
try [Link](1 == 0); // fail
}
}
Mit dem ? -Operator kann auf den Wert eines Optionals zugegriffen werden. Es sollte jedoch
sichergestellt werden, dass ein Wert existiert!
Das obige Beispiel kann auch wie folgt geschrieben werden:
chatper02/[Link]
test "Optional #2" {
const num: ?u8 = [Link](u8, @as(u32, 255));
if (num) |n| {
try [Link](n == 255);
} else {
try [Link](1 == 0); // fail
}
}
Optionale Variablen können als Bedingung, innerhalb einse If-Statements, verwendet werden.
Sollte num einen Wert besitzen, so wird dieser an n gebunden und der If-Block wird betreten.
Andernfalls wird der Else-Block ausgeführt.
Variablen die nicht als „optional“ deklariert wurden enthalten garantiert immer einen Wert! Dies
vereinfacht es, Fälle bei denen ein Wert fehlen kann, wie etwa der Zeiger bei einer verketteten
53
Liste, zu handhaben. Optionals erzwingen es, explizit auf den Wert einer optionalen Variable
zuzugreifen. Entweder durch Verwendung des ? -Operators oder eines If-Statements. Damit
wird verhindert, dass ein optionaler Wert aus Versehen als nicht optionaler Wert gehandhabt
wird.
Je nach Situation kann wie folgt mit fehlenden Werten umgegangen werden:
• Überspringe den Code der auf den eigentlichen Wert angewandt werden würde.
• Bereitstellen eines Fallback-Werts.
• Propagiere den null Wert an die darüber liegende Funktion oder beende den Prozess
vorzeitig.
chatper02/[Link]
test "Handling" {
var num: ?u8 = [Link](u8, 250);
Neben If-Statements können Optionals auch in While-Schleifen verwendet werden. Sollte das
verwendete Optional null sein so wird aus der Schleife ausgebrochen, andernfalls wird eine
Iteration der schleife Durchlaufen.
chatper02/[Link]
test "while" {
const S = struct {
pub fn next() ?u2 {
const T = struct {
var v: ?u2 = 0;
};
defer {
if (T.v) |*v| {
if (v.* == 3) T.v = null else v.* += 1;
}
}
54
Zig Basics
return T.v;
}
};
In diesem Beispiel wird eine Funktion next() definiert, die eine statische, lokale Variable v
besitzt. Diese wird mit 0 initialisiert. Bei jedem Aufruf von next() wird der aktuelle Wert von
v zurückgegeben. Der defer Block wird vor der Rückkehr aus der Funktion ausgeführt und
inkrementiert v , jedoch nur falls v nicht gleich drei ist. Sollte v gleich drei sein, so wird v
der null -Wert zugewiesen.
Verwendet man den Rückgabewert von next() als Bedingung einer While-Schleife so wird der
Rückgabewert an value gebunden, solange dieser nicht gleich null ist, das heißt value hat
den Typ u2 .
Führt man den Test aus, so sieht man, dass die Zahlen 0 bis 3 auf der Kommandozeile ausgegeben
werden, bevor aus der Schleife ausgebrochen wird.
Zeiger (Pointer)
Zig unterscheidet zwischen zwei Arten von Zeigern, single-item und many-item Pointer.
Ein single-item Pointer *T zeigt auf exakt einen Wert im Speicher und kann mit der Syntax
ptr.* dereferenziert werden. Mit Hilfe des Address-of-Operators & kann ein single-item
Pointer bezogen werden.
55
// Dereferenziere den Zeiger `v_ptr` und addiere 1 zu `v`
v.* += 1;
Ein multi-item Pointer [*]T zeigt auf eine lineare Sequenz an Werten im Speicher mit unbe-
kannter Länge. Der Zeiger eines Slice ( .ptr ) ist ein multi-item Pointer. Allgemein teilen Slices
und multi-item Pointer die selbe Index- und Slice-Syntax.
• ptr[i]
• ptr[start..end]
• ptr[start..]
chapter02/[Link]
var array = [_]i32{ 1, 2, 3, 4 };
[Link]("{d}", .{array_ptr[0]});
array_ptr += 1;
[Link]("{d}", .{array_ptr[0]});
Nach dem Compilieren mit zig build-exe chapter02/[Link] können wir die Beispiel
Anwendung ausführen und sehen, dass die ersten beiden Zahlen von array ausgegeben werden,
obwohl wir den selben Index für array_ptr verwenden. Grund dafür ist, dass wir den Zeiger
selbst, zwischen dem ersten und zweiten Aufruf von [Link]() , inkrementiert haben.
$ ./pointer
info: 1
info: 2
Ein weit verbreitetes Konzept in C sind NULL -terminierte Strings, d.h. ein 0 -Byte wird hinter
den letzten Character eines Strings geschrieben und markiert so dessen Ende. Zig bietet etwas
sehr ähnliches, nämlich sentinel-terminated Pointer, auf die im nächsten Abschnitt noch näher
eingegangen wird.
56
Zig Basics
• Der Typ [N]T repräsentiert ein Array vom Typ T bestehend aus N Werten. Die Größe eines
Arrays ist zur Compilezeit bekannt und Arrays werden grundsätzlich auf dem Stack alloziert.
Damit kann ein Array weder erweitert noch verkleinert werden.
• Der Typ []T bzw. []const T repräsentiert ein Slice vom Typ T , bestehend aus einem Zeiger
und einer Länge. Die Länge eines Slices ist zur Laufzeit bekannt. Slices referenzieren eine
Sequenz von Werten. Dies kann z.B. ein Array sein oder auch eine auf dem Heap gespeicherte
Sequenz. Die von einem konstanten Slice []const T referenzierten Werte können gelesen,
jedoch nicht verändert werden, während die Werte eines Slices []T sowohl gelesen als auch
verändert werden können.
Sowohl Arrays als auch Slices erlauben den Zugriff auf deren Länge durch den Ausdruck .len .
chapter02/[Link]
var a = [_]u8{ 1, 2, 3, 4 };
[Link]("length of a is {d}", .{[Link]});
const s = &a;
[Link]("length of a is still {d}", .{[Link]});
Mit dem Address-Of Operator & kann ein Slice für ein Array erzeugt werden. Alternativ
kann auch der Ausdruck a[0..] verwendet werden, der einen Bereich innerhalb des Arrays
beschreibt. Grundsätzlich liegt das erste Element einer Sequenz immer an Index 0 und es kann
mit a[0] auf dieses zugegriffen werden. Das letzte Element liegt immer an der Stelle [Link] - 1
und es kann mit a[[Link] - 1] darauf zugegriffen werden. Der Index muss dabei immer ein
Integer vom Typ usize oder ein Literal sein, das zu diesem Typ konvertiert werden kann. Die
Verwendung anderer Typen als Index führt zu einem Fehler zur Compilezeit.
Auf den Zeiger eines Slices kann mit .ptr zugegriffen werden, z.B. [Link] .
Zig überprüft bei dem Zugriff auf eine Array oder Slice zur Laufzeit, dass der Index innerhalb
des Speicherbereichs der Sequenz liegt. Ließt eine Anwendung über die Grenzen der Sequenz,
so führt dies zu einem Fehler zur Laufzeit der den Prozess beendet. Dies verhindert typische
Speicherfehler wie Buffer-Overflows and Buffer-Overreads die in Sprachen wie C weit verbreitet
sind und in der Vergangenheit zu Hauf von Angreifern ausgenutzt wurden um Anwendungen
zu exploiten.
chapter02/[Link]
var i: usize = 0;
while (true) : (i += 1) {
a[i] += 1;
}
57
$ zig build-exe [Link] -Doptimize=ReleaseFast
$ ./slices
info: length of a is 4
info: length of a is still 4
thread 1232 panic: index out of bounds: index 4, len 4
[Link]:10: 0x103544c in main (slices)
a[i] += 1;
^
[Link]:22: 0x1034c99 in posixCallMainAndExit (slices)
[Link]();
^
[Link]:5: 0x1034801 in _start (slices)
asm volatile (switch (native_arch) {
^
???:?:?: 0x0 in ??? (???)
Aborted (core dumped)
Arrays
Es gibt eine Vielzahl von Möglichkeiten um Arrays in Zig zu definieren. Die einfachste Möglich-
keit ist, eine Sequenz von Werten in geschweiften Klammern anzugeben.
Für den Fall, dass initial keine Werte bekannt sind kann ein Array mit undefined initialisiert
werden. In diesem Fall ist der Inhalt des Speichers undefiniert.
Arrays können aber auch mit einem bestimmten Wert initialisiert werden. Im unteren Beispiel
wird das gesamte Array mit 0 Werten initialisiert.
Die Länge eines Arrays muss immer zur Compilezeit bekannt sein. Dementsprechend können
keine Variablen zur Angabe der Länge verwendet werden, außer die Variable ist vom Typ
comptime_int . Sollte ein Array benötigt werden, dessen Länge nur zur Laufzeit bekannt ist, so
muss der Speicher entweder manuell alloziert oder auf einen Kontainertypen wie ArrayList
aus der Standardbibliothek zurückgegriffen werden33.
58
Zig Basics
Viel Funktionen die über Sequenzen arbeiten erwarten ein Slice und kein Array. Zig konvertiert
dabei nicht automatisch Arrays zu Slices, d.h. bei einem Aufruf muss explizit der Address-Of
Operator & auf das Array angewandt werden oder alternativ ein Slice mit dem [] Operator
festgelegt werden.
chapter02/[Link]
const std = @import("std");
foo(&a);
foo(a[1..]);
}
$ ./coersion
info: 1
info: 2
info: 3
info: 4
info: 5
info: 2
info: 3
info: 4
info: 5
Slices
Slices []T werden ohne Angabe einer Länge geschrieben und repräsentieren eine lineare Se-
quenz an Werten. Konzeptionell ist ein Slice eine Zeiger vom Typ [Link] .
Schaut man sich die Definition von Slice in zig/src/mutable_value.zig34 an, so sieht man, dass
ein Slice durch einen Zeiger ( ptr ), der auf den Beginn des referenzierten Speicherbereichs zeigt,
sowie eine Länge ( len ) beschrieben wird.
[Link]/ziglang/zig/src/mutable_value.zig
34 [Link]
59
pub const Slice = struct {
ty: [Link], // wir ignorieren dieses Feld :)
/// Must have the appropriate many-ptr type.
ptr: *MutableValue,
/// Must be of type `usize`.
len: *MutableValue,
};
Je nach Typ einer Variable bzw. eines Parameters konvertiert Zig die Referenz zu einem Struct
automatisch in ein Slice.
Da b eine Konstante ist, muss auch das Slice sb ( []const T ), sowie der Pointer rb auf das
Array ( *const [N]T ) konstant sein. Wäre b eine Variable, so wäre auch das const , in Bezug
auf das Slice bzw. den Pointer, optional, je nachdem ob das Array durch die jeweilige Referenz
verändert werden soll oder nicht.
/----------------------\
b | |
-------v------------------------|----------------------
stack | | | | | | |3| |
-----|-|-|---------------------------------------------
| \ \---------------- sb
\ | |
| -------| |
-------v--------v----------v---------------------------
data | |"David"|"Franziska"|"Sarah"| |
-------------------------------------------------------
Mithilfe des [] Operators können Slices für einen bestehenden Speicherbereich angegeben
werden. Innerhalb der eckigen Klammern muss dafür ein Bereich spezifiziert werden, der durch
das Slice eingegrenzt werden soll:
• [0..] : Der gesamte Bereich, vom ersten bis zum letzten Element.
• [N..M] : Ein Bereich beginnend ab Index N (eingeschlossen) und endend bei Index M
(ausgeschlossen).
60
Zig Basics
Um Buffer-Overreads vorzubeugen überprüft Zig, dass die angegeben Indices valide sind. Sind
die Indices zur Compilezeit bekannt, so führt ein invalider Index zu einem Compile-Fehler,
andernfalls zu einer Panic zur Laufzeit.
chapter02/slice_error.zig
const a = "this won't work";
// ...
const n: usize = 20;
[Link]("{s}", .{a[1..n]});
Versucht man den obigen Code mit zig build-exe chapter02/slice_error.zig zu Compi-
lieren so erhält man den folgenden Fehler:
Sentinel-Terminierte Slices
Slices definieren einen Speicherbereich, durch einen Zeiger und eine Länge. Dadurch wird der
Speicherbereich explizit eingegrenzt, was in vielen Fällen die aus C bekannten NULL-Termina-
toren überflüssig macht. Jedoch unterstützt Zig auch NULL-terminierte Strings.
Allgemein werden Slices, deren Ende durch einen bestimmten Wert begrenzt wird (zum Beispiel
ein NULL-Byte), als sentinel-terminated Slices bezeichnet. Der Sentinel (Wächter) ist eine vorde-
finierter Wert der das Slice abschließt.
Sentinel-terminierte Slices werden mit der [:x]T Syntax definiert und besitzen wie auch alle
anderen Slices eine Länge, auf die über das .len Feld zugegriffen werden kann. Die Länge ist
dabei die Länge des Slices ohne den Sentinel-Wert!
61
try [Link]([Link] == 5);
try [Link](name[6] == 0);
}
Sentinel-terminierte Slices können auch mit der data[start..end :x] Syntax erzeugt werden,
wobei data eine Zeiger auf mehrere Werte, ein Array oder ein Slice seien muss. Der Wert x
ist der Sentinel.
const arr = [_]u8{'h', 'e', 'l', 'l', 'o', 0, 'h', 'e', 'l', 'l', 'o', 0};
const s = arr[0..5 :0];
Wichtig dabei ist, dass das mit data[start..end :x] erzeugte Slice auch tatsächlich vom ange-
gebenen Sentinel terminiert wird! Sollte dies nicht der Fall sein, führt dies je zu undefiniertem
Verhalten. Je nach gewählter Optimierung führt dies im besten Fall zur einer Panic zur Laufzeit,
die den Prozess vorzeitig beendet.
Padding
Ein Thema, welches immer wieder zu Verwirrung führt und teilweise online falsch dargestellt
wird, ist der Speicherverbrauch von bestimmten Arten von Arrays beziehungsweise Slices. Ein
klassisches Beispiel ist hierbei der Typ [8]u1 .
Oft findet man Behauptungen, dass [8]u1 genau ein Byte Speicher benötigt, da das Array
aus exakt acht Bit besteht. Das ist jedoch nicht richtig. Das minimale Padding für einen
Typen im Speicher beträgt ein Byte, d.h. obwohl u1 nur ein Bit Speicher Benötigt wird
trotzdem ein Byte pro u1 reserviert. Hierfür gibt es verschiedene Erklärungsansätze. Einer
davon ist die Adressierbarkeit einzelner Elemente eines Arrays. Angenommen ein Array
const arr: [8]u1 = .{0} ** 8; . Von solch einem Array erwarten wir, einzelne Elemente
adressieren zu können, zum Beispiel &arr[3] (Adresse des vierten Elements des Arrays
arr ). Die Zeigerarithmetik hierfür wäre ELEM3 = ARR_BASE + 3 , wobei ARR_BASE die Basis-
adresse von arr im Speicher ist. Das erste Element beginnt dementsprechend an der Adresse
ARR_BASE + 0 , das zweite Element an der Adresse ARR_BASE + 1 und so weiter (Ausgehend
davon, dass jedes Element ein Byte benötigt).
Gehen wir nun davon aus, jedes u1 eines [8]u1 würde tatsächlich nur ein Bit im Speicher
belegen. Die Adressierung des ersten Elements wäre noch möglich ( ARR_BASE + 0 ) aber wie
62
Zig Basics
würden die restlichen Elemente referenziert? Die Antwort hierauf lautet: gar nicht! Die meisten
CPUs erlauben die Adressierung einzelner Bytes, was auch als Byte-Addressing35 bezeichnet
wird. Es gibt weiterhin einzelne, vor allem ältere Arichtekturen die nur Wörter adressieren
können (Word-Addressing36). Nicht existent ist jedoch Bit-Adressierung in der Computerachi-
tektur.
Das Padding (von Arrays und Slices) ist dabei nicht nur nice to know, sondern hat reelle Konse-
quenzen beim Programmieren. Gehen wir nämlich im obigen Beispiel davon aus, dass ein [8]u1
genau ein Byte im Speicher belegt, so könnten wir in die Versuchung geraten das folgende zu
versuchen: @as(*u8, @ptrCast([Link])) . In diesem Fall wäre die Erwartungshaltung, dass
u8 äquivalent zu einem [8]u1 ist. Das ist jedoch falsch und würde zwangsläufig zu Bugs
führen!
Mit @sizeOf lässt sich die Menge an Bytes bestimmen die benötigt werden um einen
Bestimmten Typ im Speicher abzulegen. Für die Anzahl an Bits kann @bitSizeOf verwendet
werden, zum Beispiel @bitSizeOf([8]u8) . In meinem Fall gibt @bitSizeOf([8]u8) den Wert
64 zurück, was das benötigt Padding mit einschließt.
Errors
Während der Ausführung von Zig-Code kann ein Programm auf Fehler zur Laufzeit stoßen.
Dabei kann es sich zum Beispiel um eine fehlende Datei handeln, die nicht geöffnet werde kann.
Im Gegensatz zu Optionals, welche die Abwesenheit eines Wertes kommunizieren können, geben
Fehler mehr Aufschluss über den Grund, warum der Aufruf einer Funktion fehlgeschlagen ist.
Außerdem unterstützt Zig das Propagieren von Fehlern.
Zig betrachtet Errors als Werte, die in einem Error-Set zusammengefasst werden. Ein Error-Set ist
vergleichbar zu einem Enum, wobei jedem Error-Bezeichner ein eindeutiger ganzzahliger Wert
größer 0 zugewiesen wird37. Wird ein Error-Bezeichner (zum Beispiel [Link] )
mehrfach definiert, so wird diesem immer der selbe numerische Wert zugewiesen.
Error-Sets können mit dem error Schlüsselwort definiert werden. Ein Error-Typ wird dekla-
riert, indem dem Basistypen der Name des zugehörigen Error-Sets, gefolgt von einem ! ,
vorangestellt wird. Angenommen eine Funktion gibt potenziell einen Fehler aus dem Error-Set
MyErrors oder void (kein Rückgabewert) zurück, dann kann der Rückgabewert der Funktion
wie folgt geschrieben werden: MyErrors!void . Um einen Error zurück zu geben kann der
entsprechende Error-Wert, genau wie andere Rückgabewerte, mit return an die aufrufende
Funktion gereicht werden.
chapter02/[Link]
35 [Link]
36 [Link]
37 Standardmäßig ist der einem Error zugrunde liegende Integer-Typ ein u16 .
63
const std = @import("std");
Error-Set Coercion
Angenommen es existieren zwei Error-Sets, wobei das eine Error-Set eine Teilmenge des
Anderen darstellt. In einem solchen Fall erlaubt Zig die Coercion, das heißt das Umwandeln, von
der Tielmenge in die Obermenge.
chapter02/[Link]
const FileOpenError = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};
64
Zig Basics
return err;
}
Was jedoch nicht funktioniert ist die Umwandlung einer Obermenge in eine Teilmenge!
Globales Error-Set
Zig erlaubt es das explizite Error-Set links vom ! wegzulassen, zum Beispiel !void . In diesem
Fall ist das Error-Set implizit anyerror , das globalen Error-Set, dem alle Errors der gesamten
Compilation-Unit angehören. Jedes Error-Set kann in anyerror umgewandelt werden. Außer-
dem kann eine Element aus dem globalen Error-Set explizit in ein nicht globales Error-Set
ge-castet werden.
Im obigen Beispiel wird der Rückgabewert automatisch in einen Wert vom Typ anyerror!void
umgewandelt.
In vielen Fällen ist es praktisch das Error-Set einer Funktion von Zig ableiten
zu lassen. Je nach Anwendungsfall kann dies jedoch auch Nachteile mit sich
bringen. Vor allem bei der Entwicklung von Modulen, die mit anderen Program-
mieren geteilt werden, sollten Sie sich angewöhnen explizite Error-Sets zu
verwenden.
catch
Mit catch können Errors, die von einer Funktion zurückgegeben werden, abgefangen und
entsprechend behandelt werden.
chapter02/[Link]
pub fn main() void {
const n = 7;
checkNumber(n) catch |e| {
[Link]("The number {d} is not equal 8: {any}", .{ n, e });
};
}
65
Das catch folgt direkt hinter dem Aufruf der Funktion. Optional kann der Fehler-Wert an eine
Variable (im obigen Fall e ) gebunden werden. Der catch -Block (eingegrenzt durch geschweifte
Klammern {} ) wird nur ausgeführt, falls die Funktion einen Error als Rückgabewert liefert.
catch eignet sich ebenfalls um im Fehlerfall einen Default-Wert bereitzustellen.
chapter02/[Link]
test "Default-Wert" {
const n = [Link](u64, "0xdeaX", 16) catch 16;
try [Link](n == 16);
}
In diesem Beispiel ist n entweder gleich dem entpackten Rückgabewert von parseInt() oder,
falls parseInt() einen Error zurück gibt, 16. Wie zu sehen ist muss nicht zwangsläufig ein Block
auf catch folgen, genauso zulässig ist ein Ausdruck. Der entpackte Rückgabewert der Funktion
und der Ausdruck rechts vom catch müssen den selben Typ besitzen (in diesem Beispiel u64 ).
Alternativ kann auch ein Block mit einem frei wählbaren Bezeichner (zum Beispiel blk )
verwendet werden. Der Bezeichner muss dabei die selben Anforderungen wie ein Variablen-
Name erfüllen.
Mittels break kann der Default-Wert 16 in den umschließenden Block gereicht werden, wo er
an die Konstante n gebunden wird. Das Literal wird dabei automatisch vom Typ comptime_int
in einen u64 umgewandelt.
try
In vielen Fällen reicht es aus, beim Auftreten eines Errors, selbst einen Error an die aufrufende
Funktion zurückzugeben. Dies wird als Fehler-Propagierung bezeichnet und kann in Zig durch
die Verwendung von try umgesetzt werden. Hierzu wird vor den Aufruf einer Funktion, die
einen Fehler-Typen als Rückgabetyp besitzt, das Schlüsselwort try gesetzt.
66
Zig Basics
Das Schlüsselwort try evaluiert den zugehörigen Ausdruck und kehrt im Fehlerfall mit dem
selben Error aus der Funktion zurück. Andernfalls wird der Rückgabewert der aufgerufenen
Funktion entpackt.
Dies ist die Kurzform für den folgenden Code:
errdefer
Es gibt Situationen, bei denen Code nur im Fehlerfall ausgeführt werden soll, zum Beispiel
um Speicher zu de-allozieren, der nicht mehr benötigt wird. Für solche Fälle kann errdefer
verwendet werden, das die gleichen Eigenschaften wie defer aufweist, mit dem großen Unter-
schied das errdefer nur ausgeführt wird, sollte die Funktion einen Fehler zurückgeben.
return mem;
}
Das praktische ist, dass durch errdefer die Allokation und Deallokation sehr nahe beieinander
liegen können. Dies macht es einfacher sicherzustellen, dass im Fehlerfall kein Speicherleck
(engl. Memory-Leak) entsteht.
Sowohl defer als auch errdefer beziehen sich auf den umschließenden
Block. Dadurch wird errdefer nicht ausgeführt, sollte der Error außerhalb des
Blocks zurückgegeben werden!
Error-Sets zusammenführen
Errors-Sets können mit dem || -Operator zusammengeführt werden.
67
const A = error{
Foo,
};
const B = error{
Bar,
};
const AB = A || B;
68
Zig Basics
Kapitel 3
Speicherverwaltung
Im Vergleich zu anderen Sprachen, wie etwa Java oder Python, muss der Speicher in Zig manuell
verwaltet werden. Dies bringt einige Vorteile mit sich, birgt aber auch Risiken, die bei Nichtbe-
achtung zu Schwachstellen in den eigenen Anwendungen führen können. Was Zig von anderen
Sprachen mit manueller Speicherverwaltung hervorhebt ist die explizite Verwendung und Ver-
waltung von Allokatoren, in der Programmiersprache repräsentiert durch den Allocator Typ.
Dies kann von anderen Programmiersprachen kommenden Entwicklern anfangs ungewohnt
vorkommen, bietet jedoch ein hohes Maß an Flexibilität, da Speicher zur Laufzeit dynamisch von
verschiedenen Speicherquellen alloziert werden kann.
Grundlagen
In den meisten Fällen kann ein Programm von zwei verschiedenen Quellen Speicher allozieren,
dem Stack und dem Heap. Wird eine Funktion aufgerufen, so alloziert diese Speicher auf
dem Stack der von den lokalen Variablen und Parametern zur Speicherung der zugehörigen
Werte verwendet wird. Dieser, von einer Funktion allozierte, Speicherbereich wird als Stack-
Frame bezeichnet. Die Allokation eines Stack-Frames wird durchgeführt, indem der Wert eines
spezielles CPU-Register, der sog. Stack-Pointer welcher auf das Ende des Stacks zeigt, verringert
wird. Die Anzahl an Bytes um die der Stack-Pointer verringert werden muss um alle lokalen
Variablen halten zu können wird vom Compiler zur Compilezeit berechnet und in entsprechende
Assemblerinstruktionen übersetzt.
Durch die Einschränkung, dass die Größe eines Stack-Frames zur Compilezeit bekannt sein
muss, lassen sich bestimmte Aufgaben schwer lösen. Angenommen Sie wollen eine Zeichenkette
unbekannter Länge von Ihrem Programm einlesen lassen, um diese später zu verarbeiteten. Eine
Möglichkeit um die Zeichenkette zu speichern wäre innerhalb der main Funktion eine Variable
vom Typ Array mit fester Länge zu deklarieren, jedoch ist dieser Ansatz sehr unflexibel da
Sie in dem gegebenen Szenario die Länge der zu erwartenden Zeichenkette nicht kennen. Bei
besonders kurzen Zeichenketten verschwenden Sie ggf. Speicher während sich besonders lange
Zeichenketten nicht einlesen lassen, da nicht genügend Speicher auf dem Stack alloziert wurde.
Um Probleme solcher Art besser lösen zu können, kann Speicher dynamisch zur Laufzeit eines
Programms alloziert werden. Der Heap kann als linearer Speicherbereich betrachtet werden, der
69
von einem Allokator verwaltet wird. Wird Speicher zur Laufzeit benötigt, so kann der Allokator
durch einen Funktionsaufruf angewiesen werden eine bestimmte Menge an Bytes zu allozieren.
Der Allokator sucht ein Stück Speicher mit der passenden Länge heraus, markiert dieses als
alloziert und gibt einen Zeiger auf den Beginn des Speicherbereichs zurück. Wird der Speicher
nicht mehr benötigt, so kann der Allokator durch einen weiteren Funktionsaufruf aufgefordert
werden den Speicher wieder frei zu geben. In C und C++ verwenden Sie i.d.R. malloc und free
um Speicher zu allozieren bzw. freizugeben, in den wenigsten Fällen müssen Sie sich jedoch
Gedanken um den zu verwendenden Allokator machen. Im Gegensatz dazu verwenden Sie in
Zig immer explizit einen Allokator.
In vielen Fällen, vor allem als Neuling, ist die Unterscheidung zwischen den vielen verschiedenen
Arten von Allokatoren, welche die Zig Standartbibliothek bereitstellt, weniger interessant. Wird
ein Standard-Allokator, im Sinne von malloc und free , benötigt, so kann auf den GeneralPur-
poseAllocator zurückgegriffen werden.
Allokation fehlschlagen kann. Dies spiegelt sich auch im Zig-Zen wieder, in welchem es u.a.
heißt: ,,Resource allocation may fail; resource deallocation must succeed’‚ (auf Deutsch: Die
Allokation von Resourcen kann fehlschlagen; die deallokation von Resourcen muss gelingen).
// chapter03/hello_world.zig
const std = @import("std");
Das obige Programm gibt ,,Hello, World’‚ auf der Kommandozeile aus, jedoch allozieren wir vor
der Ausgabe, zur Veranschaulichung, Speicher für den auszugebenden String auf dem Heap. Die
Funktion alloc() erwartet als Argument den Typ, für den Speicher alloziert werden soll ( u8 ),
sowie die Anzahl an Elementen. Aus dem Typ T und der Anzahl L berechnet sich die Anzahl an
Bytes die benötigt werden um L mal den Typ T im Speicher zu halten ( @sizeOf(T) * L ). Wie
bereits erwähnt kann die Speicherallokation fehlschlagen, aus diesem Grund müssen wir denn
71
Fehlerfall berücksichtigen bevor wir auf den Rückgabewert von alloc() zugreifen können38.
Da alloc() Speicher für mehr als ein Objekt alloziert, gibt die Funktion anstelle eines Zeigers
auf den Typ T einen Slice vom Typ T zurück. Ein Slice ist ein Wrapper um einen Zeiger, der
zusätzlich die Länge des referenzierten Speicherbereichs kodiert. Nach außer verhält sich ein
Slice wie ein Zeiger in C, d.h. mit dem Square-Bracket-Operator [] kann auf einzelne Element
zugegriffen werden, jedoch wird vor jedem Zugriff überprüft, ob der angegebene Index innerhalb
des allozierten Bereichs liegt um Out-of-Bounds-Reads zu vorzubeugen. Slices ersetzen in vielen
Fällen null-terminierte Strings, was dabei hilft Speicherfehlern vorzubeugen.
Ein wichtiger Punkt der zu jeder Allokation gehört ist die Deallokation des allozierten Speichers.
In Zig kann diese direkt nach dem Aufruf von alloc() bzw. create() platziert werden, indem
dem Aufruf von free() der defer Operator vorangestellt wird. Defer sorgt dafür, dass vor dem
Verlassen eines Blocks, im obigen Beispiel ist dies der Funktionsblock von main, alle defer Blöcke
ausgeführt werden und zwar in umgekehrter Reihenfolge in der sie deklariert werden. Dies ist
vor allem zum Aufräumen von Ressourcen sehr hilfreich.
Sehen Sie beim Lesen von Zig-Code keinen defer Block zur Bereinigung von
Speicher direkt nach einer Allokation sollten Sie erst einemal stutzig werden.
Es gibt aber auch Situationen, z.B. bei der Verwendung eines ArenaAllocators,
in denen nicht jede einzelne Allokation manuell bereinigt werden muss. In
solchen Fällen ist es aber durchaus nützlich für Leser Ihres Quellcodes, wenn Sie
durch ein Kommentar ersichtlich machen, dass das Fehlen einer Deallokation
beabsichtigt ist.
Lifetimes
Bei der Verwendung von Programmiersprachen mit manuellem Speichermanagement ist die
Berücksichtigung der Lifetime (Lebenszeit) von Objekten essenziell um Speicherfehler zu
vermeiden. Die Lifetime eines Objekts beschreibt ein abstraktes Zeitintervall zur Laufzeit, in
welchem ein bestimmtes Objekt oder eine Sequenz von Objekten im Speicher existieren und auf
diese zugegriffen werden darf. Die Art wie bzw. wo Speicher für ein Objekt alloziert wird hat
dabei großen Einfluss auf dessen Lebenszeit. Im Allgemeinen beginnt die Lifetime eines Objekts
mit dessen Erzeugung und endet wenn der Speicher des Objekt wieder freigegeben wird. Bezogen
auf die Art der Allokation kann grob zwischen den folgenden Fällen unterschieden werden:
• Statische Allokation
• Automatische Allokation
• Dynamische Allokation
Static Memory
38 Anstelle eines catch Blocks hätten wir an dieser Stelle auch try verwenden können.
72
Zig Basics
In Zig, wie auch in C, befinden sich statische Variablen und Konstanten, die im globalen Scope,
bzw. im Fall von Zig in einem Container39, einer Anwendung deklariert werden, in der .data
oder .bss Section eines Programms. Speicher für diese Sektionen wird beim Start eines Prozesses
gemapped und er bleibt bis zur Terminierung des Prozesses valide. Variablen die dies betrifft
haben eine statische Lifetime, d.h. sie sind vom Start eines Prozesses bis zu dessen Beendigung
valide. Selbes gilt für statische, lokale Variablen.
Die Konstante hello wird im umschließenden Container, dargestellt durch die Quelldatei,
deklariert und ist damit zum einen statisch, zum anderen ist sie aufgrund des const Modifiers
zur Compilezeit bekannt. Selbes gilt für die lokale, statische Variable x . Im gegensatz zu C
werden statische, lokalen Variablen nicht mit dem static Keyword deklariert sondern innerhalb
eines lokalen Structs welches ebenfalls einen Container darstellt. Lokale, statische Variablen
können nützlich sein um z.B. einen gemeinsamen „Shared-State“ zwischen Aufrufen der selben
Funktion zu verwirklichen.
In Zig wird jede Translationunit, d.h. jede Datei in der Quellcode liegt, als Struct
und damit als Container betrachtet. Dementsprechend gibt es eigentlich keine
global deklarierten Variablen wie man sie aus C kennt, sondern nur statische
Variablen die in Containern deklariert werden.
Automatic Memory
Objekte die durch Deklaration bzw. Definition innerhalb eines (Funktions-)Blocks erzeugt
werden erben ihre Lifetime von dem umschließenden Block. Für Variablen und Parameter von
Funktionen bedeutet dies, dass sich ihre Lifetime an der Lifetime eines Stack-Frames orientiert.
Bei jedem Funktionsaufruf wird für den Aufruf ein Stück zusammenhängender Speicher auf dem
Stack alloziert (der Stack-Frame) welcher groß genug ist um alle lokalen Variablen und Parameter
zu halten. Der Frame wird dabei durch zwei Spezialregister der CPU, dem Stack-Pointer (SP) und
dem Base-Pointer (BP), eingegrenzt. Der Stack-Pointer zeigt dabei auf das Ende vom Stack.
39Ein Container in Zig ist jedes Konstrukt, das als Namensraum (engl. namespace) dient. Dazu zählen u.a. Structs
aber auch Sourcedateien.
73
Es gibt verschiedene Arten von Stacks, jedoch ist die wohl häufigst auftretende
Form der Full-Descending-Stack. Das bedeutet, dass der Stack nach unten
„wächst“ (Descending), d.h. von höheren zu niedrigeren Speicheradressen, und
der Stack-Pointer auf das erste valide Element des Stacks zeigt (Full).
// chapter03/stack_01.zig
const std = @import("std");
pub fn foo(a: *u64) !void { // Begin der Lifetime von 'a' --/
a.* += 1; // /
} // Ende der Lifetime von 'a' -----------------------------/
Das obige Programm übergibt eine Referenz auf die Variable i als Argument an die Funktion
foo() , welche i inkrementiert. Die Lifetime der Variable startet mit ihrer Definition und endet
mit dem Funktionsblock von main. Innerhalb der Lifetime darf i von anderen Programmteilen,
in diesem Fall der Funktion foo() , referenziert und ggf. modifiziert werden.
Die Lifetime der Referenz a zu i beginnt mit dem Funktionsblock von foo() und endet mit
dem Ende des Funktionsblocks. Wichtig ist, dass die Lifetime einer Referenz immer innerhalb
der Lifetime des referenzierten Objekts liegen muss. Überschreitet die Lifetime einer Referenz
die Lifetime des referenzierten Objekts so spricht man von einem dangling Pointer (auf Deutsch
hängender Zeiger). Die Verwendung solcher dangling Pointer können zu schwerwiegenden
Programmfehlern führen, da der referenzierte Speicher als undefiniert gilt.
Um das Verhalten der Anwendung besser nachvollziehen zu können, besteht die Möglich-
keit mithilfe des Programms objdump die kompilierte Anwendung zu disassemblieren:
objdump -d -M intel stack_01 . Der Eintrittspunkt einer jeden Anwendung ist dabei die
main Funktion.
00000000010349b0 <stack_01.main>:
10349b0: push rbp ; Begin Prolog ---|
10349b1: mov rbp,rsp ; |
10349b4: sub rsp,0x10 ; End Prolog -----|
10349b8: mov QWORD PTR [rbp-0x8],0x0 ; i = 0
10349bf: 00
10349c0: lea rdi,[rbp-0x8] ; &i
10349c4: call 10348e0 <stack_01.foo>
10349cb: add rsp,0x10 ; Begin Epilog --|
74
Zig Basics
Jede Funktion besitzt ein Symbol (im Fall von main ist dies stack_01.main ) welches repräsen-
tativ für die Adresse der ersten Instruktion steht. Beim Funktionsaufruf wird diese Adresse in
das Instruktions-Zeiger-Register (Instruction Pointer - IP) geschrieben, welcher immer auf die
nächste auszuführende Instruktion zeigt. Jede Funktion beginnt mit dem sogenannten Funkti-
ons-Prolog, welcher einen neuen Stack-Frame für den Funktionsaufruf erzeugt, und endet mit
dem Funktions-Epilog, welcher den Stack-Frame wieder entfernt, d.h. den Stack in den Zustand
vor dem Funktionsaufruf zurückversetzt.
Im Prolog wird zuerst der Zustand des Base Pointers (BP), welcher auf den oberen Teil des
derzeitigen Stack-Frames zeigt, auf den Stack gepushed, um diesen im Epilog wieder herstellen
zu können. Danach wird der BP mit dem Wert des SP überschrieben, d.h. BP und SP zeigen
beide auf den alten BP auf dem Stack. Danach werden 16 Bytes (0x10) für die Variablen und
Parameter von main auf dem Stack alloziert, indem der SP um die entsprechende Anzahl an Bytes
verringert wird. Innerhalb des allozierten Speicherbereichs wird die Variable i mit dem Wert
0 initialisiert. Die Adresse der Variable i (BP - 8) wird an die Funktion foo() mittels des RDI
Registers übergeben40.
00000000010348e0 <stack_01.foo>:
10348e0: push rbp
10348e1: mov rbp,rsp
10348e4: sub rsp,0x20
10348e8: mov QWORD PTR [rbp-0x18],rdi
40Wer mehr über Assembler-Programmierung lernen möchte, dem empfehle ich das Buch ,,x86-64 Assembly
Language Programming with Ubuntu’‚ von Ed Jorgensen. Diese ist öffentlich zugänglich und bietet einen sehr
guten und verständlichen Einstieg.
75
10348ec: mov QWORD PTR [rbp-0x8],rdi
10348f0: mov rax,QWORD PTR [rdi]
10348f3: add rax,0x1
10348f7: mov QWORD PTR [rbp-0x10],rax
10348fb: setb al
10348fe: jb 1034902 <stack_01.foo+0x22>
1034900: jmp 1034924 <stack_01.foo+0x44>
1034902: movabs rdi,0x101ed99
1034909: 00 00 00
103490c: mov esi,0x10
1034911: xor eax,eax
1034913: mov edx,eax
1034915: movabs rcx,0x101dfb0
103491c: 00 00 00
103491f: call 1034940 <builtin.default_panic>
1034924: mov rax,QWORD PTR [rbp-0x18]
1034928: mov rcx,QWORD PTR [rbp-0x10]
103492c: mov QWORD PTR [rax],rcx
103492f: xor eax,eax
1034931: add rsp,0x20
1034935: pop rbp
1034936: ret
Nach dem Aufruf von foo() wird zuerst ein neuer Stack-Frame für den Funktionsaufruf erzeugt.
Innerhalb dieses Stack-Frames wird der Parameter a mit der Adressen von i initialisiert. Was
auffällt ist, dass die in RDI gespeicherte Adresse gleich mehrmals auf den Stack geschrieben wird
und zusätzlich direkt dereferenziert wird um den Wert von i in das Regsiter RAX zu laden.
Schaut man sich jedoch den gesamten Funktionskörper an so sieht man, dass von der Speicher-
stelle BP - 24 die Adresse von i zum Zurückschreiben des inkrementierten Werts geladen wird.
Damit ist BP - 24 in diesem Fall der Parameter a . Dementsprechend sieht der Stack nach aufruf
von foo() wie folgt aus.
76
Zig Basics
Diese im Funktionsprolog vollzogenen Schritte werden vor dem Verlassen der Funktion, im
Epilog, umgekehrt, d.h. beim ausführen der ret Instruktion befindet sich der Stack, bezogen auf
sein Layout, im selben Zustand wie vor dem Fuktionsaufruf. Was sich natürlich geändert hat ist
der Wert der Variable i .
Dynamic Memory
Wir haben uns die Allokation von dynamischem Speicher anhand des
GeneralPurposeAllocator am Anfang dieses Kapitels schon etwas angeschaut. Die Lifetime
von dynamsich allozierten Objekten ist etwas tückischer als die von statisch oder automatisch
allozierten. Der Grund ist, dass bei komplexeren Programmen sowohl die Allokation als auch
die Deallokation eines Objekts an verschiedenen Stellen im Code passieren kann, z.B. abhängig
von einer Bedingung.
Ein Beispiel hierfür ist eine verkettete Liste, bei der alle Element dynamisch auf dem Heap
alloziert werden. Bezogen auf die Allokation würde es in diesem Szenario mindestens eine Stelle
geben und zwar der Bereich des Codes, in dem ein neues Listenelement erzeugt wird. Bei der
Deallokation eines Elements muss zumindest unterschieden werden, ob ein Element aus der
Liste entfernt wird oder ob die gesamte Liste, zum Ende des Programms, dealloziert werden soll,
wobei letzteres als ein Sonderfall angesehen werden kann. Ein weiterer Aspekt auf den geachtet
werden muss ist, dass nach dem Löschen eines Elements der Liste, alle Referenzen auf dieses
Element invalide sind, d.h. es darf nicht mehr auf den Speicher zugegriffen werden.
// chapter03/[Link]
const std = @import("std");
77
pub fn new(i: u32, allocator: [Link]) !*@This() {
var self = try [Link](@This());
self.i = i;
return self;
}
};
[Link] = middle;
[Link] = lhs;
[Link] = rhs;
[Link] = middle;
// ... welches als nächstes aus der Liste (manuell) entfernt wird.
[Link] = [Link];
[Link] = [Link];
78
Zig Basics
[Link](middle);
// ... ab diesem Zeitpunkt ist `x` ein dangling Pointer und
// darf nicht mehr dereferenziert werden...
In folgendem Beispiel erzeugen wir eine verkettete Liste mit drei Elementen. Als nächstes defi-
nieren wir eine Konstante L , die das erste Element der Liste lhs referenziert und geben nach
und nach, durch Dereferenzierung, die Werte aller drei Elemente aus. Da weder lhs , middle
noch rhs bis zum Zeitpunkt der Ausgabe dealloziert wurden, ist die Dereferenzierung erlaubt.
Wie Sie vielleicht gesehen haben ist die dritte Ausgabe, die letzte Stelle an der L dereferenziert
wird. Damit überschreitet die Lifetime von L zwar theoretisch die Lifetime von lhs , middle
und rhs , in der Praxis spielt dies für die Korrektheit der Anwendung jedoch keine Rolle. Anders
sieht es mit der Konstanten x aus. Zwischen der ersten und zweiten Dereferenzierung von x
wird middle aus der Liste entfernt und dealloziert. Damit ist x ab der Deallokation von middle
ein dangling Pointer was zum Problem wird, da x später noch einmal derefernziert wird. Auch
nach der Deallokation zeigt x weiterhin auf eine existierende Speicherstelle, jedoch wurde diese
durch die Deallokation freigegeben, d.h. die Daten an dieser Stelle sind undefiniert. Das hält Zig
jedoch nicht davon ab den referenzierten Speicher als Instanz von Elem zu interpretieren, was
sich auch in der Kommandozeilenausgabe widerspiegelt.
$ ./linked-list
info: Wert von lhs: 1
info: Wert von middle: 2
info: Wert von rhs: 3
info: Wert von Elem ... von x vor deallokation: 2
info: Wert von Elem ... von x NACH deallokation: 2863311530
Diese Art von Speicherfehler wird als Use-After-Free bezeichnet und kann unter den richtigen
Bedingungen von Angreifern genutzt werden, den Kontrollfluss des Programms zu übernehmen,
sollte es für den Angreifer möglich sein die Speicherstelle zu kontrollieren.
Etwas das Sie sich grundsätzlich Angewöhnen sollten ist, Referenzen die Sie nicht mehr benö-
tigen zu invalidieren. Eine Möglichkeit dies zu tun ist anstelle eines Pointers einen optionalen
Pointer zu verwenden.
79
[Link] = [Link];
[Link] = [Link];
[Link](middle);
x = null; // wir invalidieren x direkt nach der Deallokation
Häufige Fehler
Im Gegensatz zu speichersicheren (engl. memory safe) Sprachen wie etwa Rust, bietet Zig einige
Fallstricke, die das Leben als Entwickler schwer machen können, aber nicht müssen! In diesem
Abschnitt werden wir uns einige davon näher anschauen und ich zeigen Ihnen, wie Zig Ihnen
dabei hilft sicheren Code zu schreiben.
Diese Art von Fehlern können genutzt werden um Daten von naheliegenden Objekten oder sogar
Adressen zu überschreiben. In der Vergangenheit wurde diese Art von Fehler von Angreifern ge-
nutzt um Schadcode in Anwendungen einzuschleusen, die Rücksprungadresse zu überschreiben
und so die Kontrolle über den Prozess zu übernehmen. Moderne Compiler injizieren deswegen
sogenannte Stack-Canaries, einen randomisierten Wert der von einem Angreifer nicht erraten
werden kann und der vor der Rückkehr in die aufrufende Funktion überprüft wird, in Stack-
Frames die potenziell von einem Buffer-Overflow betroffen sein könnten. Ist ein Stack-Frame von
einem Buffer-Overflow betroffen und wurde die Rücksprungadresse überschrieben, so bedeutet
dies, dass auch der Canary überschrieben wurde. In diesem Fall wird der Prozess zur Sicherheit
beendet. Wie das Zig-Zen so schön sagt: ,,Laufzeit-Crashes sind besser als Bugs’‚ (engl. „Runtime
crashes are better than bugs“).
80
Zig Basics
Im obigen Fall wird der Buffer-Overflow schon zur Compile-Zeit erkannt, da Arrays eine
zur Compile-Zeit bekannte Länge besitzen (Zig-Zen: „Compile errors are better than runtime
crashes“).
Allozieren wir den Speicher jedoch dynamisch so kann der Compiler uns nicht mehr vor unserem
Fehler bewahren.
Da wir in Zig jedoch in den meisten Fällen mit Arrays oder Slices arbeiten und nicht mit rohen
Zeigern und Zig für beide Datenstrukturen die Grenzen bei einem Speicherzugriff überprüft,
wird der Buffer-Overflow zumindest zur Laufzeit erkannt und der Prozess beendet (Zig-Zen:
„Runtime crashes are better than bugs“).
81
Zusammenfassung
Bei dem Arbeiten mit Referenzen bzw. Slices sind zwei Fragen von essenzieller Bedeutung:
Umschließt die Lifetime des referenzierten Objekts die der Referenz und wenn nein, habe ich
dafür gesorgt, dass nach dem Ende der Lifetime des Objekts nicht mehr versucht wird auf dieses
zuzugreifen. Fall Sie diese Fragen nicht beantworten können besteht eine hohe Wahrscheinlich-
keit, dass sich Speicherfehler in Ihren Code einschleichen.
82
Zig Basics
Kapitel 4
Jetzt wird es Zeit, dass wir die ganze Theorie einmal in die Praxis umsetzen. Das Ziel: einen
Taschenrechner programmieren. Am Ende dieses Kapitels werden Sie einen Taschenrechner in
den Händen halten, der die grundlegenden Rechenoperationen Plus, Minus, Mal und Geteilt
unterstützt.
83
API durch die sich GUIs beschreiben lassen ohne sich Gedanken um Low-Level-Konzepte
machen zu müssen.
Dvui befindet sich, genau wie Zig selbst, noch in Entwicklung, weshalb wir für unsren
Taschenrechner einen spezifischen Git-Commit verwenden, der in diesem Buch verwendeten
Zig-Version kompatibel ist.
Projekt anlegen
Erzeugen Sie einen neuen Projektordner mit dem Namen taschenrechner und initialisieren Sie
diesen.
$ cd taschenrechner/
$ zig init
info: created [Link]
info: created [Link]
info: created src/[Link]
info: created src/[Link]
info: see `zig build --help` for a menu of options
taschenrechner/[Link]
.dependencies = .{
.dvui = .{
.url = "[Link]
[Link]",
.hash =
"12202bc99ddacde83c39ae59dc29b31b192ea20c9c67f62a4cffb19b2d2a31f0bccb",
},
},
Innerhalb von [Link] können Sie im Anschluss auf die dvui Dependency zugreifen und das
darin enthaltene Modul dvui_sdl importieren. Grundsätzlich kann eine Dependency mehrere
Module und andere Ressourcen exportieren. Wie genau diese zu verwenden sind wird im besten
Fall durch die jeweilige Dokumentation deutlich. Im Fall von dvui gibt es ein eigenständiges
Demoprojekt44, das als Vorlage dient.
taschenrechner/[Link]
pub fn build(b: *[Link]) void {
// ...
44 [Link]
84
Zig Basics
// ...
exe.root_module.addImport("dvui", dvui_dep.module("dvui_sdl"));
// ...
}
Durch addImport importieren wir dvui_sdl unter dem Namen dvui , das heißt wir können
im Anschluss mit @import("dvui") auf das Modul zugreifen.
Hello dvui
Als nächstes brauchen wir ein Fenster, in dem unser Taschenrechner angezeigt werden soll.
Kopieren sie hierfür den folgenden Code in [Link].
taschenrechner/src/[Link]
const std = @import("std");
const dvui = @import("dvui");
comptime {
[Link](dvui.backend_kind == .sdl);
}
const Backend = [Link];
85
});
g_backend = backend;
defer [Link]();
// Dieser Aufruf markiert den Anfang eines Frames. Nach diesem Aufruf
// können dvui-Funktionen verwendet werden.
try [Link](nstime);
// SDL hilft auch bei der Verarbeitung von Events, wie etwa
// Tastatureingaben. Mit diesem Aufruf schicken wir alle SDL
// Events zu dvui, zur Verarbeitung.
const quit = try [Link](&win);
if (quit) break :main_loop;
// Cursor-Management
[Link]([Link]());
[Link]([Link]());
86
Zig Basics
[Link]();
Der obige Code besteht aus zwei Hauptteilen: dem Erzeugen eines neuen Fensters und der
Hauptschleife main_loop . Die Funktion initWindow erwartet verschiedene Optionen, wobei
für die meisten Standardwerte definiert sind, die automatisch übernommen werden. Für die
Allokation von dynamischem Speicher verwenden wir einen GeneralPurposeAllocator , als
initiale Fenstergröße geben wir 400 mal 600 Pixel an und der Titel unserer Applikation ist
Taschenrechner.
Innerhalb von main_loop implementieren wir den Hauptteil der Anwendung. Die zu sehenden
Funktionsaufrufe sind dabei Boiler-Plate-Code und bei allen dvui-Anwendungen mehr oder
weniger gleich.
Wenn Sie nun zig build run ausführen, sollten Sie ein leeres Fenster mit dem Titel Taschen-
rechner sehen.
User Interface
87
User Interface
Unser Taschenrechner besteht rein konzeptionell aus zwei Teilen: einer Anzeige und einem
Nummernblock, wobei der Block wiederum in einzelne Tasten unterteilt werden kann. Durch
drücken dieser Tasten lässt sich der Zustand des Taschenrechners verändern, welcher über die
Anzeige zurück an den Nutzer gespiegelt wird. Der interne Zustand kann dabei als Zustandsau-
tomat betrachtet werden. Bevor wir uns jedoch um die Logik des Taschenrechners kümmern,
wenden wir uns der Nutzeroberfläche zu.
Als erstes fügen wir einen Puffer für die Nutzereingaben hinzu. Hierfür bietet sich eine
ArrayList(u8) an, welche eine lineare Sequenz an Bytes darstellt. Der Vorteil von ArrayList
gegenüber einem Array ist, dass sich ArrayList s mühelos erweitern lassen, ohne das wir uns
Gedanken über den zu allozierenden Speicher machen müssen.
taschenrechner/src/[Link]
// ...
// ...
display_text.deinit();
}
Die Variable display_text , an die unsere ArrayList(u8) gebunden wird, definieren wir im
umschließenden Kontainer der Main-Funktion. Der Grund hierfür ist lediglich, dass wir damit
auch von anderen Funktionen, innerhalb von [Link], auf die Variable zugreifen können.
Da unsere Anwendung mit nur einem Thread auskommt, ist das auch in Ordnung und
wir müssen uns keine Gedanken um etwaige Wettlaufsituationen (Race-Condition/ -Hazard)45
machen. Außerhalb von Funktionen können wir außerdem kein defer verwenden, weshalb wir
display_text am Ende der Main-Funktion deinitialisieren, das heißt den allozierten Speicher
wieder freigeben.
Für das Layout des Taschenrechners definieren wir eine Funktion mit dem Namen
taschenrechner (ja ich weiß… sehr originär), die innerhalb der Hauptschleife aufgerufen wird.
taschenrechner/src/[Link]
// ...
45 [Link]
88
Zig Basics
// ...
// ...
// ...
// Ziffernblock
var block = try [Link](@src(), .vertical, .{});
{
var row1 = try [Link](@src(), .horizontal, .{});
{
if (try [Link](@src(), "7", .{}, .{ .gravity_y = 0.5 })) {
try display_text.append('7');
}
if (try [Link](@src(), "8", .{}, .{ .gravity_y = 0.5 })) {
try display_text.append('8');
}
if (try [Link](@src(), "9", .{}, .{ .gravity_y = 0.5 })) {
try display_text.append('9');
}
if (try [Link](@src(), "/", .{}, .{ .gravity_y = 0.5 })) {
try display_text.append('/');
}
}
[Link]();
89
var row2 = try [Link](@src(), .horizontal, .{});
{
if (try [Link](@src(), "4", .{}, .{ .gravity_y = 0.5 })) {
try display_text.append('4');
}
if (try [Link](@src(), "5", .{}, .{ .gravity_y = 0.5 })) {
try display_text.append('5');
}
if (try [Link](@src(), "6", .{}, .{ .gravity_y = 0.5 })) {
try display_text.append('6');
}
if (try [Link](@src(), "*", .{}, .{ .gravity_y = 0.5 })) {
try display_text.append('*');
}
}
[Link]();
90
Zig Basics
}
}
[Link]();
}
[Link]();
}
[Link]();
}
Dvui erlaubt es mit einer Box mehrere graphische Elemente entweder horizontal ( .horizontal )
oder vertikal ( .vertical ) anzuordnen. Mit einem Aufruf der Funktion box() wird eine
Container vom Typ Box geöffnet. Wird die deinit() Methode auf einer Box-Variable aufge-
rufen, so ist dies mit dem Schließen des Containers gleichzusetzen. Damit sind alle graphischen
Element, zum Beispiel ein Button der mit der button() Funktion erzeugt wird, die zwischen der
Definition einer Containervariable und dem Aufruf von deinit() erzeugt werden automatisch
Teil des Containers.
Die GUI unseres Taschenrechners besteht aus einer vertikalen Box vbox die zwei Elemente
enthält: das Display, welches die Eingabe anzeigt, und dem Ziffernblock.
Für das Display verwenden wir die Funktion label() mit der Text dargestellt wird. Die Funk-
tion erwartet unter anderem einen Format-String, sowie eine beliebige Anzahl an Ausdrücken,
deren Werte in den Format-String übernommen werden sollen. In unserem Fall soll nur der Inhalt
von display_text angezeigt werden. Aus diesem Grund verwenden wir den Format-String
"{s}" ( {s} steht für ersetze durch String) und übergeben als zugehörigen Ausdruck den in der
ArrayList gespeicherten String.
Der Ziffernblock besteht aus einer vertikalen Box, die insgesamt vier horizontale Boxen
umschließt. Jeder der horizontalen Boxen enthält vier Buttons, die jeweils eine Taste unseres
Taschenrechners darstellen. Der Vorteil von dvui ist, dass wir für Buttons nicht umständlich
Callbacks registrieren müssen (vielleicht erinnern Sie sich noch an das GTK-Beispiel aus Kapitel
1), stattdessen gibt jeder Button direkt true zurück, sollte er gedrückt worden sein. Das heißt
wir können einfach mit if überprüfen ob der jeweilige Button gedrückt wurde und eine
gewünschte Aktion ausführen. Fürs Erste begnügen wir uns damit, das Symbol der jeweiligen
gedrückten Taste an display_text anzufügen.
Nach erneutem kompilieren und starten der Anwendung sollten Sie das folgende sehen.
91
Abbildung 16: Taschenrechner mit grundlegendem Layout aber ohne Logik
Zustände Bitte
Um die Logik des Taschenrechners zu implementieren gibt es verschiedene Ansätze. Manche
Mathematikbibliotheken besitzen zum Beispiel eine eval() Funktion, mit der sich beliebige
mathematische Ausdrücke evaluieren lassen. Die Logik unseres Taschenrechners implementie-
ren wir jedoch selber. Damit dies nicht komplett ausartet, reduzieren wir die Möglichkeiten des
Taschenrechners auf ein Minimum.
Der in Abbildung 17 abgebildete Taschenrechner akzeptiert Ausdrücke bestehend aus genau
zwei (Fließkomma-)Zahlen, getrennt durch ein mathematisches Symbol sym (Plus + , Minus - ,
Mal * oder Geteilt / )46.
Ausgehen vom initialen Zustand start können wir eine beliebige Folge an Ziffern (Null bis
Neun) eingeben. Danach folgt entweder ein Komma oder eines der erlaubten, mathematischen
Symbole. Nach dem Symbol folgt wieder eine (Fließkomma-)Zahl. Durch Eingabe des Gleich-
heitszeichens = erreichen wir den Endzustand, das heißt der Ausdruck wird ausgewertet und
auf dem Display des Taschenrechners angezeigt. Folgt keine Ziffer hinter einem Komma, so wird
dieses bei der Auswertung ignoriert.
Eingaben für die keine Kante existiert, werden von unserem Taschenrechner ignoriert, das heißt
sie werden nicht an display_text angefügt.
92
Zig Basics
taschenrechner/src/[Link]
// ...
// ...
Für die Verarbeitung der Nutzereingaben definieren wir eine Funktion addValue , sowie zwei
Hilfsfunktionen isDigit und isSym . Die Funktion addValue bildet den Zustandsautomaten
aus Abbildung 17 ab und fügt die jeweilige Eingabe zu unserem Textbuffer display_text hinzu.
93
Die Funktion isDigit prüft ob die Eingabe eine Zahl zwischen Null und Neun ist, während
isSym überprüft, ob die Eingabe eines der Symbole + , - , * oder / darstellt.
taschenrechner/src/[Link]
fn addValue(v: u8) !void {
switch (state) {
.start => {
if (isDigit(v)) {
try display_text.append(v);
state = .num1;
}
},
.num1 => {
if (isDigit(v)) {
try display_text.append(v);
} else if (isSym(v)) {
// Wir nutzen ein Leerzeichen um Zahlen von +, -, * oder /
// zu trennen. Das erleichtert uns später das Parsen.
try display_text.append(' ');
try display_text.append(v);
state = .sym2;
} else if (v == ',') {
try display_text.append(v);
state = .num2;
}
},
.num2 => {
if (isDigit(v)) {
try display_text.append(v);
} else if (isSym(v)) {
try display_text.append(' ');
try display_text.append(v);
state = .sym2;
}
},
.sym2 => {
if (isDigit(v)) {
try display_text.append(' ');
try display_text.append(v);
state = .num3;
}
},
.num3 => {
if (isDigit(v)) {
try display_text.append(v);
} else if (v == '=') {
state = .E;
94
Zig Basics
} else if (v == ',') {
try display_text.append(v);
state = .num4;
}
},
.num4 => {
if (isDigit(v)) {
try display_text.append(v);
} else if (v == '=') {
state = .E;
}
},
.E => {}, // Nichts zu tun...
}
}
Die einzige Aufgabe von addValue ist es sicher zu stellen, dass der Nutzer nur (nach unseren
Maßstäben) korrekte Eingaben tätigen kann. Damit die Funktion jedoch auch Anwendung findet
müssen die Aufrufe von display_text.append() , innerhalb von taschenrechner() , durch
einen Aufruf von addValue() ersetzt werden ,zum Beispiel try display_text.append('+');
durch try addValue('+'); .
Danach können Sie mit zig build run die Anwendung neu kompilieren, welche jetzt, je nach
Zustand, nur noch bestimmte Eingaben annimmt.
Ausdruck Evaluieren
Nachdem wir die Regeln für (aus unserer Sicht) korrekte Ausdrücke festgelegt haben, müssen
wir diese nun nur noch Evaluieren. Zum Glück bestehen unsere Ausdrücke jeweils nur aus zwei
Zahlen. Damit müssen wir uns um die Reihenfolge der Auswertung keine Sorgen machen.
taschenrechner/src/[Link]
fn eval() !void {
// Die Auswertung findet nur statt, sollten wir im
// Endzustand sein.
switch (state) {
95
.E => {
// Wir müssen das Komma durch einen Punkt ersetzen,
// da ansonsten der Parser einen Fehler wirft.
for (display_text.items) |*item| {
if (item.* == ',') item.* = '.';
}
// Erste Zahl
const n1_ = [Link]().?;
const n1 = if (n1_[n1_.len - 1] == '.') n1_[0 .. n1_.len - 1]
else n1_;
// Symbol
const sym = [Link]().?;
// Zweite Zahl
const n2_ = [Link]().?;
const n2 = if (n2_[n2_.len - 1] == '.') n2_[0 .. n2_.len - 1]
else n2_;
96
Zig Basics
Die Funktion eval() parsed, falls wir im Endzustand sind, den in display_text enthaltenen
String in drei Bausteine: die erste Zahl, ein Symbol und die zweite Zahl. Da wir alle eingaben
Überprüfen wissen wir beim Parsen, dass der String das erwartete Format hat, wodurch wir beim
Aufruf von next() den null -Fall nicht explizit prüfen müssen.
Das Ergebnis wird zurück in display_text geschrieben. Dadurch können wir mit dem Ergebnis
direkt weiterrechnen. Je nachdem, ob das Ergebnis ein Komma enthält, ist der Taschenrechner
nach der Berechnung entweder in Zustand num1 oder num2 .
Damit das Ergebnis auch berechnet wird, fügen sie einen Aufruf der eval() Funktion an das
Ende der taschenrechner() Funktion hinzu.
taschenrechner/src/[Link]
pub fn taschenrechner() !void {
// ...
try eval();
}
Herzlichen Glückwunsch! Sie haben Ihren ersten, sehr minimalistischen Taschenrechner in Zig
programmiert.
Refactoring
Bevor wir unser kleines Projekt abschließen nutzen wir die Gelegenheit, um den Taschenrechner
etwas aufzubessern. Zum einen kann der Code für den Ziffernblock vereinfacht werden. Zum
anderen haben die Tasten teilweise unterschiedliche Größen.
Anstelle den Ziffernblock manuell zu definieren, bietet es sich an diesen als zweidimensionales
Array abzubilden.
97
.{ "1", "2", "3", "-" },
.{ "0", ",", "=", "+" },
};
Im Anschluss könne wir über die einzelnen Elemente iterieren. Dazu verwenden wir zwei
verschachtelte For-Schleifen (eine für jede Dimension des Arrays). Mit der äußeren Schliefen
iterieren wir über die Zeilen und mit der Inneren über die einzelnen Elemente der Zeile.
Jedes Element in dvui, egal ob Box oder Button, wird mit einer Id versehen. Wird die selbe
Funktion, zum Beispiel box() innerhalb einer Schleife, mehrfach verwendet, so kann dvui dem
Element nicht automatisch eine eindeutige Id zuweisen. In solchen Fällen muss die id_extra
Option beim jeweiligen Funktionsaufruf mit übergeben werden. Aus diesem Grund iterieren wir
in den gezeigten For-Schleifen nicht nur über das Array pad sondern parallel auch über die
Reihe 0, 1, 2, … und verwenden den jeweiligen Index als Extra-Id bei den Funtkionsaufrufen zu
box() und button() .
taschenrechner/src/[Link]
pub fn taschenrechner() !void {
var vbox = try [Link](@src(), .vertical, .{
.expand = .both,
});
{
// Display
try [Link](
@src(),
"{s}",
.{display_text.items},
.{
.expand = .horizontal,
.gravity_y = 0.5,
.gravity_x = 0.5,
},
);
98
Zig Basics
// Ziffernblock
var block = try [Link](@src(), .vertical, .{
.expand = .both,
.gravity_x = 0.5,
});
{
const pad: [4][4][]const u8 = .{
.{ "7", "8", "9", "/" },
.{ "4", "5", "6", "*" },
.{ "1", "2", "3", "-" },
.{ "0", ",", "=", "+" },
};
row_box.deinit();
}
}
[Link]();
}
[Link]();
try eval();
}
Die restlichen Optionen werden dazu verwendet, die Elemente zu zentrieren. Mit der .expand
Option sagen wir dvui, dass die Boxen das gesamte Fenster (entweder nur in der Horizontalen
.horizontal , in der Vertikalen oder in beide Richtungen .both ) einnehmen sollen. Die
.gravity_x Option definiert, dass das jeweilige Element innerhalb eines Containers zentral
angeordnet werden soll.
99
Für die einzelnen Buttons definieren wir, mittels .min_size_content , eine einheitliche, mini-
male Größe von 16.
Damit sieht unsere Anwendung schlussendlich wie folgt aus.
Zusammenfassung
In diesem Kapitel haben Sie gelernt, wie Sie graphische Anwendungen mit dvui entwickeln. Wir
haben uns angeschaut, wie ein neues Fenster erzeugt wird und haben dieses mit verschiedenen
graphischen Elementen befüllt. Durch die Interaktion mit Buttons haben wir den Zustand
unserer Anwendung verändert. Im weiteren haben wir gesehen, wie wir mit der Hilfe von Enums
den Zustand unserer Anwendung im Blick behalten und ausgehend von diesem Zustand nur
bestimmte Interaktionen zulassen.
Bei unserem Taschenrechner gibt es noch viel Verbesserungsbedarf. Scheuen Sie sich deshalb
nicht, mit dem bestehenden Code zu experimentieren. Wie wäre es zum Beispiel mit einer
Rücksetzfunktion oder der Möglichkeit negative Zahlen eingeben zu können?
100
Zig Basics
Kapitel 5
Control Flow
Zig bietet eine ganze Reihe an Kontrollstrukturen, mit denen vorgegeben werden kann, in
welcher Reihenfolge die Handlungsschritte eines Algorithmus, beziehungsweise einer Funktion,
abzuarbeiten sind. Hierzu zählen bedingte Anweisungen ( if , else if , else und switch )
mit denen verschiedene Zweige, basierend auf einer Bedingung, ausgeführt werden können,
Schleifen ( while und for ) durch die eine bestimmte Aufgabe mehrere male ausgeführt werde
kann und break , sowie continue , um den Ausführungsfluss an einer anderen Stelle fortzu-
führen. Zigs for -Loops machen es nicht nur einfach über Arrays, Slices und die Elemente von
Many-Item-Pointers zu iterieren, sondern sie erlauben auch das parallele Iterieren über mehrere
Kollektionen.
Bedingte Anweisungen
Eine der grundlegendsten Kontrollstrukturen in der strukturierten Programmierung sind
bedingte Anweisungen. Hierdurch können einzelne Ausdrücke oder Blöcke, basierend auf einer
Bedingung, ausgeführt werden.
Zig bietet zwei Wege, Verzweigungen zu implementieren: If-Statements und Switch-Statements.
Während sich If-Statements vor allem für die Überprüfung einer kleinen Menge an Bedingungen
eignen, machen es switch -Statements einfach, auch auf einer größeren Menge an möglichen
Bedingungen zu agieren.
If
Die einfachste Form eines If-Statements ist eine bedingte Anweisung, bestehend aus einer
Bedingung und einem zugehörigen Code-Abschnitt. Dieser Abschnitt kann entweder eine Block
sein, eingegrenzt durch geschweifte Klammern {} oder eine Ausdruck (engl. Expression).
Jede einfache bedingte Anweisung beginnt mit dem Schlüsselwort if , gefolgt von einer Bedin-
gung in runden Klammern () . Die Bedingung muss ein Ausdruck sein, der zu einem bool
evaluiert, das heißt der Ausdruck ist entweder true oder false . Nach der Bedingung folgt
entweder ein Block oder ein Ausdruck, welcher ausgeführt wird, sollte die Bedingung zu true
evaluieren.
101
If
Im obigen Beispiel wird geprüft, ob es weniger als 20 Grad Celsius hat. Ist dies der Fall wird eine
Nachricht ausgegeben, dass es kalt ist und man eine Jacke einpacken soll. Andernfalls wird der
zum if gehörende Block nicht ausgeführt.
Oft ist es nötig, entweder einen bestimmten Fall abzudecken oder falls dieser nicht eintritt,
unabhängig von weiteren möglichen Fällen, auf eine Alternative zurückzufallen. Hierzu werden
Verzweigungen verwendet. Die einfachste Verzweigung ist ein if - else . Ein else ist optional
und kann niemals alleine stehen, es muss immer auf ein if beziehungsweise else if folgen.
Es wird ausgeführt, sollte keiner der zuvorkommenden Bedingungen erfüllt worden sein.
Das obige Beispiel bedeutet: falls es weniger als 20 Grad Celsius hat gib die erste Nachricht
aus, ansonsten gib die zweite Nachricht aus. Dabei ist garantiert, dass einer der beiden Zweige
definitiv ausgeführt wird.
Unter der Verwendung von else if können beliebig viele Zweige miteinander verkettet
werden. Genau wie bei if enthält ein else if eine Bedingung die erfüllt sein muss, damit
der zugehörige Zweig ausgeführt wird. Dabei ist zu betonen, dass bei verketteten, bedingten
Anweisung immer nur einer der definierten Fälle zutreffen kann! Die Fälle werden dabei von
oben nach unten abgearbeitet.
chapter04/[Link]
const temp = 31;
if (temp < 20) {
[Link]("Es hat {d} Grad! Pack ne Jacke ein!", .{temp});
} else if (temp > 30) {
[Link]("Wow {d} Grad! Pack die Badehose ein!", .{temp});
} else {
[Link]("Eigentlich ganz schön heute!", .{});
}
Das obige Beispiel liest sich wie folgt. Hat es weniger als 20 Grad Celsius so wird der Nutzer
aufgefordert eine Jacke einzupacken. Hat es mehr als 30 Grad Celsius, so wird er aufgefordert
102
Zig Basics
eine Badehose einzupacken. Ansonsten, falls die Temperatur zwischen 20 und 30 Grad liegt, wird
der else -Block ausgeführt.
if , else if und else können auch dazu verwendet werden, basierend auf einer oder meh-
reren Bedingungen, eine Variable zu initialisieren. Hierzu wird eine If-Expression verwendet.
Der Unterschied ist, dass anstelle eines Blocks ein Ausdruck auf die jeweilige Bedinung bzw.
else folgt. Wie alle anderen Ausdrücke auch, müssen If-Expressions mit einem Semilkolon ;
abgeschlossen werden.
chapter04/[Link]
const nachricht =
if (temp < 20)
"Pack ne Jacke ein!"
else if (temp > 30)
"Pack die Badehose ein!"
else
"Eigentlich ganz schön heute!";
[Link](nachricht, .{});
In diesem Beispiel wird je nachdem welche Bedingung wahr ist, die Konstante nachricht mit
dem entsprechenden String initialisiert. Alle Zweige müssen hierfür einen Wert des selben Typs
zurückgeben, das heißt der Rückgabetyp der If-Expression wird durch die Rückgabewerte aller
zweige bestimmt. Außerdem ist in diesem Fall das else nicht optional, da es sonst zu Fällen
kommen kann, in denen nachricht gar kein Wert zugewiesen wird.
Sollte für bestimmte Fälle kein vernünftiger Rückgabewert angegeben werde können, so kann
innerhalb einer If-Expression auch null verwendet werden. In diesem Fall ist der Rückgabewert
des Ausdrucks ein Optional.
If mit Errors
Mit if kann auch auf Fehler geprüft werden. Hierfür muss die Bedingung eines If-Statements
ein Ausdruck sein, der zu einem Error-Typ evaluiert (zum Beispiel anyerror!u32 ). Das else
ist bei der Prüfung von Fehlern nicht optional und muss immer verwendet werden.
chapter04/[Link]
103
test "error capture #1" {
const a: anyerror!u32 = 7;
if (a) |value| {
try [Link](value == 7);
} else |err| {
_ = err;
unreachable;
}
}
Die durch || eingerahmten Variablen hinter dem if und else werden als Capture bezeichnet.
Sie „fangen“ jeweils den zum Zweig gehörenden Wert des Error-Typs.
Evaluiert der Ausdruck (im obigen Fall ist dies lediglich die Variable a ) zu einem Error, so wird
der else -Zweig betreten und der Error wird an err gebunden. Andernfalls wird der If-Zweig
betreten und der Wert (in diesem Beispiel 7 vom Typ u32 ) wird an value gebunden. Die
Namen der Captures sind, nach den Syntax-Regeln für Variablen, frei wählbar. Wird ein Capture
nicht benötigt, so kann anstelle eines Namen auch ein _ verwendet werden.
Im Kontrast zu catch entpackt ein if den Wert eines Ausdrucks nicht automatisch, sondern
bindet ihn lediglich an ein Capture. Das Verhalten von catch lässt sich jedoch auch mit if
reproduzieren:
chapter04/[Link]
test "error capture #2" {
const a: anyerror!u32 = 7;
const value = if (a) |value| value else |_| {
unreachable;
};
try [Link](value == 7);
}
If mit Optionals
Analog zu Errors kann mit if auch auf null getestet werden. Während bei Errors der else -
Zweig Pflicht ist, kann dieser für Optionals auch weggelassen werden.
chapter04/[Link]
104
Zig Basics
Evaluiert der gegebene Ausdruck (im obigen Beispiel die Variable a ) zu null , so wird der else
-Zweig ausgeführt, falls dieser vorhanden ist. Andernfalls wird der If-Zweig ausgeführt und der
entpackte Wert an den Capture value gebunden.
Pointer-Capture
Durch Pointer-Capture können, bei der Verwendung von if mit Errors oder Optionals, die
Werte einer Variable modifiziert werden. Dabei ist das Capture ein Zeiger auf die ursprüngliche
Variable.
chapter04/[Link]
test "optionals with pointer-capture #1" {
var a: ?u32 = 7;
if (a) |*value| {
try [Link](value.* == 7);
value.* += 1;
}
try [Link](a == 8);
}
Switch
Während bei If-Statements der Zweig basierend auf einem Boolean ( true ) ausgewählt wird,
verwenden Switch-Statements einen Musterabgleich (engl. Pattern-Matching). Hierfür wird ein
Wert, mit einem oder mehreren Werten des selben Typs, verglichen. So lässt sich zum Beispiel
überprüfen, ob ein Integer in einem bestimmten Wertebereich liegt.
Jedes Switch-Statement beginnt mit dem Schlüsselwort switch gefolgt von einem Ausdruck
in runden Klammern. Der Wert dieses Ausdrucks wird mit einem oder mehreren Mustern
verglichen, die jeweils einen Zweig darstellen. Die Zweige werden in geschweiften Klammern zu-
sammengefasst und bestehen aus Muster => {} oder Muster => Ausdruck , jeweils getrennt
durch ein Komma.
chapter04/[Link]
105
Switch
switch (b) {
// Jeder Zweig kann aus einem einzigen Wert bestehen.
1 => b += a,
// Mehrere Wert können mit `,` verknüpft werden.
2, 3, 4, 5, 6 => b *= a,
// Auf der Rechten Seite des `=>` kann neben einem
// Ausdruck auch ein Block stehen.
7 => {
b -= a;
},
// Als Muster für einen Zweig können beliebige Ausdrücke
// verwendet werden, solange diese zur Kompilierzeit
// bekannt sind!
blk: {
const x = 5;
const y = 3;
break :blk x + y;
} => b /= a,
// Der `else`-Zweig deckt alles bisher nicht abgedeckte ab.
else => b = a,
}
Wichtig ist, dass bei einem switch alle möglichen Fälle abgedeckt sein müssen. Ist dies nicht
möglich oder zu aufwendig kann ein else -Zweig verwendet werden, der alle bisher nicht
abgedeckten Möglichkeiten abdeckt. Sollten nicht alle möglichen Fälle abgedeckt werden, so
resultiert dies in einem Fehler während des Kompilierens.
Jeder Fall eines switch steht für sich alleine, das heißt es muss nicht explizit aus
dem switch „ausgebrochen“ werden. Dies steht im Kontrast zu Sprachen, wie
etwa C, bei denen, von oben nach unten, durch die einzelnen Fälle durchgefallen
werden kann, das heißt solange nicht das Ende des switch erreicht ist, wird in
C der nächste Switch-Case ausgeführt.
Genau wie bei else kann auch ein switch als Ausdruck verwendet werden.
chapter04/[Link]
106
Zig Basics
b = switch (b) {
// `b + a` ist ein Ausdruck, dessen Resultat, falls der Zweig
// ausgewählt wird, als Resultat des `switch`-Ausdrucks verwendet
// wird.
1 => b + a,
2, 3, 4, 5, 6 => b * a,
// Durch die Verwendung eines Labels (in diesem Fall `blk`). kann
// das Ergebnis von `b - a` aus dem Block herausgereicht werden.
7 => blk: {
break :blk b - a;
},
blk: {
const x = 5;
const y = 3;
break :blk x + y;
} => b / a,
else => a,
};
Genau wie alle anderen Ausdrücke auch, wird switch mit einem Semilkolon abgeschlossen. Bei
der Verwendung eines switch als Ausdruck ist wichtig, dass alle Zweige einen Rückgabewert
besitzen und dass die Rückgabewerte aller Zweige vom selben Typ ist, bzw. in einen gemein-
samen Typ konvertiert werden kann. Dies schließt die Verwendung von null mit ein.
Mit Switch-Cases kann ebenfalls überprüft werden, ob ein Wert in einem bestimmten Wertebe-
reich liegt. Wertebereiche werden durch einen Start- und Endwert eingegrenzt, der entweder
inklusive ( start...end ) oder exklusive ( start..end ) angegeben werden kann.
[Link]
fn encode(out: anytype, head: u8, v: u64) !void {
switch (v) {
0x00...0x17 => {
try [Link](head | @as(u8, @intCast(v)));
},
0x18...0xff => {
try [Link](head | 24);
try [Link](@as(u8, @intCast(v)));
},
107
0x0100...0xffff => try cbor.encode_2(out, head, v),
0x00010000...0xffffffff => try cbor.encode_4(out, head, v),
0x0000000100000000...0xffffffffffffffff => try cbor.encode_8(out,
head, v),
}
}
Das obige Beispiel ist Teil eines CBOR-Parsers47, implementiert in Zig. Der Code ist dafür
verantwortlich, einen Wert v so klein wie möglich zu kodieren. Hierfür wird, anhand des Wer-
tebereichs, geprüft, wie viele Bytes zur Kodierung benötigt werden. Die Wertebereiche werden
in diesem Beispiel inklusive angegeben. Liegt v zum Beispiel zwischen 0 und 23 (beide Werte
eingeschlossen), so wird exakt ein Bytes zur Kodierung verwendet.
While-Schleifen
While-Schleifen führen eine Code-Block wiederholt aus, bis eine Bedingung zu false evaluiert.
For-Schleifen
Es kann äußerst nützlich sein über den Inhalt eines Arrays oder Slices zu iterieren. Eine
Möglichkeit dies zu tun ist mit Hilfe einer For-Schleife.
chapter02/[Link]
const names = [_][]const u8{ "David", "Franziska", "Sarah" };
Eine for-Schleife beginnt mit dem Schlüsselwort for , gefolgt von einer Sequenz, über die iteriert
werden soll, in runden Klammern. Danach wird ein Bezeichner zwischen zwei | | angegeben.
Dem Bezeichner wird für jede Iteration der aktuelle Wert zugewiesen, d.h. für das obige Beispiel
wird im ersten Schleifendurchlauf name der Wert "David" zugewiesen, im zweiten Durchlauf
"Franziska" und so weiter. Nachdem über alle Elemente iteriert wurde, wird automatisch aus
der Schleife ausgebrochen.
Eine Besonderheit von Zig ist, dass innerhalb einer for-Schleife simultan über mehrere Sequen-
zen iteriert werden kann.
47 [Link]
108
Zig Basics
Die Sequenzen werden, getrennt durch ein Komma, innerhalb der runden Klammer angegeben.
Selbes gilt für die Bezeichner, an die die einzelnen Werte der Sequenzen gebunden werden. Im
obigen Beispiel wird als zweite Sequenz 0.. angegeben, d.h. eine Sequenz von Ganzzahlen
beginnend bei 0. Zig sorgt dabei automatisch dafür, dass names und 0.. über die selbe Länge
verfügen, indem das Ende von 0.. automatisch bestimmt wird, d.h. für das gegebene Beispiel
ist 0.. äquivalent zu 0..3 .
Sollten Sie über mehrere Arrays bzw. Slices gleichzeitig iterieren, so müssen sie sicherstellen,
dass alle die selbe Länge besitzen!
Mit dem Schlüsselwort break kann aus einer umschließenden Schleife ausgebrochen werden,
d.h. das Programm wird unter der Schleife fortgeführt.
Mit dem Schlüsselwort continue können sie den restlichen Körper der Schleife überspringen
und mit der nächsten Iteration beginnen. Sollten continue in der letzten Iteration der Schleife
ausgeführt werden, so wird aus dieser ausgebrochen.
Schleifen können auch geschachtelt werden. Wenn Sie innerhalb einer der inneren Schleifen, aus
einer der Äußeren ausbrechen wollen, müssen Sie sogenannte Label verwenden, mit der sie einer
bestimmten Schleife einen Namen geben können. Labels kommen vor dem for Schlüsselwort
und enden immer mit einem : . Sie können sowohl mit break als auch continue verwendet
werden.
109
outer: for (names) |name| {
for (dishes) |dish| {
[Link]("({s}, {s})", .{ name, dish });
// Da wir an dieser stelle aus der äußeren Schleife ausbrechen
// ist nur eine Ausgabe auf der Kommandozeile zu sehen.
break :outer;
}
}
In diesem Beispiel suchen wir nach einem Namen der mit dem Buchstaben P bzw. p beginnt.
Sollte aus der Schleife mit break ausgebrochen werden, so wird der else Block nicht ausge-
führt. Da names keinen solchen Namen beinhaltet wird der else Block aufgerufen und der
String "no name starts with p!" der Konstanten pname zugewiesen.
Neben Schleifen können auch if / else Blöcken Label zugewiesen werden. Dies erlaubt es,
mittels break , Werte aus dem Block heraus zu reichen, wie oben zu sehen ist.
Sie können das Beispiel mit zig build-exe chapter02/[Link] && ./loop compilieren und
ausführen.
110