- Le compilateur Zig, qui intègre nativement la compilation de code C et la compilation croisée, est, parmi tous les langages qu’a connus l’auteur en 45 ans de carrière, le plus étonnant
- Avec des fonctionnalités uniques comme l’exécution à la compilation, les variables de taille de bit arbitraire et les blocs de test, il va bien au-delà d’un simple remplaçant de C/C++ et propose une toute nouvelle manière de programmer
- Grâce à une syntaxe concise et claire — déclaration de variables avec inférence de type, structures anonymes, break étiqueté — on peut l’apprendre rapidement
- Il prend en charge le débogage de code optimisé grâce à des tests de modules indépendants via les blocs de test et la fonction intégrée
@breakpoint
- Avec sa prise en charge de la programmation bas niveau via les champs de bits et les opérations sur les bits, il atteint à la fois efficacité et robustesse, tout en intégrant dans un langage compilé des avantages habituellement associés aux langages interprétés
Préface
- En 45 ans de carrière, l’auteur n’a jamais vu de langage aussi étonnant que Zig
- Zig n’est pas simplement un nouveau langage, mais un outil qui transforme fondamentalement la manière de programmer
- Le considérer seulement comme un remplaçant de C ou C++ serait une forte sous-estimation
- Le but de cet article est de présenter des fonctionnalités simples mais séduisantes de Zig et d’aider les programmeurs à démarrer rapidement
- Il existe encore bien d’autres fonctionnalités qui influencent l’adoption de Zig dans l’industrie
Le compilateur Zig
- En fournissant nativement la compilation de code C et la compilation croisée sans configuration séparée, il a un impact majeur sur l’industrie
- L’installation consiste à télécharger le compilateur correspondant au processeur/à l’OS depuis la page de téléchargement de Ziglang, à le décompresser, puis à le copier dans le répertoire souhaité
- Sous Windows 10, il est conseillé de copier le fichier zip x86_64 dans « Program Files » et de renommer le répertoire racine en « zig-windows-x86_64 » afin d’éviter de devoir modifier la variable d’environnement Path lors des mises à jour de version
- Après avoir ajouté le chemin du répertoire racine à la variable d’environnement Path, le compilateur peut être utilisé en mode CLI
- Pour construire un programme « Hello World! », il est recommandé de consulter la section « Getting Started » du site officiel
Concepts et commandes clés
Déclaration de variables
- Une déclaration de variable se compose d’une première partie avec la visibilité (
pub ou omission), var/const et le nom de la variable, d’une deuxième partie pour le type, puis d’une troisième pour l’initialisation
- Seules la première et la troisième parties sont obligatoires, et le type peut être inféré à partir de la valeur d’initialisation
- Exemple :
var sum : usize = 0;
- Une variable déclarée sans
pub n’est accessible qu’à l’intérieur du module (similaire à une variable static en C)
- Il est déconseillé de déclarer des variables
pub, et il est recommandé de limiter les fonctions pub afin de réduire le couplage et d’augmenter la cohésion
Structures, structures anonymes, blocs de test
- Les littéraux de structure anonyme entourés par
.{ et } servent à initialiser des éléments d’une autre structure ou à créer une nouvelle structure avec ses éléments initialisés
.{ } est un littéral de structure anonyme vide
- La forme
struct { } correspond à une déclaration de structure
- Les blocs de test permettent de compiler et d’exécuter des tests sans produire d’exécutable
Champs de bits
- Les champs de bits sont déclarés comme des champs ayant un type d’une taille spécifique dans une
packed struct
- Les pointeurs peuvent pointer vers un champ de bits spécifique
Boucles for
- La syntaxe Zig est plus claire que celle du C, mais utilise un intervalle ouvert
[0..9) au lieu de [0..8]
- Le type, l’initialisation, le test et l’incrémentation de la variable de boucle
i sont gérés automatiquement
Tableaux
[_] définit un tableau dont la taille n’est pas spécifiée, suivi du type des éléments et de l’initialisation
- Exemple :
var grid = [_]u8{0} ** 81; initialise 81 éléments de type u8 à 0
- La taille du tableau est inférée à partir de l’argument de répétition de l’initialisation
- Dans un environnement de test, on peut parcourir les éléments d’un tableau et en calculer la somme
- Les variables déclarées entre
| dans une boucle for sont automatiquement considérées comme ayant le même type que les éléments du tableau
usize est l’entier non signé naturel de la plateforme (u64 sur 64 bits, u32 sur 32 bits)
Pointeurs multi-éléments
- Pour qu’un pointeur vers tableau puisse utiliser l’arithmétique des pointeurs, il doit être explicitement déclaré comme pointeur multi-éléments, par exemple
[*]const i32
- Même si le tableau est
const, le pointeur peut être déclaré en var
Déréférencement de pointeur
- Un pointeur auquel est assignée l’adresse d’une position précise dans un tableau ne peut pas être mis à jour par arithmétique des pointeurs
- Le déréférencement d’un pointeur s’écrit
ptr.*
Break étiqueté
- Il est possible d’effectuer à la compilation diverses opérations, comme l’initialisation d’un tableau
- Le break étiqueté consiste à ajouter
: après le nom d’un bloc et à renvoyer une valeur hors du bloc avec break
0.. représente une plage infinie à partir de 0
- Dans une boucle
for, les variables sont automatiquement initialisées et incrémentées, et la boucle se termine après le traitement de la dernière position du tableau
- Un tableau peut ne pas être explicitement initialisé avec
undefined
Les fonctions en Zig
- Les fonctions sont déclarées avec
fn et sont statiques par défaut (utilisables uniquement dans le fichier)
- Déclarées comme
pub fn, elles peuvent être importées depuis d’autres fichiers
- Les fonctions peuvent être « inlined »
- Les pointeurs de fonction s’écrivent avec
const en tête, suivi du prototype de fonction
La programmation orientée objet en Zig
- Les structures peuvent contenir des fonctions
- Dans l’exemple de pile, on peut stocker jusqu’à 81 éléments (de type
StkNode)
- Les opérateurs
++ et -- n’existent pas en Zig ; on utilise += et -=
- Le pointeur de pile est un entier utilisé comme indice du tableau
stk
- Le pointeur
self n’est pas passé explicitement comme paramètre ; il est implicitement supposé être le pointeur de l’instance de pile sur laquelle la fonction est appelée
- Dans un appel comme
stack.pop(), self est un pointeur vers stack (semblable à this en Java/C++)
- La fonction
init() est le constructeur de la pile
- Les fonctions
pop et push sont « inlined »
Construire et exécuter un programme Zig
Construire un exécutable
- Pour produire un exécutable, il faut une fonction
main qui représente le point d’entrée du programme
- Un programme simple peut inclure la fonction
main dans le même fichier
- Pour déboguer un module indépendamment, on peut insérer une fonction
main à la fin du fichier, puis la commenter une fois le débogage terminé
- Commande de compilation :
zig build-exe -O ReleaseFast program.zig
Exécuter les blocs de test d’un module
- C’est l’une des meilleures fonctionnalités de Zig, utile pour les tests et le prototypage
- Un bloc de test commence par
test "message" { et se termine par }
- « message » est la chaîne affichée lors de l’exécution du test
- Les blocs de test s’exécutent indépendamment de l’exécutable, et l’exécutable final n’exécute pas les tests
- Commande de test :
zig test module.zig
- Le bloc de test de
example.zig teste les fonctions set et print ; set prend en paramètre une chaîne décimale, et print affiche l’en-tête « Input Grid » avant d’afficher la grille
La sortie de Zig
- L’instruction
std.debug.print appelle la fonction print située dans debug.zig de la bibliothèque standard Zig std
- Le premier paramètre est une chaîne de format, le second une structure anonyme contenant la liste des variables à afficher
- Lorsqu’il n’y a pas de format, la structure est vide
- L’affichage se fait sur
stderr par défaut
- Contrairement au
printf du C, Zig peut traiter à la compilation les chaînes littérales et la liste des variables
Déboguer un exécutable
- L’usage d’un débogueur n’est pas simple, sauf dans un IDE avec débogueur intégré (Eclipse, IntelliJ IDEA) ou un kit de développement intégré (
w64devkit)
- L’intégration des symboles alourdit le code et impose une compilation en mode Debug, produisant un code exécutable nettement moins efficace
- Zig fournit une solution pratique pour éviter ces problèmes
Fonction intégrée @breakpoint
- En insérant
@breakpoint(); dans le code source, le programme s’arrête à cet endroit lorsqu’il est exécuté dans le débogueur
- C’est une fonctionnalité utile pour déboguer du code Zig optimisé, sans symboles
- Juste avant
@breakpoint();, on peut utiliser std.debug.print pour afficher les variables à suivre et vérifier leur valeur à cet instant
- Dans l’exemple
debug_example.zig, du code affichant grid et diverses variables est inséré dans la fonction set, avec @breakpoint();
- Commande de build :
zig build-exe debug_example.zig
- On lance ensuite
debug_example.exe dans un débogueur comme gdb, puis on exécute le programme avec la commande r
- La commande
c permet de continuer l’exécution tout en suivant le contenu de la grille et les variables
- En répétant la touche Entrée pour continuer, on peut vérifier que les valeurs de la grille correspondent à celles du bloc de test de
example.zig
La programmation bas niveau en Zig
Représentation matricielle
- Les chiffres décimaux sont stockés dans la matrice sous forme d’entiers
u8 standard
- La grille d’entrée est une chaîne, mais les caractères ASCII sont convertis en interne en entiers
u8
- Le stockage des nombres se fait linéairement, ligne par ligne, dans le tableau
grid de 81 positions : var grid = [_]u8{0} ** 81;
- Pour vérifier la validité de la grille, il faut accéder aux éléments par ligne et par colonne
- On crée un tableau de 9 pointeurs, chacun pointant vers le début de chaque ligne
- Le break étiqueté permet de renvoyer une valeur depuis un bloc de code :
break :fill9x9 m; initialise matrix avec m
- Notation d’accès aux éléments :
element = matrix[i][j]
Représenter les chiffres décimaux sous forme de bits
- Concept clé : remplacer le chiffre décimal entier
i par l’entier code
i ∈ [1,9] → code = 2ⁱ⁻¹
i = 0 → code = 0
- La position du seul bit mis à
1 dans code est i-1 (quand i est entre 1 et 9), sinon tous les bits valent 0
- Un tableau fournit la valeur de
code pour chaque chiffre (1→1, 2→2, 3→4, ..., 9→256)
Calculer code en Zig
- La valeur
code est calculée avec l’opérateur de décalage à gauche uniquement si c n’est pas nul : code = @as(u9,1) << (c-1);
- En Zig, les constantes doivent avoir une taille adaptée pour que l’opération soit compilée et que le résultat soit assigné à une variable
code est déclaré de type u9 (la valeur maximale 256 nécessite au moins 9 bits)
- Zig permet d’avoir des variables avec une taille de bit arbitraire
- La fonction intégrée
@as sert à caster la constante 1 en type u9
Représentation de la grille avec des champs de bits
Grille en champs de bits par ligne
- Le tableau
lines reflète toute la grille en représentant chaque ligne par un entier de 9 bits : var lines = [_]u9{0} ** 9;
- En accédant au tableau avec la ligne
i, on vérifie par un ET bit à bit (&) si un chiffre donné est déjà présent dans cette ligne : lines[i] & code
- Si le résultat vaut 0, le chiffre n’est pas encore présent dans la ligne
i, sinon c’est un doublon
Grille en champs de bits par colonne
- Le tableau
columns reflète toute la grille en représentant chaque colonne par un entier de 9 bits : var columns = [_]u9{0} ** 9;
- En accédant au tableau avec la colonne
j, on vérifie par un ET bit à bit si un chiffre donné est déjà présent dans cette colonne : columns[j] & code
- Si le résultat vaut 0, le chiffre n’est pas encore présent dans la colonne
j, sinon c’est un doublon
Règles du Sudoku
- Lorsqu’on insère un nouveau chiffre dans une grille de Sudoku vide, il ne doit pas déjà exister dans toute la ligne, la colonne et la cellule contenant ce nouvel élément
- Les cellules correspondent à chacun des 9 blocs 3x3 délimités par des traits épais
- Dans une grille 9x9, chaque élément appartient à une ligne, une colonne et une cellule uniques
- Dans la grille d’exemple, la première cellule contient 3, 5, 6, 8 et 9, tandis que 1, 2, 4 et 7 en sont absents
- Les tableaux
lines et columns gèrent les contrôles de doublons pour les lignes et les colonnes
- Un nouveau tableau est nécessaire pour vérifier les doublons au niveau des cellules
Grille en champs de bits par cellule
- Le tableau
cells reflète toute la grille en représentant chaque cellule par un entier de 9 bits : var cells = [_]u9{0} ** 9;
- Il est plus simple d’accéder à
cells comme à une matrice 3x3
- On remplit le tableau
cell de manière similaire à ce qui a été fait pour la matrice 9x9
- Il faut déterminer, à partir de la ligne et de la colonne d’un élément de la grille 9x9 d’origine, la ligne et la colonne correspondantes dans la matrice
cell
- Comme la division entière est très lente, on utilise le tableau
cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 }; pour fournir le résultat de la division
- En accédant à la matrice avec la ligne
i et la colonne j de l’élément de la grille 9x9, on vérifie par un ET bit à bit si un chiffre donné existe déjà dans la cellule correspondante : cell[cindx[i]][cindx[j]] & code
- Si le résultat vaut 0, le chiffre n’est pas encore présent dans la cellule ; sinon c’est un doublon
Test des doublons d’un élément
- Une fois combinés par OU bit à bit (
|) tous les éléments précédents de la même ligne, colonne et cellule, il suffit d’effectuer un ET bit à bit avec le code de l’élément pour valider l’absence de doublon
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {
unreachable;
}
- Si le résultat vaut 0, l’élément n’existe pas encore dans la ligne, la colonne ou la cellule
- Si le résultat n’est pas 0, le programme s’arrête en exécutant l’instruction
unreachable
- C’est le moyen le plus simple d’exprimer explicitement une erreur d’exécution en Zig
- Le code réel affiche également des détails sur l’endroit où l’erreur s’est produite
- Exemple : si l’on remplace le
0 juste après le premier 8 de la chaîne d’entrée par 5, une erreur survient, car il y a déjà un 5 à la ligne 3 de la colonne 1
Mise à jour des structures de données
- Dans la fonction
set, une double boucle for parcourt les lignes pour copier chaque nouvel élément de la chaîne d’entrée s dans la grille
- La variable
k conserve l’indice du nouveau caractère d’entrée dans la chaîne s
- On convertit le caractère en retirant
'0' pour obtenir un u4 (variable c)
- Si le nouvel élément à insérer dans la grille n’est pas 0 (
c != 0), le code calculé par décalage à gauche est copié dans chacune des grilles miroir
- On effectue un OU bit à bit (
|=) avec la grille miroir correspondante :
lines[i] |= code;
columns[j] |= code;
cell[cindx[i]][cindx[j]] |= code;
- Il n’est pas nécessaire de vérifier explicitement que
c est compris entre 1 et 9, car l’opération de décalage déclenchera un overflow lors de son exécution
- Exemple : si l’on remplace le
0 juste après le premier 8 dans la chaîne d’entrée par :, une erreur d’exécution se produit
- Remplacer ce même
0 par / provoque également une erreur similaire
- Le programme ne fonctionne que si les valeurs sont comprises entre 1 et 9, c’est-à-dire si la grille d’entrée ne contient que des chiffres décimaux
- Comme beaucoup de grilles de Sudoku sur le web utilisent
. à la place de 0, la fonction set contient la ligne if (s[k] == '.') c = 0;
- Cela permet de contourner commodément l’opération de décalage, puisque
c vaut alors 0
Prototypage et robustesse
- Les erreurs forcées dans les deux sections précédentes illustrent une fonctionnalité importante de Zig
- La première est la robustesse de Zig : dans le cas de l’opération de décalage, un comportement incorrect n’est pas autorisé et est détecté à l’exécution
- Tout semble orienté vers l’efficacité, mais il s’agit d’un cas typique où la performance n’est pas échangée contre la robustesse
- En C, si une opération de décalage perd des bits, c’est le problème du programmeur, ce qui peut se traduire par de meilleures performances d’une instruction assembleur donnée
- L’autre fonctionnalité est la possibilité d’utiliser les blocs de test pour le prototypage
- Les applications possibles sont innombrables, et celle montrée ici n’est qu’un débogage d’une situation précise lorsqu’une erreur survient
- Rien qu’avec ces fonctionnalités, Zig offre des capacités étonnantes, très rares dans un langage de programmation, en particulier dans un langage compilé
Conclusion
- Zig repose sur trois éléments clés : compatibilité C, compilation croisée et installation simple
- Ces caractéristiques montrent qu’il pourrait devenir un nouveau standard des langages de programmation système
- De nombreux avantages autrefois réservés aux langages interprétés migrent progressivement vers les langages compilés afin d’offrir de meilleures performances
- Zig se distingue particulièrement par son concept d’exécution à la compilation, qui le rapproche fortement des langages interprétés
- C’est ce qui rend Zig à la fois particulièrement différent et puissant, tout en le rendant parfois difficile à comprendre
1 commentaires
Avis Hacker News
Cet article affirme au départ que « Zig n’est pas simplement un langage, mais une manière totalement nouvelle de programmer », mais en réalité il n’aborde presque aucune fonctionnalité propre à Zig
L’inférence de types, les structs anonymes, les labeled break, etc. existent déjà depuis longtemps dans d’autres langages
La vraie particularité, c’est comptime, et ce point n’est même pas mentionné
Ce n’est pas un concept entièrement nouveau comme les macros Lisp, mais la façon dont Zig l’utilise à la place des génériques est intéressante
Malgré tout, la thèse de l’article paraît fortement exagérée
Rust permet d’exprimer clairement le moment où le code s’exécute, et sa conception semblable à un moteur de requêtes parcourant tout l’espace du code est impressionnante
Voir la documentation D
Si c’est une const-expression, l’exécution se fait automatiquement
Ce sont des langages totalement différents, comme Java et Scala
Zig est plus propre que les templates C++, mais cela ressemble davantage à une alternative pratique qu’à une révolution
Personnellement, je comprends mal cet enthousiasme excessif, comme à l’époque de Rust
J’ai lu toute la documentation de Zig et je suis resté perplexe de n’y trouver rien de vraiment surprenant
Le plus gros problème de Zig, c’est qu’on ne peut pas attacher de données aux erreurs
Les erreurs ne sont transmises que par un canal auxiliaire, ce qui rend le débogage difficile, et au final les développeurs finissent par omettre les données d’erreur
Voir cette issue liée
Avec un simple code comme AccessDenied, il est difficile de comprendre la cause
En pratique, même avec un objet
Errorcomplexe, on a souvent besoin d’un canal de diagnostic distinctÀ cause du coût en performance ou de l’état du système, il est parfois plus sûr de gérer cela via un binding tardif selon le contexte
Zig privilégie cette philosophie de précision et de déterminisme
Voir cette issue liée
Mais ce qu’il faut vraiment, c’est du logging structuré et un suivi du contexte fondé sur la pile d’appels
std.zonest souvent cité comme bon exemple, et la communauté cherche à rassembler différents patterns de gestion d’erreurs pour les intégrer au standardCela peut éviter que des développeurs paresseux ajoutent systématiquement des données partout
Je suis d’accord avec l’idée que la manière même dont Zig est développé constitue une nouvelle façon de développer un langage
Ce processus d’évolution lent, où les fonctionnalités sont examinées avec soin et le superflu éliminé, est impressionnant
J’aimerais entendre plus concrètement ce qui serait vraiment propre à Zig
J’aime le fait qu’on puisse installer Zig via PyPI
Le paquet ziglang s’installe avec
pip install ziglanget peut être utilisé immédiatementOn peut aussi compiler du code C avec
uvxC’est dommage de présenter comme des « innovations » de Zig des fonctionnalités déjà présentes dans Ada, Object Pascal ou Modula-2
Le fait de les réhabiller avec une syntaxe de style C rend intéressant ce phénomène où des idées vieilles de 40 ans paraissent neuves
L’introduction de l’article était bonne, mais ensuite cela se résume à une simple liste de fonctionnalités de Zig
La syntaxe intuitive de Zig et son contrôle de flux explicite (
defer, etc.) sont séduisantsGrâce à comptime, il n’est pas nécessaire d’apprendre une syntaxe de macros séparée
Tous les composants s’emboîtent naturellement, si bien que même lors d’une première utilisation, on a l’impression d’employer un outil familier depuis longtemps
La syntaxe
for (0..9)de Zig est intuitive, mais comme il s’agit d’un intervalle ouvert, elle prête souvent à confusionComme avec
range(0, 9)en Python, on oublie facilement si la dernière valeur est incluse ou non0..9et0..=9La taille d’un intervalle se calcule simplement comme une différence, et le parcours en sens inverse devient lui aussi plus simple
0..<5(ouvert) et0...5(fermé)Je n’aime pas les règles d’identifiants de Zig
Le mélange de snake_case et de camelCase est étrange
En revanche, le système de build, les allocateurs mémoire et l’expérience de compilation sont excellents
J’utilise surtout Rust, mais ma curiosité pour Zig reste intacte
Les conventions de préfixes des bibliothèques C sont tout aussi pénibles
L’attrait de Zig ne vient pas d’une seule fonctionnalité, mais de l’accumulation de décisions pragmatiques
Des choix qui semblaient radicaux au départ deviennent convaincants à mesure qu’on les comprend mieux
Zig est un langage qui récompense les développeurs curieux
L’une des raisons pour lesquelles Zig est appréciable, c’est qu’il reconnaît la réalité du code système bas niveau
Beaucoup de langages ignorent cet aspect pour des raisons esthétiques, mais pas Zig
Voir la documentation de page_allocator