
5 Raisons d’Apprendre le Rust en 2022
Je m’appelle Jean-Baptiste Briant et je suis développeur JavaScript depuis 10 ans. Il y a maintenant 3 semaines, j’ai été évangélisé au langage de programmation Rust. Dans cet article, vous découvrirez mon retour d’expérience sur ce langage, les problématiques de JavaScript qu’il résout ainsi que les 5 raisons pour lesquelles vous devez apprendre Rust en 2022.
Si vous développez déjà avec un langage système tel que C ou C++, cet article risque de ne pas vous être destiné.
Les raisons de mon apprentissage du langage Rust
Après 10 ans de programmation informatique, j’ai fait le choix de me tourner vers Rust afin de palier aux limitations de JavaScript.
En effet, j’ai voulu développer un petit jeu vidéo avec JavaScript et je me suis vite rendu compte qu’il était impossible de faire quelque chose de performant. L’une des raisons principales à cela est le garbage collector ainsi que l’absence de compilation.
Même si Google a fait beaucoup d’efforts dans l’optimisation du langage ces dernières années, je voulais me tourner vers un langage performant, low level, sans garbage collector et bien foutu.
Rust vs JavaScript
Le problème de JavaScript
Il y a deux choses que je reproche vraiment à JavaScript.
Premièrement, l’absence de typage, bien que Microsoft a permis à l’écosystème d’évoluer avec TypeScript.
Deuxièmement, la facilité déconcertante pour accéder à des variables qui n’existent pas. La capacité à produire des RuntimeException
dans le JavaScript devient insupportable. Les bugs en production sont réguliers malgré une couverture de test correcte. Il est aussi extrêmement simple d’écrire du code « merdique » qui va tomber en erreur dès le démarrage du serveur.
Cependant, même si c’est intéressant de faire le parallèle avec JavaScript, on ne peut pas lui jeter la pierre.
Le langage a été crée à une époque différente, les problématiques n’étaient pas les mêmes. Pour ceux qui développent en C, CPP (C++) et d’autres langages, vous connaissez probablement ces erreurs : undefined behavior
, runtime exception
, etc.
Bien que des nouvelles implémentations du langage JavaScript existent (Node, Deno, etc.), il n’est pas possible de faire un « breaking change » au risque de casser des milliards de scripts. JavaScript est donc condamné à conserver ses mécaniques pour le restant de ses jours.
Rust a été crée récemment et dans le but de résoudre entre autres ces problématiques. Donc, forcément, il sera meilleur que la plupart des langages de programmation.
La promesse Rust
Depuis quelques années nous entendons de plus en parler parler du langage Rust qui paraît révolutionnaire aux premiers abords.
On compare souvent Rust au CPP (C++) à la fois en terme de performance, mais aussi grâce au fait que c’est un langage système compilé qui permet de travailler au plus proche du processeur.
Rust nous fait une liste de promesses telles que :
- la protection des « leaks » mémoire (fuites mémoire)
- un multithreading simplifié
- être « memory safe ». C’est-à-dire que la mémoire ne peut pas être corrompu, donc fini les erreurs du type
Cannot read property foo of undefined
.
Le langage Rust apporte aussi un système de typage moderne. Avec la version 1 sortie en 2015 (1985 pour C++), nous avons forcément un langage capable de répondre aux problématiques principales rencontrés ces trente dernières années.
Comme vous l’aurez compris, la promesse de Rust est complètement folle. Ce langage de programmation a été créé par des ingénieurs qui se reposent sur des dizaines d’années d’expérience et d’erreurs dans le but de créer, selon beaucoup d’experts, le langage le plus abouti à ce jour.
L’ensemble de ces problèmes me ramène à la conclusion suivante : « Il faut que j’essaye Rust ».
La courbe d’apprentissage de Rust
J’étais donc hyper excité à l’idée de tester le Rust et de me perfectionner. Mais, avant de commencer à apprendre le Rust, j’ai lu que la courbe d’apprentissage (learning curve) du langage est réputée pour être abrupte.
Le compilateur est aussi apprécié que détesté, car il ne laisse pas le programmeur écrire du code qui pourrait par la suite générer des fuites de mémoire (accès à de la mémoire qui n’existe plus, des races conditions, etc.).
Bref, plus de NullPointerException
(exception relative aux pointeurs nuls) au prix d’une rigueur qu’un développeur purement JavaScript n’a tout simplement pas.
À titre information, cette fonctionnalité de Rust est appelé le borrow checker.
Une autre façon pour le compilateur de garantir la sécurité de la mémoire dans le langage Rust est la gestion des cycles de vie.
Pour vous dire, après ces quelques semaines d’utilisation du langage je ne saisis pas encore toutes les subtilités de la gestion de la mémoire en Rust. Même après 10 ans de développement, c’est quelque chose de très nouveau pour moi.
C’était donc avec un peu d’appréhension que je me suis lancé dans l’apprentissage de ce nouveau langage. Mais, par chance, j’avais un mentor, un fidèle compagnon et un excellent développeur pour m’accompagner dans cette aventure.
5 raisons pour lesquelles vous devez apprendre Rust en 2022
1. Le Multithreading Rust
Le langage Rust rend le multithreading très simple. Si vous venez de JavaScript vous devez savoir que pour faire du multithreading vous devez soit utiliser des workers et gérer beaucoup de choses vous-même comme : un module externe comme PM2, utiliser Docker ou bien tout gérer manuellement (en manipulant les processus avec le module natif de node « child_process »).
Dans les trois derniers cas, c’est votre application complète qui tourne dans un autre thread et vous utilisez un load balancer pour envoyer les requêtes.
Avec Rust c’est beaucoup plus simple ! Vous pouvez directement déclarer du code async
(asynchrone) qui sera exécuté dans un thread séparé. Tout ça grâce à une « crate » (une bibliothèque) nommée Tokio.
Le premier programme que j’ai réalisé avec Rust est un back-end HTTP (Rust Rest API). C’est un type de programme que j’ai fait de nombreuses fois en JavaScript et en PHP.
Pour le moment, ma connaissance du multithreading en Rust est sommaire donc, je ne vais pas m’étendre sur le fonctionnement de celui-ci.
Néanmoins, j’en sais assez pour l’utiliser directement dans mes programmes et je vais vous montrer comment faire. Brièvement ici puis en details dans un autre article.
Tout est pensé dans ce langage pour que le programmeur puisse utiliser la pleine capacité de sa machine.
Un exemple avec 3 threads que le process peut répartir comme il veut !
fn spawn_twitchchat(pool: Pool) {
// Ici on invoque un second thread
tokio::spawn({
async move {
start_bot(pool).await?;
}
});
}
fn spawn_twitchapi2_poll(_pool: Pool) {
// Ici on invoque un troisieme thread
tokio::spawn({
async move {
println!("in 10 seconds we'll exit twitchapi2_poll");
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
}
});
}
// Ici on lance le thread principal
#[tokio::main]
async fn main() {
pretty_env_logger::init();
info!("Starting JeanBotiste main");
// Création d'une pool de connections
let pool = create_connection_pool();
// Ferme la pool
let pool_clone = pool.clone();
// Invoque twitchchat
spawn_twitchchat(pool);
// Invoque twitchapi2 poll
spawn_twitchapi2_poll(pool_clone);
tokio::time::sleep(std::time::Duration::MAX).await;
}
2. Gestion de la mémoire (Memory Safe et Memory Leaks)
Gérer les erreurs de cette façon permet de garder le code métier concis et clair tout en ayant une gestion d’erreur centrale.
Si le compilateur vous autorise à écrire du code qui accède à une variable, alors c’est qu’elle est accessible au moment de faire tourner votre programme. Ceci va rendre complexe l’écriture de code qui va résulter en une RuntimeException
de type NullPointerException
.
Cette fonctionnalité du langage rend nécessaire la compréhension par le programmeur de comment fonctionne la RAM (mémoire vive).
Quand on vient de JavaScript avec ce satané garbage collector et l’incapacité technique de gérer la mémoire manuellement, le choc culturel est important.
Néanmoins, cela nous force à gérer tous les cas dans le code, c’est-à-dire que vous ne pouvez rien oublier ou potentiellement une erreur va être déclenchée.
Par défaut Rust vous oblige à écrire un code qui couvre la grande majorité des cas d’erreurs. De ce fait vous êtes capable d’avoir un contrôle total sur le comportement de votre application. Ce qui est incroyable c’est que cette sécurité apportée par le « memory safe » se fait sans avoir besoin d’écrire des tests unitaires.
Comparé à JavaScript c’est une évolution considérable.
Un exemple de gestion de la mémoire et d’erreur en Rust.
Fichier: create_user.rs (utilisation du ? après chaque instructions)
async fn create_user(
db: &Pool,
signup_data: SignUpData
) -> Result<User, AppError> {
// Try/Catch automatique
// Retour en AppError si POST data n'est pas valide
signup_data.validate()?;
// Try/Catch automatique
// Retour en AppError si on ne peut pas hash le password
let password_hash = hash_password(signup_data.password.as_bytes())?;
// Create the struct to insert the new user
let new_user = NewUser {
email: signup_data.email,
password: password_hash.to_string(),
};
// Try/catch automatique, si insert_into fail, alors AppError sera renvoyé, sinon le User
// Insert new user in database
Ok(insert_into(users).values(new_user).get_result_async(&db).await?)
}
Fichier: errors_handler.rs (déclaration des types d’erreurs)
// Ici on prend en charge les erreurs du validator
impl From<validator::ValidationErrors> for AppError {
fn from(error: ValidationErrors) -> Self {
AppError::ValidationErrors(error)
}
}
// Ici les erreurs du password hasher
impl From<argon2::password_hash::errors::Error> for AppError {
fn from(error: argon2::password_hash::errors::Error) -> Self {
match error {
argon2::password_hash::errors::Error::Password => {
AppError::WrongPassword(error)
},
_ => {
AppError::PasswordHashError(error)
}
}
}
}
// Ici les erreurs du gestionnaire de chiffrement
impl From<hmac::crypto_mac::InvalidKeyLength> for AppError {
fn from(error: hmac::crypto_mac::InvalidKeyLength) -> Self {
AppError::InvalidKeyLength(error)
}
}
// Ici les erreurs de la base de donnee
impl From<async_diesel::AsyncError> for AppError {
fn from(error: async_diesel::AsyncError) -> Self {
AppError::DatabaseAsyncError(error)
}
}
3. La gestion des erreurs en Rust
Naturellement quand on est forcé a gérer tous les cas on est capable de prédire les erreurs qui vont arriver. De plus, Rust nous force à gérer les erreurs de manière précise grâce au typage.
Contrairement à JavaScript où une erreur est une « erreur », peu importe que ce soit lié à une requête HTTP, JSON ou mémoire.
Chaque erreur a alors un type bien précis et vous pouvez spécifier un comportement pour chacune d’entre elles.
Grâce à la composition il est possible de centraliser toute sa gestion d’erreur dans un seul module.
C’est la « killer feature » de Rust qui rend ce système incroyablement agréable à utiliser et très lisible tout en ayant une gestion fine des cas d’erreurs avec un try/catch implicite.
En effet, il n’est pas nécessaire d’utiliser de bloc try/catch pour que Rust le fasse. Un simple petit point d’interrogation en fin d’instruction permet de capturer une erreur sans que le programme ne plante.
4. Le testing en Rust
Les tests sont natifs au langage Rust. Vous n’avez pas besoin de librairies supplémentaires afin de tester votre code.
À l’époque où j’ai commencé à écrire du JavaScript, écrire des tests n’était pas à la mode. Il m’a fallu des années avant de découvrir la pratique du TDD (Test Driven Development) et de comprendre à quel point c’était une pratique essentielle.
Pour ceux qui n’écrivent pas encore de tests je vous conseille vivement de changer cette habitudes, car elle vous fait perdre un temps considérable à moins que vous soyez un génie (ce qui n’est pas mon cas).
// Chaque test est lancé dans un thread à part
#[tokio::test]
#[serial]
async fn check_user_can_buy_product_paid_fail() {
// Mocktopus permet de mock les modules et fonctions
use mocktopus::mocking::*;
use crate::order::repo::get_user_orders_by_product_id;
// Implémentation du mock
// let orders_res = get_fixtures_orders(1, PaymentStatus::Paid);
get_user_orders_by_product_id.mock_safe( |_, _, _| MockResult::Return(
Box::pin(async move {
// Je retourne un status particulier
let orders_res = get_fixtures_orders(1, PaymentStatus::Paid);
Ok(Some(orders_res.clone()))
})
));
// Appel du controller
let db = create_connection_pool();
// Appel de la fonction que l'on veut tester
let err = check_user_can_buy_product(&db, 1, 1).await.err().unwrap();
// Puis on vérifie que le résultat est bien ce que l'on attendait
assert_eq!(err.to_string(), AppError::BadRequest("product_already_owned".to_string()).to_string());
}
5. Les performances
En effet, là où PHP était bloquants pour tous les appels I/O (Input/Output) : les appels réseau, les accès à la base de données, les lectures de fichiers, etc.
Node utilise la fameuse librairie libuv qui permet de faire de l’asynchrone en JavaScript. Lorsque votre code est en train d’attendre un retour de la base de données le processeur est capable de continuer d’executer des instructions pendant cette attente.
Async/Await était déjà une avancée importante dans le domaine de la performance, mais Rust va encore plus loin, car il apporte non seulement le multithreading nativement à vos programmes, mais en plus de ça le langage est compilé. Ce qui veut dire que tout ce que vous écrivez est traduit en code machine (langage proche du processeur). De ce fait, le compilateur est capable de faire des optimisations extrêmement efficaces, car il dispose de beaucoup de temps (le moment où vous compilez votre programme).
4 points que vous devez connaître avant d’apprendre Rust
1. Le marché du travail
Vous l’aurez compris je suis amoureux de Rust. Ça a été un coup de foudre auquel je ne m’attendais absolument pas. Oui, je n’imagine plus ma vie sans ce langage.
Néanmoins, il y a des désavantages évidants à le considérer comme un choix de carrière.
En effet, le langage est très récent et bien qu’une énorme « hype » existe autour du langage, le marché du travail reste relativement modeste. Il y a relativement peu d’offre d’emploi et encore moins de poste exclusivement Rust.
En revanche, ce sont les emplois les mieux payés, car il est difficile de trouver des experts du langage vu son jeune âge et la profondeur de ce dernier.
Alors qu’un expert JavaScript peut trouver du travail en environ 10 minutes, il faudra sûrement un petit peu plus de temps à un expert Rust pour trouver le poste qui le fait réellement rêver.
Néanmoins, Rust est plébiscité comme étant le langage du futur. Il se veut concurrencer les langages hautes performances et bas niveau actuel comme le C et le CPP (C++). En effet, il est bien plus simple et agréable d’écrire du Rust que d’écrire du CPP.
Il faut donc s’attendre à une évolution prochaine du marché. De mon point de vue, c’est le bon moment pour s’y coller !
2. Rust est un langage difficile à apprendre
Effectivement quand on vient de JavaScript il est assez difficile d’apprendre un langage fortement typé, car il ne laisse aucune place à l’incertitude et a l’imprécision. Le mouvement des variables est très particulier et le borrow checker
vous rappelle souvent à l’ordre pour vous dire que vous ne pouvez pas utiliser telle variable de cette façon et qu’il faudra alors probablement la cloner avant.
Il faudra apprendre à utiliser mut
(variable mutable, modifiable) et &
(référence à une variable, en lecture seule).
Il est certain que vous allez « rager » et que vous allez-vous poser de nombreuses questions. Comme par exemple, la différence entre le type str et String en Rust.
Mais bon, si vous êtes développeur c’est que vous avez déjà un certain goût pour la difficulté, un sens certain du tryhard, un autisme au minimum léger, une accoutumance à la frustration, des qualités de persévérance d’humilité et d’obsession pour le travail accompli.
Donc, ne vous inquiétez pas, vous allez en « chier », mais ça va passer et au bout d’un moment vous serez fier de vous !
3. Un écosystème encore jeune
Bien que le langage soit conçu pour être extrêmement performant, il est pour l’instant compliqué de faire du jeu vidéo. Pourtant, ça serait le langage parfait pour en faire.
Il manque encore beaucoup de librairies pour faire des choses basiques, ou alors, elles ne sont pas encore finalisées comme Crates pour la gestion du XML. Cependant, pas de panique, Rust est le langage du futur et l’écosystème est bouillonnant. Nous arriverons très sûrement à un niveau ridicule de librairies qui font toutes la même chose incessamment sous peu.
4. Le compilateur lent
Le compilateur fait beaucoup de travail. Il doit transformer des instructions lisibles et compréhensibles par le programmeur en instructions efficaces et performantes pour le processeur. Cette étape est lente quand on est habitué au « hot-reloading » rapide des bundler JS.
Cela reste un point que j’espère pouvoir mieux comprendre et utiliser afin que le workflow devienne un peu plus agréable.
Alors, qu’attendez vous pour apprendre le Rust en 2022 ?
Il n’y a pas de doute, Rust est le langage du futur. Il est extrêmement polyvalent avec un écosystème diversifié. Il est aussi agréable à écrire, propre, bien rangé, lisible, précis, exhaustif, bref tout ce qui peut vous faire adorer un langage. En tout cas, c’est ce qui me plaît dans un langage de programmation !
D’ailleurs, je tiens à remercier ces personnes qui m’ont aidé à construire ce contenu sur Rust :
- PhiSyX
- Yamashi
- Jocques
- Akanoa
- Abhishek Chanda
Certaines d’entre-elles sont des spectacteurs de ma chaîne TwitchTV. Je diffuse très régulièrement du développement web, du Rust et du JavaScript. Alors, n’hésite pas à nous y rejoindre !
Je viens aussi d’ouvrir ma chaîne YouTube sur laquelle je prévois de faire des tutoriels sur Rust, mais aussi JavaScript (React, etc.).