Rust, un nouvel espoir (1/3)
Rust est le langage de programmation sponsorisé par Mozilla sorti en 1.0 en 2015. Sur le papier, il promet des performances équivalentes à C/C++ et un apport en sécurité quant aux problèmes de mémoire (leak, accès à des données détruites, race conditions…). Voilà pour les points les plus importants. Évidemment, il fallait que je mette mon nez dans ce langage, et après avoir codé un programme avec, voici mes impressions.

Prérequis
La chaîne de compilation Rust compile les programmes pour LLVM. Du coup, pour avoir l’exécutable final, on a besoin d’outils qui vont finir la compilation pour la plateforme cible. Pour Windows, ils conseillent d’installer les Visual Studio build tools 2013 ou plus récents. On peut aussi choisir GCC en passant par Cygwin ou Mingw. Pour Linux et Mac, GCC est utilisé automatiquement.
Rustup et Cargo
L’installation de la chaîne de compilation se fait très simplement, en lançant leur installeur et en suivant les instructions. On se retrouve alors avec les commandes rustup
et cargo
. La première permet de gérer la chaîne de compilation :
- la mettre à jour ;
- installer une version beta ou nightly et la maintenir à jour ;
- revenir à une version précise ;
- définir la chaîne de compilation par défaut.
Il y a peut-être d’autres choses, mais ce sont celles que j’ai utilisées jusqu’à présent.
Cargo est l’outil qui gère le lancement de tâches, que ce soit la création d’un squelette de nouveau projet (crate), une compilation, le lancement des tests unitaires, le lancement de l’application…
Pour mener sa tâche à bien, cargo va aussi gérer les dépendances nécessaires à la compilation de votre projet, qui auront été préalablement déclarées dans un fichier Cargo.toml
.
Ce couple d’outils rend l’utilisation de la chaîne de compilation ainsi que la gestion des dépendances très plaisants, là où il faut passer par des solutions tierces pour d’autres langages (Gradle ou Maven pour Java, CMake ou make pour C/C++, ces derniers étant un affront à l’ingénierie moderne).
Déjà un gros point positif. Même Kotlin (que j’adore) n’a pas cet environnent de gestion, Jetbrains utilisant Gradle pour les dépendances et se reposant entièrement sur leur IDE pour la mise à jour du compilateur (la version standalone devant être mise à jour à la main).
Structure des modules
Une des premières choses qui m’a désorienté est la structure des modules, appelés crates. Lorsque je code, j’aime bien organiser le code source en sous-modules, bonne pratique de base. En Rust, quand on crée un module mon_module
, il faut un dossier à ce nom, et dedans, un fichier mod.rs
qui est attendu par le compilateur. Il peut très bien contenir le code de tout le module, mais si plusieurs fichiers le composent, chacun de ces fichiers sera aussi un sous-module, et si on veut les utiliser ailleurs, c’est dans ce mod.rs
qu’on exporte les fonctionnalités du module pour ceux qui l’utiliseraient, y compris mon propre projet en cours.
C’est assez déconcertant pour moi qui suis habitué aux include
ou autre import
qui ne nécessitent que la présence du fichier dans l’arborescence. Voici un petit exemple.
Imaginez que vous avez cette structure de fichiers :
src
|- main.rs
|- data
|- mod.rs
|- my_data.rs
Pour pouvoir utiliser ce qu’il se trouve dans my_data.rs
, il faut que le fichier mod.rs
l’exporte :
// mod.rs
pub mod my_data;
Et il faut l’importer dans main.rs
:
// main.rs
use data::my_data;
Tout est donc très compartimenté, ce qui peut générer un peu de frustration, mais au final, ça permet d’extraire les sous-modules pour en faire des bibliothèques individuelles plus facilement.
Tests unitaires
Cet aspect est vraiment très puissant : le nécessaire pour faire tourner les tests unitaires est présent quand on installe le compilateur. Ça facilite grandement une approche TDD (Test Driven Development) qui souffre d’habitude du syndrome du « fait chier faut installer/configurer tout le framework de test et j’ai la flemmeuh ». Là, rien de plus à installer, on écrit ses tests dans un module à part (qui peut être dans le fichier en cours de test) :
// mon_fichier.rs
fn function_to_test() -> i32 {
// plein de code
}
#[cfg(test)]
mod tests
{
#[test]
fn test_function_to_test_ok()
{
// code de test
}
}
Ici, #[cfg(test)]
est une directive de compilation qui indique qu’il faut compiler ce qui suit (ici le module tests) si et seulement si la configuration test
est spécifiée. La directive #[test]
permet de baliser les fonctions à exécuter pendant les tests.
Ainsi, l’invocation de cargo test
va compiler non seulement le programme mais aussi toutes les dépendances nécessaires aux tests et exécuter toutes les fonctions marquées par #[test]
.
On notera tout de même une faiblesse de jeunesse : il n’y a pas de système de mock
idiomatique pour le moment et tout est fait à l’huile de coude, en attendant qu’un crate dominant fasse son apparition.
Conclusion sur l’outillage
Vous l’avez vu, Rust n’est pas seulement un langage de programmation. Il est accompagné de tout un ensemble d’outils qui ont pour objectif de faciliter la gestion de l’écosystème Rust. Ça rend les corvées nettement moins désagréables et permet d’entrer dans le vif du sujet plus rapidement, ce qu’on abordera dès le prochain article !