Journal Intégration d'une fenêtre de debug live en Rust 🦀

Posté par  (site web personnel, Mastodon) . Licence CC By‑SA.
Étiquettes :
20
6
mar.
2023

J'ai récemment ajouté à un de mes projets open source une fenêtre de modification en temps réel des paramètres de calcul et de rendus. La réalisation de ce travail s'est passée de manière très efficace et sans produire aucun bug. C'est-à-dire qu'une fois compilé, le logiciel n'a présenté aucune défaillance et la fonctionnalité s'est comportée exactement comme attendu. Du premier coup. L'objet de ce journal est d'en exposer le contexte.

La nature du langage Rust 🦀

Le projet en question, un jeu, codé en Rust.

Différents aspects de ce langage ont rendu cette intégration plus aisée. De manière non exhaustive :

  • Un typage fort : Aucune ambiguïté sur ce que l'on manipule. Lorsque l'on change la signature d'une fonction, tous ses appels sont contrôlés par le compilateur. Rien ne peut planter à l'exécution pour des raisons de type ou de structure.
  • Pas de "nulls" incontrôlés : Là où il peut y avoir un "null" il y a un type spécial (Option<i32> par exemple). Il est obligatoire de gérer ce type comme tel, c'est-à-dire comme pouvant être None. Pas de plantage à l'exécution à cause de "null" non pris en compte.
  • Filtrage par motif exhaustif + Énumérés :  Travailler avec des énumérés associés à du filtrage par motif exhaustif (c'est contrôlé par le compilateur) fais que l'on n'oublie aucune partie du code quand on ajoute/modifie des notions (représentés par des Énumérés) dans le code.

L'architecture du jeu

Qui dit jeu vidéo dit boucle. Lors de chaque itération, est appliqué la suite d'algorithmes du jeu qui lisent "l'état" et produisent une liste "d'opérations" à mener. Ce qui est important ici, c'est que lors de l'exécution de ces algorithmes, "l'état" n'est jamais altéré. L'exécution de ces algorithmes est ainsi parallélisable à souhait (et c'est fait) et il n'y a pas d'ambiguïté sur ce qui pourrait changer dans cet "'état" pendant l'exécution de ces algorithmes.

Ensuite, la liste "d'opérations" à mener est traité. Cette partie de code modifie "l'état" du jeu.

Cette séparation franche entre la partie "déterminer des changements" et l'application de ces "changements permettent d'éviter les nœuds de logique (là où on ne sait plus quoi est modifié ni quand). Également, toutes les altérations possibles de "l'état" du jeu sont identifiés et listés exhaustivement (à travers des énumérés) :

Liste des énumérés

Nature des changements

Mutualisation de la configuration

Une première étape a été de déplacer les quelques valeurs de configurations qui n'était pas encore dans "l'objet de configuration". Aucune difficultés particulière et aucune régression introduite (grâce aux contrôles de typage, voir "La nature du langage Rust").

Intégration de la fenêtre Egui

Egui est une bibliothèque permettant de créer des interfaces graphiques en mode immédiat. Par exemple, cette partie-ci de la fenêtre que j'ai intégré :

Interface egui

Est produite par le code suivant :

egui code

Ici, la modification de la propriété liée à la configuration correspondante se fait par une référence mutable. Je me le permets (par opposition à la production de liste de "changements") car il n'y a pas de complexité particulière. La fenêtre Egui est construite au fur et à mesure des déclarations de construction. Et le contrôleur d'emprunt du compilateur de Rust assure qu'il n'y aura jamais de problèmes d'accès concurrents à ces propriétés.

Rechargement des ressources graphiques

Le mécanisme de rechargement d'une ressource graphique pourrait être complexe, car nécessite de remplir le tampon du GPU avec la nouvelle texture. C'est le genre d'opération qui est délicat de faire pendant l'exécution de l'affichage du jeu ou durant l'exécution de la logique du jeu en rapport avec cette texture.

L'architecture du jeu (voir "L'architecture du jeu") s'y prête bien : Lorsqu'un changement est constaté sur la fenêtre, un "changement" est ajouté à la liste des changements qui seront exécutés au moment où l'on autorise l'accès par mutabilité à "l'état" du jeu :

graphics message

Mot de la fin

Cette architecture ainsi que la bonne maintenabilité des codes Rust m'ont permis de faire cette intégration très efficacement et de constater dès la première exécution du code que tout fonctionnait comme attendu. Ce qui est suffisamment satisfaisant pour justifier ce journal :)

Depuis j'ai également effectué un important remaniement de code (+8,586 −3,289) dans lequel les parties logiques et affichages du jeu ont été concrètement séparés (voir les espaces de travail en Rust). Permettant d'une part d'exécuter un serveur de jeu en ligne de commande, mais aussi de mieux identifier les différentes responsabilités du code. Permettant par exemple d'intervenir sur la partie affichage ou effets visuels et sonores sans risquer de modifier la logique de calcul du jeu.

Sachez aussi que je travaille pour la société Algoo, une boîte très impliquée dans le logiciel libre. Si vous envisagez de faire du Rust dans un contexte professionnel, on peut vous accompagner en force de développement, formation ou coaching.

  • # J'oubliais de préciser

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

    Ce que j'oublie de préciser à propos du dernier remaniement de code. Une fois les erreurs de compilation résolues, le logiciel fonctionne. Je peux vous assurer qu'après plus de 10 ans de développement, un différentiel de 8 586 lignes de code ajoutées, 3 289 lignes de code supprimées : un programme qui ne présente pas de bug à l'exécution, ça surprend !

    🦀🐍 http://github.com/buxx 🖥 https://algoo.fr 📋 https://tracim.fr

  • # Au sujet du « Ça marche du premier coup »

    Posté par  . Évalué à 9.

    J'avoue être un peu sceptique sur le « Ça marche du premier coup », du moins si son intérêt est vu en terme de temps de débogage gagné. Car il n'y a pas de secret, pour que le programme marche du premier coup, Rust utilise des règles très strictes à la compilation, qui font que les cas oubliés potentiels ne sont pas possibles, et respecter ces règles demande un effort au moment du dialogue avec le compilateur qui consiste à lui faire comprendre que le code est valide. De ce point de vue, par rapport à un langage interprété laxiste du type de Python, on transfère simplement le temps de test et débogage initial des erreurs triviales vers du temps de développement où on « débogue » les erreurs de compilation (variables non définies ou pas encore initialisées, pas du bon type, etc.) Si on veut trouver les avantages potentiels d'un langage compilé au typage fort par rapport à un langage interprété laxiste au typage faible du type de Python, ils ne sont pas tant dans la facilité ou rapidité du développement en lui-même que dans le fait que des restrictions fortes peuvent faciliter la conservation d'un code maintenable lorsqu'il est écrit à plusieurs, et éviter les bugs triviaux qui ne se manifestent pas parce qu'on a oublié un cas dans les tests. À chacun de juger si les restrictions en question en valent la peine.

    S'il suffisait d'un typage fort et riche pour avoir un langage qui fait soudainement rêver le monde entier, on le saurait déjà, puisque c'est le cas de la famille des langages ML, avec des fonctionnalités similaires (les enums Rust sont appelées ailleurs types algébriques, les traits ressemblent aux classes de type de Haskell ou autre, etc.).

    Pour moi, le vrai intérêt de Rust est dans le fait que le langage combine des caractéristiques de langage agréable (par exemple une excellente portabilité, et une simplicité de distribution et réutilisation des modules qui me fait baver en tant que personne qui vient de passer plusieurs jours à se battre contre des build systems C++) avec des caractéristiques de langage sûr (soundness) et de langage de bas niveau adapté aux applications où la performance compte. Ce qui le rend unique, ce n'est pas tant chacune de ces caractéristiques prise isolément que leur réunion dans un même langage.

    • [^] # Re: Au sujet du « Ça marche du premier coup »

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

      De ce point de vue, par rapport à un langage interprété laxiste du type de Python, on transfère simplement le temps de test et débogage initial des erreurs triviales vers du temps de développement où on « débogue » les erreurs de compilation (variables non définies ou pas encore initialisées, pas du bon type, etc.)

      Je ne suis pas tout à fait d'accord. Le mode de développement / tests / bugfix avec un langage type python génère beaucoup d'aller-retour ou changements de contexte qui ont un coût non négligeable en temps de développement.

      Tous les bugs effets de bord / cas particulier ont toutes les chances d'être identifiés tardivement, voire après livraison ou en production.

      • [^] # Re: Au sujet du « Ça marche du premier coup »

        Posté par  . Évalué à 3. Dernière modification le 07 mars 2023 à 05:51.

        Python n'entre vraiment pas en ligne ici. Il est clair que pour développer une petite IHM, ou un petit script, ce sera plus vite fait avec Python (et bidouillé/adapté aux besoins). Néanmoins, surtout pour un gros projet, le code Python a la fâcheuse tendance à devenir instable et il est difficile de savoir quel type tu reçoit réellement (int ou string?) et il faut parfois rajouter du code pour gérér plusieurs cas qui en Rust ne seraient même pas survenus. Deplus sur un gros projet, impossible de tester tous les cas (surtout ceux qui sont la "au cas ou").

        Mais dans tous les cas Rust perd le temps de la compilation (et donc du retour à la case dev) alors que Python peut être modifier en prod rapidement par un admin ce que Python perd en perfs. Et au final pour les projets "importants" on peut se demander si c'est un réel défaut puisque généralement les adminsys sont incapable de toucher à une virgule de code.

        Reste qu'en pratique un langage interprété offre quand même un peu plus de souplesse et de possibilité notamment pour les non-devs ou pour des projets ou les specs changent souvent (typiquement une IHM web) et ou la stabilité du code n'est pas si critique (car au final un site web c'est un ensemble de pages web qui chacune est un "petit projet").

        PS : C'est rapide de corriger les erreurs de compilation quand tu maîtrise ton langage.

        • [^] # Re: Au sujet du « Ça marche du premier coup »

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

          On a eu pas mal de discussions sur le sujet avec Bux ; un des gros points forts que je vois dans python, y compris pour des projets d'envergure, c'est l'écosystème. Tu trouves littéralement des modules pour tout ou presque.

          Après, c'est un langage dynamique et finalement on fait beaucoup de python à Algoo et on utilise le typage (qui n'est pas un vrai typage mais qui limite quand même la casse si utilisé avec le bon outillage de dév)

          Mais ça ne remplace en aucun cas les avantages d'un langage "rigoureux" plus statiquement typé comme C++/C# et comme Rust désormais (que je ne manipule pas du tout, mais dont Bux m'a vanté à maintes reprises les bienfaits;)

        • [^] # Re: Au sujet du « Ça marche du premier coup »

          Posté par  . Évalué à 5.

          Néanmoins, surtout pour un gros projet, le code Python a la fâcheuse tendance à devenir instable et il est difficile de savoir quel type tu reçoit réellement (int ou string?)

          C'est une critique que je ne comprends pas trop de nos jours pour python. De plus en plus de projets (surtout les complexes) utilisent les type hints et mypy ou autre pour vérifier cela avant exécution.

          • [^] # Re: Au sujet du « Ça marche du premier coup »

            Posté par  . Évalué à 4.

            Ça reste non contraignant. Tu peux ne pas annoter le type voir même ignorer les warnings. Et ça ne marche pas avec tout

          • [^] # Re: Au sujet du « Ça marche du premier coup »

            Posté par  . Évalué à 2. Dernière modification le 08 mars 2023 à 14:12.

            De plus en plus de projets (surtout les complexes) utilisent les type hints et mypy ou autre pour vérifier cela avant exécution

            1. Cela ne le vérifie pas avant l'exécution mais au cours de l'exécution puisqu'il n'y a pas d'étapes avant l'éxécution dans un langage interprété (en pré-compilation en live au mieux).
            2. Du fait que le type ne soit pas obligatoire, inévitablement, ce n'est pas toujours le cas
            3. Mais surtout il n'y a pas que ce problème. Dans le même style, il y a les argument des classes que tu peux accéder alors qu'ils n'existent pas.

            Je ne dis pas que tout soit catastrophique en python, loin de là, je l'utilise avec plaisir. Je dis juste que dire qu'un programme Python est stable et marche "du premier coup" est largement surfait. Python est simple, rapide à maîtriser (pour un novice en informatique notamment), très productif, il possède une bibliothèque très suppérieur à beaucoup (Y compris à C, j'ai utilisé Sympy qui n'existe réellement qu'en Python), il permet de faire beaucoup de chose. Pour moi son plus gros défaut reste sa vitesse d'exécution (comparé à Rust évidemment, il n'y a pas photo mais même comparé aux autres interprétés modernes), je lui préfère Julia pour ce point.

            • [^] # Re: Au sujet du « Ça marche du premier coup »

              Posté par  . Évalué à 6.

              Mypy est un analyseur statique, donc tu vérifie en partie le typage indépendamment de l’exécution si je ne m’abuse.

            • [^] # Re: Au sujet du « Ça marche du premier coup »

              Posté par  . Évalué à 4.

              1. Cela ne le vérifie pas avant l'exécution mais au cours de l'exécution puisqu'il n'y a pas d'étapes avant l'éxécution dans un langage interprété (en pré-compilation en live au mieux).

              C'est là où on voit que ceux qui critiquent python ont au final une idée assez limitée des outils autour de ce langage. MyPy fait une analyse statique, et intégré à ton IDE il te préviens en direct des erreur de typage possibles, avant l'exécution.

              1. Du fait que le type ne soit pas obligatoire, inévitablement, ce n'est pas toujours le cas

              Il "suffit" de ne pas accepter les pull requests qui n'ont pas de typage ?

              1. Mais surtout il n'y a pas que ce problème. Dans le même style, il y a les argument des classes que tu peux accéder alors qu'ils n'existent pas.

              Tu peux développer ? Je ne comprends pas trop.

              • [^] # Re: Au sujet du « Ça marche du premier coup »

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

                Il y a quand même une grosse différence entre ce qui est intégré au langage (ou du moins à son écosystème le plus primaire), et ce qui fonctionne parce qu’on a ajouté N couches d’outillage tiers et de règles à vérifier pendant les merge requests (avec toutes les erreurs possibles) pour que ça fonctionne. De plus, l’outillage additionnel et les règles complémentaires sont plus susceptibles de trous et de cas particuliers qu’une fonctionnalité intégrée au langage ou à son écosystème standard.

                L’avantage de Rust sur Python ici, c’est surtout que tout ce qui concerne le typage est fourni en standard. En arrivant sur un projet Rust, tu sais que tu auras du typage statique fort etc; alors qu’en Python, tu dépends du bon vouloir de l’équipe.

                (Et je ne fais pas de Rust, et plus de Python depuis des années).

                La connaissance libre : https://zestedesavoir.com

                • [^] # Re: Au sujet du « Ça marche du premier coup »

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

                  De toute façon, il faut à tout prix ne pas être dogmatique : chaque langage a ses avantages et inconvénients et son utilisation doit être décidée pragmatiquement :

                  • quelles sont les compétences que l'on a pour le projet
                  • quel est la pérenité / l'ampleur du projet
                  • de quel écosystème a-t-on besoin ?
                  • quelles sont les attentes en terme de performances
    • [^] # Re: Au sujet du « Ça marche du premier coup »

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

      Perso, la révélation que j'ai eue dans ma carrière de développeur, c'est la puissance de l'immutabilité.

      C'était en découvrant le langage ELM … Mais c'était au moment où j'ai arrêté de vraiment coder :-/

    • [^] # Re: Au sujet du « Ça marche du premier coup »

      Posté par  (site web personnel, Mastodon) . Évalué à 10. Dernière modification le 07 mars 2023 à 08:49.

      Derrière mon expression un peu subversive "Ça marche du premier coup", voilà ce que je veux dire :

      J'ai une intention de départ. Par exemple l'intégration de la fenêtre de debug ou le remaniement de code "profond" dont je parle. Je démarre les modifications et me laisse guider par les outils de contrôle du code qui accompagne le langage. Le terme "guider" est ici très important. C'est-à-dire que là où avec un langage comme Python (que j'aime beaucoup) je dois découvrir la plupart de mes erreurs à l'exécution, j'ai été guidé par les règles de Rust pour ne pas les faire. Et c'est extrêmement moins couteux.

      Je suis interpelé par "(un) dialogue avec le compilateur qui consiste à lui faire comprendre que le code est valide." D'expérience avec Rust, les situations où le code était valide et où le compilateur ne le voyait pas ont été extrêmement rare. À tous les coups, les erreurs venaient de moi.

      Au départ, quand j'ai commencé Rust, j'ai pensé qu'en effet, le temps passé au debug (ex. avec Python) était transféré à arranger le code pour être valide pour le compilateur Rust. Mais force et de constater après ces quelques années, que le gain de temps "au final" est énorme. Le temps passé au debug (à fouiner pour comprendre ce qui ne va pas) n'est passé quasiment que sur des problématiques "métier".

      L'intérêt de Rust n'est pas son typage strict, ou ses énumérés, etc. C'est l'assemblage de tous ces paradigmes en un seul écosystème. Et du fait que ce soit le standard du langage. Cela tend les logiciels produit à bénéficier des avantages qu'ils induisent (ces paradigmes) même quand le développeur ne le fait pas "exprès". Je te rejoins clairement sur ce point :)

      🦀🐍 http://github.com/buxx 🖥 https://algoo.fr 📋 https://tracim.fr

      • [^] # Re: Au sujet du « Ça marche du premier coup »

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

        Et du fait que ce soit le standard du langage.

        Toute la différence entre tu peux et tu dois ; ça change tout car les mauvais usages sont "interdits" (et donc en pratique il n'y en a aucun).

        L'immutabilité, par exemple, c'est possible en C++. D'ailleurs c'est très présent, il me semble, dans les lib "bien foutues" (QT par exemple).

        À L'époque où je développais en C++, j'ai jamais réussi à réintroduire ce concept dans les projets sur lesquels je suis intervenu tant il y avait de couches / d'historique à reprendre.

        • [^] # Re: Au sujet du « Ça marche du premier coup »

          Posté par  . Évalué à 9.

          Toute personne ayant essayé de mettre des const dans une base de code C++ pas prévue pour à la base sait ce que tu veux dire je pense :p Tu commences et ça se propage partout, tu te rends compte que par contagion successive ça se propage partout, et généralement tu rollbackes tout ou presque.

      • [^] # Re: Au sujet du « Ça marche du premier coup »

        Posté par  . Évalué à 6.

        L'intérêt de Rust n'est pas son typage strict, ou ses énumérés, etc. C'est l'assemblage de tous ces paradigmes en un seul écosystème.

        On appel ça un système de type. et plusieurs langages vont au moins aussi loin depuis plus longtemps. Je ne pense pas que ça suffise à le démarquer.

        • le borrow checker
        • la possibilité d'utiliser ce genre de système de type tout en ayant une grande affinité avec les manipulations bas niveau
        • le borrow checker
        • la gestion de mémoire avec peu ou pas d'overhead
        • un outillage pratique et facile d'accès
        • le borrow checker

        Il me semble que s'il y a vraiment un truc qui le rend unique c'est le borrow check. Au milieu de différentes choses qui sont plus de l'état de l'art des langages, il me semble être le seul langage à avoir intégré à son typage des informations permettant à la fois une programmation parallèle fiable et une gestion de la mémoire efficace.

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

  • # Typo

    Posté par  . Évalué à 3.

    "La réalisation de ce travail c'est passé" -> "La réalisation de ce travail s'est passé"

Suivre le flux des commentaires

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