Rust est un langage de programmation créé à l’initiative de la Fondation Mozilla. Il résulte en réalité du projet personnel de l’employé Graydon Hoare. Son but premier était d’améliorer (voir de remplacer) le moteur de rendu du navigateur Firefox nommé Gecko.
Après des années d’usage et des millions de lignes de code en C/C++, Mozilla a dressé un constat assez amer de son outil Gecko :
● Des soucis de gestion de mémoire (fuites principalement) difficiles à anticiper et à corriger ;
● Des failles de sécurité ;
● Des besoins en programmation concurrente complexe et difficile à mettre en place.
Cherchant un langage plus adapté à ses besoins, la fondation s’est rendue compte qu’aucune solution existante ne couvrait parfaitement ses exigences. C’est ainsi qu’est né Rust.
Développé timidement dans un premier temps, Rust a connu un véritable essor en 2015 avec son passage officiel en version stable. C’est d’ailleurs à peu près à cette période que j’ai commencé à m’y intéresser sérieusement. Peu à peu, la communauté autour de Rust a grandi. Le 18 août 2020, elle est même devenue une fondation indépendante sponsorisée par de nombreuses entreprises.
Le langage Rust a été construit autour de trois objectifs : la performance, la fiabilité et la productivité. On peut dire qu’il en poursuit également un quatrième de façon officieuse : la prise en charge d’un maximum d’architectures de processeurs.
Le nom Rust, qui signifie “rouille” en anglais, peut faire sourire. En effet, l’idée est que le langage soit adapté pour des conditions réelles et ne se veut pas académique (en testant de nouveaux concepts tel que le ferait Haskell, par exemple). Il reprend donc tout ce qui se fait de mieux dans d’autres langages sans réinventer la poudre. Ainsi, Rust n’implémente que des paradigmes qui ont eu le temps de mûrir et d’être éprouvés :
● Paradigme déclaratif : logique sans état qui évite les effets de bord. Il peut s’agir, par exemple, d’une fonction appelée avec les mêmes paramètres d’entrées et qui fournira le même résultat peu importe le contexte ;
● Paradigme fonctionnel : inférence de type, lambda-calcul, récursivité, évaluations paresseuses, curryfication, filtrage par motif, closures, functeurs, monades, map/reduce/filter, etc ;
● Paradigme de généricité (ou plus précisément le Polymorphisme paramétrique) : capacité de manipuler des objets très différents du moment qu’ils implémentent les mêmes traits (un trait est en Rust l’équivalent des interfaces en POO) ;
● Paradigme de concurrence : capacité de faire des tâches en simultanées.
Quel intérêt ?
Il est vrai que je n’ai pas eu mention d’un projet en Rust au sein de Versusmind. Alors, pourquoi m’y intéresser ?
Notre secteur d’activité se réinvente perpétuellement et les “technologies stars” d’aujourd’hui sont probablement les “dinosaures” de demain. Certaines vont indéniablement sortir du lot dans les années qui viennent et pourront potentiellement intéresser les entreprises avec qui je serais amené à travailler. Rust fait partie du peloton de tête. Il suffit de regarder des statistiques sur le sujet :
● L’index TIOBE : https://www.tiobe.com/tiobe-index/
● La popularité dans les recherches Google : https://pypl.github.io/PYPL.html
● Le croisement de la popularité sur Github et stackoverflow : https://redmonk.com/sogrady/2020/07/27/language-rankings-6-20/
D’une part, je pense que c’est en s’engageant dans des expertises nouvelles, via le biais de ses consultants, qu’une entreprise comme Versusmind pourra proposer une palette de compétences plus large à ses clients.
D’autre part, je suis convaincu qu’il est opportun d’élargir son spectre de connaissances et de ne pas se limiter à la stack connue. Apprendre un autre langage informatique et, par conséquent, découvrir de nouvelles approches pour résoudre des problématiques communes permet d’affiner son expertise, sortir de son périmètre de confort et avancer. Ainsi, mon idée derrière la rédaction de cet article n’est bien évidemment pas de vous faire progressivement abandonner quoi que ce soit pour autre chose mais bien d’élargir votre cercle de connaissances.
Enfin, rien n’interdit de faire du Rust dès maintenant à titre professionnel. Comment ? Je m’explique. Que vous codiez majoritairement en PHP, .Net, Node, rien ne vous interdit d’utiliser un autre langage pour des petits projets, par exemple en réalisant des bots Discord, des hook Git, etc. Dans un second temps, cela peut vous permettre de réaliser des scripts de migration ou de petits utilitaires pour croître en productivité. Enfin, même si cela est peu recommandé, Rust peut s’interfacer avec d’autres langages grâce à une API dédiée à l’interopérabilité (https://doc.rust-lang.org/rust-by-example/std_misc/ffi.html).
Les grands atouts de Rust
1 – La performance
L’idée principale de Rust est d’être au plus proche du comportement de la machine tout en donnant le maximum d’abstraction au développeur final. Ce langage se distingue de ses homologues grâce à la performance brute qui sera toujours privilégiée à l’expérience du développeur. Ainsi, certains concepts n’existeront jamais nativement en Rust. Puisqu’il n’y a pas de “ramasse-miettes” (Garbage Collector), la responsabilité de la gestion mémoire est à la charge du développeur. Ceci implique l’impossibilité de s’abstraire de la notion de pointeur et de distinction stricte entre mémoire sur le “tas” et sur la “pile” (heap and stack).
Qui dit performance optimale dit aussi compilation et absence de machine virtuelle. Cela implique qu’un binaire Rust compilé sur une architecture cible ne fonctionnera pas vers une seconde architecture différente (par exemple, de l’ARM ne fonctionnera pas sur de l’AMD 64). Les langages interprétés ( .NET ou Java) sont quant à eux indépendants du système sur lequel les codes sources sont exécutés car ils le sont au sein de leurs propres machines virtuelles.
Les langages compilés permettent eux de faire du très bas niveau, comme la création d’un OS. D’ailleurs, Redox est un OS entièrement écrit en Rust. Linus Torvalds envisage également des contributions au Kernel Linux avec le langage Rust (dans un premier temps uniquement pour des drivers bien cloisonnés du reste du code, mais c’est historique). Microsoft, Apple et Amazon ne sont pas en reste et utilisent déjà Rust dans des projets internes. On peut difficilement miser sur un système performant sans typage fort. Cela implique que si vous choisissez mal vos types, vous n’aurez pas forcément les performances souhaitées.
Enfin, Rust permet de différencier la logique de la compilation et de l’exécution. Ainsi, on se rend compte que beaucoup de choses peuvent être définies statiquement, réglées à la compilation et que la partie réellement dynamique est souvent secondaire. Par exemple, on peut effectuer des calculs réguliers avec la racine de PI alors qu’une valeur avec 10 décimales suffit. Ainsi, pourquoi ne pas créer une constante à la compilation pour éviter de la calculer un nombre défini de fois à l’exécution ?
Une attention toute particulière a été mise sur l’asynchronisme avec les mots clés Async/Await et les notions de Future et de Stream. Dès que l’on souhaite faire de l’asynchrone pour de l’accès au disque, du TCP/UDP, le mieux est de partir sur des libs comme Tokio https://docs.rs/tokio/1.0.1/tokio/ qui vous donneront l’abstraction nécessaire pour gagner en productivité et relecture. Vous pouvez également utiliser, dans des cas plus marginaux des threads : https://blog.guillaume-gomez.fr/Rust/3/5.
2 – La fiabilité
Si vous faites ou avez déjà fait du C/C++, vous savez à quel point l’utilisation de la mémoire peut être une épée de damoclès (fuite mémoire, dépassement de tampon, etc). Rust évite la plupart de ces problèmes grâce à trois principes :
● Les variables sont, par défaut, des constantes : il faut préciser explicitement quand elles sont “mutables” ;
● L’ownership : https://blog.guillaume-gomez.fr/Rust/2/6 ;
● La durée de vie : https://blog.guillaume-gomez.fr/Rust/2/7.
Comme je l’ai déjà mentionné, Rust est typé fortement et ces types “primitifs” sont assez étendus :
● Nombres entiers signés/non signés de 8, 16, 32, 64 bits
● Chaîne de caractère définie et dynamique
Retrouvez d’autres types pimitifs ici : https://doc.rust-lang.org/rust-by-example/primitives.html
On note une distinction importante par rapport à plusieurs autres langages : les types nullables n’existent pas. Pour qu’une variable puisse être vide, il faut l’enroller dans un enum de type Option (https://doc.rust-lang.org/stable/std/option/enum.Option.html) qui prend deux valeurs possibles : Some (qui contient une donnée) ou None (ne contient rien). Le compilateur de Rust incite à déclarer explicitement, à chaque utilisation, les deux scénarios. En somme, définir ce qui se passe si la variable contient une donnée ou non. Ce choix dénote une problématique rencontrée par les développeurs sous C# : https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare
En Rust, il n’existe pas de système d’exception (try/except/finally/throw n’existe pas). Une erreur est un type en soi et elle est tout aussi importante qu’un comportement “attendu”. L’intérêt est de responsabiliser le développeur dès le début du projet sur la notion d’erreur et quel comportement adopter dans ce cas. La nature humaine nous pousse assez naturellement à n’envisager que les scénarios dans lesquels tout se passe bien et à éviter ou reporter ceux qui ne le sont pas. Par conséquent, certains bugs n’apparaissent qu’en production et le coût de correction (et de stress) qu’ils occasionnent est rarement favorable.
Du côté Rust, traiter une erreur est tout aussi important qu’un succès et tous les scénarios doivent être anticipés pour passer la phase de compilation. Pour la plupart des développeurs, il peut être frustrant d’écrire beaucoup de code pour une “mini” fonctionnalité mais, au final, c’est bien plus reposant car on évite une paire d’oublis. Ainsi, on focalise notre matière grise sur ce qui est vraiment important.
3 – La productivité
Souvent, les avantages se recoupent : écrire du code fortement typé c’est éviter de la perte de temps sur du debuggage et, par conséquent, s’allouer un temps non négligeable sur de la fonctionnalité. Un développeur qui travaille sur un projet sérieux va forcément passer par la case “suppression de code” et se rendre compte que cela peut être bien plus difficile que d’en ajouter.
Dans ce cas de figure, deux politiques s’opposent : soit on ne supprime rien (c’est la solution souvent choisie malgré de la bonne volonté), soit on déprécie et on supprime dans un second temps. Dans ce second scénario, la recette a une importance cruciale. Néanmoins, lors de la mise en production, on croise les doigts tout en gardant les yeux rivés sur les logs.
Rust s’oriente sur un autre axe : la vérification à la compilation du code mort. Si une constante, une fonction ou une structure de donnée n’est utilisée nul part, Rust considère que c’est du code mort et le signale avec un gros warning. Il est fort plaisant de refactorer un projet que vous n’avez pas développé (ou que vous avez abandonné depuis longtemps) et ne rien oublier à la suppression dès le premier commit.
La communauté de Rust a aussi fait le choix de doter le langage d’outils fournis par défaut (battery included). L’un d’entre eux est indispensable car c’est l’utilitaire qui manage tout et qui évolue à la même vitesse que le langage. Il s’agit de Cargo, qui fait office de :
● Gestionnaire de paquets (qui d’ailleurs est couplé avec le dépôt “crate.io”). Il brille par sa qualité et sa simplicité. On édite un fichier Cargo.toml (pour ceux qui ne connaissent pas TOML, je vous invite à le découvrir car il présente de nombreux avantages par rapport à YAML, JSON et cie pour de la conf) et la magie opère.
● Librairie de tests : un simple “Cargo test” va lancer les tests unitaires et fonctionnels de votre applicatif et tout ceci sans librairie complémentaire.
● Tests de montée en charge avec “Cargo bench”
● Générateur de documentation : “Cargo doc” va sonder l’ensemble de votre API et créer une documentation correspondante (hiérarchie de docs HTML).
Cargo peut être étendu et, par conséquent, apporter des outils complémentaires.
Les inconvénients
Il serait malhonnête de ma part de vous présenter uniquement les aspects positifs de Rust. Voici une liste non-exhaustive de ses inconvénients :
● Rust nécessite une courbe d’apprentissage élevée et les débuts peuvent être laborieux. Ce n’est pas un outil magique. Il permet d’éviter les pièges mais reste dans du bas niveau.
● Les 3 objectifs que se fixe Rust ne sont pas pleinement atteints : par exemple, certaines instructions ARM ne sont pas encore supportées par le compilateur ce qui fait que le binaire produit peut être lent.
● Rust fait beaucoup de vérifications à la compilation et ceci a un coût : les temps de compilation peuvent être frustrants surtout sur de gros projets avec de la refacto. Même s’il y a eu beaucoup d’améliorations à ce sujet ces derniers temps, cela peut rester insuffisant.
● Rust n’est sans doute pas fait pour prototyper, créer du script rapide et jetable etc. Comme beaucoup d’outils, si il n’est pas utilisé à bon escient, il perd de son intérêt. Pour ma part, je jongle souvent entre Python (faire des trucs vite) et Rust (faire du durable).
● Plusieurs librairies sont des bindings de code en C, ce qui sous-entend que certaines parties (certes cloisonnées) sont unsafe. Cela peut donc potentiellement avoir des conséquences sur les trois axes fixés par Rust.
● Le compilateur n’est pas vérifié formellement ce qui sous-entend qu’il peut y avoir des bugs dans son interprétation. Des projets sont en cours dans ce sens, en particulier dans la vérification des parties “unsafe”.
● Le langage n’est pas normalisé (à la différence de C et C++ qui ont des normes ANSI et ISO) et il n’y a qu’un seul réel compilateur.
● Certains concepts fonctionnels sont encore effleurés : les notions d’évaluations paresseuses ne sont pas encore stabilisées, la notion de monade n’existe pas vraiment (mais les plus utiles comme Maybe sont implémentés).
Quelques conseils et exemples de projets réalisés en Rust
Pour trouver des projets (lib, frameworks, softs) devs en Rust, je vous invite à passer par https://github.com/rust-unofficial/awesome-rustfmt. Vous trouverez aussi votre bonheur sur https://wiki.mozilla.org/Areweyet. Ce tableau liste tous les projets “Are we … yet” et donne donc une liste des projets les plus courants pour un thème précis.
Par exemple, https://www.arewewebyet.org/ va lister tous les frameworks cool pour faire du web en Rust : Actix et Rocket sont actuellement les frameworks les plus populaires et matures. Yew est un framework pour faire du Webassembly et j’invite à le tester pour faire du front sans une once de javascript et permettant de faire de l’isomorphisme si on utilise Rust également côté serveur.
Ripgrep : https://github.com/BurntSushi/ripgrep si vous aimez grep mais que vous le trouvez un peu verbeux et surtout lent, je vous invite à installer ripgrep (aussi bien sur linux, windows ou mac). C’est le genre de petit soft dont je ne peux plus me passer.
La lib Nom : https://github.com/Geal/nom va vous permettre de parser du texte et/ou du contenu binaire. La vraie plus value vient de la partie combinatoire. Au lieu de s’appuyer sur des expressions régulières où chaque modifs peuvent avoir un effet big bang, le parser Nom se décompose en petits algos unitaires totalement indépendants les uns des autres.
La communauté Rust
Pour bien démarrer
● Programming Rust que j’ai dévoré https://www.abebooks.fr/products/isbn/9781491927281?cm_sp=rec-_-pd_hw_o_1-_-plp&reftag=pd_hw_o_1 et qui a désormais sa traduction https://www.amazon.fr/Programmer-avec-Rust-programmation-collection/dp/241204659X
● The Rust Programming Langage de Carols Nichols : https://www.abebooks.fr/Rust-Programming-Language-Manga-Guide-Carol/30645072052/bd?cm_mmc=ggl-_-FR_Shopp_TradeStandard-_-naa-_-naa&gclid=Cj0KCQiA88X_BRDUARIsACVMYD8o_qMPKplUEOMXeC0N5AheAzPcWqmc6pWv15xvdrOoLKogq-SMVeEaAicOEALw_wcB