10 points par GN⁺ 2025-08-28 | 3 commentaires | Partager sur WhatsApp
  • Rust améliore la productivité et la maintenabilité grâce à de solides garanties de sécurité, qui permettent de refactoriser en toute confiance même dans de grandes bases de code
  • Le compilateur détecte à l’avance les bugs liés à l’ordonnancement asynchrone et renforce la stabilité en empêchant les comportements indéfinis
  • Des langages comme TypeScript découvrent souvent les bugs asynchrones en production à cause d’un système de types plus permissif
  • Le système de types de Rust indique clairement l’impact des changements de code, ce qui renforce la confiance et l’envie d’expérimenter dans les projets complexes
  • Contrairement à Rust, Zig peut laisser passer des bugs dus à des fautes de frappe à cause de vérifications plus souples dans la gestion des erreurs, ce qui réduit sa fiabilité

Résumé et contexte

  • Le backend de Lubeno est écrit à 100 % en Rust, et sa base de code a atteint une taille où il devient difficile d’en garder une vue d’ensemble mentale
    • Dans les grands projets, il est généralement difficile de vérifier les effets de bord des modifications, ce qui entraîne une baisse de productivité
  • Les garanties de sécurité de Rust indiquent clairement l’impact des modifications de code, ce qui réduit la peur du refactoring
    • Cela contribue à améliorer la maintenabilité et la productivité sur le long terme
  • Cet article part d’un cas où le compilateur Rust a détecté un bug asynchrone pour explorer les avantages de Rust en matière de productivité

Exemple des garanties de sécurité de Rust

  • Situation : une structure est encapsulée dans un mutex pour permettre un accès concurrent, puis une opération asynchrone est exécutée après acquisition du verrou
    let lock = mutex.lock();  
    db.insert_commit(commit).await;  
    
  • Découverte du problème : rust-analyzer n’affichait pas d’erreur, mais une erreur de compilation est apparue dans le fichier de définition des routes
    .route("/api/git/post-receive", post(git::post_receive))  
                                         ^^^^^^^^^^^^^^^^^  
    error: future cannot be sent between threads safely  
    
  • Analyse de la cause :
    • Le framework web crée une tâche asynchrone pour chaque connexion HTTP, et l’ordonnanceur de tâches déplace ces tâches entre les threads
    • Le mutex doit être libéré sur le même thread, et un changement de thread au point .await peut provoquer un comportement indéfini
    • Le compilateur Rust suit la durée de vie du verrou et détecte la possibilité qu’il soit libéré depuis un autre thread
  • Solution : libérer le verrou avant .await
  • Portée : Rust empêche à la compilation des bugs asynchrones difficiles à reproduire en environnement de développement

Cas comparatif avec TypeScript

  • Situation : apparition d’un bug de redirection asynchrone dans du code TypeScript
    if (redirect) {  
        window.location.href = redirect;  
    }  
    let content = await response.json();  
    if (content.onboardingDone) {  
        window.location.href = "/dashboard";  
    } else {  
        window.location.href = "/onboarding";  
    }  
    
  • Cause du problème :
    • window.location.href ne redirige pas immédiatement mais planifie la redirection, et l’exécution du code se poursuit
    • Une condition de concurrence provoque ainsi une redirection non souhaitée
  • Solution : ajouter return dans le bloc if
    if (redirect) {  
        window.location.href = redirect;  
        return;  
    }  
    
  • Limite : TypeScript ne dispose ni de suivi de durée de vie ni de règles d’emprunt, et ne peut donc pas détecter ce type de bug à la compilation
    • Le problème est découvert en production, et le débogage prend beaucoup de temps

Les avantages de Rust pour le refactoring

  • En développement web, Python, Ruby et JavaScript/Node.js offrent une forte productivité initiale, mais quand la base de code grandit, leur couplage lâche rend les changements plus difficiles
    • Des erreurs inattendues apparaissent après modification, ce qui réduit la volonté de retoucher le code
  • Avec Rust, le système de types indique clairement l’impact des changements, ce qui réduit la peur du refactoring
    • Exemple : un avertissement du type « ce changement peut affecter d’autres parties » permet de prévenir les problèmes en amont
  • Même lorsque la base de code grossit, la productivité augmente, car on peut réutiliser le code existant et conserver sa stabilité lors des modifications

Comparaison avec les tests

  • Les tests sont utiles pour éviter les régressions lors du refactoring, mais comme ils ne sont pas imposés par le compilateur, ils peuvent être omis
    • Écrire des tests impose une charge mentale importante : il faut décider du niveau d’abstraction, du comportement par rapport aux détails d’implémentation, et de ce qu’il faut couvrir pour prévenir les erreurs
  • Rust permet au compilateur de bloquer à l’avance les erreurs courantes, ce qui réduit le poids des décisions liées aux tests
    • Les propriétés que le système de types ne peut pas vérifier sont alors complétées par des tests

Comparaison avec Zig

  • Zig est un langage de programmation système proche de Rust, mais plus permissif dans sa gestion des erreurs
    • Exemple de code de gestion d’erreur :
      const FileError = error{ AccessDenied };  
      fn doSomethingThatFails() FileError!void {  
          return FileError.AccessDenied;  
      }  
      pub fn main() !void {  
          doSomethingThatFails() catch |err| {  
              if (err == error.AccessDenid) {  
                  std.debug.print("Access was denied!\n", .{});  
              }  
          };  
      }  
      
    • À cause de la faute de frappe AccessDenid, un bug apparaît, mais le compilateur Zig traite cela comme un nombre et la compilation réussit quand même
  • Avec une instruction switch, la faute de frappe est détectée, mais elle est ignorée dans une instruction if, ce qui pose un problème de fiabilité
  • Rust évite ce type de faille de conception et vérifie strictement les fautes de frappe comme les erreurs logiques

Enseignements

  • Rust améliore la productivité et la stabilité des grands projets grâce à ses garanties de sécurité et à son système de types strict
  • Même des problèmes complexes comme les bugs asynchrones peuvent être détectés à la compilation, ce qui réduit les coûts de maintenance
  • Les exemples de TypeScript et Zig montrent les risques de vérifications trop souples et soulignent la valeur du compilateur strict de Rust
  • Rust s’impose aussi dans le développement web comme un outil puissant non seulement pour la productivité initiale, mais aussi pour la gestion à long terme de la base de code

3 commentaires

 
taptaps 2025-08-30

Chaque fois que je vois des gens dire que c’est le meilleur, que c’est un langage puissant !!
ce que je me dis, c’est qu’il y a en fait moins de développeurs Rust qu’on ne le pense, alors ils essaient peut-être de nous appâter pour qu’on fasse du Rust ??

 
colus001 2025-08-29

Les articles recommandés sur Rust, c’est un peu comme le « Try! Try! » de Sikgaek ? Je suis le seul à le ressentir ?

 
GN⁺ 2025-08-28
Commentaires Hacker News
  • L’an dernier, j’ai porté un pilote réseau virtio-host écrit en Rust. Il fallait changer le backend, le mécanisme d’interruptions, et passer d’une bibliothèque à un processus autonome. C’était un programme complexe qui gérait le mapping mémoire, les interruptions de VM, les sockets réseau et le multithreading. J’avais très peu d’expérience avec Rust, et peu aussi avec virtio, mais au moment où le projet compilait, il fonctionnait parfaitement. À part un bug lié à Drop, qui a été facile à corriger. Je pense que les bibliothèques Rust m’ont beaucoup aidé, car elles sont conçues de façon à éviter les mauvais usages

    • Je développe en Rust depuis longtemps, et la plupart du temps, si ça compile, le code fonctionne bien. Il arrive parfois d’avoir des deadlocks ou des bugs d’ordre d’exécution, mais en gros, quand la compilation réussit, cela signifie qu’une grande partie du projet fonctionne correctement
  • Je pense que Rust est excellent. En revanche, je ne suis pas d’accord avec l’idée selon laquelle le bug d’assignation de href serait la faute de TypeScript. Le cœur du problème, c’est que définir href ne provoque pas immédiatement la navigation vers la page, cela est traité plus tard. Le même problème pourrait exister en Rust. Si Rust avait une fonction set_href dont l’effet était traité plus tard, un code comme celui-ci serait possible :

    set_href('/foo')

    if (some_condition) { set_href('/bar') }

    Je pense simplement que Rust ne concevrait pas cela ainsi. Déclencher une action dans un setter, ce n’est pas une bonne conception de bibliothèque, et le fait qu’une navigation ne se produise pas au moment exact où l’on assigne href est étrange. La bibliothèque standard de Rust n’aurait probablement pas une implémentation aussi absurde. Ce n’est pas une question Rust vs TypeScript, mais une différence entre la bibliothèque standard de Rust et les API de la Web Platform. En revanche, je suis d’accord sur le fait que Rust ne proposerait probablement pas cette expérience utilisateur

    • Pour être plus rigoureux, concevoir un setter qui déclenche immédiatement une action n’est pas souhaitable. Il vaudrait mieux un nom comme navigate_to(href). Dans l’environnement navigateur, tout le code JS fonctionne via des callbacks et est piloté par la boucle d’événements, donc le fait que cela ne s’exécute pas immédiatement est aussi une situation naturelle

    • L’exemple Rust est intéressant, mais l’exemple TypeScript ne permet pas, à lui seul, de dire si TS convient aux grands projets. En Ruby, je dois souvent attraper des bugs à l’exécution, ce qui me met mal à l’aise, mais au final cela fonctionne bien avant le commit, et c’est agréable et facile à lire et à modifier. Le problème de navigation vient de JavaScript, et TS l’hérite. Il existe parce que JS permet de modifier librement les propriétés. Mais comme la page ne disparaît pas immédiatement non plus, ce comportement reste raisonnable une fois qu’on le connaît

    • Techniquement, en Rust, set_href pourrait donner un indice plus clair sur sa sémantique selon qu’il retourne () ou !. Mais dans le cas d’une redirection conditionnelle, il resterait difficile d’empêcher totalement un mauvais usage

    • Ce que je voulais dire, c’est qu’avec le modèle de propriété de Rust, on pourrait concevoir une API où l’appel à window.set_href('/foo') prend possession de window, ce qui rendrait impossible un second appel. TypeScript n’a tout simplement pas de concept de suivi des durées de vie, donc cela n’est pas possible. Et comme l’API JS existe déjà, il n’y a aucun moyen d’introduire un système de propriété côté TypeScript. Je voulais montrer un exemple où plusieurs fonctionnalités de Rust se combinent pour fournir des garanties plus fortes

    • On dirait que ton argument pour dire que Rust est meilleur revient finalement à « les programmeurs Rust sont meilleurs ». J’ai du mal à croire que des programmeurs Rust feraient ce type de raisonnement circulaire

  • Le code après une assignation continue à s’exécuter tant qu’il n’y a pas de retour anticipé explicite. Franchement, je ne vois pas pourquoi quelqu’un penserait qu’assigner une valeur devrait arrêter l’exécution du script. Il manque peut-être du contexte dans l’exemple TS, mais c’est un exemple étrange à présenter comme une « data race »

    • Assigner une valeur à window.location.href a pour effet de bord que le navigateur navigue vers ce lien. C’est un comportement surprenant, et comme une simple assignation charge une nouvelle page, cela ressemble un peu à execve, donc il n’est pas absurde de penser que l’exécution JS va s’arrêter immédiatement. Il ne faut évidemment pas dépendre de cette hypothèse en programmant, mais je pense que cela peut prêter à confusion parce que le comportement lui-même est inhabituel

    • Qu’on y pense ou non, c’est le genre de bug qui devient évident à corriger dès que quelqu’un vous le signale. Le point essentiel que l’auteur voulait faire, c’est que ce genre de bug, que TS ne peut pas attraper, peut être réellement difficile à identifier et coûter beaucoup de temps

    • exit(), execve(), etc. arrêtent effectivement l’exécution immédiatement, donc on peut imaginer qu’une redirection fonctionne pareil

    • C’est étrange de reprocher à quelqu’un de simplement partager son expérience

    • Cette assignation a un effet de bord majeur, puisqu’elle fait quitter la page. Il n’est donc pas déraisonnable de la voir comme une action asynchrone qui agit immédiatement. Moi aussi j’ai déjà fait cette hypothèse

  • C’est simplement l’histoire d’un développeur qui a découvert que les systèmes de types statiques sont utiles. Je trouve toujours ce genre de billet amusant

    • Ce que je voulais montrer sur mon blog, c’est que le suivi des durées de vie et le système de traits de Rust peuvent détecter des problèmes bien plus complexes qu’une simple incompatibilité de types. TypeScript est aussi un langage à typage statique, mais il ne fournit pas des garanties comparables à Rust
  • La plupart des avantages ne viennent-ils pas simplement du fait d’utiliser un langage compilé à typage statique ? Java, Go et C++ offrent aussi cela. TypeScript a son propre côté astucieux : il compile en JS et hérite aussi des problèmes de JS, mais il reste utilisable. Rust a un système de types plus strict, ce qui permet des vérifications supplémentaires à la compilation, mais en contrepartie il me semble plus difficile à apprendre et à lire

    • Je suis d’accord dans une certaine mesure, mais Rust ajoute d’autres dimensions à son système de types : propriété, accès partagé/exclusif, sûreté des threads, sum types, etc. Grâce au système de propriété/emprunt, il devient clair si le passage d’un argument donne une vue temporaire ou transfère entièrement la valeur. C’est un gros avantage dans les grands programmes ou lorsqu’on utilise des bibliothèques externes. Par exemple, en Go, le type slice ne dit pas clairement à l’exécution quelles opérations sont autorisées, et il est difficile de l’emprunter en lecture seule. Rust permet aussi de garantir la sûreté des threads au niveau du système de types, et peut donc empêcher à la compilation des data races que d’autres langages ne détecteraient qu’avec difficulté à l’exécution

    • Mettre tous les langages à typage statique dans le même sac, c’est ne pas encore avoir vraiment ressenti la puissance des union types et du pattern matching. Une fois qu’on s’y habitue, les langages statiques plus traditionnels deviennent difficiles à supporter

    • Un autre grand avantage, ce sont les traits et les impl traits. Rust permet d’ajouter des traits à un type plus tard, un peu comme les Extension Methods en C#. Dans la plupart des langages, tout est figé au moment où le type est défini dans une bibliothèque, tandis qu’en Rust on peut continuer à enrichir progressivement même des types simples. Ce caractère late-bound injecte une certaine dynamique dans le système de types. En poussant un peu le trait, on pourrait dire que le vrai superpouvoir de Rust n’est pas le borrow checker, mais l’ouverture et la flexibilité de son système de types. Il n’est pas nécessaire de tout concevoir dès le départ, on peut étendre progressivement

    • Tous les langages à typage statique ne produisent pas les mêmes effets. Java finit par dépendre de Object et des cast à l’exécution. Go n’a pas d’enum. C++ a ajouté un concept de variant, mais pour l’utiliser de manière sûre il faut encore des traitements manuels de type try/except, ce qui reste structurellement peu pratique

    • On dit souvent que Rust est difficile à apprendre, mais en réalité, une fois qu’on l’a vraiment assimilé, ce n’est pas difficile. Au début, en programmation, il est assez important de pouvoir bricoler rapidement quelque chose qui marche vaguement, et Rust n’est pas du tout accueillant pour cette manière de faire. Je ne le recommanderais pas comme langage d’initiation, mais il n’est pas difficile à lire

  • La forte sûreté de Rust donne beaucoup plus de confiance lorsqu’on touche à une base de code. Avec cette confiance, on n’a pas peur de refactorer les parties critiques, et au final cela améliore fortement la productivité et la maintenabilité. Mais c’est précisément à cela que servent les tests. Sans tests, un compilateur strict aide énormément, mais avec de bons tests, on peut refactorer avec confiance dans n’importe quel langage

    • Il vaut mieux que le compilateur prouve statiquement ce qu’il peut prouver. Les tests sont optimaux pour les situations où les garanties statiques sont difficiles à obtenir. L’idéal absolu serait la vérification formelle, même si c’est très difficile en pratique ; ce n’est donc pas une généralité applicable partout, mais le principe reste juste

    • De bons tests et un système de types bien exploité sont tous deux efficaces pour éliminer les bugs. Mais écrire des tests me fait parfois penser au xkcd « Standards » : on corrige un standard en en créant un nouveau, comme on corrige des bugs en écrivant encore plus de code. Cela dit, la maintenance du système de types est assurée par les concepteurs du langage, donc il n’est pas nécessaire de la gérer projet par projet

    • Si l’on doit refactorer les tests à chaque fois qu’on refactore le code, cela double la charge de travail

  • Je pense que les systèmes de types de Rust ou de F# brillent particulièrement lors des refactorings. L’expression « refactoring sans peur » leur va parfaitement

    • Le revers de la médaille, c’est que Rust ne tolère pas le code inachevé, donc pendant un refactoring il est impossible d’avoir un « code partiellement fonctionnel ». Il faut soit aller au bout, soit ne rien faire du tout, ce qui peut être peu pratique pour écrire du code expérimental. Mais cette rigueur est aussi l’une des raisons qui mènent au final à du bon code
  • L’exemple Zig est choquant. Cela paraît tellement instable que j’ai du mal à comprendre comment on peut juger ce design acceptable

    • Je pense que c’est probablement un bug. Mais dans un langage centré sur les créateurs comme Zig, pour qu’un bug soit corrigé, il est important que ses créateurs reconnaissent aussi que c’en est un. S’ils considèrent cela comme intentionnel, ils peuvent continuer dans cette direction

    • Tous les langages ont un peu de design instable. Par exemple, en Go ou en Zig, il faut toujours appeler explicitement mutex.unlock(), et rien n’est libéré automatiquement quand on sort de la portée. À l’inverse, avec l’opérateur as de Rust, il est très facile d’effectuer des conversions entre types numériques, et j’ai déjà passé une journée entière à traquer un bug à cause de cela

    • Je n’avais pas vu cette erreur au départ, et je ne l’ai remarquée qu’en lisant ce commentaire

    • Je me demande si un linter ne pourrait pas avertir lorsqu’on référence une erreur inexistante dans le système, et recommander d’utiliser switch

    • Je pensais que les ensembles d’erreurs étaient générés à partir des signatures de fonctions. C’est assez particulier

  • J’apprécie qu’un système de types statique puissant et sain fournisse autant de fonctionnalités. J’ai moi aussi eu l’expérience de grands refactorings faciles dans une base de code Haskell (1 million de SLOC). Même sans fonctionnalités très avancées, le système de types suffisait largement

  • Rust a bien détecté qu’un verrou était maintenu à travers une frontière await, mais pour savoir si le relâcher avant le await est réellement sûr, il faut davantage de contexte. J’ai l’impression que le verrou doit être conservé jusqu’à la création du commit de transaction, et le relâcher avant le await pourrait introduire des problèmes de concurrence. Je ne connais pas très bien l’async de Rust, mais après le commit, ne faudrait-il pas bloquer avec join ou select ?

    • S’il faut conserver un verrou pendant un await, il suffit d’utiliser un mutex compatible async. Les crates futures ou tokio implémentent ce type de verrou. On les utilise surtout quand il faut garder le verrou longtemps ou le conserver entre plusieurs await. Ils sont plus coûteux que les verrous ordinaires

    • S’il est nécessaire de conserver le verrou à travers une frontière await, on peut utiliser le mutex async-aware de Tokio. Voir la documentation de tokio/sync/struct.Mutex