Initiation à la programmation en Zig

JDLL, Lyon, 26 mai 2024. Par Stéphane Bortzmeyer. Atelier. (Source du support en Markdown. Tous les fichiers.)

Pourquoi Zig

Ressources à garder sous la main

Zig en dix minutes

Si vous avez votre propre ordinateur

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.

Pour tout le monde

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.

Solution

Explications

Pour compiler et garder le programme : zig build-exe hello.zig.

Il faut tout déclarer

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

Le typage est strict

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é ?

Solution

Erreurs à l’exécution

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 ?

Solution

Variables optionnelles

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 ?

Solution

Gestion mémoire

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 maindevient  :

    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});
}

Utiliser un paquetage

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).

Solution

Une autre fonction que 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.

Solution

Les tableaux

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 :

Un client réseau (TCP)

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 :

Un 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.

Solution

Le système de construction

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.

Exemple avec le paquetage Ziglang Set

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.

Solution

Lier à du code en C

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.

Et encore, on n’a pas vu…

Extra

Mode Emacs

Installable via MELPA (package-install zig-mode). Source en https://github.com/ziglang/zig-mode

Tutoriels

Zul

Un exemple de bibliothèque ajoutant de nombreuses fonctions. (Avec exemples.)

Autres forums et sites utiles