JDLL, Lyon, 26 mai 2024. Par Stéphane Bortzmeyer. Atelier. (Source du support en Markdown. Tous les fichiers.)
cd /tmp
wget https://ziglang.org/download/0.12.0/zig-linux-x86_64-0.12.0.tar.xz
unxz zig-linux-x86_64-0.12.0.tar.xz
cd /usr/local
mkdir zig
cd zig
tar xvf /tmp/zig-linux-x86_64-0.12.0.tar
Puis modifier PATH
pour y mettre ce répertoire /usr/local/zig/zig-linux-x86_64-0.12
.
Tester avec zig version
.
EXERCICE Mettez dans un fichier hello.zig
:
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, World!\n", .{});
}
Exécutez-le avec zig run hello.zig
.
main
, ici, ne renvoie rien.{}
sera expliqué plus tard.Pour compiler et garder le programme : zig build-exe hello.zig
.
EXERCICE Essayez de compiler :
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, {any}!\n", .{miss});
}
et :
const std = @import("std");
pub fn main() void {
const i = 42;
const s = "toto";
const p = &s; // Adresse de s
std.debug.print("Hello, {any}!\n", .{i + p});
}
Les tests de déclaration et typage sont faits à la compilation.
Solution avec déclaration et typage corrects
Ce code est correct (mais les lignes commentées provoqueraient une erreur) :
const std = @import("std");
pub fn main() void {
const pi: f32 = 3.1416; // Type indiqué explicitement
const i: i32 = 2; // Type indiqué explicitement
// const j: i32 = i + pi; // Refusé, on ne peut pas mettre un réel dans un entier
const r: f32 = i; // L'inverse est accepté.
const p = π // Type inféré automatiquement ('*const f32', donc pointeur)
// const l = p + i; // Refusé, types incompatibles
var k: i32 = 3;
k = 8;
std.debug.print("{any} {any} {any} {any}!\n", .{ pi, i, r, p });
}
Vous pouvez afficher le type inféré avec la fonction @TypeOf
.
Toutes les vérifications de type sont faites à la compilation (comptime en terminologie Zig, vous verrez souvent ce terme).
EXERCICE Essayez de modifier p
(par exemple p = &r
). Et la valeur vers lequel il pointe (pi = 2.5
). Que faut-il modifier pour que ce soit accepté ?
const std = @import("std");
pub fn main() void {
const name = "myfile.txt";
const f = std.fs.cwd().openFile(name, .{}); // https://ziglang.org/documentation/0.12.0/std/#std.fs.cwd
std.debug.print("File opened? {any}\n", .{f});
}
Et .{}
? C’est un littéral représentant un tableau, ici, un tableau vide (les options d’ouverture). .{1, 2, 3}
serait un tableau de trois éléments.
EXERCICE Quel est le type de f
? Le point d’exclamation indique l’union de deux types.
EXERCICE On va essayer de lire le contenu du fichier ouvert :
const std = @import("std");
pub fn main() void {
const name = "myfile.txt";
var buffer: [100]u8 = undefined;
const f = std.fs.cwd().openFile(name, .{}); // https://ziglang.org/documentation/0.12.0/std/#std.fs.cwd
const result = f.readAll(&buffer); // https://ziglang.org/documentation/0.12.0/std/#std.net.Stream.readAll
std.debug.print("Result of read is {d} and content is \"{s}\"\n", .{ result, buffer[0..result] });
}
Le problème est que f
peut être un fichier ouvert ou une erreur. Zig ne permet pas d’ignorer les erreurs (contrairement à C). Il y a plusieurs solutions, on utilisera le try
:
pub fn main() void {
const name = "myfile.txt";
var buffer: [100]u8 = undefined;
const f = try std.fs.cwd().openFile(name, .{});
const result = try f.readAll(&buffer);
std.debug.print("Result of read is {d} and content is \"{s}\"\n", .{ result, buffer[0..result] });
}
Il reste une erreur à la compilation. main
doit être déclaré comme pouvant renvoyer une erreur :
pub fn main() !void {
EXERCICE Que fait le programme si le fichier n’existe pas ? Et s’il existe ?
En C, on choisirait une valeur spéciale (par exemple 0) pour dire « pas de valeur ». En Zig, c’est explicite :
pub fn main() void {
var i: ?u8 = null; // Pas de valeur
var j: u8 = 42;
std.debug.print("Hello, {any}!\n", .{i});
std.debug.print("Hello, {any}!\n", .{i orelse 0}); // orelse va déballer la valeur (ou mettre 0)
i = 7;
j = 33;
std.debug.print("Hello, {d}!\n", .{i orelse 0}); // orelse va déballer la valeur (ou mettre 0)
}
EXERCICE j = i
serait refusé à la compilation (erreur de typage). Que faudrait-il faire pour que j
prenne la valeur de i
?
La cause de la majorité des failles de sécurité dans les programmes en C.
En Zig, la gestion doit être explicite. On utilise des allocateurs (la bibliothèque standard en compte plusieurs mais on peut aussi écrire le sien).
pub fn main() !void {
var myallo = std.heap.GeneralPurposeAllocator(.{}){}; // L'allocateur le plus courant.
const allocator = myallo.allocator();
var myarray = try allocator.alloc(u8, 10); // Peut échouer par manque de mémoire
defer allocator.free(myarray);
std.debug.print("{any}\n", .{myarray}); // Selon l'allocateur utilisé, donnée initialisées ou pas
for (0..myarray.len) |i| {
myarray[i] = @truncate(i);
}
std.debug.print("{any}\n", .{myarray});
}
EXERCICE Que se passe-t-il si on demande 10000000000000000000 octets ? Rappel : Zig vous empêche d’ignorer les erreurs.
On remplace l’allocateur par un autre, qui affiche allocations et libérations. La première ligne de main
devient :
const loggingallo = std.heap.LoggingAllocator(std.log.Level.debug, std.log.Level.debug);
var myallo = loggingallo.init(std.heap.page_allocator);
EXERCICE Qu’affiche-t-il ? Et si on met en commentaire la ligne qui commence par defer
?
Beaucoup de bibliothèques vous demandent de passer un allocateur en paramètre. Par exemple dans la bibliothèque standard, allocPrint
:
pub fn main() !void {
const a = 2;
const b = 3;
var myallo = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = myallo.allocator();
const mystring = try std.fmt.allocPrint(
allocator,
"{d} + {d} = {d}",
.{ a, b, a + b },
);
defer allocator.free(mystring);
std.debug.print("{s}\n", .{mystring});
}
La méthode la plus officielle est le Zig Build system. Mais on va d’abord faire plus basique.
On veut générer des UUID, une famille d’identificateurs normalisée dans le RFC 9562. Il existe un paquetage dans Zul. On le copie localement et on le compile :
git clone https://github.com/karlseguin/zul.git
cd zul
zig build
(zig build
utilise le fichier build.zig
qu’on verra à la fin.) On peut ensuite importer le paquetage dans un programme (la deuxième ligne) :
const std = @import("std");
const zul = @import("zul/src/uuid.zig");
pub fn main() void {
// UUID version 4 : aléatoire
const uuid1 = zul.UUID.v4();
std.debug.print("UUID v4: {any}\n", .{uuid1});
}
EXERCICE Créer un UUID de version 7 (fondé sur le temps, faites-le tourner deux-trois fois de suite pour voir).
main
On veut trouver le PGCD de deux nombres. Voici la structure générale du programme, ce qui permet également de voir une des façons de traiter les paramètres sur la ligne de commande :
const std = @import("std");
fn pgcd(l: u32, r: u32) u32 {
???
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len != 3) {
std.debug.print("Usage: find-pgcd L R\n", .{});
return error.WrongArgs;
}
const l = try std.fmt.parseInt(u32, args[1], 10);
const r = try std.fmt.parseInt(u32, args[2], 10);
std.debug.print("PGCD({d},{d}) = {d}\n", .{ l, r, pgcd(l, r) });
}
EXERCICE Remplacer le corps de la fonction pgcd
par un code correct. Je recommande la version originale de l’algorithme d’Euclide. Note : outre for
, Zig a while
.
Le tableau de base en Zig :
var my_array = [3]u8{ 8, 9, 4 };
…
var i = my_array[1];
Le tableau est toujours de taille fixe. Mais, on a :
Supposons un protocole réseau encodé en binaire. Le client doit envoyer un octet valant 1, 2 ou 3, les autres valeurs étant invalides.
Le serveur répond par un octet, 0 si la requête est correcte, 1 autrement.
Vous aurez besoin de :
net.Address.resolveIp
et tcpConnectToAddress
@as
pour interpréter une valeur comme appartenant à un autre type et de @bitCast
pour convertir les bitsUn serveur à ces spécifications écoute sur TODO:9999
. Son source en Python.
EXERCICE Écrire le client en Zig. Commencez par une des trois valeurs possibles pour la requête du client.
Dès que le programme devient un peu complexe, il vaut mieux passer au système de compilation officiel. On écrit un build.zig
, un build.zig.zon
avec les méta-données et on tape zig build
.
On n’est pas obligé de tout écrire en partant de zéro, zig init
fournit déjà des fichiers qu’on n’a plus qu’à modifier.
Ce paquetage permet de gérer des ensembles.
Ajouter les dépendances au build.zig.zon
peut être fait simplement :
zig fetch --save https://github.com/deckarep/ziglang-set/archive/c489ab4e28afd477d928fc242963ffc103521c4a.tar.gz
zig build
(Pas encore de release donc on doit utiliser l’identificateur de commit.)
EXERCICE Après un zig init
, ajoutez un paquetage (par exemple Ziglang Set) aux dépendances, et modifiez src/main.zig
pour l’utiliser. Un zig build
doit vous laisser un exécutable zig-out/bin/NOM-DE-VOTRE-PROJET
.
Une des forces de Zig est qu’il est plutôt facile d’utiliser les innombrables bibliothèques C existantes.
On ré-écrit le calcul du PGCD en C. On va l’utiliser dans le programme modifié, qui va utiliser cImport
. On compile :
zig build-exe pgcd-with-c.zig -I.
Pour les cas plus compliqués, là encore, on utilisera le système de construction. Les détails de son utilisation sont une bonne lecture.
while
ou un for
(un exemple) ou dans les traitements d’erreurs avec catch
,comptime
par exemple pour écrire du code générique (qui fonctionne avec plusieurs types) (un exemple),zig test pgcd-with-test.zig
),Installable via MELPA (package-install zig-mode
). Source en https://github.com/ziglang/zig-mode
Un exemple de bibliothèque ajoutant de nombreuses fonctions. (Avec exemples.)