Unsafe en Rust
Toutes les garanties dont ce cours a parlé (pas de références pendantes, pas de data races, pas d’accès hors limites) sont imposées par le compilateur. unsafe est la façon de dire au compilateur : « laisse-moi faire quelque chose dont tu ne peux pas vérifier la sûreté ; c’est moi qui en prends la responsabilité ».
Ce que fait unsafe, et ce qu’il ne fait pas
Section intitulée « Ce que fait unsafe, et ce qu’il ne fait pas »On croit souvent que unsafe désactive complètement les vérifications de sécurité de Rust. Ce n’est pas le cas. Dans un bloc unsafe, vous pouvez faire exactement cinq choses supplémentaires, interdites le reste du temps :
- Déréférencer un pointeur brut
- Appeler une fonction
unsafe - Lire ou modifier une variable
staticmutable - Implémenter un trait
unsafe - Accéder aux champs d’une
union
Tout le reste, les règles du borrowing (emprunt), la sémantique de déplacement, la vérification des types, s’applique toujours pleinement dans le code unsafe. C’est un scalpel pour cinq opérations précises, pas un interrupteur qui coupe tout.
Les pointeurs bruts
Section intitulée « Les pointeurs bruts »Les références normales de Rust (& et &mut) pointent toujours vers des données valides et correctement alignées : cela fait partie de ce que garantit le compilateur. Un pointeur brut (raw pointer, *const T ou *mut T) abandonne cette garantie, et c’est pourquoi le lire demande unsafe :
fn main() { let value = 5; let pointer = &value as *const i32;
unsafe { println!("{}", *pointer); }}Sortie :
5Vous pouvez créer un pointeur brut dans du code sûr. Seul le fait de le déréférencer (lire la valeur vers laquelle il pointe) demande unsafe, car le compilateur ne peut plus vérifier que le pointeur est valide.
Appeler une fonction unsafe
Section intitulée « Appeler une fonction unsafe »Certaines fonctions sont elles-mêmes marquées unsafe, parce qu’elles ont des conditions que le compilateur ne peut pas vérifier à votre place. Les appeler, c’est promettre que vous avez vérifié ces conditions vous-même :
unsafe fn dangerous() { println!("cette fonction compte sur vous pour avoir vérifié ses conditions");}
fn main() { unsafe { dangerous(); }}Sortie :
cette fonction compte sur vous pour avoir vérifié ses conditionsEnvelopper unsafe dans une fonction sûre
Section intitulée « Envelopper unsafe dans une fonction sûre »La bibliothèque standard contient de vraies fonctions unsafe que vous pouvez appeler. L’une d’elles est get_unchecked, sur les slices. Elle fonctionne comme numbers[i], mais saute la vérification des limites. Si i est hors limites, elle ne panique pas : elle lit ce qui se trouve en mémoire à cet endroit, ce qu’on appelle un comportement indéfini (undefined behavior). Votre programme peut planter, afficher n’importe quoi, ou sembler fonctionner… jusqu’au jour où il ne fonctionne plus.
La règle pour l’appeler est donc « uniquement avec un indice dont vous savez qu’il est valide ». La méthode habituelle consiste à vérifier cette règle vous-même, à l’intérieur d’une fonction normale et sûre :
fn first_or_zero(numbers: &[i32]) -> i32 { if numbers.is_empty() { return 0; } // SAFETY: la slice n'est pas vide, donc l'indice 0 est dans les limites. unsafe { *numbers.get_unchecked(0) }}
fn main() { println!("{}", first_or_zero(&[7, 8, 9])); println!("{}", first_or_zero(&[]));}Sortie :
70Regardez ce que voit l’appelant : fn first_or_zero(numbers: &[i32]) -> i32. Il n’y a pas d’unsafe dans la signature, et aucun moyen de mal l’appeler, car la fonction vérifie la règle avant même d’atteindre le bloc unsafe. Le code unsafe reste petit et caché à l’intérieur.
Deux habitudes que suivent les développeurs Rust expérimentés :
- Écrire un commentaire
// SAFETY:au-dessus de chaque blocunsafe, pour expliquer pourquoi les règles sont respectées. C’est pour la prochaine personne qui lira le code, et pour vous dans le futur. Le motSAFETY:reste en anglais : c’est une convention que reconnaissent les outils comme Clippy. - Garder le bloc
unsafeaussi petit que possible, pour avoir moins de code à revérifier.
Pour être honnête, vous n’écririez pas cette fonction telle quelle dans du vrai code : numbers.first() fait la même chose de façon sûre, et le compilateur est très doué pour supprimer les vérifications de limites dont il peut prouver l’inutilité. Mais le principe (vérifier les règles, puis appeler du code unsafe à l’intérieur d’une fonction sûre) est exactement celui qui sert à construire Vec, String et compagnie. La page FFI : appeler du C utilise la même astuce pour envelopper des fonctions C.
Pourquoi unsafe existe
Section intitulée « Pourquoi unsafe existe »Si Rust tient tant à la sécurité, pourquoi autoriser ça ? Parce que certaines choses sont vraiment impossibles à prouver sûres à la compilation, alors qu’elles sont parfaitement sûres en pratique : dialoguer directement avec le matériel, appeler une bibliothèque C (voir FFI : appeler du C), ou construire certaines structures de données très performantes. unsafe permet à la bibliothèque standard (et, rarement, à votre propre code) d’implémenter ces briques de bas niveau, tandis que tout ce qui est construit par-dessus reste du Rust sûr ordinaire, entièrement vérifié.
Le compromis, en toute honnêteté
Section intitulée « Le compromis, en toute honnêteté »Écrire du code unsafe ne veut pas dire que votre code est désormais bogué. Cela veut dire que le compilateur ne peut plus détecter certaines catégories d’erreurs dans ce bloc précis, et que c’est à vous de maintenir les mêmes garanties à la main. La plupart des développeurs Rust expérimentés écrivent très peu de code unsafe directement ; il se trouve généralement dans de petits recoins soigneusement relus, tout en bas d’une bibliothèque, enveloppé dans une API sûre que tout le monde peut utiliser.
Exercices
Section intitulée « Exercices »1. Modifier une valeur à travers un pointeur brut
Section intitulée « 1. Modifier une valeur à travers un pointeur brut »Créez un let mut score = 10;, créez un pointeur brut *mut i32 vers cette variable, et utilisez le pointeur pour ajouter 5. Ensuite, affichez score. Indice : créez le pointeur avec &mut score as *mut i32, et écrivez *pointer += 5 dans un bloc unsafe.
Voir la solution
fn main() { let mut score = 10; let pointer = &mut score as *mut i32; // le créer est sûr
unsafe { *pointer += 5; // écrire à travers lui ne l'est pas }
println!("score = {score}");}Sortie :
score = 15C’est sûr en pratique, car score est toujours vivante et rien d’autre ne l’utilise pendant qu’on écrit. Tenir ces promesses devient votre travail dès que vous utilisez unsafe.
2. Un get_or sûr
Section intitulée « 2. Un get_or sûr »Écrivez fn get_or(numbers: &[i32], index: usize, default: i32) -> i32. Elle renvoie l’élément situé à index, ou default si l’indice est hors limites. Utilisez get_unchecked pour la lecture, et assurez-vous que la fonction ne puisse jamais être appelée d’une façon qui enfreint sa règle. Indice : comparez d’abord index < numbers.len(), et n’appelez get_unchecked que si c’est vrai.
Voir la solution
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 { if index < numbers.len() { // SAFETY: on vient de vérifier que index est dans les limites. unsafe { *numbers.get_unchecked(index) } } else { default }}
fn main() { let numbers = [10, 20, 30]; println!("{}", get_or(&numbers, 1, -1)); println!("{}", get_or(&numbers, 5, -1));}Sortie :
20-1L’indice 5 n’atteint jamais le bloc unsafe : il est donc traité de façon sûre. À titre de comparaison, la version entièrement sûre est numbers.get(index).copied().unwrap_or(default), et c’est ce que vous écririez normalement.
Récapitulatif
Section intitulée « Récapitulatif »unsafedébloque exactement cinq opérations (surtout les pointeurs bruts et l’appel de fonctions unsafe) : tout le reste est toujours entièrement vérifié.- Un pointeur brut (
*const T/*mut T) peut être créé dans du code sûr ; le déréférencer demandeunsafe. - Enveloppez
unsafedans une petite fonction sûre qui vérifie d’abord les règles, et expliquez pourquoi c’est correct dans un commentaire// SAFETY:. unsafeexiste pour qu’on puisse construire du code de bas niveau (accès au matériel, interopérabilité avec le C), tandis que le code qui l’utilise reste sûr.- En tant que débutant, vous aurez rarement besoin d’en écrire vous-même : cherchez d’abord une abstraction sûre.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Que débloque réellement un bloc unsafe ?
2. Peut-on créer un pointeur brut dans du code sûr ?
3. En tant que débutant, à quelle fréquence devriez-vous écrire unsafe vous-même ?
4. Écrire du code unsafe signifie-t-il que votre programme contient désormais des bugs ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Unsafe Rust — The Rust Book (en anglais) : les cinq « super-pouvoirs » d’unsafe en détail, y compris la construction d’un
split_at_mutsûr à partir de pointeurs bruts. - The Rustonomicon (en anglais) : le guide officiel pour écrire correctement du code unsafe. C’est avancé, mais le premier chapitre vaut vraiment la lecture.
- Le mot-clé
unsafe— documentation de la bibliothèque standard (en anglais) : les blocs, fonctions et traits unsafe, ainsi que la convention du commentaire// SAFETY:. - Unsafety — The Rust Reference (en anglais) : la liste exacte des opérations qui demandent
unsafe. slice::get_unchecked— documentation de la bibliothèque standard (en anglais) : une vraie fonction unsafe, dont la section « Safety » détaille la règle à respecter.