Lorsque l'on parle d'apprendre à développer, il est souvent question du trio à tout faire HTML/CSS/JS, pensé pour le web. Parfois de langages plus classiques comme C#, Java, Python ou Ruby. Plus rarement de langages bas niveau tels que C(++), Go ou Rust. Penchons-nous sur ce dernier.
Ce week-end a débuté la semaine du code. Une initiative européenne visant à faire découvrir, notamment aux plus jeunes, le développement logiciel. Comme Thierry Breton, nous aurions pu faire des blagues sur le nom de C++, Java et Python. On aurait aussi pu vous apprendre à développer une première application... mais c'est déjà fait.
Si vous nous suivez depuis quelques années, vous savez le faire avec une interface graphique, du Node.js pour un serveur web ou une application packagée, suivre l'actualité avec un script Python et de la synthèse vocale, créer votre extension de navigateur, une fonction en ligne via l'approche serverless ou une PWA en puissance.
Le tout avec un zeste de stockage objet via votre propre serveur MinIO. Bref, grâce à nous, vous êtes déjà full-stack ! Dès lors, que faire de plus ? Vous apprendre à devenir des dieux de l'Unreal Engine et du ray tracing ? Tout comprendre de la comparaison trilatérale introduite dans C++20 ? Non, il fallait regarder vers le futur, le vrai.
Nous avons donc décidé de vous apprendre à développer dans un langage mêlant simplicité et efficacité, tout en étant parfois très différent de ce que vous avez déjà vu ailleurs, multiplateforme et open source : Rust. Si vous faites partie de ceux qui pensent que développer dans un langage dit de bas niveau est compliqué, que cela demande des outils de folie, finissons-en avec vos idées reçues dans ce premier article d'une (longue ?) série.
Notre dossier sur le développement Rust :
- Développez votre première application en Rust
- Rust et ses spécificités : l'essentiel à connaître (à venir)
On passe tout de suite à la pratique
Ainsi, plutôt que de commencer en vous expliquant ce qu'est Rust, en vous chantant ses louanges pour justifier notre choix du jour, mettons-nous plutôt directement au travail. Vous verrez, une fois les différents éléments de base installés et sans rien faire de bien compliqué... tout sera vite bouclé (sauf si vous partez manger une raclette).
- L'installation via rustup
Première étape : télécharger Rust ou plutôt rustup. Il s'agit d'une application permettant d'installer (et de mettre à jour) simplement les différents outils nécessaires, proposés pour Linux, macOS et Windows.
Tout sera configuré automatiquement ou presque. C'est ainsi que l'on va récupérer Cargo, un élément central dans l'écosystème Rust, étant à la fois utilisé pour la compilation, la gestion de paquets ou encore la publication (nous y reviendrons). Pour savoir comment procéder pour votre système, rendez-vous sur cette page.
Lorsque le gestionnaire de paquets de votre système permet l'installation de rustup, vous pouvez l'utiliser. S'il est basé sur UNIX (Linux, macOS), un script global est également à votre disposition :
curl https://sh.rustup.rs -sSf | sh
Pour le sous-système Linux de Windows 10 (WSL), l'équipe conseille la commande suivante :
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Si vous préférez récupérer manuellement le paquet d'installation de votre choix, ça se passe par ici.
Sous Linux il faudra penser à installer les éléments de base pour le développement et la compilation. Dans les distributions dérivées de Debian, cela passe par la commande suivante :
sudo apt update && sudo apt install build-essential
Sous Windows il faut obligatoirement récupérer la dernière version en date de rustup puis l'exécuter. Une étape préalable est là aussi nécessaire : l'installation des outils de développement Visual C++ (Build Tools). Le SDK Windows 10 et le pack de langue anglaise doivent être sélectionnés, soit 4,85 Go tout de même :
C'est la seule étape qui peut éventuellement prendre du temps. Si vous ne voulez pas en passer par là, utilisez le sous-système Linux, un petit serveur ou même un Raspberry Pi. Pour simplement tester du code, sans le compiler localement, l'équipe de Rust met à votre disposition un bac à sable en ligne.
Dans la pratique, rustup vérifie que les pré-requis sont en place, récupère et installe les outils dans une version adaptée à votre système et ne vous posera que peu de questions. Dans la procédure personnalisée, vous pourrez choisir le dossier (path) d'installation, le canal utilisé (stable, beta, nightly) mais aussi entre trois profils d'installation : par défaut, minimale ou complète. Laissez les paramètres initiaux (voir ci-dessous).
Notez aussi la notion de « host triple », il s'agit de la suite d'outils qui sera installée pour l'hôte détecté. Dans le cas de Windows, ce sont ceux de Visual C++ (MSVC) par défaut. Vous pouvez également opter pour GCC si vous le préférez, la méthode à suivre est décrite dans la documentation. Comme nous le verrons tout au long de ce dossier, la documentation complète et plutôt claire de l'écosystème Rust est d'ailleurs l'une de ses forces.
Vous pouvez alors ajouter le support de l'auto-complétion, disponible pour plusieurs interpréteurs de commandes : Bash, Fish, PowerShell ou Zsh. Une fois la procédure terminée, pensez à fermer puis rouvrir votre terminal.
L'installation dans le sous-système Linux (WSL), elle sera similaire sous macOS et Windows
Pour mettre à jour les composants de Rust, rien de plus simple :
rustup update
- Hello, World!
Pour vous montrer à quel point compiler votre première application Rust est simple, nous allons le faire en quelques commandes seulement. Attention, cela va aller très vite :
cargo new hello-nxi
cd hello-nxi
cargo run
Et voilà ! Vous avez créé, compilé et exécuté votre première application Rust. L'exécutable se trouve dans le dossier target/debug
. Vous pouvez le compiler en version de production en précisant un argument :
cargo run --release
Il sera alors généré dans une version plus optimisée dans le dossier target/release
que vous pourrez librement distribuer. Vous pouvez simplement compiler l'application sans la lancer en utilisant la commande cargo build
. Si vous disposez d'un gestionnaire de versions (VCS) comme Git d'installé, un dépôt sera automatiquement initialisé. Si vous ne voulez pas que ce soit le cas ajoutez --VCS none
à la création du projet (cargo new
).
Point intéressant, l'application n'a pas la même taille sur tous les systèmes. Par exemple celle générée sous Windows 10 (hors WSL) se contente de 150 ko, contre 3 Mo pour sa version Linux. Bien que les améliorations aient déjà été nombreuses depuis les premières critiques sur ce point, Rust continue d'évoluer au fil des versions.
Il existe quelques astuces pour réduire la taille du fichier généré si cela vous pose souci. De la « cross compilation » est possible, mais encore un peu complexe à mettre en place à ce stade. Nous y reviendrons plus tard.
Premières modifications et création d'une macro
On aurait pu s'arrêter là, mais allons tout de même un peu plus loin, en commençant par vous expliquer ce qui s'est passé. À la racine du projet, un fichier Cargo.toml
a été créé. Son extension indique qu'il utilise le format TOML. Il s'agit du fichier de configuration (ou manifeste) de votre application. Si vous l'éditez, vous verrez son nom, celui de l'auteur, sa version, l'édition de Rust (2018) et un emplacement pour ses dépendances. Nous en reparlerons.
Puis il y a le dossier src
, contenant le code source de l'application. Il se compose seulement d'un fichier main.rs
pour le moment. Vous pouvez d'ailleurs l'utiliser directement avec le compilateur rustc sans passer par Cargo :
rustc src/main.rs
Cela va créer un exécutable nommé main
à la racine (supprimez-le ensuite). Ouvrons le projet, dans notre cas avec Visual Studio Code, mais vous pouvez lui préférer Atom, Brackets, Notepad++ ou encore Sublime Text. Nombreux sont d'ailleurs les environnements de développement (IDE) et éditeurs disposant d'un support natif ou d'un plugin pour Rust (et TOML). Même Emacs, nano (depuis la version 2.6.1) et vim ! Pensez à les installer.
Comme nous utilisons le sous-système Linux de Windows 10, Visual Studio Code a un avantage : il peut être utilisé pour développer depuis le système hôte via son dispositif d'accès distant aux données. Pour cela, il suffit de le lancer comme depuis n'importe quelle plateforme, il installera son serveur la première fois :
code .
Le code de départ est assez simple, se composant d'une fonction main()
affichant le classique « Hello, World ! » :

Il utilise pour cela une première spécificité de rust : les macros. Il s'agit de sorte de « super fonctions » distinguées par un point d'exclamation à la fin de leur nom. Elles sont « étendues » au sein du code source avant la phase de compilation plutôt qu'à l'exécution, entre autres spécificités intéressantes.
Dans le cas présent, println!()
permet d'afficher du texte (string literal) pouvant contenir des valeurs ou même des expressions positionnées via des accolades {}
. Il le fait suivre d'un saut de ligne contrairement à print!()
.
Par exemple cette commande ne poserait aucun problème et affichera « Hello, World 42! » :
println!("{}, {crowd} {1}!", "Hello", 2*21, crowd = "World");
On peut adapter le comportement des macros selon la façon dont elles sont appelées. Pour le voir, ajoutons-en une au début de notre code, hors de la fonction principale. Elle sera nommée customPrint
:
macro_rules! customPrint {
() => {
println!("Hello, World!");
};
($dest:expr) => {
println!("Hello, {}!", $dest);
};
($act:expr => $dest:expr) => {
println!("{} for {}!", $act, $dest);
};
}
Elle pourra agir de trois manières différentes. Soit l'utilisateur n'utilise aucune expression dans son appel et le message « Hello, World! » sera affiché. S'il ajoute une simple expression, le texte sera personnalisé avec son contenu. S'il utilise la forme spécifique « Action => Destinataire » et les deux termes seront personnalisés.
Si vous compilez ce code, tout fonctionnera comme avant, mais vous aurez un avertissement du compilateur vous indiquant qu'elles ne sont pas utilisées. On remercie ici l'outil clippy de Rust qui analyse le code et donne des conseils au développeur. Par exemple, s'il ne suit pas la convention pour le nom d'une fonction.
Vous pouvez d'ailleurs le lancer manuellement hors d'une compilation :
cargo clippy
D'autres outils sont installés dans la configuration de base de Rust via rustup comme cargo fmt
qui permet de formater correctement vos fichiers ou cargo fix
pour corriger automatiquement certains problèmes.
Pour corriger l'alerte, on ajoute le code suivant à main()
qui exploite les possibilités de notre macro :
customPrint!();
customPrint!("You");
customPrint!("Hurrah" => "Next INpact");
Comme prévu, trois messages seront affichés les uns à la suite des autres :
L'alerte affichée quand notre macro n'est pas exploitée, puis le résultat final
Gestion de paquets et dépendances
Bien qu'il s'agisse d'un langage bas niveau, Rust vient avec des outils permettant une gestion « moderne ». C'est notamment le cas de la distribution de paquets, inspirée de ce que l'on trouve avec npm pour Node.js ou en Python avec PyPi par exemple. Chacun peut créer les siens et les mettre à disposition de tous.
Dans l'écosystème Rust, on parle de Crates, qui viennent compléter les bibliothèques de base du langage. Ils sont référencés sur Crates.io et peuvent être ajoutés au sein du projet simplement en ajoutant leur nom et version à la liste des dépendances du manifeste. Notez qu'un registre alternatif existe : Lib.rs.
Par exemple, le crate num_cpus permet de connaître le nombre de cœurs du processeur de la machine. Pour l'utiliser dans sa dernière version, on ajoute cette ligne dans la section dependencies
du manifeste Cargo.toml
:
num_cpus = "*"
On peut également utiliser un numéro de version précis ou d'autres indications. Une fois le manifeste enregistré, num_cpus
est directement exploitable dans notre code. Lors de la prochaine compilation, les fichiers nécessaires seront téléchargés et ajoutés au projet. Par exemple, pour afficher le nombre de cœurs physiques du processeur de la machine et le nombre de threads qu'il peut gérer, on ajoute ces deux lignes dans la fonction main()
:
p
rintln!("CPU Cores : {}", num_cpus::get_physical());
println!("CPU Threads : {}", num_cpus::get());
Fonction, valeur de retour et déstructuration de tuple
Pour rendre le défi un peu plus intéressant, on peut utiliser une fonction pour renvoyer directement les valeurs à afficher. Ce n'est pas très utile, mais permet de mettre en avant quelques fonctionnalités de Rust.
On ajoute pour cela une fonction dans main.rs
. En Rust, sa position importe peu.
fn get_cpu_count() -> (usize, usize)
{
(num_cpus::get(), num_cpus::get_physical())
}
Si vous avez déjà goûté à d'autres langages, vous aurez compris que get_cpu_count()
renvoie un tuple. Il s'agit d'un groupement de valeurs très flexible puisqu'il n'a pas de taille fixe et peut être composé de différents types. Ici, il s'agit de deux entiers non signés usize, puisque c'est le type de valeur renvoyé par le crate num_cpus
.
Vous noterez que la seule ligne qui compose cette fonction ne termine pas par un « ; ». C'est parce que c'est sa dernière expression, qui indique la valeur de retour. Si la dernière expression d'une fonction finit par « ; », sa valeur de retour est ()
précise la documentation (à moins d'utiliser le mot-clé return
).
Ainsi, la version suivante serait également considérée comme correcte :
fn get_cpu_count() -> (usize, usize)
{
return (num_cpus::get(), num_cpus::get_physical());
}
Dans la fonction main()
, on ajoute les deux lignes suivantes pour utiliser la fonction :
let (cpu_log, cpu_phy) = get_cpu_count();
println!("CPU Cores : {}, CPU Threads : {}", cpu_phy, cpu_log);
La première consiste à déstructurer le tuple de retour de la fonction get_gpu_count()
pour attribuer ses deux valeurs à cpu_log
et cpu_phy
... en une seule ligne. On utilise ensuite ces valeurs dans un même message.
Modules et découpage en plusieurs fichiers
Imaginons maintenant que nous voulions organiser notre code sous la forme de différents fichiers réutilisables. Vous utiliserez pour cela des modules. Créez un fichier cpu.rs
et déplacez-y get_cpu_count()
. On ajoute le mot clé pub
devant sa déclaration pour indiquer qu'elle est publique et peut donc être accédée depuis l'extérieur.
Dans main.rs
, get_cpu_count()
devient cpu::get_cpu_count()
, le nom dépendant de celui donné au fichier contenant le module. Pour le référencer on ajoute cette ligne en tête de fichier :
mod cpu;
Si vous recompilez votre code, il fonctionnera comme avant. Mais désormais, vous pouvez utiliser cpu.rs
dans d'autres projets. Vous pouvez regrouper plusieurs modules ensemble (dans un même fichier ou plusieurs) et déclarer leurs propres dépendances. C'est la définition de ce qui compose un crate.
Vous trouverez le code final de notre projet et ses différentes étapes d'évolution sur GitHub. Dans notre prochain article, nous reviendrons plus en détail sur les spécificités et la syntaxe de Rust.