Mon bilan de Rust🔗
Introduction
Ça fait maintenant quelques années que j'essaye de consacrer un peu de temps au langage qui a le vent en poupe depuis maintenant presqu'une dizaine d'années, je veux parler de Rust. Langage qui à l'air assez cool pour qu'émerge un mouvement qui veut tout recoder avec.
Dans mon cas personnel, j'utilise Python3 depuis plus de 10 ans (déjà), le C depuis à peu près 20 ans (putain déjà). J'ai fait un peu de Zig et j'avais bien aimé. Alors, je me suis dit que j'allais faire un test de Rust. J'ai commencé en 2022 et en 2024, il est temps de faire une espèce de bilan. C'est l'objet de cet article.
Je précise que je ne suis pas un développeur profesionnel. Je peux consacrer à peu près en moyenne 5h par semaine de taff sur le sujet. J'ai fait mon apprentissage de Rust de manière saccadée, avec de grandes périodes d'interruption. Après la première d'entre elles, j'ai eu un vrai rejet du langage que je trouvais vraiment trop compliqué pour mon cerveau pas forcément brillant. Mais, après avoir laissé reposé, je me suis accroché et j'ai repris mon exploration, jusqu'à aujourd'hui, ce qui me permet d'avoir un peu de recul sur la situation.
Encore une fois, tout ce que j'écris est à prendre à l'aune de mon expérience. Si j'étais un développeur Rust payé pour coder sur ce langage, je n'aurais sans doute pas la même approche, ni le même regard.
- Introduction
- Ce que j'ai fait pour apprendre et tester Rust
- The Ugly
- The Bad
- The Good
- Cargo
- La doc
- Des trucs pour gérer les erreurs et les valeurs vides
- La bibliothèque standard pas si mal foutue
- Des Strings et des str safe
- Pleins de trucs nouveaux à apprendre
- Conclusions
Ce que j'ai fait pour apprendre et tester Rust
Je me suis fixé comme objectif de recoder fts5_fcgi en Rust et en mieux. Et, après pas mal de temps, je crois que j'y suis arrivé, dans tous les cas, à une version qui me satisfait et que je pourrais pousser dans ma prod (probablement pas celle de mes employeurs présents et passés).
J'ai commencé comme il faut commencer: j'ai lu le Rust Book: The Rust Programming Language. D'ailleurs, je le lis encore souvent, tellement que j'ai installé le paquet rust-doc sous Debian (où je code exclusivement). Bien sûr le compagnon idéal de ce livre, c'est la documentation de la librairie standard qui est aussi fournie dans le paquet rust-doc (qui prend environ 500 Mo au passage).
J'ai aussi installé elpa-rust-mode pour intégrer Rust dans Emacs (enfin, la coloration syntaxique, c'est déjà pas mal).
Pour le choix des crates Rust et de la version de Rust, je me suis basé sur les choix de Debian en la matière. Comme je suis sous Debian Bookworm, ça limite à rustc en version 1.63.0 et des crates pas si nombreux, même s'il faut vraiment souligner l'effort de l'équipe Debian en charge de packager Rust et son écosystème.
Mais surtout, pour avancer, j'ai codé tout en prenant soin de relire la documentation lorsque je faisais face à un problème. Parfois, j'ai passé 2 ou 3h pour modifier une ou deux lignes de code (rien que ça). Hé oui, on en arrive à ce niveau, comme quand tu reprends la marche après un accident et que tu te rends compte que ce n'est pas comme le vélo.
The Ugly
On va commencer avec ce que j'ai trouvé de vraiment affreux sous Rust.
Putain, la taille des binaires !
Bon, Rust produit des binaires "statiques" qui embarquent pratiquement tout. Ce n'est pas tout à fait vrai car les binaires appellent déjà toutes les bibliothèques accédées par FFI et sans doute appelle la libc systématiquement. Mais pour autant, ça fait des gros trucs. De base, fts5_fcgi fait 14 Mo! La version en C fait 17 ko en comparaison (on doit ajouter la libsqlite qui fait 1,4Mo et la libfcgi qui fait 51Ko).
Pour relativiser, il faut savoir qu'il n'y pas de lib partagée Rust donc, on se tape les liens vers les fonctions qu'on utilise directement dans le binaire. A l'inverse, ça permet de simplifier le déploiement et de juste copier le binaire pour pouvoir l'exécuter (je grossis le trait évidemment).
Putain, les dépendances !
Même pour un petit projet comme fs5_fcgi, je me tape une floppée de dépendances de malade. Même en utilisant, les paquets Debian des crates Rust, je trouve que ça fait quand même mal. J'en suis à 252 paquets d'installés. C'est énorme !
Je te dis pas la surface d'attaque que ça fait. Autant quand on code en C, on a finalement peu de dépendances si on souhaite faire quelque-chose de simple, autant en Rust, si tu veux gérer les arguments de la ligne de commande, tout le monde te dit de prendre le crate Clap, crate qui te ramène 50000 dépendances.
Idem pour sqlite. Au début, j'étais bien pris au dépourvu: quel crate je prends pour attaquer une base SQLite ? Il y en avait une bonne dizaine à tester, à choisir. J'ai finis par prendre rusqlite qui semble le plus utilisé. Mais même sur cette implémentation, j'ai trouvé qu'il manquait des options (je n'ai pas trouvé comment faire pour récupérer les options de compilation de SQLite par exemple). Pareil pour fastcgi: il y a au moins 5 crates qui font plus ou moins la même chose. A toi de voir ce qui te va. Surtout, si tu choisis tokio-fastcgi, tu te tapes l'apprentissage de tokio et de async/await, clairement pas un truc pour un débutant. En C, l'écosystème est plus simple et plus rodé: il y a libfcgi, point.
Alors certains diront que c'est l'esprit de Rust: trouver un problème ciblé et le régler par un crate dédié. Certes, mais je pense qu'on gagnerait soit à faire grossir la lib standard (qui est déjà pas si mal), un peu à la Python3. On encore, on pourrait faire moins de paquets ou des paquets avec moins de dépendances.*
Et puis, ce qui est pénible, c'est qu'à chaque fois que tu compiles après quelques mois, faut tout mettre à jour
Putain, le borrow checking !
Alors, c'est une force de Rust pour régler les problèmes d'attaque mémoire, mais qu'est-ce-que j'en ai chié du borrow checker. D'abord, même si son principe est simple à comprendre, on se fait vite piégé, notamment parce que Rust fait des raccourcis tout seul, ce qui rend peu clair ce qui est vraiment emprunté ou pas.
Et puis, quand tu viens des langages qui n'utilisent pas cette technique, ou tu copies des variables à tour de bras, surtout pour décomposer ton algo ou rendre plus clair le code, pour éviter d'avoir une instruction qui s'étale sur 10 lignes, tu finis par acquérir des mécanismes qui ne sont pas d'emblée compatibles avec Rust.
Par exemple, tu veux garder une variable qui est une simple copie d'un autre objet, juste pour faire un autre traitement plus tard et bien, tu ne peux souvent pas. Il faut faire une copie ou un clone, sinon le borrow checker t'insultes. Pour le débutant initié, c'est un mauvais moment à passer.
The Bad
Viennent ensuite quelques trucs qui sont pénibles au quotidien pour le rustacean débutant.
Les trucs masqués
Le truc le plus génant et pas qui fait mal comme les points dessus, c'est que j'ai trouvé que l'apprentissage de Rust est finalement compliqué parce qu'il y a des choses que j'ai appelés les trucs masqués.
Par exemple, dans certains cas, les règles du borrow checker ne s'appliquent pas parce que les structs concernés ont les bons Traits (Copy et Clone). Donc le débutant va les appeler et les utiliser comme de bon structs en C, sans se douter qu'en fait Rust gère la copie des objets à la volée (enfin au moment de le compilation).
Et puis, tu passes à d'autres structs qui n'ont pas ces traits et là, la galère commence. Tu tu dis "pourquoi ma fonction vient de péter alors que j'ai juste changé les types de structs ?". Après 30 minutes de lecture de la doc, tu te rends compte que tu dois utiliser des références. Et tu pestes d'avoir perdu autant de temps pour ça.
Tout encapsuler dans des Results/Options
Avec un peu de pratique, on se rend compte que les bonnes idées de gérer des résultats dans des structures dédiées (des enums) rend le code verbeux à souhait. Combien de fois je me suis retrouvé à faire des fonctions de base qui renvoie des structures simples pour au final devoir faire des Result
Et ça tout simplement parce qu'en Rust, la majorité des fonctions de base renvoie des Results ou des Options. Moi qui vient du C et de Python, ce n'est pas naturel, je trouve ça verbeux et pas naturel, presqu'overkill.
Par exemple, parfois, je fais exprès de virer le cas d'erreur en premier (un guard). Je n'ai plus que des cas normaux de données à traiter. Mais pourtant, au final, je suis obligé de sortir un Result en utilisant l'opérateur ? à tout bout de champ pour éviter les unwrap ou les expect à tout bout de champ.
The Good
Mais pour autant, Rust, c'est aussi plein de bonnes choses.
Cargo
L'approche d'avoir un outil qui remplace make, les autoconfs, apt-get, le générateur de doc de code, le générateur de tests unitaires, c'est quand même sympa. Tu commences en Rust, tu apprends à te servir rapidement de Cargo et à le configurer et c'est tout. Pas besoin d'apprendre à gérer une tripoté d'outils unitaires, tout est dans Cargo quelquechose.
J'ai trouvé ça pas très Unix spirit, mais au final, très pratique. On a moins le choix, mais on a plus de normes de code et moins de dispersion sur la chaîne de développement. Manquerait plus qu'on ait à se taper autant de complexité dans ces outils que dans le langage Rust lui-même et je crois que tout le monde aurait été dégouté.
La doc
Rust utilise Cargo et des commentaires pour faire la doc des bibliothèques et des crates. C'est unifié et partout la même chose. Et cette doc est vachement pratique. Pas de là à simplifier le langage, mais s'il n'y avait pas ça, je suis persuadé que Rust ne serait pas aussi populaire.
J'ai trouvé également que l'accès offline à la documentation est un vrai plus. Parfois, je code loin d'un accès Internet. Ça m'oblige à réfléchir plus que d'avoir le réflexe stackoverflow (même si stackoverflow c'est pas si mal). Quand je génère une doc avec Cargo, je peux avoir aussi accès la doc des dépendances, y compris de manière offline. Et ça, c'est bien !
Des trucs pour gérer les erreurs et les valeurs vides
Dans Rust, il faut se le dire, tout se gère à coup de Result ou d'Option. Au début, c'est déstabilisant, mais au final, c'est rassurant. Enfin, on a des valeurs vides qui sont vraiment vides ou des erreurs qui sont vraiment des erreurs et pas des trucs qui sont des codes qu'il faut interpréter et qui changent d'une fonction à l'autre.
Ça demande un peu d'apprentissage, mais au final, on s'y fait et ça semble plus propre, même si ça peut paraître verbeux à souhait. Après tout, ce n'est pas pour rien que l'opérateur ? a été créé, non ?
La bibliothèque standard pas si mal foutue
Alors, l'esprit de Rust, c'est de ne pas tout mettre dans la bibliothèque standard. Mais j'ai trouvé qu'il y avait déjà pas mal de choses dedans comme les collections, une gestion de HashMap en particulier, les Path, les itérateurs, les threads, etc.
Ok, il manque les regexp et des choses standardisables comme la gestion simple des options d'appel de ligne de commande. Mais dans l'ensemble, on trouve déjà pas mal de choses.
Des Strings et des str safe
Avec Rust, on a accès à une gestion de chaînes de caractères safe, c'est-à-dire sans problème de gestion de mémoire (débordement principalement). Même si on peut se retrouver avec des ennuis de borrow checker suivant qu'on utilise des Strings ou des str, on n'a pas cette angoisse comme en C de devoir gérer finement les chaînes de caractères avec telle ou telle fonction qui risquerait de créer une faille de sécurité ou de planter le programme.
Et j'ai trouvé ça finalement apaisant dans la manière de coder. A la fin, on sait que si ça compile, alors on n'aura pas de problèmes de sécurité liée à la mémoire. Alors qu'en C, j'ai toujours un doute (et j'en aurai toujours).
Bon après, pour relativiser, cet apaisement va arriver uniquement une fois que vous arriverez à compiler sans erreur, ce qui peut se révéler parfois ardu…
Pleins de trucs nouveaux à apprendre
Et puis, au final, il y a plein de choses à découvrir dans Rust: les iterateurs, async/await, les closures, les lifetimes, les Traits (un mécanisme qui ajoute des fonctionnalités transverses à des objets différents), les threads et les Mutex, les smart pointeurs avec des fonctionnalités différentes (les Box/Arc/Rc/etc.), les macros, les directives de compilation, etc.
Si comme moi, vous aimez apprendre de nouveaux concepts, alors vous allez être servis.
Conclusions
Même si j'ai dû me battre avec pendant trop longtemps, je trouve que Rust est un language à qui je vais donner sa chance dans mon avenir (s'il me reste encore assez d'années de vie pour devenir un peu plus compétent). Avant de pouvoir le prendre en main, pour produire des programmes utilisables par le commun des mortels, il est vrai que ça prend du temps, qu'il y a beaucoup de concepts à comprendre. D'où la réputation de langage complexe pour Rust.
Et effectivement, le C en comparaison, c'est simple en grammaire. Après, on doit lire abondamment la documentation de la bibliothèque principale, mais en moins de 5 jours, on a les concepts. Avec Rust, je dirais qu'il faut plutôt un bon mois et plus de pratique. Souvent, je devais revoir telle ou telle partie du livre, pour mieux comprendre.
Je crois que, dans un avenir proche, je vais essayer d'être plus régulier sur l'utilisation de Rust, d'en faire un peu tous les jours, une heure peut-être si je trouve le temps. Car je me rends bien compte que le concept du borrow checker, je l'ai vu et recompris probablement 5 fois. Je pense que Rust reste un langage exigeant en matière de temps de cerveau disponible et qu'il n'est pas fait pour de longues périodes d'arrêt de code, chose que je fais maintenant facilement en Python, à l'inverse, sans que ça pose de problèmes.
Voilà pour mon bilan de Rust, un langage exigeant pour le développeur amateur mais qui permet de progresser dans cet art. Si vous voulez vous jeter dedans, pas de souci, mais vous serez prévenu !