- 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
- 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
Aucun commentaire pour le moment.