Une peu de zig dans tout ça ! 🔗

Posted by Médéric Ribreux 🗓 In blog/Code/

#code #C #Zig

En 2022, j'ai enfin pris le temps d'explorer un nouveau langage de programmation. Ça devait probablement faire plus de 15 ans que je n'avais pas testé ça et ça me tiraillait depuis le début de l'année 2021.

En règle générale je suis loin d'explorer toujours plus et toujours plus loin. Je ne le fais que si ça vaut le coup ou si mon intérêt a été suffisamment piqué au vif pour me replonger dans de la documentation et dans un code avec un nouveau format.

Ainsi, je n'ai pas cédé à la tentation de Go lorsqu'il est sorti, probablement aussi parce que tout ce qui vient de chez Google me fait maintenant fuir. Idem pour Rust. J'en ai lu des palanquées de titre de types qui démontraient par a+b que Rust, c'était l'avenir. J'ai failli céder et puis un jour, j'ai eu à utiliser un vrai programme en Rust, à compiler et à installer et ça m'a bien refroidi, vu le kilomètre de dépendances qu'il y avait juste pour pouvoir embarquer un binaire wasm de moteur de recherche de base avec les données intégrées.

Pour autant, depuis que je fréquente lobste.rs, j'ai vu passer un grand nombre de titre sur Zig et j'ai été assez séduit par les sirènes. Fin 2021, pendant les vacances, je me suis finalement décidé à l'évaluer dans un temps limité de 3 mois avec un maximum de 50h de travail et un petit programme utile à réaliser. J'ai installé la version 0.9.0 (puis la 0.9.1) sur mes bécanes et j'ai pu commencer à travailler avec.

Dans cet article, je retrace mes impressions sur Zig, ce que j'ai aimé, ce que j'ai détesté et, au final, ce que je vais en faire.

Pourquoi Zig ?

Je suis arrivé à la conclusion qu'avec le temps, je suis devenu très conservateur par rapport aux langages de programmation. J'ai commencé avec le BASIC (le plan informatique pour tous et les 8bits oblige) et puis j'ai découvert Pascal et enfin, le C. Depuis, j'ai l'impression que le C a tout inventé et que le reste n'est qu'une pâle copie.

J'ai fait du Perl et je le regrette, car il m'a bien fait souffrir. J'ai fait un peu de PHP, puis je suis tombé sur Python et c'est devenu mon langage de prédilection car ça correspond bien à la cible de ce que je produis: des trucs rapides à faire où les performances n'ont pas besoin d'être dithyrambiques. Et c'est bien supporté dans le monde des SIG.

Zig me semblait cool parce que:

Ce que ne sera pas cet article

Je ne vais pas perdre mon temps à vous faire un tutoriel sur Zig. Ce n'est pas que ça ne sert à rien, mais il en existe déjà pléthore sur Internet et comme le langage est encore dans sa (plus tout à fait) prime jeunesse, le risque de raconter des choses qui ne seront plus valables dans 3 mois est grand. J'ai autre chose à faire que ça.

Par ailleurs, j'ai constaté que les concepts de Zig sont assez simples dans l'ensemble mais qu'il faut lire et relire la documentation tout en testant à côté pour développer la mémoire musculaire de son cerveau. C'est le meilleur moyen pour progresser dans un langage de programmation. Si vous ne faites pas cet effort, je crains qu'il faille que vous passiez tout le temps par la case: je relis la documentation de référence pendant 1h à chaque fois que je veux écrire une expression ou une simple ligne de code.

Ensuite, je suis loin d'être un développeur professionnel reconnu et donc mon avis ne vaut pas grand-chose. J'essaye toujours de rester modeste par rapport à la production de code même si intrinsèquement, je resterai toujours attiré par le concept qui vise à se dire: mais comment est-ce-que je pourrais faire ça de la manière la plus efficace, la plus élégante et la plus rapide possible. Et cette quête m'a déjà emmené dans des contrées farouchement hostiles et risquées où je me suis perdu à tout jamais. Alors, ce qui reste dans ces lignes c'est juste mon impression après 50h de Zig, sans prétention et sans faire de leçon.

Ce qui n'est pas cool dans Zig

Bon, on va écluser les sujets de ce que j'ai trouvé qui était limites dans Zig. Une fois débarrassé de ces trucs, on pourra plus facilement se focaliser sur les concepts nouveaux ou intéressants du langage. Par ailleurs, les points que j'évoque ici ne sont que le reflet de mon appréciation personnelle. Certains y verront d'autres choses, grand bien leur fasse, je m'en fous !

Le premier truc chiant dans Zig, c'est que pour compiler le "compilateur" Zig, il faut vraiment beaucoup de RAM. J'ai commencé sur une bécane de 16Go de RAM et ça passait sans aucun souci. Puis un jour, j'ai essayé d'installer Zig sur un PC portable qui "n'avait que" 8Gb (ma perception d'enfant des années 80, c'est que 8GigaOctets de RAM, c'est énorme et ça le restera toujours) et ça a planté sec.

J'ai cru à un bug, mais j'ai bien vu les traces d'oomkiller dans les journaux du noyau et ça m'a inquiété. Il ne fallait pas. Dans la pratique, il faut aux alentours de 8.5Go de RAM de disponible (en plus de ce qui tourne) pour compiler le compilateur Zig. C'était pas beaucoup plus que ce que j'avais, mais j'ai dû contourner ce qui m'a permis de voir qu'on pouvait aussi ajouter du swap à chaud, via fallocate et swapon (c'est toujours utile au cas où).

Après ça, j'en ai bavé pour comprendre la gestion des chaînes de caractères. Alors, le concept dans Zig, c'est que les chaînes de caractères, ça n'existe pas. C'est un peu comme en C, on utilise plutôt un tableau (array) de binaire et les fonctions qui gèrent des trucs en UTF-8 gèrent le tableau. L'ennui, c'est que quand on commence à coder, on a vite besoin de concaténer deux chaînes de caractères pour faire joli ou tout simplement pour afficher des contenus de variables. Et là, ça n'a pas été fluide du tout pour moi. J'ai dû relire la doc de référence au moins 50 fois avant de comprendre à peu près comment je pouvais faire et surtout me dépatouiller des chaînes littérales stockées dans le code à faire coexister avec les chaînes générées pendant le déroulement du programme (l'un est un const, l'autre non et ça génère pas mal d'erreur de compilation). Mais après quelques heures, ça a fini par rentrer. C'est con, parce que si j'avais uniquement travaillé avec des entiers ou des structs, j'aurai eu l'impression d'évoluer plus vite. Pour nuancer ce point, une fois que j'ai compris la manière dont ça fonctionnait, oui, c'était logique de faire comme ça. C'est juste que d'habitude, dans les autres langages de programmation, l'exemple le plus trivial c'est de concaténer deux chaînes de caractères. Pour Zig, il en faut un peu plus pour comprendre la subtilité.

Bon, un point qui pourrait être amélioré même si ce qui est préconisé par l'équipe de dev reste cohérent, c'est la documentation de la bibliothèque standard. Autant la doc de référence du langage Zig est plutôt bien foutue (c'est une seule page web !) et elle s'améliore régulièrement, autant celle sur la lib standard, je n'ai jamais rien trouvé que des pistes à creuser dedans, jamais une réponse compréhensible. Exemple, j'ai cherché comment faire le plus simplement du monde pour vérifier qu'un fichier existe dans un système de fichier en ayant comme information une string avec le chemin complet du fichier. Je me suis dit, c'est dans std.fs.File ou un truc comme ça. J'ai trouvé pas mal de choses qui m'indiquaient de suivre la voie: "ouvre le fichier, si ça pète une erreur, c'est qu'il n'existe pas". Mais j'avais un doute quand même, ça me semblait un peu moyen comme traitement et pas super élégant. Après avoir fouillé le code de la lib std, j'ai fini par trouver que c'était en utilisant std.fs.accessAbsoluteZ() qui faisait le job. Impossible à trouver dans le moteur de recherche de la doc de la lib std en ligne. Tout ça pour une ligne, on va dire que c'est mal fichu. Néanmoins, pour nuancer, je dirais que lire le code de la lib standard permet de voir comment on code en Zig bien comme il faut et ça a répondu à tout un tas de questions que je me posais avant. Un bien pour un mal. Je suis sûr que ça va s'améliorer avec le temps.

Un truc qui m'a bien surpris, c'est la taille des binaires produits sans options d'optimisation: la vache, 600Ko ça reste gros pour un simple hello world ! Bon après, on a rien de monstrueux non plus, surtout que ça tient sans avoir recours à la libc ce qui est déjà pas mal.

Pour terminer, il y a beaucoup de choses dans la lib standard et dans les documentations qui invite les gens à gérer l'allocation de la RAM avec des éléments spécifiques: les allocators. Il existe un paquet de fonctions qui sont faites pour utiliser des allocators pour du stockage rapide ou du formatage. Les exemples sont légions. Mais pour moi qui avais juste envie de stocker des choses dans la stack plutôt que la heap, je n'ai pas facilement trouvé mon bonheur. Même si j'y suis arrivé, j'ai pas mal galéré pour l'assignation d'éléments en utilisant du stockage non variable dans la stack. Je n'avais pas envie de me taper une gestion d'allocation pour afficher une string de base dynamique, ça me semblait vraiment overkilling. Avec le recul, les allocators c'est vraiment simple à utiliser, mais je ne sais pas, au début, ça m'a semblé un nouveau concept vs le simple malloc que j'ai hésité.

Voilà, c'est tout pour le pas terrible.

Qu'est-ce-qui est cool dans Zig ?

Bon, voyons quelles sont les promesses tenues par rapport à ce que j'avais anticipé. Hé bien à peu près toutes !

D'abord, effectivement, c'est un langage avec une grammaire contenue. Je ne dirais pas que c'est aussi succinct que le C, mais on s'en approche fortement. Par exemple, il y a plusieurs types qui s'approchent des tableaux avec des variantes (array, slice et array avec sentinelle). par contre, c'est plus concis (en termes de choses à apprendre) que le Python par exemple. Attention, certains éléments doivent être maîtrisés pour comprendre ce qu'on fait et même quand on souhaite coder des choses simples ou basiques. Bon, certains disent qu'ils en ont fait le tour en 5h, moi, ça m'a bien pris une vingtaine d'heures. Dans la pratique, j'ai essayé de coder quelque-chose en même temps que je lisais la documentation de référence du langage. Mais à la fin, j'ai compris qu'il valait mieux lire l'intégralité de la documentation plusieurs fois et ensuite se mettre à coder. Pourquoi ? Simplement parce que la progression de la documentation n'est pas strictement linéaire et en allant du plus simple au plus complexe. Parfois, il faut comprendre des notions qui sont présentées plutôt à la fin de la doc pour comprendre ce qui est présent au milieu ou même au début.

Ensuite, sur le système de build, il est effectivement paramétrable en Zig. En fait, c'est réellement un programme Zig qui est compilé et qui se charge ensuite de la compilation du projet ainsi que de la gestion des tests et des installations. Une espèce de Makefile mais en Zig. Au début, j'avoue que ça semble bizarre parce que ce n'est pas un fichier de configuration mais bien du code. Mais avec la pratique, c'est plutôt une bonne idée: je n'ai pas à apprendre autre chose que Zig et sa bibliothèque de build qui reste assez simple à utiliser. C'est vrai que souvent, on essaye de faire du déclaratif mais assez rapidement, il y a un cas qui n'est pas géré facilement (cross-compilation ou cross-installation et empaquetage, c'est de vous dont je parle). Après, j'ai un peu galéré pour comprendre les concepts de la bibliothèque de build. Je voulais tout simplement ajouter plusieurs fichiers de tests unitaires et je me suis retrouvé comme un con à vouloir en ajouter à un jeu de test. En définitive, j'ai tout simplement ajouté plusieurs jeux de test que j'ai fait exécuter à l'étape du lancement de tests. Ça m'a permis de me renseigner sur la bibliothèque de build rapidement (j'avoue, j'ai lu le code source, c'était plus simple). Dans la pratique, je crois que si vous avez un projet simple, ce que produit zig init-exe suffira dans 80% des cas.

En ce qui concerne la sécurité apportée par le langage, j'ai pu la mesurer assez facilement au nombre d'erreurs remontées par le compilateur lors de mes nombreuses tentatives de compilation. Putain, c'est harsh, il ne laisse rien passer. J'ai eu ma période de maudissage de const vs var pour les paramètres de fonction en chaînes de caractères (slices en fait). En même temps, ça permet de comprendre un peu certaines erreurs de débutant, ce n'est pas plus mal.

Autre point, Zig gère les tests (unitaires ou autres) directement dans le langage (un petit truc qui s'appelle "test"). C'est assez intéressant pour faire du TDD: on fout les tests directement au niveau du code et on peut les lancer très facilement avec zig build test, avant même de compiler le projet. D'ailleurs, toute la documentation de référence du langage est présentée sous forme de tests. On peut donc dire que Zig facilite la création de tests et c'est plutôt une bonne chose.

Après, j'ai lu que Zig a quand même un concept d'undefined behavior, que certaines opérations ne sont pas assurées de ne pas planter. Donc, automagiquement, il y risque de plantage. J'ai eu quelques segfaults dans ma démarche d'ailleurs mais c'était principalement des libs C qui plantaient, pas du tout du code Zig natif. Je pense qu'un mauvais développeur comme moi doit arriver à produire du code Zig qui plantera mais, je dois avouer qu'il y a dans le langage pas mal de choses qui servent de garde-fous (comptime, les unions d'erreur, les pointeurs qui ne peuvent pas être null sauf à être optionnels, etc.). Ils ne sont pas tous obligatoires mais c'est bien de les avoir sous la main, ça rassure.

Enfin, à propos du lien avec le C, j'ai été absolument bluffé. C'est vraiment une grande force de Zig: ne pas devoir recoder ce qui existe et qui est éprouvé. Dans mon domaine (le SIG), la bibliothèque de référence c'est GDAL/OGR pour la gestion des formats de fichiers et les imports/exports. Pour mon test de Zig, je me suis mis dans la tête de réaliser un simple outil d'import export du format PBF (OpenStreetMap) vers Spatialite mais en utilisant une requête pour extraire uniquement certains éléments du fichier PBF. GDAL/OGR est codée en C++ nativement mais propose une API en C (y en a aussi une en Python que j'utilise souvent). C'était donc un bon candidat pour tester les liens entre Zig et C.

Quand j'ai commencé j'ai cru que j'allais galérer mais, au final, en ajoutant un @cImport et en référençant GDAL dans le fichier de build, j'ai pu utiliser tout de suite GDAL et OGR nativement. Je n'ai même pas eu à faire un fichier intermédiaire pour gérer la translation de type pour des structures qui n'auraient pas été interprétées correctement. Strictement rien ! Tout de suite, on peut utiliser les structures ou pointeurs retournés par GDAL/OGR, les mélanger dans du code Zig, et ça passe crème. La seule difficulté c'était, comme toujours, pour récupérer des chaînes de caractères. A un seul moment j'ai dû faire un cast d'un pointeur c ([*c]pointer) vers un autre type et c'était bon. Pas plus d'accroche que ça, pas de concept fumeux à apprendre, pas de centaine de lignes de code pour que ça marche, rien que du très simple. C'était vraiment inattendu et c'était la bonne surprise de ma démarche. Donc, oui, on peut conclure que Zig et C sont très copains.

Si vous voulez lire du code (sale), vous pouvez lire ce que j'ai codé dans ma forge. Attention, ça n'a rien d'une référence et c'est mal commenté. Mais ça illustre comment utiliser une libc externe, comment ajouter un deuxième jeu de test, comment séparer son code dans plusieurs fichiers, etc.

Et sinon, y a quoi de bien comme concepts dans Zig ?

En termes de langage pur, j'ai découvert quelques concepts d'intérêt. Je fais la liste de ceux que j'ai le plus remarqué:

Bon, il y a d'autres choses sympathiques comme les defer qu'on peut placer avant la fin d'un bloc pour ne pas oublier de faire du nettoyage (on met le code du nettoyage juste en dessous de l'allocation par exemple). On va s'arrêter là, si ça vous branche, lisez la documentation de référence du langage pour vous faire un avis.

Sans être révolutionnaire, ça reste abordable, pas trop compliqué même s'il faut être assidu pour tout intégrer.

Conclusions

Bon, Zig c'est finalement très bien. Un peu galère sur la documentation ou sur certains concepts mais après une trentaine d'heures, on peut commencer à faire des choses avec et ce, d'autant plus facilement que les bibliothèques utilisables en Zig sont légion: tout le patrimoine de ce qui a été codé en C est quasiment utilisable nativement. C'est ce qui m'a le plus bluffé.

Maintenant que c'est un langage qui m'a plu, qu'est-ce-que je vais en faire ? La réponse immédiate, c'est rien ! Hé oui, j'ai autre chose que ça à faire que d'explorer des langages pas stabilisés pour coder des choses que je vais devoir jeter dans 3 mois. Car, il faut bien l'avouer, Zig n'est pas encore au stade d'une version 1.0 qui apporterait un peu de stabilité. Il faut être patient, c'est normal. On a une petite équipe mais qui semble bien travailler, en tout cas, dans le bon sens. Je préfère qu'ils prennent le temps de gérer les problèmes et de stabiliser les concepts plutôt que d'avoir un truc stable rapidement mais qui est devenu un bloatware.

Le premier truc que je vais surveiller, c'est l'étape où Zig se compile lui-même, sans avoir besoin de LLVM. Ce sera déjà une étape importante. Le deuxième truc que je vais observer, c'est évidemment le niveau de stabilisation du langage. À partir du moment où ce sera à peu près mûr pour les grandes lignes de la grammaire et des concepts, je pense que je referais une autre passe, même si on est encore en dessous de la version 1.0.

Dans tous les cas, c'est prometteur, ça fait bien 15 ans que je n'ai pas été aussi content d'investir 50 heures dans un nouveau langage. Tout ce que j'espère, c'est que ça ne fera pas comme Rust (enfin, c'est mon à priori): passer du langage hype que tout le monde dit qu'il est simple à un truc officiellement reconnu comme étant compliqué.