Rust par la pratique

Introduction

Dans ce second article consacré à Rust (le premier est disponible ici), nous allons tenter de nous familiariser avec ce langage via un atelier pratique. L'objectif est de faire un tour rapide de certains concepts en essayant de monter crescendo en expérience.

Pour chacun des exemples de code, il vous suffit de les copier/coller dans https://play.rust-lang.org et d'appuyer sur Run pour compiler et exécuter.

D'ailleurs, le résultat va distinguer 2 zones : la sortie du compilateur et la sortie de l'exécution.

Pour ceux qui ont envie d'utiliser Rust directement sur leur poste, j'ai créé un dépôt git avec l'ensemble des exemples que vous pouvez retrouver via ce lien : https://github.com/mothsART/tutoRust. Soit git clone https://github.com/mothsART/tutoRust.

J'ai alimenté un README pour vous aiguiller sur son utilisation.

Pour chacun des exemples cités, je mets l'instruction à lancer pour générer le code (en commentaire).

Exemple : make variable_1

Premiers pas sur les variables

Avant toute chose, pour que votre code fonctionne sur https://play.rust-lang.org, il va falloir l'enroller dans une fonction main. La fonction main est comme en C, c'est la fonction principale et le point d'entrée. Sur un exécutable Rust elle est obligatoire (par contre, une librairie n'en contiendra pas) .

            // make variables_un

            fn main() {

                        let x; // On peut déclarer une variable "x" sans rien lui assigner (un peu comme en C).

                        x = 42; // On attribue à notre variable une valeur après coup.

            }

Mais au final, on fera bien plus souvent :

            // make variables_deux

            fn main() {

                        let x = 42;

            }

Dans notre cas, le compilateur a attribué à la variable x automatiquement un type (i32 = entier signé de 32 bits). C'est le principe de l'inférence de type.

Néanmoins, imaginons que dans notre code, nous savons pertinemment que notre variable n'aura pas besoin d'être convertie dans un format standard.

Par conséquent, il est inutile d'avoir un entier signé et encore moins occupant une taille aussi grande. On peut donc la forcer à être la plus petite possible soit u8 (entier non signé sur 8 bits) et donc occuper 4 fois moins de place en mémoire :

            // make variables_trois

            fn main() {

                        let x:u8 = 42;

            }

ou

            // make variables_quatre

            fn main() {

                        let x = 42u8; // Je suis moins fan mais les goûts et les couleurs...

            }

 

Pour avoir une bonne visibilité sur les types primitifs (à apprendre par coeur), je vous invites à lire : https://doc.rust-lang.org/std/index.html#primitives

En Rust, les variables sont des constantes par défaut donc :

            // make variables_cinq

            fn main() {

                        let x = 42;

                        x = 50; // Renvoie une erreur de compilation.

            }

Si on veut que cette variable soit mutable, on doit le préciser explicitement :

            // make variables_six

            fn main() {

                        let mut x = 42; // On initialise x à la valeur 42.

                        // ...

                        x = 50; // On change la valeur de x pour qu’elle vaille désormais 50.

            }

"Struct" et "enum"

Les données primitives c'est bien, mais on est vite limité. En Rust, pas de programmation orientée objet. En effet, on sépare 2 principes bien distincts : les données et les fonctions. En utilisant Rust, vous pouvez donc très bien architecturer votre code pour avoir dans des fichiers différents :

  • • Les structures de données : on va dire équivalentes à des classes sans méthodes.
  • • Les données en elle-même : si elles ne sont pas dynamiques (flux d'une api, issues d'une base de données etc) bien évidemment.
  • • Les algorithmes : la manière dont sont exploitées ces données.
  • Pour construire des données complexes, on a à notre disposition principalement les enum et les struct.

Les "struct"

C'est la donnée dans sa forme la plus pure. Elle a un gabarit qui va définir la topologie d'une donnée. Les données créées à partir des struct (instanciées dans le jargon POO) suivront cette nomenclature.

Exemple : https://github.com/mothsART/tutoRust/blob/main/src/bin/struct.rs (make struct)

Dans notre exemple, on a utilisé des primitives pour les propriétés de notre structure mais il est tout à fait possible de le composer à partir d'autres structures voir même de lui-même. Ceci sous-entend que l’on peut utiliser des structures récursives du genre :

https://github.com/mothsART/tutoRust/blob/main/src/bin/struct_recursif.rs (make struct_recursif)

 

Pour des raisons de simplification, un dossier ne peut avoir qu'un sous-dossier mais il vous serait tout à fait possible de prendre en compte une liste. Une fois avoir abordé la section "tableau", vous serez dans la capacité de faire cela. (à bon entendeur)

 

En attendant, j'ai introduit 2 notions : Option et Box.

Rust étant centré sur la mémoire, il ne compile rien sans connaître à l'avance la taille d'une donnée. Le compilateur ne pouvant définir à l'avance la profondeur sur des données récursives, on se retrouve coincé. C'est à ce moment que Box intervient.

Sans rentrer dans les détails. Box est un pointeur sur le tas permettant d'allouer un espace mémoire dynamique. Option est un enum prenant 2 valeurs : soit Some soit None. En Rust, aucune valeur ne peut être nullable. Il faut donc encapsuler une valeur avec un Option pour permettre cela.

Dans notre cas, sans Option, notre hiérarchie a une profondeur infinie : ce qui n'est pas possible dans un contexte fini. Rust ne compile donc pas. En ajoutant un Option, on arrête donc la hiérarchie au dernier sous-dossier en lui passant None.

Les enum

Comme mentionné pour Option (qui est natif), on peut utiliser des enum.

À la différence de la plupart des langages, les enum vont être capables de lister des valeurs simples dans l'ordre (une liste d'entiers par exemple) mais également d'utiliser des structs (et inversement), d'autres enum ou encore d'utiliser de la récursivité. C'est à ce moment que vous commencez à vous rendre compte que Rust, malgré le fait qu'il n'utilise pas de POO, n'a vraiment pas à pâlir.

Un exemple vaut mieux que mille discours. Imaginez que vous développiez le module de paiement de votre banque. Vous avez plusieurs modes de paiement à votre disposition avec chacun leurs caractéristiques.

https://github.com/mothsART/tutoRust/blob/main/src/bin/enum.rs (make enum)

 

Je vous laisse apprécier l'élégance produite : dans d'autres langages, on aurait eu le droit à plusieurs if qui parasitent la compréhension (des instructions normalement réservées pour des algorithmes se retrouvent à structurer de la donnée dans plein de langages et c’est pas vraiment adéquat) et qui posent pas mal de pièges à l’exécution.

Ici, tout est vérifié côté compilation. On ne fait que proposer un gabarit et alimenter notre programme avec des données qui respectent ce gabarit. Un constat à mi-chemin sur Rust : bien structurer ces données fait parti de l'ADN du langage et si on peut passer pas mal de temps sur cette partie, c'est pour notre bien.

Pour la petite subtilité syntaxique : on accède à des propriétés d'une donnée via un “.” (ex: MaStruct.ma_propriete) et “::” quand on accède à des espaces de noms. Les enums utilisent des espaces de noms donc MonEnum::maValeur

L'ownership et la durée de vie

En Rust, une variable ne vit que dans un espace donné.

Par exemple, une variable déclarée dans une fonction ne vivra qu'au sein de cette fonction. En dehors, elle disparaîtra. Ce fonctionnement présente de vrais avantages : on limite les effets de bord et on rend chaque fonction totalement isolée du reste du programme. L'inconvénient est de transiter une donnée d'une fonction à une autre.

Vous pouvez bien évidemment cloner cette donnée mais cela signifie que l’on va la dupliquer en mémoire. Du coup, on utilise une référence (ou pointeur dans la terminologie C/C++). Tout comme l'on va se communiquer une adresse web pour parler d'un article qui nous tient à cœur, on communiquera la référence d'une donnée plutôt que la donnée elle-même.

Exemple : https://github.com/mothsART/tutoRust/blob/main/src/bin/ownership.rs (make ownership)

 

Dans notre exemple, on commence à rentrer dans le vif du sujet (enfin un peu d'algo). On déclare une struct nommée Adresse, on l'alimente avec une nouvelle donnée et on souhaite vérifier qu’elle est valide (avant enregistrement par exemple).

Vous pouvez remarquer qu'on utilise 2 formats de représentations d'une chaîne de caractères : &'static str et String. Pour faire simple, &'static str est une représentation dans son plus simple appareil ! Une adresse et une longueur (attention, longueur en mémoire ne veut pas dire longueur de la chaîne).

Elle est tout à fait suffisante dans plein de cas mais ici nous souhaitons connaître la taille de notre chaîne et ce genre d'opération n'est pas possible.

Pour tout ce qui est manipulation, vous utiliserez String (et il est aisé de passer d’un format à l'autre). Afin d'effectuer le test de vérification, on a créé une fonction qui renvoie true ou false.

Jusque là, rien de compliqué. Seulement voilà : comme dit précédemment, on déplace une donnée d'un emplacement à un autre et si on ne veut pas bêtement copier cette donnée, on est obligé de le passer par référence. C'est là qu'intervient le & qui précise que l'on ne désire pas communiquer la valeur en question mais bien un pointeur sur celle-ci.

Je vous invite à tester sans le &, le compilateur vous dira que la structure Adresse n'implémente pas le trait Copy.

Si vous remplacez #[derive(Debug)] par #[derive(Debug, Copy)], cela résoudra votre soucis mais d'une mauvaise manières : en recopiant votre donnée dans la fonction.

Beaucoup de débutants en Rust contournent ce type de soucis ainsi. Je vous le déconseille fortement car vous ne progresserez pas (sans compter les problèmes de performances engendrés). Cloner a bien une utilité mais dans un cadre restreint : vous avez 2 objets (on parle aussi d'objet en Rust bien que l'on ne soit pas dans un contexte POO) très proches dont l'un est créé à partir de l'autre. Plutôt que de recopier bêtement chaque propriété, on clone l'objet et on ne modifie que ce qui diffère.

Les tableaux

Bien évidemment, pour interagir avec des données du monde réel, il va falloir utiliser des tableaux. Rust utilise 3 notions :

1. Les array

Ce sont des tableaux statiques avec une taille en mémoire finie.On les privilégie pour le code en dur.

Exemple :

            // make array

 

            fn main() {

                        let _array: [i32; 3] = [1, 3, 8];

                        // Cette boucle va afficher 1, 3, 8

                        for x in &_array {

                                   print!("{} ", x);

                        }

            }

array ne peut pas être modifié, uniquement lu et on est obligé d'ajouter un & pour pouvoir itérer dessus. Si vous mettez la variable _array en mut, on pourra changer les valeurs mais pas sa taille. Son utilisation reste donc assez limitée.

2. Les vec

Ce sont des tableaux dynamiques donc avec une taille non déterminée à l'avance :

            // make vec

 

            fn main() {

                        let mut v: Vec<u8> = Vec::new(); // On crée un nouveau Vec rempli d'entiers non signés de 8 bits.

                        v.push(1);

                        v.push(3);

                        v.push(8);

                        println!("{:?}", v);

            }

C'est le tableau le plus courant et le plus pratique à utiliser. Sa principale limitation est qu'il représente des données stockées dans un emplacement mémoire. Si on venait à communiquer cette donnée (la passer d'une fonction à une autre), ça nécessiterait de la copier en mémoire. Pour éviter ça, on utilise une troisième représentation : les slices.

3. Les slices

Ils vont représenter un morceau de tableau via un pointeur et une taille. Si vous créez un slice à partir d'un Vec, vous ne créerez aucune donnée en mémoire supplémentaire vu que cette slice va seulement contenir l'adresse de ce morceau de Vec.

            // make slice

 

            fn main() {

                        let v = vec![1, 2, 3, 4, 5];

                        let v2 = &v[2..4];

                        println!("v2 = {:?}", v2); // on affiche : 2 = [3, 4]

    }

Le pattern matching

Comme dans chaque langage, les if/else sont de la partie. Cependant, en Rust j'aurais tendance à dire de les oublier. En effet, l'instruction match vous permettra de faire la même chose mais de manière bien plus élégante.

Premier exemple : https://github.com/mothsART/tutoRust/blob/main/src/bin/pattern_matching_un.rs ( make pattern_matching_un)

 

Vous allez me dire que c'est un switch (notion qui n'existe pas en Rust) et vous aurez raison sur des cas simples. En Rust, match peut être utilisé sur n'importe quelle structure de données, émettre des conditions dessus et surtout, il vous obligera à être exhaustif. Voici un exemple qui va vous donner un petit aperçu de ce que l'on peut faire :

https://github.com/mothsART/tutoRust/blob/main/src/bin/pattern_matching_deux.rs (make pattern_matching_deux)

 

On retrouve des notions vues précédemment : les structures, les références, les fonctions. J'ai rajouté une boucle for qui ne devrait pas vous poser soucis. Il existe également l'instruction while et loop pour les plus curieux d'entre vous.

map, filter et closure

Si vous avez l'habitude des langages fonctionnels, vous avez l'habitude de manipuler vos données sans une succession de if (ou des match en Rust) illisibles mais avec map, filter, reduce etc. Rust inclut bien évidemment ces primitives. Voici un exemple concret qui pourrait vous en dire davantage :

https://github.com/mothsART/tutoRust/blob/main/src/bin/map_filter_closure.rs (make map_filter_closure)

Si vous ne connaissez pas les closures (propre à plein de langages), ces dernières permettent de capturer leur environnement (toutes les variables présentent dans leur scope) et d’effectuer un traitement.

En Rust, vous trouverez les closures principalement pour faire de l'événementiel, de l'asynchronisme, des évaluations paresseuses et du traitement de données.

L'implémentation et les traits

Pour l'instant, vous avez découvert des fonctions simples. Néanmoins, on peut définir une liste de méthodes en une structure. Vous allez voir qu'au final, on est très proche de l'orienté objet mais sans rentrer dans ces travers et toujours en dissociant structures, données et calculs.

Exemple : https://github.com/mothsART/tutoRust/blob/main/src/bin/implement.rs (make implement)

Pour forcer une implémentation précise, Rust met à disposition les trait. C'est grosso modo l'équivalent des interfaces dans le monde objet.

Revoyons notre exemple avec un trait : https://github.com/mothsART/tutoRust/blob/main/src/bin/trait.rs (make trait)

Pour notre exemple, ça ne change pas grand chose mais imaginons que nous voulions avoir la même API pour des rovers pilotables sur Terre, Mars et Vénus. Leur implémentation sera très différente mais vu de l'extérieur, ils auront les mêmes fonctions !

Bien évidemment, vous pouvez implémenter une infinité de trait pour une structure. Par exemple, pour les rovers de Mars et Vénus, on pourrait avoir un trait CapteurDeTemperature qui serait probablement inutile pour celui terrestre. Le rover Persévérance pourrait implémenter le trait SpectrometreLaser inutile pour bien d'autres rovers qui n'ont pas besoin de détecter des molécules organiques etc.

Conclusion

Vous avez fait un tour d'horizon de bons nombres de fonctionnalités de Rust. Il resterait sans doute des choses à dire sur :

  • • La généricité
  • • L'asynchronisme
  • • Les macros
  • • Les types RefCell et Rc
  • • Les Foreign Function Interface et l'unsafe
  • • La documentation
  • • Les tests unitaires et les benchmarks
  • • Les espaces de noms, les projets multi-fichiers et la gestion des libs/dépendances
  • • L'utilisation de Cargo
  • • etc.

 

Néanmoins, je pense vous avoir déjà donné les premières armes pour un démarrage rapide. Le mieux étant déjà d'expérimenter et surtout en Rust, d'apprivoiser le compilateur. Ce dernier vous semblera à vos débuts votre pire ennemi mais cette tendance va s'inverser !

Je vous conseille dans la phase de prototypage :

  • De préfixer vos variables non utilisées avec un "_" : le compilateur n'émettra pas de warnings.
  • D'utiliser des unwrap() plutôt que de tout gérer parfaitement du premier coup.

Cela vous permettra d'avancer vite sans vous soucier des détails. En revanche, une fois votre structure de données maîtrisée et avant la phase de production, remplacez dûment tout ce beau monde !

Jérémie Ferry

Consultant Versusmind

Introduction à Rust

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 :
 
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.
 
Redox : un système d'exploitation complet type Unix basé sur un microkernel.
Deno : le successeur de NodeJS
SWC : transpiler du Javascript bien plus vite que Babel : https://github.com/swc-project/swc
Sonic : une alternative à ElasticSearch https://github.com/valeriansaliou/sonic
Sozu : Un reverse proxy qui est configurable à chaud, possibilité de montée de version à chaud également, rapide et sécurisé.
Servo : https://github.com/servo/servo un navigateur entièrement en Rust dont certaines parties ont été intégrées à Firefox.
Zola : créé son site web statique https://github.com/getzola/zola
 
Voici également une liste de petits utilitaires malins tel que :
exa https://the.exa.website à la place de ls.
bat https://github.com/sharkdp/bat à la place de cat.
fd https://github.com/sharkdp/fd à la place de find.
hyperfine https://github.com/mothsART/hyperfine à la place de time.
delta https://github.com/dandavison/delta à la place de diff.
dust https://github.com/bootandy/dust à la place de du.
sd https://github.com/chmln/sd à la place de sed.
 
Il existe même un projet de réécriture des core utils GNU en Rust : https://github.com/uutils/coreutils (qui permet un support complet sur Windows notamment).

La communauté Rust

Rust est un langage plutôt bien accueilli dans la francophonie. Vous trouverez pas mal d'articles et d'aide à son sujet.
 
Pour ceux qui ont du mal avec l'anglais :
 
Un chan IRC : #rustfr sur le serveur chat.freenode.net
des podcasts sur le sujet : https://www.clever-cloud.com/fr/podcast
 
Pour les autres, voici des liens utiles vers des communautés :
 
Un discord actif : https://discord.gg/rust-lang
Plein de réponses aux questions récurrentes sur stackoverflow
Plein d'échanges sur Reddit
Des projets github et gitlab à foison

Pour bien démarrer

Pour démarrer un projet Rust, je vous conseille de vous référer au site de Rust, tout simplement : https://www.rust-lang.org. Le tutoriel en français de Guillaume Gomez (un développeur Core) est aussi un bon élément pour entamer un premier projet en Rust : https://blog.guillaume-gomez.fr/Rust.
 
Si vous préférez commencer par un entraînement, voici des petits exercices divers et variés : https://github.com/exercism/rustfmt. Il existe également un ensemble de petits exemples de code pour une entrée en matière d’une demi-heure : https://fasterthanli.me/articles/a-half-hour-to-learn-rust.
 
Je me suis également référé à deux livres pour m’aider dans mon apprentissage de Rust :
 
La plupart des développeurs supportent au moins la coloration syntaxique de Rust mais Visual Code semble avoir le meilleur support actuellement. Voici un lien vers éditeur de texte web pour des tests rapides : https://play.rust-lang.org.
 
J'invite les plus combatifs d'entre vous à convertir des mini softs ou des algos d'un langage à un autre. J'ai tenté l'opération pour du C, du C++, du Python et c'est assez formateur de réécrire littéralement dans un premier temps puis de le penser avec la philosophie de Rust dans un second temps.
 
Voici enfin un point d'entrée intéressant (si vous ne le connaissez pas déjà) : https://rosettacode.org ou des algos avec la même finalité sont présentés dans divers langages.
 

Quelques outils pour aller (encore) plus loin

rustfmt : Pour que votre code soit lisible et toujours formaté de la même façon, je vous encourage à utiliser https://github.com/rust-lang/rustfmt C'est une extension a Cargo (Cargo ffmt) qui va vous imposer une ligne directrice sur la présentation.
 
clippy : un analyseur de code statique qui va vous permettre d'éviter des bugs au runtime, de simplifier du code, d'améliorer les perfs etc. Il existe également une extension Cargo appelée Cargo Clippy.
Miri : un analyseur de code dynamique (encore expérimental) qui permet de détecter des bugs plus sournois notamment dans les parties unsafes : https://github.com/rust-lang/miri.
 
Clap : https://clap.rs pour créer rapidement des utilitaires en ligne de commande.
 
Diesel : un ORM en Rust http://diesel.rs.
 
afl : rajouter du fuzzing dans vos projets https://github.com/rust-fuzz/afl.rs.

 

Jérémie Ferry

Consultant Versusmind

S'abonner à RSS - Le blog de JérémieF