L’équipe de développement de Rust a annoncé la sortie d'une nouvelle version de Rust, la 1.100.0. Rust est un langage de programmation qui permet à chacun de créer des logiciels fiables et performants. Parmi les nouveautés de la version stable 1.100.0, on peut citer : le type « never », l'API Allocator, l'API DropGuard, la création de types de taille nulle, le signalement des dépendances inutilisées dans Cargo, la réduction des collisions dans le cache de Cargo, et "min-publish-age" de Cargo, alias « délais de sécurité » pour les dépendances.Rust est un langage de programmation polyvalent qui met l’accent sur les performances, la sécurité des types, la concurrence et la sécurité de la mémoire. Rust prend en charge plusieurs paradigmes de programmation. Il s’inspire de concepts issus de la programmation fonctionnelle, notamment l’immuabilité, les fonctions d’ordre supérieur, les types de données algébriques et la correspondance de motifs.
Il prend également en charge la programmation orientée objet via les structs, les énumérations, les traits et les méthodes. Rust garantit la sécurité de la mémoire (c’est-à-dire que toutes les références pointent vers de la mémoire valide) sans recourir à un ramasse-miettes classique ; à la place, les erreurs de sécurité de la mémoire et les conflits d’accès sont évités par le « vérificateur d’emprunt », qui suit la durée de vie des objets référencés lors de la compilation.
L’équipe de développement de Rust a annoncé la sortie d'une nouvelle version de Rust, la 1.100.0. Rust est un langage de programmation qui permet à chacun de créer des logiciels fiables et performants. Parmi les nouveautés de la version stable 1.100.0, on peut citer : le type « never », l'API Allocator, l'API DropGuard, la création de types de taille nulle, le signalement des dépendances inutilisées dans Cargo, la réduction des collisions dans le cache de Cargo, et "min-publish-age" de Cargo.
Le développement rapide de Rust intervient quelques mois après que Google a annoncé avoir adopté Rust pour des raisons de sécurité et a constaté une réduction de 1 000 fois des vulnérabilités liées à la sécurité de la mémoire, un taux de rollback 4 fois plus faible et une réduction de 25 % du temps consacré à la révision du code par rapport au C et au C++. Google a partagé comment une stratégie de sécurité mémoire axée sur la prévention des vulnérabilités ne se contente pas de corriger les problèmes, mais aide également à aller plus vite. Selon Google, les données de 2025 continuent de valider cette approche, les vulnérabilités liées à la sécurité mémoire passant pour la première fois sous la barre des 20 % du total des vulnérabilités.
Voici la présentation des améliorations de la version 1.100.0 :
Type « never »
Après 10 ans et 5 tentatives de stabilisation infructueuses, le type « never » est enfin stable ! Cela marque également l’aboutissement de l’objectif du projet pour ce travail. Le type « never », noté ! dans le code, est le type canonique inoccupé. Il représente le type des calculs divergents – c’est-à-dire des calculs qui ne aboutissent jamais à une valeur – pour lesquels tout code suivant est inaccessible. Les expressions qui mettent fin au flux de contrôle, telles que return, break et std::process::exit, ont également le type !.
Comme il est impossible d’avoir une valeur de type « never », Rust autorise une coercition « never-to-any », permettant de transformer « ! » en n’importe quel autre type (c’est ce qui le distingue de tous les autres types inhabités). Cela peut être utilisé, par exemple, pour ne pas renvoyer de valeur depuis l’une des branches d’un « match » ou d’un « if » :
| Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | fn mrrrow(option: Option<u32>) { let value = match option { // `x` has type `u32` Some(x) => x, // `return` has type `!`, which is then coerced to `u32`, // allowing the `match` to pass type checking. None => return, }; // ... } |
Alors que le type `never` pouvait déjà être utilisé et observé de cette manière, et même être nommé dans le type de retour des fonctions (-> !), cette version permet de nommer le type never n’importe où :
| Code : | Sélectionner tout |
1 2 3 4 5 | fn infallible_unwrap<T>(res: Result<T, !>) -> T { // ^^ writing `Result<_, !>` was not allowed before let Ok(v) = res; v } |
De plus, cette version fait d’ Infallible un alias du type never. Cela rend toutes les anciennes API qui utilisaient Infallible compatibles avec l’utilisation de !. C’était le plan depuis l’introduction d’Infallible et c’est l’une des raisons pour lesquelles la stabilisation du type never a été si difficile (waffle a donné une présentation sur certaines des difficultés rencontrées lors de cette stabilisation).
Malheureusement, pour permettre la modification d’Infallible, nous avons dû introduire un changement rompant. Cette version modifie la « valeur par défaut du type never » de () à ! dans les éditions antérieures à 2024 (c’est-à-dire 2015, 2018 et 2021), à l’instar de ce qui a déjà été fait dans l’édition 2024. Ce changement devrait passer inaperçu dans la plupart des cas, mais peut affecter le code dans de rares situations (par exemple, lorsqu’on ignore le résultat d’une fonction renvoyant un paramètre générique). Notez que ce changement pourrait également amener le compilateur à détecter davantage de code comme inaccessible.
Nous avons essayé de minimiser autant que possible l’impact de ce changement rompant, en fournissant bien à l’avance des avertissements de compatibilité future et en publiant des correctifs pour les crates publiques affectées par ce changement. Si une dépendance échoue en raison de problèmes liés au type `never`, envisagez de la mettre à jour, car un correctif a peut-être déjà été publié à cet effet.
API Allocator:
Cette version marque la stabilisation des éléments centraux d’allocator_api, permettant aux auteurs de bibliothèques de définir des allocateurs personnalisés pouvant être utilisés avec certains types de la bibliothèque standard — actuellement Vec et Box. Ces fonctionnalités sont rendues possibles grâce au trait Allocator récemment stabilisé.
Il s’agit là du fondement de l’objectif du projet Allocators 1.0. Il reste encore beaucoup à faire, notamment étendre la surface de l’API utilisable avec les allocateurs personnalisés et stabiliser leur utilisation sur davantage de collections et de types de pointeurs ; cependant, maintenant que le trait principal et la conception sont en place, attendez-vous à voir d’autres éléments stabilisés prochainement !
DropGuard : exécuter une fermeture sur chaque chemin de sortie
Le trait Drop en Rust permet d’exécuter du code lorsqu’une variable sort de sa portée, que l’on utilise return, ? ou panic. Le fait que Drop fasse partie intégrante des types signifie qu’en Rust, vous n’avez jamais besoin de fermer un socket.
L’implémentation de Drop peut toutefois sembler un peu contraignante lorsque vous souhaitez simplement exécuter du code de manière inconditionnelle à la sortie d’une fonction. C’est pourquoi nous introduisons core::mem::DropGuard : un nouveau type d’aide qui exécute une fermeture lorsqu’il est abandonné.
DropGuard prend deux arguments : une variable et une fermeture. Il implémente Deref et DerefMut, ce qui vous permet de continuer à utiliser la variable normalement. Mais lorsqu’il est supprimé, il exécute la fermeture et libère la variable.
DropGuard implémente également une méthode dismiss qui renvoie la variable depuis le garde sans exécuter le destructeur. Cela permet d’implémenter rapidement des API de type « tout ou rien », semblables à des transactions, avec une annulation. Voici un exemple d’implémentation d’un analyseur syntaxique qui utilise un garde pour annuler les mutations en cas d’échec :
| Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | use std::mem::DropGuard; use std::num::ParseIntError; /// Attempt to parse all `strings` and push them into `nums`. /// If parsing any of them fails, `nums` remains unchanged. fn parse_all(nums: &mut Vec<u32>, strings: &[&str]) -> Result<(), ParseIntError> { let len = nums.len(); let mut nums = DropGuard::new(nums, move |nums| { nums.truncate(len); // ← 1. Reset the vec on failure }); for s in strings { nums.push(s.parse()?); // ← 2. If `?` returns an error, roll back } DropGuard::dismiss(nums); // ← 3. Parsing successful, dismiss the guard Ok(()) } |
Création de types de taille nulle
Alors que les directives relatives au code non sécurisé de Rust sont encore en cours d’élaboration, il est apparu clairement qu’il serait très utile de définir précisément ce que signifie la création de types de taille nulle de manière non sécurisée, et ce que nous devrions recommander aux utilisateurs pour y parvenir.
Dans Rust 1.100, cette question trouve sa réponse dans une seule fonction : std::mem::conjure_zst.
Au lieu de transmuter (), MaybeUninit::uninit().assume_init() ou diverses autres formules, il s’agit ici de la méthode dédiée pour créer une valeur d’un type de taille nulle lorsque l’on sait que cela peut être fait en toute sécurité. Si un type instancié n’a pas de taille, on sait exactement quelle valeur il doit prendre : rien.
Bien sûr, les API s’appuient souvent sur l’impossibilité de construire de tels types pour garantir la sécurité, et c’est pourquoi cette fonction est considérée comme non sûre. Par exemple, Python<'py> de PyO3 est un jeton marqueur de taille nulle qui représente la détention du GIL (global interpreter lock) de Python. De nombreuses API Python exigent la détention de ce verrou pour être sûres, mais faire apparaître un jeton de nulle part permettrait à des appels PyO3 sûrs de violer cette condition préalable.
Signaler les dépendances inutilisées dans Cargo
La compilation de dépendances qui ne sont plus utilisées ou qui ne sont nécessaires que pour des plateformes spécifiques peut ralentir les compilations. Rustc dispose du lint unused_crate_dependencies à cet effet, mais il génère des faux positifs lorsqu’il est utilisé avec Cargo, car les dépendances peuvent être partagées entre plusieurs cibles de compilation ; par exemple, dependencies.tracing-subscriber est disponible pour tous les binaires d’un paquet alors que seuls certains d’entre eux peuvent l’utiliser.
Cargo signale désormais les dépendances inutilisées aux utilisateurs, par exemple :
| Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | warning: unused dependency `tracing-subscriber` --> Cargo.toml:23:1 | 23 | tracing-subscriber.workspace = true | ^^^^^^^^^^^^^^^^^^ | = note: `cargo::unused_dependencies` is set to `warn` by default help: consider removing the dependency on `tracing-subscriber` |
Une limite de cet avertissement est qu’il n’apparaîtra que lors de la compilation de toutes les cibles de compilation susceptibles d’utiliser une entrée d’une table de dépendances. Par exemple, l’exécution de cargo check --bin foo n’affichera pas d’avertissements concernant les [dependencies] inutilisées, car un autre binaire utilisant ces dépendances pourrait être présent. Aucun avertissement ne s’affichera pour les dépendances de développement inutilisées, car il n’existe aucun moyen de garantir que chaque cible de compilation de développement soit effectivement compilée.
Cet avertissement est introduit dans le cadre d’un nouveau système de linting pour Cargo, contrôlé par la table [lints.cargo] dans Cargo.toml, ce qui permet d’atteindre cet objectif du projet. Voir également les autres lints qui ont été introduits.
Réduction des conflits dans le cache de Cargo
Cargo compile toutes les bibliothèques et tous les binaires dans build-dir, puis transfère les artefacts finaux vers le répertoire cible target-dir. À ce stade, ceux-ci partagent un répertoire parent commun. Différentes configurations de compilation, telles que --features, peuvent entraîner la présence de copies différentes des bibliothèques et des binaires dans build-dir. Ces copies sont rendues uniques par un hachage de configuration de compilation, mais ce hachage n’était pas toujours appliqué en raison de contraintes liées à la plateforme. Cela peut entraîner des recompilations superflues, des recompilations manquées, une confusion quant aux binaires ou bibliothèques utilisés, etc. Ces problèmes s’aggravent lorsque build-dir est partagé entre plusieurs arborescences Git.
build-dir applique désormais systématiquement le hachage de configuration de compilation à l’ensemble du contenu en organisant les répertoires en fonction de leur étape de compilation, plutôt que de leur type de fichier, et en déplaçant le hachage du nom du fichier vers le répertoire parent.
Ce travail a débuté avec...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.