2 points par GN⁺ 2025-08-25 | 1 commentaires | Partager sur WhatsApp
  • La version 0.15 de Zig a introduit la nouvelle interface IO (std.Io.Reader, std.Io.Writer)
  • Objectif : améliorer la complexité et les problèmes de performance de l’ancien modèle IO, mais cela a entraîné une confusion sur l’usage réel
  • L’utilisation de tls.Client et des buffers ajoute à la confusion en raison d’un mode de passage des paramètres incohérent
  • Même dans des exemples d’utilisation basiques, il faut gérer des exigences complexes comme la taille de plusieurs buffers et des champs d’options
  • Faute de documentation officielle, d’exemples de code et de fonctions utilitaires, l’ensemble est peu intuitif pour les débutants

La nouvelle interface IO introduite dans Zig 0.15 et son contexte

  • La version 0.15 de Zig a introduit de nouveaux types IO : std.Io.Reader et std.Io.Writer
  • L’ancienne interface IO entraînait de la complexité à cause de problèmes de performance, du mélange des types et d’un usage excessif de anytype
  • Le but principal de la nouvelle structure IO est de clarifier la séparation des types entre interfaces et d’améliorer les performances

Les problèmes concrets lors de l’utilisation de tls.Client et de l’interface IO

  • Lors de la mise à jour d’une bibliothèque SMTP existante, de la confusion est apparue autour de l’utilisation de la fonction tls.Client.init
  • D’après la documentation, la fonction init reçoit en arguments des pointeurs vers Reader et Writer, ainsi qu’un ensemble d’options
  • Dans Zig, net.Stream renvoie respectivement Stream.Reader et Stream.Writer via les méthodes reader() et writer()
    • Mais Stream.Reader/Writer et std.Io.Reader/Writer ne sont pas exactement les mêmes types, ce qui impose une conversion
    • Pour Reader, il faut appeler la méthode interface(), tandis que pour Writer, il faut utiliser le champ &interface, ce qui manque de cohérence

Le problème de configuration des buffers et des champs d’options

  • stream.writer et stream.reader prennent chacun un buffer en argument
    • Le buffer est ainsi présenté comme un élément essentiel de la nouvelle interface IO
  • Lors de l’appel à tls.Client.init, quatre champs d’options sont requis : ca_bundle, host, write_buffer et read_buffer
    • La logique de séparation entre les valeurs passées dans les options et celles passées directement en argument semble peu claire
var tls_client = try std.crypto.tls.Client.init(
  reader.interface(),
  &writer.interface,
  .{
    .ca = .{.bundle = bundle},
    .host = .{ .explicit = "www.openmymind.net"; } ,
    .read_buffer = &read_buf2,
    .write_buffer = &write_buf2,
  },
)
  • En pratique, si les pointeurs de buffer ne sont pas correctement fournis, le programme peut mal fonctionner, se bloquer ou planter

Le manque d’intuitivité lors de l’utilisation de Reader

  • Bien que le champ reader de tls.Client soit lui-même un « flux déchiffré », std.Io.Reader ne propose pas de méthode read classique
  • À la place, on ne trouve que des méthodes moins intuitives comme peek, takeByteSigned ou readSliceShort
  • L’API la plus proche d’un usage courant consiste à utiliser la méthode stream pour lire les données dans un buffer
var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const n = try tls_client.reader.stream(&w, .limited(buf.len));

Exemple de code complet et difficultés en pratique

  • Même pour produire un exemple minimal entièrement fonctionnel, il faut gérer de nombreux détails : options, taille des buffers, conversions de type, etc.
  • Le manque de tests, de documentation et d’exemples augmente la difficulté d’apprentissage et la barrière à l’entrée
  • Sans une bonne compréhension de la cohérence interne du langage Zig ou de son design sous-jacent, de nombreux aspects peuvent sembler étranges
  • Même dans la bibliothèque standard, cette approche est encore peu utilisée, ce qui limite les références pratiques

Retour d’expérience et conclusion

  • Avec des changements comme le renommage de std.fmt.printInt, les évolutions de design d’API, etc., la migration elle-même n’est pas simple
  • L’auteur a rencontré de manière répétée plusieurs difficultés : reader.interface(), la forme &writer.interface, le passage des options, ou encore la nécessité de plusieurs buffers
  • Lorsqu’on n’est pas familier avec les protocoles réseau ou de sécurité comme TLS, il est encore plus difficile de comprendre les exigences
  • Globalement, il reste de nombreuses insuffisances en matière de clarté, de documentation et de confort d’utilisation par rapport à l’existant

1 commentaires

 
GN⁺ 2025-08-25
Avis Hacker News
  • L’auteur précise qu’il est l’auteur du projet. Il a enfin réussi à le faire fonctionner correctement. Le writer chiffré comme le stream writer nécessitaient tous deux une étape de flush, et il y avait aussi un problème du côté lecture. Le streaming fonctionne, mais comme Writer.Fixed n’implémente pas sendFile, la première lecture renvoie toujours 0. Après le premier appel, cela bascule en interne du mode streaming au mode lecture, et soudain tout se met à fonctionner (lien vers le code concerné : Zig File.zig #L1318). Il essaie maintenant de réactiver la compression dans sa bibliothèque websocket

    • Ça rappelle le mème YouTube « n’oubliez pas le flush » (vidéo YouTube)

    • Je me demande bien ce qu’est devenu le principe de moindre surprise

    • C’est assez impressionnant d’être passé de l’ancienne interface à la situation actuelle. La surprise est de taille

  • Je ne suis pas PM Zig, mais la première réponse évidente au problème rencontré par l’OP serait d’avoir une meilleure documentation et davantage d’exemples d’utilisation (même en très grand nombre). Ce serait aussi une bonne occasion, en faisant ce travail, de se demander si on n’en demande pas trop aux utilisateurs. Si l’objectif était la performance absolue ou d’éviter des abstractions qui la dégradent, alors cet objectif semble atteint, mais la DX (expérience développeur) a l’air d’être partie dans une autre galaxie

    • Je crois que vous connaissez mal la culture de la communauté Zig. Si vous vous plaignez du manque de documentation, préparez-vous à voir pleuvoir des commentaires disant « lisez directement le code de la stdlib ». La plupart des API sont difficiles à utiliser, comme dans ce billet, et même les tâches de base comme HTTP ou le système de fichiers sont vraiment pénibles si on n’y est pas déjà habitué. Au final, seuls les très compétents survivent

    • Rédiger de la documentation a un coût et prend du temps. Ce temps pourrait aussi servir à améliorer d’autres parties de Zig. Si le code est encore en chantier, repousser la documentation jusqu’à ce qu’il se stabilise peut être un choix rationnel. Bien sûr, documenter est une bonne chose, mais quand il faut choisir entre de nouvelles fonctionnalités, des corrections de bugs importantes ou du travail de documentation, on ne peut pas toujours tout avoir

    • zig semble trop focalisé sur le fait d’indiquer ce qu’il ne faut pas faire. J’aimerais qu’il évolue davantage vers une présentation structurée de différentes approches et d’exemples d’usage. L’absence de documentation sur cette interface en est un bon exemple

    • Écrire une bonne documentation ou de bons exemples demande énormément d’efforts. Vu l’ampleur des changements actuellement en cours dans zig, même une documentation bien faite risque vite de devenir inutile avant que tout ne soit vraiment stabilisé

    • Je ne suis pas développeur Zig, mais je pense que l’une des raisons pour lesquelles sa documentation est si succincte est que le langage est encore jeune et continue d’évoluer. Je comprends qu’il soit difficile d’investir du temps et de l’énergie dans une documentation quand on sait qu’elle sera bientôt obsolète

  • Le langage Zig lui-même est vraiment pas mal, mais la bibliothèque standard est encore très inachevée, change sans cesse, manque de beaucoup de choses, et certaines parties sont soit trop abstraites, soit au contraire trop bas niveau. À ce stade, je pense qu’il vaut mieux utiliser directement les API de l’OS plutôt que la bibliothèque standard. Sauf si vous êtes prêt à jouer les bêta-testeurs, je recommande d’éviter la stdlib

    • En pratique, c’est aussi ce que je fais le plus souvent avec zig : j’utilise surtout les API de l’OS. Les cImports marchent bien, donc c’est facile à utiliser quand on n’a pas envie d’écrire des définitions zig

    • À mes yeux, Zig essaie d’en faire trop d’un coup, au point de ne même pas atteindre le seuil minimal de qualité que j’attends. On force les utilisateurs à encaisser des changements brutaux et des expérimentations, dans une situation où suffisamment de gens ont déjà investi pour n’avoir d’autre choix que de croire à l’illusion du « avant la 1.0, c’est normal que ce soit cassé, ça finira par devenir bien un jour » (à mon avis, ce jour n’arrivera jamais). Je ne trouve pas souhaitable de faire porter à d’autres le coût de ses propres expérimentations. Même si on annonce d’avance que c’est instable, même si on dit de ne pas en dépendre, cela reste un problème pour ceux qui se font ensuite retirer le tapis sous les pieds. Je ne sais pas très bien ce qu’est zig. Matklad parle de machine level language (interview liée : lobste.rs - interview de Matklad), tandis que la page officielle parle d’un langage robust, optimal, reusable general-purpose. Ces deux visions se contredisent. Et il existe beaucoup de problèmes qui n’ont pas besoin de gestion manuelle de la mémoire, donc zig n’est en aucun cas un langage généraliste. Toute cette confusion se reflète dans l’instabilité de zig et dans l’ampleur démesurée de sa bibliothèque standard. Prétendre à la simplicité et à l’universalité avec une bibliothèque aussi grosse est contradictoire. Async aussi était présenté comme une solution universelle alors qu’une telle fonctionnalité ne peut pas être implémentée efficacement et de façon générale sur toutes les plateformes. On l’a autrefois vendu comme la solution au problème de la coloration des fonctions, puis cette tentative a déjà été abandonnée. Il est étrange de demander qu’on y croie de nouveau. En réalité, il suffirait des instructions assembleur de base nécessaires à l’implémentation du compilateur sur chaque plateforme, et luajit a carrément un parseur écrit en pur assembleur qui fonctionne très bien partout. Je programme surtout en lua, et j’ai rarement rencontré des bugs dans l’interpréteur. Je ne vois pas quel problème zig résoudrait mieux que luajit. Et même s’il y avait un cas que seul zig résout bien, on pourrait l’embarquer dans du code lua et l’interfacer via FFI. La plupart du code n’a pas réellement besoin de ce niveau d’optimisation bas niveau. Adopter zig ne ferait qu’ajouter des ennuis. Aujourd’hui, l’emballement autour de zig atteint un écart avec la réalité comparable à celui de l’IA. Pour croire en zig, il faut croire à l’espoir vain qu’il finira par acquérir des capacités qu’il n’a pas. En pratique, sans plan d’exécution concret, on se contente de dire « attendez encore un peu »

  • Je ne comprends pas qu’une bibliothèque ou une interface exige l’allocation d’un buffer de mon propre type. Si c’est moi qui fais le parsing, je n’ai pas vraiment besoin de la bibliothèque, et si je l’utilise, cela peut même casser l’interopérabilité. L’interface particulière de go vient du fait que certaines interfaces étendent l’interface writer (voir l’interface hijacker), ou que l’objet request est réutilisé de multiples façons dans différents middlewares. En résumé, une requête (request) n’a pas besoin d’être extensible, mais une réponse (response) peut prendre des formes très diverses, comme websocket, wrapper TCP, etc.

    • Que la bibliothèque exige un buffer externe ne me paraît pas étrange. Cela apporte de la flexibilité au prix de plus de travail manuel. Par exemple, si on a déjà un pool de buffers, on peut vouloir le réutiliser. Si le type alloue de son côté en interne, c’est impossible. Ou bien, dans un environnement où toutes les ressources doivent être allouées à l’avance, une allocation ultérieure est interdite. L’inconvénient, c’est que seuls 10 % des utilisateurs ont vraiment besoin de cette flexibilité, tandis que les 90 % restants vont juste allouer un buffer et le passer, ce qui complique la vie de tout le monde. La meilleure approche serait d’offrir une grande flexibilité tout en gardant les cas simples faciles à gérer. Par exemple, si on passe un buffer de longueur zéro (ou null en Zig), le type pourrait allouer lui-même, et on pourrait aussi fournir un constructeur simple sans buffer supplémentaire. Bien sûr, tout cela est un cauchemar à documenter

    • C’est un peu comme les conventions différentes d’un langage à l’autre (radians/degrés, etc.). N’importe quelle E/S peut être librement transformée. D’un côté on appelle ça un mock, dans un autre langage on l’appellerait peut-être unsafeFoo. Andrew Kelley a redécouvert par lui-même en live stream des motifs dont la communauté Haskell discute depuis 30 ans. Donc l’avenir, c’est Zig. Il a eu l’illumination en premier

    • Le sens d’un buffer externe, c’est justement que la fonction évite d’allouer le buffer

  • Je n’ai aucune intention de faire passer mon projet zig secondaire à la version 0.15.x. Je comprends pourquoi Andrew a choisi cette release et je respecte le fait de mettre la nouvelle E/S entre les mains des early adopters. Mais cela ne fait que quelques jours depuis la refonte massive des readers/writers. C’est une bonne chose pour les contributeurs de la bibliothèque standard, mais pour quelqu’un comme moi qui utilise zig comme hobby, il me semble plus sage d’attendre après la stabilisation en 0.16.0

    • Si le langage s’appelle Zig, il faudrait peut-être parfois faire Zag aussi

    • Loris Cro, membre du noyau Zig, a lui aussi mentionné dans une interview récente qu’il reporte la mise à jour de ses projets zig jusqu’à ce que les secousses dues aux changements d’E/S se calment. Mais les perspectives restent positives ensuite. Andrew et Loris semblent tous deux penser qu’il s’agira du dernier grand changement, ce qui laisse espérer une 1.0 relativement proche. Le principal facteur d’incertitude à ce stade est l’impact des stack-less coroutines en cours de réintroduction

  • Après avoir lu ce billet sur la nouvelle interface E/S, j’ai décidé d’éviter zig. J’ai l’impression que mon instinct était juste. Même si mes raisons sont différentes, j’y retrouve au final une complexité comparable à la verbosité d’avant C++11. C’est encore ce schéma familier où un nouveau langage censé remplacer les autres finit par devenir aussi complexe qu’eux

    • Je précise que mon propre billet en fait partie. Je ne pense pas qu’il faille avoir peur d’essayer zig à cause de ce genre de texte. L’équipe zig est prête à changer radicalement les choses quand elle voit une meilleure solution. Si vous considérez zig comme un investissement pour l’avenir, ces changements peuvent ne pas vous convenir, mais pour un individu ou une petite équipe, c’est déjà un langage formidable, avec des objectifs clairs et de bons outils
  • La remarque de l’OP selon laquelle il est incohérent de devoir appeler la méthode interface() pour convertir Stream.Reader en std.Io.Reader, alors que pour obtenir std.io.Writer depuis Stream.Writer il faut prendre l’adresse du champ &interface, aurait probablement été rejetée d’emblée par la communauté Go. Dans Go, même les petits changements sont décidés après une analyse extrêmement poussée. Mon exemple préféré d’issue Go : Go github issue #45624. Ils peuvent débattre pendant 4 ans avant de conclure. C’est lent, mais ils examinent soigneusement la cohérence, la réflexion de conception et l’usage réel du code. C’est lent, oui, mais à la vitesse nécessaire. Et les décisions qui en sortent sont au final de très grande qualité

    • Rust aussi fonctionne comme ça. Il y a beaucoup de fonctionnalités utiles présentes uniquement dans nightly rust et pas dans stable (par exemple generator). C’est frustrant, mais les fonctionnalités qui entrent dans stable sont examinées en profondeur. Je suis impatient, mais je pense que l’approche de l’équipe Rust est la bonne

    • Avant Go 1.0, ce n’était pas aussi lent. Il y avait souvent de gros changements, même s’ils étaient moins fondamentaux (suppression des points-virgules, changement du type d’erreur, etc.), et il y avait aussi des outils de migration automatique. À partir de la 1.0, ils ont promis la stabilité et sont passés à l’approche actuelle

  • Zig est le premier langage auquel je pense pour du travail bas niveau. Et c’est très cool de pouvoir aussi utiliser Zig comme cross-compilateur C/C++

  • La plupart des problèmes semblent simplement venir d’un manque ou d’une faiblesse de documentation

    • Trop de choses changent trop souvent dans Zig, donc la documentation ne semble pas prioritaire. Même les tutoriels Zig donnent presque une impression de « recueil d’exemples de code » (avec beaucoup d’exemples qui ne tournent plus sur les compilateurs récents), et pour de nombreuses définitions de la bibliothèque standard, il faut lire directement le code source. Quand on maîtrise toutes les astuces de la syntaxe Zig, les fonctions simples sont courtes, logiques et bien nommées, donc faciles pour l’auteur. Le concept d’allocators n’est pas difficile conceptuellement, même si je n’ai pas envie d’en écrire moi-même. Mais avec les concepts complexes, les limites deviennent flagrantes. Le nouveau système d’E/S de Zig ressemble à la structure Streams/Readers/Writers de Java, avec plusieurs couches d’enrobage. Il cherche à permettre une sortie simple du genre output.write("hello"), mais en pratique il crée de la confusion faute d’explications suffisantes. On peut même se demander s’il était nécessaire d’exprimer un système de types aussi complexe dans la bibliothèque standard. L’ensemble de Zig repose sur des méthodes claires, concises et faciles à lire, alors que le nouveau système d’E/S s’en éloigne fortement et manque d’intuitivité
  • (Le nouveau système de zig) pose problème parce qu’il mélange dans tout le moteur d’exécution un concept qui ne servait qu’à séparer des frontières d’exécution, sans montrer clairement comment relier les deux côtés