Les crates avec Cargo
On l’a évoqué précédemment, les modules de Rust permettent d’organiser son code et de créer des barrières d’encapsulation, mais ils ne sont pas une unité de compilation. Ce qui joue ce rôle en Rust, ce sont les crates, un concept général qui regroupe tous les produits possibles de la compilation.
Que vous soyez en train de travailler sur un programme ou une bibliothèque, du point de vue de Rust, le source associé est composé d’une ou plusieurs crates, chacune de ces crates est traitée comme une unité de compilation, et le but du processus de compilation est de transformer chaque crate en un objet binaire du bon type (exécutable, archive statique, shared object/DLL…).
Rôle de Cargo
Si vous avez installé un environnement de développment Rust en
local, alors vous avez d’ors et déjà créé une ou
plusieurs crates avec la commande cargo new
.
En Rust, la façon recommandée de créer, gérer et compiler des crates est d’utiliser Cargo. Il s’agit d’un outil qui remplit plusieurs fonctions normalement assurées par des outils séparés dans l’environnement de développement C++ traditionnel :
- Il permet de sélectionner de bonnes versions des dépendances de votre projet, les télécharger et les compiler automatiquement, à la manière de Spack et des gestionnaires de paquets plus orientés systèmes d’exploitation (APT, DNF, Zypper, Brew, portage…).
- Il permet de configurer le processus de compilation, à la manière de CMake et Meson.
- Il détecte ce qui doit être (re-)compilé, dans quel ordre, et gère l’ordonnancement et l’exécution du processus de compilation parallèle, à la manière de GNU Make et Ninja.
- Il s’interface avec les fonctionnalités de documentation (analogues à Doxygen), de test unitaire et de benchmarking du compilateur Rust pour permettre de lancer tout ça facilement.
- Il s’interface également avec les outils d’analyse statique
rustfmt
etclippy
fournis avec le compilateur Rust pour fournir du formatage automatique et des lints plus poussés.
Grâce à cette centralisation des fonctionnalités, et à certains choix de conception plus heureux que ceux de la pile C++ traditionnelle (configuration déclarative, accent sur l’édition de lien statique et la réduction des dépendances vis à vis des bibliothèques/outils de l’OS hôte…), cargo rend le processus de compilation typique beaucoup plus simple et sans bavure que ce à quoi on est habitué en C++.
Ainsi, lorsqu’on compile un programme ou une bibliothèque Rust, la norme est que
ça fonctionne du premier coup, sans aucune étape préparatoire, en une simple
commande cargo build
ou cargo install
. Et les rares fois où l’on rencontre
des problèmes, ils sont quasiment toujours liés aux dépendances C/++ auxquelles
on n’a pas réussi à se soustraire, comme openSSL ou HDF5. Cela peut expliquer
en partie, sans l’excuser, la tendance souvent observée des personnes
nouvellement formées à Rust à vouloir réécrire l’intégralité de leurs
dépendances dans ce langage.
Utilisation de Cargo
Nous avons déjà présenté brièvement l’utilisation de Cargo dans le chapitre sur
l’installation d’un environnement de développement local, il est maintenant
temps de détailler un peu plus. Voici un petit tour d’horizon des principales
commandes disponibles après avoir fraîchement installé un environnement de
développement Rust via rustup
:
cargo new
permet de créer une nouvelle crate avec un squelette de code et une configuration minimale. Par défaut on crée un exécutable, l’option--lib
permet de créer une bibliothèque.- Un grand nombre de commandes permettent de construire et exécuter des
binaires :
cargo build
compile le programme sans l’exécuter.cargo run
compile le programme, puis l’exécute.cargo test
compile et exécute les tests unitaires, d’intégration, et les exemples de la documentation intégrée au code.cargo bench
compile et exécute les microbenchmarks du code.
- Plusieurs outils d’analyse statique sont intégrés d’office :
cargo check
vérifie que le programme passe les vérifications de typage et les lints du compilateur, sans essayer de construire un binaire.cargo fix
permet d’appliquer automatiquement les suggestions fournies par certaines lints decargo check
à votre code.cargo clippy
applique des lints plus agressives, qui voient des choses que les lints de base ne voient pas mais au prix d’un taux de faux positifs plus élevé.cargo fmt
formate votre code selon des normes communément admises par la communauté Rust.cargo doc
génère automatiquement une documentation HTML de référence à partir de commentaires spéciaux dans votre code, à la manière de Doxygen.
- D’autres commandes relèvent plutôt de la gestion de paquets, elles
fonctionnent par défaut en utilisant le dépôt public de paquets
crates.io.
cargo install
permet d’installer des exécutables depuis leur code source et les mettre dans lePATH
, etcargo uninstall
permet de les désinstaller.cargo add
permet d’ajouter facilement des dépendances à son projet, etcargo remove
permet de les enlever.cargo tree
permet de visualiser l’arbre des dépendances de son projet.cargo update
permet de mettre à jour ses dépendances en respectant SemVer.- D’autres commandes permettent de publier facilement ses bibliothèques.
En sus de ça, cargo
dispose d’un système de plug-ins qui permettent
d’ajouter d’autres sous-commandes cargo. On peut par exemple mentionner…
cargo-outdated
pour visualiser l’intégralité des mises à jour de dépendances disponibles, y compris celles qui changent d’API et nécessitent des adaptations manuelles.cargo-criterion
pour exécuter plus efficacement des microbenchmarks basés surcriterion
(l’analyse est centralisée au lieu d’être dupliquée dans chaque benchmark).cargo-show-asm
pour visualiser l’assembleur de ses fonctions quand on optimise son code.cargo-miri
qui analyse dynamiquement le code unsafe en vérifiant l’absence de comportement indéfini, à la manière de Valgrind et des sanitizers en C++.cargo-llvm-lines
qui permet de détecter le code bloat associé aux fonctions génériques quand on abuse du polymorphisme statique, pour éliminer plus facilement les désagréments associés (compilation lente, gros binaires…).
Configuration de Cargo
A la racine du code source de votre projet, cargo new
crée un fichier
Cargo.toml
qui permet de configurer le comportement de Cargo. On peut par
exemple y spécifier…
- Des options de compilation (ex : activer les informations de déboguage en
mode release pour pouvoir profiler son code avec
perf
ou VTune). - Des métadonnées (ex : nom d’auteur, contact, version, licence…) qui sont par exemple utilisées quand on publie son projet sur crates.io.
- Des fonctionnalités optionnelles (features) que l’utilisateur peut choisir d’activer ou non au moment de la compilation.
- La liste de ces dépendances, même si celle-ci peut aussi être gérée
avec
cargo add
etremove
.- Les dépendances peuvent être déclarées comme optionnelles, auquel cas cela crée automatiquement une feature qui porte le même nom. Une feature déclarée manuellement peut aussi activer des dépendences optionnelles. Plus d’informations ici.
Pour plus d’informations sur ce qui peut être configuré, et sur les autres façons possibles de le configurer (notamment via des fichiers dans son dossier personnel ou des variables d’environnement), ainsi que sur d’autres fonctionnalités plus avancées que je ne couvre pas dans cette introduction (gestion conjointe d’un ensemble de crates via les workspaces, profilage de la compilation…), je vous invite à consulter le manuel de Cargo.