De Zig et des zags

Posté par  . Édité par _seb_, orfenor, Ysabeau 🧶 🧦, BAud, palm123, Benoît Sibaud, Strash et Arkem. Modéré par patrick_g. Licence CC By‑SA.
Étiquettes :
50
7
sept.
2023
Programmation

Cette dépêche, sous son titre énigmatique, va vous présenter un langage de programmation relativement nouveau, nommé Zig, avec ses caractéristiques principales ainsi que son écosystème (toolchain). Le but n’est pas de vous faire à tout prix adopter ce langage, mais seulement de découvrir quelques morceaux choisis, pour le plaisir des yeux.
Logo de Zig

Sommaire

Note : ce langage est relativement jeune, la première version date de 2016 et la dernière version stable (0.10.1) parue en janvier 2023 est déjà considérée comme obsolète par rapport à la branche master ! Le développement est donc très actif et les diverses documentations ne peuvent rester à jour bien longtemps. Une nouvelle version 0.11.0 est d’ailleurs sortie pendant la rédaction de la dépêche.

Présentation

Le langage Zig est une idée d’Andrew Kelley. Il est l’un des contributeurs principaux et travaille à plein temps sur le projet.

Zig est un langage compilé impératif polyvalent, typé statiquement, « bas niveau » type C/C++/Rust. Il se décrit ainsi :

Zig est un langage de programmation généraliste ainsi qu’une toolchain ayant pour objectifs la robustesse, l’optimisation et la réutilisation du code.

Ce langage se veut plus simple que ses prédécesseurs (« Concentrez-vous sur le debug de votre application et non sur le debug de votre connaissance du langage. ») et néanmoins puissant et sécurisé.

Il a aussi l’ambition avouée dès ses débuts de vouloir être un C meilleur que C, une véritable évolution, tout en restant compatible. C’est pourquoi vous pouvez faire en Zig tout ce que vous faisiez en C, mais en mieux.

Installation

L’installation de Zig est des plus simples et universelle, puisqu’il s’agit d’une simple archive tar à extraire. On y trouve un exécutable nommé zig qui contient tous les outils nécessaires, et quelques fichiers de documentation ainsi que la librairie standard zig. (À noter que la source de la libc musl est également fournie pour pouvoir compiler un bon nombre de cibles, bien que Zig n’en dépende pas.) Le tout tient dans une petite archive de 45 Mo, ce qui est fort raisonnable.

La deuxième étape, optionnelle, consiste à permettre l’appel à l’exécutable zig depuis n’importe où, soit en ajoutant le dossier contenant zig au PATH, soit en créant un lien symbolique pointant sur cet exécutable dans votre dossier bin.

Notons que certains gestionnaires de paquets proposent déjà un package zig, même si parfois celui-ci est obsolète à cause du développement très actif de Zig.

Hello, world!

Voici le traditionnel « Bonjour, le monde ! » :

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hello, {s}!\n", .{"world"});
}

Dans votre terminal :

$ zig build-exe hello.zig

$ ./hello
Hello, world!

Un autre exemple qui sera peut-être plus intéressant, il s’agit d’extraire des entiers d’une chaine de caractère, séparés par des espaces ou des virgules. À noter que ce programme s’auto-teste :

const std = @import("std");
const parseInt = std.fmt.parseInt;

test "parse integers" {
    const input = "123 67 89,99";
    const ally = std.testing.allocator;

    var list = std.ArrayList(u32).init(ally);
    // Ensure the list is freed at scope exit.
    // Try commenting out this line!
    defer list.deinit();

    var it = std.mem.tokenize(u8, input, " ,");
    while (it.next()) |num| {
        const n = try parseInt(u32, num, 10);
        try list.append(n);
    }

    const expected = [_]u32{ 123, 67, 89, 99 };

    for (expected, list.items) |exp, actual| {
        try std.testing.expectEqual(exp, actual);
    }
}

D’autres exemples sont disponibles sur https://ziglearn.org/.

Vous pouvez aussi voir le code d’un TapTempo abondamment commenté.

Un mot sur l’écosystème

Zig fournit une chaîne de compilation qui compile le langage Zig, mais aussi C et C++. Cette chaîne de compilation prend en charge la cross-compilation ainsi que la compilation statique. Zig intègre également son propre système de construction rendant superflus l’usage de Make/Cmake/etc. Un gestionnaire de paquets est prévu, pour gérer aussi bien les bibliothèques écrites en C qu’en Zig. La version 0.11.0 a commencé à introduire ce gestionnaire de paquets et il est déjà considéré suffisamment stable pour être utilisé.

Le compilateur est à lui seul une petite merveille puisqu’il est fourni avec musl (une implémentation de la bibliothèque standard C) qu’il compile à la demande et utilise pour générer des binaires statiques. Il peut construire des exécutables pour un grand nombre d’OS et d’architectures, intègre un cache de compilation et quatre modes de compilation. Il supporte même une génération du code en WebAssembly sans manipulations ésotériques, simplement en spécifiant la cible appropriée.

Il est à noter qu’en mode de compilation Release, le compilateur est déterministe jusque dans ses optimisations, et il produira le même binaire pour la même source, au bit près !

Tout ceci fait que le compilateur C du projet Zig est tout à fait honorable (voir plus pour certains) face aux principaux compilateurs C du marché.

Spécificité du langage

Quant au langage lui-même, il est conçu pour être simple et lisible, sans rien de caché : ni flots de contrôle, ni allocations de mémoire.
Si un code en Zig ne semble pas faire appel à une fonction, c’est qu’il ne le fait pas. L’objectif de cette conception est d’améliorer la lisibilité.

Zig ne gère pas lui-même les allocations mémoire sur le tas. Il n’y a pas de mot clé new ou autre fonctionnalité qui utiliserait un allocateur de mémoire (comme un opérateur de concaténation de chaînes de caractères par exemple). Le concept de tas est géré par une bibliothèque ou le code de l’application, pas par le langage. Le principal problème avec les allocations de mémoire cachées est qu’elles empêchent la réutilisation du code dans certains environnements. Certains cas nécessitent de n’avoir aucune allocation mémoire, donc le langage de programmation doit fournir cette garantie.

La bibliothèque standard de Zig est entièrement optionnelle. Chaque API n’est compilée dans le programme que si elle est utilisée. Zig a la même prise en charge avec ou sans libc. Zig encourage son utilisation directement sur le matériel et le développement à haute performance. Ceci est le meilleur des deux mondes. Par exemple, les programmes WebAssembly peuvent utiliser les fonctionnalités habituelles de la bibliothèque standard, et quand même avoir des exécutables de petite taille comparés aux autres langages prenant en charge WebAssembly.

Zig n’a pas de macros ni de métaprogrammation, et pourtant le langage exprime des programmes complexes d’une manière claire, non répétitive. Même Rust implémente en dur certaines macros, comme format!. L’équivalent en Zig est implémenté dans la bibliothèque standard sans code en dur dans le compilateur.

Zig 0.5.0 a introduit les fonctions async. Cette fonctionnalité n’a pas de dépendance au système d’exploitation hôte ou même à l’allocation de mémoire dans le tas. Cela veut dire que les fonctions async sont disponibles pour la cible « freestanding » (sans système d’exploitation).

Dans d’autres langages de programmation, les références null sont sources d’erreurs à l’exécution, et sont même soupçonnées être la pire erreur en informatique. Les pointeurs en Zig ne peuvent pas être null. Il existe néanmoins un type optionnel.

Zig défini un type spécial de gestion des erreurs. Les erreurs ne peuvent être ignorées. Zig vous permet d’énumérer toutes les raisons possibles de défaillance de manière exhaustive et de traiter chacune d’entre elles de manière spécifique si vous le souhaitez. Cette énumération est produite et vérifiée par le compilateur (ce qui fait partie des particularités du langage Zig), de sorte que vous ne pouvez manquer aucun cas. Il convient donc d’en tenir compte et de les traiter au moyen des mots-clés catch, try, switch et unreachable.

Il a été dit plus haut que Zig n’a pas de macros. Mais il a mieux. Il peut exécuter du code à la compilation plutôt qu’à l’exécution. En effet, une variable marquée du mot-clé comptime sera connue à la compilation. Les boucles marquées du mot-clé inline seront exécutées à la compilation. Cela permet d’exécuter à l’avance les parties du code qui ne dépendent pas des entrées utilisateur, et d’optimiser l’exécution, ou simplement automatiser des tâches répétitives pour le programmeur sans que cela ne se ressente à l’exécution.
Un exemple : je veux que mon programme affiche son nom à différent stade de son exécution, avec de grandes lettres en art ASCII. On dispose pour cela d’une chaîne de caractère contenant le nom du programme, et d’une fonction foo qui retourne une chaine avec de grandes lettres en art ASCII en échange d’une chaîne de caractère. La plupart du temps, on se contenterait d’appeler une fois la fonction foo au début du programme et à stocker dans une variable le résultat pour l’afficher.
Mais Zig permet d’aller plus loin. Avec l’exécution à la compilation, on peut n’exécuter la fonction donnant les grandes lettres qu’à la compilation, augmentant ainsi légèrement le temps d’exécution. La fonction foo n’étant pas utile à l’exécution, elle ne sera pas incluse dans le binaire, tout comme la chaîne contenant le nom du programme.
Cet exemple est trivial, mais peut vous donner un aperçu de la puissance du comptime.

Pour un article plus détaillé sur le comptime Zig, voir https://zig.news/edyu/wtf-is-zig-comptime-and-inline-257b

Vous pourrez en apprendre encore plus en lisant la vue d’ensemble du projet, ou en jouant avec les ziglings !

Les outils

Il n’existe à ce jour aucun IDE conçu pour Zig, mais certains éditeurs de texte supportent déjà des fonctionnalités telles que la coloration syntaxique. Ces éditeurs sont Emacs, Vim, Kate, VS Code et Sublime Text.
Un serveur de langue est fourni également pour les clients le supportant.

Quant au débogage, il peut se faire tout simplement avec gdb, ou n’importe quel débogueur C/C++. Néanmoins, le compilateur fournit des indications si précises sur les erreurs qu’on a pu commettre que le recours à un débogueur pour la plupart des erreurs triviales est inutile.

Les librairies Zig, elles, sont, pour l’instant, peu nombreuses, et il n’existe pas d’index « officiel » pour les référencer puisque la communauté est décentralisée. À la place, on retrouve plusieurs petits index maintenus par la communauté.
Il existe tout de même un projet en cours de gestionnaire de paquet livré avec Zig, qui n’est relié à aucun index en particulier, et qui gèrera aussi bien les modules Zig que C/C++.

Usage

Zig est un langage de programmation assez généraliste, il peut donc convenir à la plupart des usages.

Il est notamment plébiscité pour des programmes ayant des contraintes de performance et de consommation mémoire que l’on peut retrouver dans les systèmes embarqués et applications en « temps réel ». Une communauté d’utilisateurs (des ziguanas) dans ce domaine (zig embedded group) s’organise autour du projet microzig afin d’offrir l’outillage nécessaire pour ce type de développement.

On peut trouver plusieurs listes de projets écrits en Zig, parmi lesquels on trouve un lecteur de flux Fediverse pour bureau GTK, un solveur de sudoku, un interpréteur Brainfuck, un éditeur de pixel art, un système d’exploitation, un environnement d’exécution JavaScript, un jeu RPG 2D… Les usages de Zig sont multiples et variés !

Zig est aussi utilisé par des entreprises comme Uber ou TigerBeetle.

La version 0.11.0

Cette sortie de version inclut 8 mois de travail : des changements de la part de 269 contributeurs différents, répartis dans 4457 changements. Un total de 1012 tickets résolus ! C’est également « the début of Package Management ».

Le mot de la fin

Zig est sous licence MIT. L’organisation Zig Software Foundation est une organisation à but non lucratif 501(c)(3)) dont l’objectif est de soutenir le projet Zig que ce soit par la rémunération, voire, l’embauche, de développeurs (Jakub Konka, Rich Felker, Meghan Denny, Andrew Kelley) ou le sponsoring d’autres projets gravitant autour du projet (Zig books par exemple ou indirectement musl).

Zero the Ziguana, une des mascottes de Zig

Aller plus loin

  • # Zig c'est la vie !

    Posté par  . Évalué à 6.

    Merci pour cette dépêche !

    Plus je lis sur Zig et plus j'aime ce langage.
    Je ne l'utiliserai certainement pas pour tous les usages, mais quand on fait de l'embarqué ou du système, c'est vraiment chouette.

    Le système de build intégré au langage est au début assez déroutant, mais l'idée de pouvoir tout faire dans le même langage et avec un seul binaire est vraiment séduisante.

    J'apprécie aussi particulièrement la possibilité de pouvoir utiliser une bibliothèque C par un simple @cImport
    J'adore ce petit exemple, qui montre que Zig est plus lisible que C même en appelant des bibliothèques C. Bref, c'est un meilleur C, comme le dit son auteur.

    Le comptime aussi c'est juste magique, je n'ai pas encore trop joué avec, mais ça donne plein d'idées. J'avais été impressionné par le code de print présenté dans la doc. Ça vaut le coup de passer un peu de temps pour comprendre une telle fonction.
    Ce qui est fou c'est que comptime permet de faire de la généricité sans rien ajouter au langage !

    Il y a plein d'autres aspects à explorer comme la gestion "sécurisée" des pointeurs.

    Il y a aussi plein de conf intéressantes à voir sur le sujet, notamment celle sur le recodage de la libC, malheureusement je n'arrive pas remettre la main dessus, peut-être que c'était pas ça le titre…

    Bref, c'est un beau projet qui a de beaux jours devant lui.

  • # Mais pourquoi diantre pas de RAII ?

    Posté par  (site web personnel) . Évalué à 10.

    Merci beaucoup pour cette dépêche très sympa ;-)

    Je trouve globalement Zig très rafraîchissant et bourré de bonnes idées.

    Typiquement l'intégration d'un compilo C capable de cross-compilation dans quelques dizaines de Mo. Ça donne même des idées pour les logiciels ayant besoin d'embarquer un compilo pour faire du JIT.

    Bref j'en arrive à ma question: mais pourquoi donc des gens aussi compétents refusent de mettre du RAII (c'est à dire des fonctions destructeur appelées automatiquement quand un objet se fait free ou bien est enlevé de la stack) ?

    De ce que je comprends l'argument avancé c'est:
    1 - c'est pour qu'il n'y ait rien de caché
    2 - c'est plus lisible car c'est explicite

    Pour le premier point on peut objecter que nos CPU modernes ne se privent pas pour faire toute sorte de magie noire avec notre code (out-of-order, processeur super-scalaires).
    L'OS de sont côté décide arbitrairement du scheduling des threads, déclenches des interruptions quand ça lui chante et fait semblant d’allouer de la mémoire quand on fait un malloc pour finalement tuer notre process sur un out-of-memory quand il a été trop optimiste…
    Bref des choses cachées quand on un impact sur notre programme c'est pas ça qui manque dans un ordinateur moderne !

    Le second point me semble encore plus incompréhensible: on se retrouve avec un pattern consistant à systématiquement devoir écrire var foo = init(); defer foo.destroy();.
    - Si on oublie le defer on a fait un bug
    - Donc faut faire gaffe à ça en code review
    - Et faut des outils pour détecter ce type d'erreurs
    - Et ça peut être plein de classes de bug: memory leak, resource leak (e.g. on ne ferme pas un fichier), deadlock (e.g. la ressource représente la prise de mutex)

    En règle générale avoir un langage qui permet d'exprimer des structures ne pouvant pas représenter un état invalide est un gain énorme (exemple typique: en Rust un mutex contient l'objet qu'il protège, de fait il est impossible d'accéder à ce dernier sans avoir pris le mutex).

    Et si on veut pinailler, defer est déjà quelque de magique vu qu'on n'est pas explicite sur l'endroit où sera fait l'appel de fonction (j'imagine que personne n'est assez fou pour défendre l'utilisation de free sans defer ni RAII comme en C ).
    De fait plutôt qu'un defer, j'aurai plutôt vu un mot clé permettant d'indiquer qu'une fonction retournait un object avec destructor.
    Quitte à devoir explicitement fournir le destructeur dans la fonction d'initialisation si on veut rester plus explicite, par exemple:

    var foo = raii foo_factory() -> foo_destructor; // pseudo syntax,
    On peut même obliger les variables raii à avoir un nom particulier (genre avec un symbole en préfixe) étant donné que la variable peut survivre à la fonction l'ayant créée (typiquement en la retournant à l'appelant), ce qui était moins utile pour le defer car il s'exécute uniquement dans la fonction d'où il est déclaré.

    Bref, de ma fenêtre le RAII apporte des avantages monstrueux comparés à ses "inconvénients" théorique (et ça semble plutôt simple de faire un defer sous stéroïde qui garde le meilleurs des deux mondes). Et du coup je n'arrive à comprendre le raisonnement des devs de Zig (qui, je le rappelle sont des mecs sacrément brillants !)

    Dernier point: on n'est pas vendredi, ce post n'est pas un troll (ni un flim sur le cyclimse). Il est bien possible que j'ai fait un homme de paille sans le vouloir avec les arguments pour l'absence de RAII, si c'est le cas merci de me corriger dans la joie et la bonne humeur ;-)

    • [^] # Re: Mais pourquoi diantre pas de RAII ?

      Posté par  . Évalué à 1.

      En même temps, vu que Zig peut utiliser un "module" C++ qui lui supporte le RAII, rien ne t'empêche de RAIIser ton code en C++ et de l'utiliser dans Zig, non ?

    • [^] # Re: Mais pourquoi diantre pas de RAII ?

      Posté par  . Évalué à 1.

      Le RAII, c'est surtout de la performance processeur en moins (car il faut surveiller l'état des variables à l'exécution) et en embarqué c'est aussi une consommation mémoire non maîtrisé (on ne sait pas quand il va passer libérer la mémoire).

      Le RAII simplifie grandement la programmation mais quand on recherche des performances optimale, c'est du gâchis. Enfin la solution C++ est pas mal avec les smartpointer que l'on peut utiliser quand on accepte cette "perte" pour plus de productivité.

      • [^] # Re: Mais pourquoi diantre pas de RAII ?

        Posté par  (site web personnel) . Évalué à 7.

        Le RAII, c'est surtout de la performance processeur en moins […] c'est aussi une consommation mémoire non maîtrisé

        Je pense que tu confonds RAII et garbage collector ;-)

        RAII n'a strictement aucun impacte au runtime: c'est le compilo qui détermine où une variable n'est plus utilisée et met à cet endroit le code d'appel du destructeur.

        Si tu ne fais pas de RAII tu devras mettre à la main l'appel au destructeur au même endroit (dans tous les cas faut bien faire un close sur le file descriptor ou un free sur le buffer !).

        (et les smartpointers de C++ sont implémentés grâce au RAII)

        • [^] # Re: Mais pourquoi diantre pas de RAII ?

          Posté par  . Évalué à 2.

          Le compilo ne peut pas toujours savoir quand mettre un appel au destructeur. Les smartpointer, c'est en quelques sortes un garbage collector compilé avec le code (Enfin, c'est Go qui fonctionne réellement comme ça). Mais au lieu d'avoir un processus séparé qui regarde le nombre de pointeurs sur chaque instance le smartpointer vérifie s'il est à 0 et détruit l'instance si besoin (le travail du GC). Certes, l'implémentation est sans doute un peu plus complexe/optimisé avec des destruction "prévisibles" mais c'est le principe. Il y a donc du code qui "compte et regarde", c'est de la ressources perdu. Certes c'est peanuts, mais des variables, il peut y en avoir beaucoup, sur des langages ou tout est géré ainsi, l'incidence est loin d'être négligeable.

          • [^] # Re: Mais pourquoi diantre pas de RAII ?

            Posté par  (site web personnel) . Évalué à 8.

            Mais là tu parles de garbage collector (dont une des implémentation est le compteur de références qui est mis en oeuvre dans les smartpointers C++)

            Le garbage collector est utilisé uniquement pour les allocations dynamiques (si une allocation a lieu sur la stack, on sait que sa durée de vie est liée à l'appel de fonction l'ayant créé, donc pas besoin de check à la runtime pour savoir quand la libérer).

            Le RAII est un concept orthogonal au garbage collector: si ton smartpointer est capable de décrémenter le compteur de référence et de faire la libération de mémoire quand celui-ci arrive à zéro, c'est bien que du code a été appelé automatiquement quand la variable contenant le smarpointer a été détruite. Et cela quelque soit l'endroit où se trouve la variable ne question (stack, dans une structure elle-même sur le tas etc.).
            De plus le RAII peut servir a gérer autre chose que de la mémoire: fichier/socket ouvert, mutex etc.

            (et Go n'utilise pas de compteur de référence pour son garbage collector, mais du mark and sweep)

    • [^] # Re: Mais pourquoi diantre pas de RAII ?

      Posté par  . Évalué à 2.

      Zig a déjà des mécanismes permettant de détecter les fuites de mémoire (sauf en ReleaseFast), mais c'est vrai que le principe de RAII est intéressant.

      ça s'implémente bien en C++ car il y a cette notion de destructeur. Dans un langage comme Zig, il faudrait trouver un moyen d'associer une fonction à une structure de donnée qu'on alloue.

      Après, comme tu le dis, ça pourrait passé par une sorte de "super defer" ou peut-être justement par les allocateurs ?
      Comme on passe l'allocateur, on pourrait peut-être préciser si la mémoire doit être libérée à la sortie du scope ? Bon dans tous les cas, tu pourrai oublié d'utiliser cet allocateur.

      De ce que j'avais compris des premières présentation de Zig, Andrew semblait avoir des idées pour améliorer la gestion de la mémoire. Le langage est encore jeune, peut-être que ce genre de changement est encore possible ? Ça a peut-être été proposé (j'ai pas regardé) ?

    • [^] # Re: Mais pourquoi diantre pas de RAII ?

      Posté par  (site web personnel) . Évalué à 6.

      J'en rajoute une couche avec ce post de l'auteur de Zig

      RAII works fine in Zig

      Il semble considérer que le defer fait office de RAII. Mais c'est oublier que le defer s'exécute quand on quitte la fonction dans laquelle il a été déclaré. Du coup on ne peut pas retourner des objets avec destructeur.

      Par exemple:

      const std = @import("std"); 
      
      const LockGuard = struct {
          mutex: *std.Thread.Mutex,
      
          fn take(mutex: *std.Thread.Mutex) !LockGuard {
              std.debug.print("Lock!\n", .{});
              mutex.lock();
              return .{
                  .mutex = mutex,
              };
          }
      
          fn release(lg: *LockGuard) void {
              std.debug.print("Unlock!\n", .{});
              lg.mutex.unlock();
          }
      };
      
      var m: std.Thread.Mutex = .{};
      fn take_lock() !LockGuard {
          var lk = try LockGuard.take(&m);
          defer lk.release();
      
          return lk;
      }
      
      pub fn main() !void {
          std.debug.print("taking lock!\n", .{});
          var guard = try take_lock();
          std.debug.print("lock taken {}!\n", .{guard});
      }
      

      va retourner (vous pouvez aller sur https://zig-play.dev/ pour jouer avec):

      taking lock!
      Lock!
      Unlock!
      lock taken [...] !

      alors qu'un langage supportant le RAII n'aurait libéré le lock que quand guard n'est effectivement plus référencé, c'est à dire à la fin de la fonction main !

      Et puisque maintenant c'est Vendredi, je vous laisse avec la justification pour fermer l'issue pour faire du vrai RAII en Zig ;-)

      • [^] # Re: Mais pourquoi diantre pas de RAII ?

        Posté par  . Évalué à 2.

        alors qu'un langage supportant le RAII n'aurait libéré le lock que quand guard n'est effectivement plus référencé, c'est à dire à la fin de la fonction main !

        Hum, oui mais en Zig tu ne l'aurai pas codé comme ça. Vu que Zig n'a pas de notion de destructeur, t'es obligé de mettre ton defer (et le unlock) dans le main (c'est d'ailleurs ce qu'il fait dans le post reddit que tu as cité).

        Et puisque maintenant c'est Vendredi, je vous laisse avec la justification pour fermer l'issue pour faire du vrai RAII en Zig ;-)

        La proposition du mot clé clean est intéressante, après je ne sais pas si ça rentre dans la philosophie du langage car ça masque le fait que ça va appeler une fonction, puis tu pourrai toujours oublié d'appeler clean.
        Mais c'est moins verbeux que ce qui est pratiqué aujourd'hui.

        Je comprends la volonté du/des créateurs de Zig d'être très conservateurs sur ce qui rentre ou pas dans le langage. Après, je trouve que cette gestion de la mémoire avec allocateur explicite est trop lourde dans bien des cas, c'est pourquoi je réserverai Zig pour de l'embarqué, de la programmation système ou des librairies qui ont besoin d'être très optimisées (genre librairie standard d'un langage, ce que fait Bun ou Roc).

        • [^] # Re: Mais pourquoi diantre pas de RAII ?

          Posté par  (site web personnel) . Évalué à 4. Dernière modification le 13 septembre 2023 à 00:08.

          Vu que Zig n'a pas de notion de destructeur, t'es obligé de mettre ton defer (et le unlock) dans le main

          Oui tout à fait, mais du coup on se retrouve avec une fonction qui retourne une ressource devant être impérativement nettoyée… mais rien pour garantir que ce sera fait ! (le fameux RTFM quoi ¯\(ツ)/¯ )

          C'est le retour du bug classique façon:

          int foo() {
            lock_mutex();
            if (a) {
              // fait plein de truc compliqués
              release_mutex();
              return x;
            } else if (b) {
              // encore d'autres trucs compliqués
              release_mutex();
              return y;
            } else if (c) {     // Hop ! une PR ajoute cette nouvelle condition
              // toujours plus de trucs compliqués...
              // ... et on a oublié de release le mutex, c'est vendredi la review est un peu molle, ça part en prod, Ariane 5 explose en vol
              return z;
            }
          }

          La solution qu'a choisi Zig pour contrer ce problème est un puissant système de vérification à la runtime quand on compile avec le profile de test (et aussi en mode release-safe).
          C'est largement mieux que C, mais ça ne vaut pas une vérification exhaustive à la compilation (comme permet le RAII): on ne détectera le problème que si on passe dedans pendant notre phase de test.
          Et donc c'est typiquement le code peu utilisé (ou galère à tester) genre gestion de cas d'erreur dans un cas aux limites qui se retrouve à contenir des bugs (comprendre: les bugs les plus horribles à reproduire)

          Enfin, autant ce type de checks marche pour vérifier des erreurs d'allocations genre use-after-free, autant il n'est d'aucune utilité pour de la gestion de ressource comme dans mon exemple…

          c'est pourquoi je réserverai Zig pour de l'embarqué, de la programmation système ou des librairies qui ont besoin d'être très optimisées

          J'ai l'impression que Zig est très séduisant pour les devs C car on garde la philosophie une très forte compatibilité avec le C (Zig se vent comme étant capable de s'intégrer dans un projet C existant) tout en fournissant un langage avec tooling et écosystème au bas mot 20ans en avance par rapport au C99… Ho wait !

    • [^] # Re: Mais pourquoi diantre pas de RAII ?

      Posté par  . Évalué à 3.

      Vale un language en alpha a même un RAII ou les destructeurs peuvent prendre des paramètres:
      https://verdagon.dev/blog/higher-raii-7drl

  • # Que du bon

    Posté par  . Évalué à 6.

    Je ne me suis pas lancé dans Zig car cela reste pour moi un langage compliqué, comme Rust, et dont je n'ai pas usage immédiat. Je ne fais pas de programmation système. Je suis plus sur du Python/Nim/Elixir.

    Mas j'adore les contenus à propos de zig, vidéos et billets de blog. Je trouve que c'est plein de bonnes idées, que le créateur a semble visionnaire et très sympathique, la communauté très animée.

    Il commence à y avoir plein de bonnes réalisations. En vrac: une réécriture de ncdu, écrire des traitements rapides pour Elixir (en fait pour la BEAM), un serveur web.

    À noter, Uber utilise Zig comme beaucoup de monde : pour sa toolchain qui permet de (cross-)compiler du C.

    Je l'ai utilisé pour cross-compiler du Nim de amd64 vers arm7, c'est terriblement efficace, par rapport à gcc.

    Mais le projet se questionne sur ce qui semble une bonne idée pour créer de l'adoption mais pourrait les freiner dans le développement d'une toolchain plus efficace pour le langage lui-même.

    Un langage réussi c'est un mélange de:
    - ses fonctionnalités, sa syntaxe : comment ça m'aide à produire du code qui me plait
    - son tooling : comment je passe du code à qqch qui tourne
    - son écosystème, bibliothèque, framework : comment je ne pars pas de rien
    - sa communauté : comment j'obtiens de l'aide, des idées

    Je pense que Zig est bien parti sur tous ces aspects.

    • [^] # Re: Que du bon

      Posté par  . Évalué à 2.

      Mais le projet se questionne sur ce qui semble une bonne idée pour créer de l'adoption mais pourrait les freiner dans le développement d'une toolchain plus efficace pour le langage lui-même.

      Je ne savais pas qu'ils souhaitaient à long terme se défaire de LLVM. C'est un noble objectif et en même temps super ambitieux !
      Ils ont l'air d'être déterminé et j'ai hâte de voir ce que ça peut donner.

  • # Les pointeurs peuvent être null

    Posté par  . Évalué à 1.

    Contrairement à ce qui est indiqué dans l'article, on peut parfaitement avoir des pointeurs null dans Zig. Cf cet exemple. Sinon, impossible d'implémenter une liste chaînée, une queue, etc…

  • # bien mais pas top

    Posté par  . Évalué à 0. Dernière modification le 08 septembre 2023 à 10:19.

    Pour moi Rust est juste mieux quand on veut de la performance, il permet de tout optimiser:
    - allocation mémoire sûr (plus que Zig)
    - macro qui permettent de déporter à la compilation tout ce qui peut l'être (même plus que C)

    Alors certes Rust est plus complexe (quoique) mais si on veut des performances au top… sinon on prends Go ou Java.

    Zig n'est pas inintéressant pour autant.

    • [^] # Re: bien mais pas top

      Posté par  . Évalué à 6.

      Pour moi Rust est juste mieux quand on veut de la performance, il permet de tout optimiser:

      Je ne comprend en quoi Rust est plus performant concernant l'allocation mémoire ?
      Ni en quoi les macros de Rust sont plus efficaces que le comptime de Zig.

      Pour moi, l'intérêt principal de Rust reste la garantie qu'il n'y aura pas de problème avec la mémoire (fuite etc.), mais niveau perf, ce sont deux langages qui permettent d'avoir des performances similaires.

      Peut-être même que Zig encourage des façon de coder plus adaptés à la performance, comme le "Data Oriented Design", notamment avec des sucres syntaxiques pour pouvoir facilement boucler sur des structures de tableaux.

      En tout cas, Zig est réputé pour permettre d'écrire du code extrêmement performant.

      • [^] # Re: bien mais pas top

        Posté par  . Évalué à 2.

        mais niveau perf, ce sont deux langages qui permettent d'avoir des performances similaires.

        Et encore, en Rust, ponctuellement, pour de la perf, tu as besoin de muté in place et donc de passer en unsafe. C'est très propre car tu sais exactement où c'est fait dans le code (CTRL+F "unsafe") mais il n'y a pas de magie.

      • [^] # Re: bien mais pas top

        Posté par  . Évalué à 2.

        C'est peut-être que je ne connais pas assez Zig, mais je trouve que Rust apporte plus (Notamment les sucres syntaxiques me semble pas vraiment plus lisible). Rust apporte aussi une programmation fonctionnelle puissante, et la sécurité n'est pas rien.

        • [^] # Re: bien mais pas top

          Posté par  (site web personnel) . Évalué à 2.

          Bah ça me semble logique que Rust apporte plus, vu qu'il me semble que zig a pour but de rester un langage assez simple.
          Donc moins de manière d'exprimer la même chose.
          Rust, c'est cool, mais les code avec 8 itérateurs chainé, ce n'est pas ce qu'il y a de plus simple ni de plus reposant à lire.

  • # Optimisation explicite ?

    Posté par  (site web personnel, Mastodon) . Évalué à 2.

    « En effet, une variable marquée du mot-clé comptime sera connue à la compilation. Les boucles marquées du mot-clé inline seront exécutées à la compilation. » Je ne vois pas cela comme un avantage. Ce genre d'optimisation, devrait être implicite, le compilateur reconnaissant ce qui peut se faire à la compilation, sans avoir besoin d'un mot-clé particulier.

    • [^] # Re: Optimisation explicite ?

      Posté par  (site web personnel) . Évalué à 3.

      Cette histoire de inline for m'a aussi encouragé à creuser, et il semblerait que le compilo fait parfois des choses inattendues.

      Par exemple en Rust, il ne faut pas utiliser la variable d'incrément dans une boucle for.

      En plus il faut prendre en compte le fait que chaque version du compilo peut changer l'heuristique, donc un code qui faisait du loop unrolling peut ne plus le faire.

      J'imagine que c'est encore pire pour le C où il y a plein de compilos différents avec chacun leur heuristique pour faire le loop unfolding…

      Bref ça semble légitime d'avoir un inline for pour s'assurer que le comportement est bien celui attendu, typiquement si on est sur une plateforme embarquée ou la place/puissance est comptée.

      • [^] # Re: Optimisation explicite ?

        Posté par  . Évalué à 3.

        Et surtout parfois, des variables peuvent être modifier, des boucles "innutiles" en apparences utiles… Les bons compilateurs essayent déjà de pré-compiler un maximum de chose (1000*1000 sera traduit en 1000000) mais ils sont vite limités par ce qui pourrait avoir un sens différent. Classiquement, l'évaluation de certaines fonctions basiques peut changer.

  • # Et le parallélisme ?

    Posté par  (site web personnel, Mastodon) . Évalué à 4.

    Sauf erreur, cette dépêche ne parle pas du tout de la programmation parallèle (une grosse faiblesse de Rust et la principale raison pour laquelle Rust ne m'intéresse pas trop, par rapport à Go ou Elixir). Que permet Zig dans ce domaine ?

    • [^] # Re: Et le parallélisme ?

      Posté par  . Évalué à 3.

      Je ne suis pas sûr de bien comprendre la question, mais Zig permet l'utilisation de fonctions async et fournis un certain nombre de mot-clés. Malheureusement, il y a eu une régression pour la 0.11.0 et les fonctions async ne sont pas disponibles pour cette version de Zig.

      Il y a 10 sortes de gens dans le monde – ceux qui comprennent le ternaire, ceux qui ne le comprennent pas et ceux qui le confondent avec le binaire.

      • [^] # Re: Et le parallélisme ?

        Posté par  (site web personnel, Mastodon) . Évalué à 3.

        Ça me parait un mécanisme de très bas niveau. Pas de moyen d'avoir plusieurs fils d'exécution ? Comment on utiliserait ce mécanisme pour, par exemple, un serveur réseau ? J'ai regardé le code du serveur HTTP Zap et je ne vois absolument pas comment il fait (en tout cas, il n'utilise pas async/await).

        • [^] # Re: Et le parallélisme ?

          Posté par  . Évalué à 1.

          À ma connaissance, Zig n'inclut que ce système dans sa syntaxe, qui est mieux expliquée ici, et en quoi elle permet de faire du parallélisme : https://kristoff.it/blog/zig-colorblind-async-await/
          Mais je suppose qu'il existe des tas de librairies C et bientôt des librairies Zig qui implémentent des fonctions beaucoup plus avancées.

          Il y a 10 sortes de gens dans le monde – ceux qui comprennent le ternaire, ceux qui ne le comprennent pas et ceux qui le confondent avec le binaire.

    • [^] # Re: Et le parallélisme ?

      Posté par  . Évalué à 7. Dernière modification le 09 septembre 2023 à 13:43.

      En quoi le parallélisme est-il une faiblesse de rust?

      Il me semble que c'est au contraire plutôt une force, avec le système de type/ownership qui évite certaines erreurs comme les data-races, la bibliothèque rayon ou encore async/tokio pour le io-bound.

      • [^] # Re: Et le parallélisme ?

        Posté par  (site web personnel, Mastodon) . Évalué à 4.

        La faiblesse est justement que Rust ne propose rien pour le parallélisme, uniquement des évènements (async/await).

        • [^] # Re: Et le parallélisme ?

          Posté par  (site web personnel) . Évalué à 6.

          Ça dépend pas mal de ce que l'on appelle parallélisme. Rayon cité juste au-dessus permet de faire du parallélisme sur des données, ie utiliser facilement plusieurs coeurs CPU pour faire du calcul.

          Par contre, si on parle de parallélisme dans un contexte avec beaucoup d'IO, il y a async/await et tokio. Et là, les avis sont beaucoup plus partagés, on va dire. Certains apprécient que Rust reste bas-niveau, tout en évitant les bugs où des données partagées peuvent être lues/écrites depuis plusieurs contextes d'exécution. D'autres regrettent que ça reste bas niveau et peu pratique à utiliser. Il y a https://bitbashing.io/async-rust.html qui a pas mal fait parler de lui ces derniers temps.

    • [^] # Re: Et le parallélisme ?

      Posté par  (site web personnel) . Évalué à 9.

      Rust, Go & Elixir sont des langages très différents, et leur approches du parallélisme est du coup différente :

      • Rust est un langage système généraliste sans runtime par défaut (ce qui est indispensable quand on veut faire un module noyau par exemple, moins si on veut faire une petite web-app—l'absence de GC pour des gros volumes de données peut faire la différence parfois). Il permet d'avoir les performances brutes de C/C++ et des garanties fortes sur le code, mais le prix à payer est en autres que le langage est clairement difficile à maîtriser. Pour le parallélisme, on travaille de base avec des threads mais il apporte la garantie qu'on ne pourra pas modifier un même objet depuis 2 threads sans protection (genre mutex), ce qui n'est pas le cas en Go.
      • Go se compile en natif mais vient avec un runtime pour le garbage collector et l'ordonnanceur des routines. Le langage est très simple à apprendre et on code rapidement des choses qui marchent correctement, mais c'est le langage le moins expressif des 3 et il va y avoir plus de code boilerplate et moins de garanties sur l'exactitude du code qu'avec les 2 autres.
      • Elixir (je connais plus Erlang, je vais m'appuyer donc sur ce dernier, j'espère ne pas dire trop de bêtises) se base sur la VM Erlang, on a donc affaire à quelque chose de solide pour faire des services réseaux scalables et fiables, mais au détriment de performances brutes plus faibles (VM + typage dynamique).

      Dans la mesure où tu regardes du côté de Go & Elixir, même si tu ne le précises pas, je suppose que tu envisages ces langages dans le cadre d'un service réseau. Le parallélisme de Rust est très bien si tu veux faire un moteur 3D par exemple, mais c'est sûr qu'il n'aura pas forcément la facilité d'utilisation des goroutines/micro-processus Erlang dans des cas plus spécifiques.

      Si j'ai bien compris il existe en Rust plusieurs bibliothèques permettant d'utiliser async/wait (comme vu au dessus, vu les objectifs "système" du langage, imposer un runtime de base n'est pas souhaitable), mais on n'arrive pas à la facilité d'utilisation des routines (on a les garanties de Rust en compensation). J'ai aussi vu passer des systèmes d'acteurs façon Erlang pour Rust ; ça me semble une piste très intéressante, mais tout va dépendre de la maturité/pérennité de ces briques évidemment…

      Pour répondre à ta question, bien que ne connaissant pas vraiment Zig, vu ses objectifs (système etc…), je suppose que sa situation doit être à comparer à celle de Rust.

      • [^] # Re: Et le parallélisme ?

        Posté par  . Évalué à 2.

        Merci pour ce commentaire pertinent.

        Je connais mal Rust (j'en ai fait un tout petit peu), mais je croyais qu'il contenait des mécanismes plus poussés pour le parallélisme.
        Dans mon souvenir, un des auteurs de Rust disait l'avoir créer justement car c'était difficile d'écrire du code parallèle fiable en C++.

        Si je comprends bien, avec Rust on a une garantie de fiabilité, mais pas plus d'outil que ça pour écrire facilement du code parallèle.

        J'ai beaucoup entendu parlé de lib comme tokio et les retours que j'ai pu avoir c'est que c'est très puissant, mais difficile à appréhender.

        • [^] # Re: Et le parallélisme ?

          Posté par  . Évalué à 7.

          Franchement, on en fait tout une histoire de faire du code parallèle, mais il est sacrément plus difficile de faire du code perpendiculaire.

          Là, aucun langage ne propose quoi que ce soit, surtout dès qu'il s'agit d'utiliser un nombre de dimensions supérieures à 2.

          À la limite, il y aurait le brainfuck:

             ,
             ,
           .+++.
             ,
             ,
          
        • [^] # Re: Et le parallélisme ?

          Posté par  . Évalué à 5.

          J'ai beaucoup entendu parlé de lib comme tokio et les retours que j'ai pu avoir c'est que c'est très puissant, mais difficile à appréhender.

          C'est surtout que ça répond à des besoins très différents même si le vocabulaire employé se chevauche beaucoup entre les 2 usages :

          1. tokio ou async/await sont là pour avoir des traitements asynchrones ils permettent d'utiliser de manière simplifiée (et qui correspond mieux aux paradigmes du langage) aux API asynchrone. L'objectif ici est de ne pas bloquer de thread sur une attente I/O. Ça permet de gérer énormément d’interactions en consommant peu de ressources
          2. les threads par exemple sont fait pour faire du calcul intensif, l'objectif est alors de tirer le meilleur parti des différentes unités de calculs de ton CPU

          Évidement il est rare d'être tout l'un ou tout l'autre et il faut savoir comment les combiner pour tirer le meilleur parti pour ton besoin.

          https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

  • # Nim

    Posté par  (site web personnel) . Évalué à 3.

    Zig est vraiment pas mal, surtout dans son aspect de compilation ou il permet de cross-compiler du C ce qui n'est en général pas facile.. J'ai découvert Zig pour la première fois lorsque je l'ai vu mentionner dans la communauté Nim, un autre langage dont j'ai compris que Zig était issu.

    Nim est un langage compilé qui utilise une syntaxe qui s'approche du Python, mais ce n'est pas là le mieux. Comme Zig, il a été précurseur pour l'exécution de code à la compilation et Nim est à la fois un interpréteur et un compilateur. A la différence de Zig, il possède par contre un système de macros très étendu qui permet de faire pratiquement n'importe quoi avec le langage. C'est comme du Lisp mais avec une syntaxe accessible. Et en fait, les macros sont des bouts de codes exécutés à la compilation et qui utilisent l'arbre syntaxique et peuvent le transformer presque dans n'importe quel sens.

    Nim se place un peu plus haut niveau et gère les allocations et destructions mémoire. Nim v1 possédait un garbage collector classique mais avec Nim v2 on passe sur un comptage de référence moderne (ARC) avec des optimisations pour gérer le déplacement mémoire et d'autres cas. Il possède également une détection de cycles (ORC). Le gros avantage de la gestion ARC/ORC c'est que les allocations / destructions sont déterministes. Très utile pour de l'embarqué ou même lorsqu'on se soucie des performances mémoire d'un programme. Et de plus cela permet de basculer progressivement vers quelque chose qui ressemble au RAII même si la lib standard n'est pas adaptée à ça encore.

  • # Mon expérience de débutant Zig

    Posté par  (site web personnel, Mastodon) . Évalué à 5.

    Cet excellent résumé m'a motivé pour essayer Zig, j'en rends compte dans cet article : https://www.bortzmeyer.org/mes-debuts-en-zig.html

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.