Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Le Rust unsafe

Tout le code que nous avons vu jusqu’à présent bénéficiait des garanties de sécurité mémoire de Rust appliquées au moment de la compilation. Cependant, Rust contient un second langage caché qui n’applique pas ces garanties de sécurité mémoire : il s’appelle Rust unsafe et fonctionne comme le Rust normal mais nous donne des super-pouvoirs supplémentaires.

Le Rust unsafe existe car, par nature, l’analyse statique est conservatrice. Lorsque le compilateur essaie de déterminer si le code respecte les garanties, il vaut mieux pour lui rejeter certains programmes valides que d’accepter des programmes invalides. Bien que le code puisse être correct, si le compilateur Rust n’a pas assez d’informations pour en être certain, il rejettera le code. Dans ces cas, vous pouvez utiliser du code unsafe pour dire au compilateur : “Fais-moi confiance, je sais ce que je fais.” Soyez cependant averti que vous utilisez le Rust unsafe à vos propres risques : si vous utilisez du code unsafe incorrectement, des problèmes liés à l’insécurité mémoire peuvent survenir, comme le déréférencement de pointeur nul.

Une autre raison pour laquelle Rust à un alter ego unsafe est que le matériel informatique sous-jacent est intrinsèquement non sécurisé. Si Rust ne vous permettait pas de faire des opérations unsafe, vous ne pourriez pas accomplir certaines tâches. Rust doit vous permettre de faire de la programmation système bas niveau, comme interagir directement avec le système d’exploitation ou même écrire votre propre système d’exploitation. Travailler avec la programmation système bas niveau est l’un des objectifs du langage. Explorons ce que nous pouvons faire avec le Rust unsafe et comment le faire.

Utiliser les super-pouvoirs unsafe

Pour passer en Rust unsafe, utilisez le mot-clé unsafe puis ouvrez un nouveau bloc contenant le code unsafe. Vous pouvez effectuer cinq actions en Rust unsafe que vous ne pouvez pas faire en Rust safe, que nous appelons les super-pouvoirs unsafe. Ces super-pouvoirs incluent la capacité de :

  1. Déréférencer un pointeur brut.
  2. Appeler une fonction ou méthode unsafe.
  3. Accéder ou modifier une variable statique mutable.
  4. Implémenter un trait unsafe.
  5. Accéder aux champs de unions.

Il est important de comprendre que unsafe ne désactive pas le vérificateur d’emprunt ni aucune des autres vérifications de sécurité de Rust : si vous utilisez une référence dans du code unsafe, elle sera toujours vérifiée. Le mot-clé unsafe ne vous donne accès qu’à ces cinq fonctionnalités qui ne sont alors pas vérifiées par le compilateur pour la sécurité mémoire. Vous bénéficierez toujours d’un certain degré de sécurité à l’intérieur d’un bloc unsafe.

De plus, unsafe ne signifie pas que le code à l’intérieur du bloc est nécessairement dangereux ou qu’il aura forcément des problèmes de sécurité mémoire : l’intention est que vous, en tant que programmeur, vous assurerez que le code à l’intérieur d’un bloc unsafe accédera à la mémoire de manière valide.

Les gens sont faillibles et les erreurs arrivent, mais en exigeant que ces cinq opérations unsafe soient dans des blocs annotés avec unsafe, vous saurez que toute erreur liée à la sécurité mémoire doit se trouver dans un bloc unsafe. Gardez les blocs unsafe petits ; vous vous en féliciterez plus tard lorsque vous chercherez des bogues mémoire.

Pour isoler le code unsafe autant que possible, il est préférable d’encapsuler ce code dans une abstraction sûre et de fournir une API sûre, ce dont nous discuterons plus loin dans le chapitre lorsque nous examinerons les fonctions et méthodes unsafe. Des parties de la bibliothèque standard sont implémentées comme des abstractions sûres par-dessus du code unsafe qui a été audité. Encapsuler du code unsafe dans une abstraction sûre empêche les utilisations d’unsafe de fuiter dans tous les endroits où vous ou vos utilisateurs pourriez vouloir utiliser la fonctionnalité implémentée avec du code unsafe, car utiliser une abstraction sûre est sûr.

Examinons chacun des cinq super-pouvoirs unsafe à tour de rôle. Nous verrons aussi quelques abstractions qui fournissent une interface sûre au code unsafe.

Déréférencer un pointeur brut

Au chapitre 4, dans la section « Les références pendantes », nous avons mentionné que le compilateur s’assuré que les références sont toujours valides. Le Rust unsafe possède deux nouveaux types appelés pointeurs bruts qui sont similaires aux références. Comme les références, les pointeurs bruts peuvent être immuables ou mutables et s’écrivent respectivement *const T et *mut T. L’astérisque n’est pas l’opérateur de déréférencement ; il fait partie du nom du type. Dans le contexte des pointeurs bruts, immuable signifie que le pointeur ne peut pas être directement assigné après avoir été déréférencé.

Contrairement aux références et aux pointeurs intelligents, les pointeurs bruts :

  • Peuvent ignorer les règles d’emprunt en ayant à la fois des pointeurs immuables et mutables ou plusieurs pointeurs mutables vers le même emplacement
  • Ne sont pas garantis de pointer vers de la mémoire valide
  • Sont autorisés à être nuls
  • N’implémentent aucun nettoyage automatique

En renonçant à ce que Rust applique ces garanties, vous pouvez abandonner la sécurité garantie en échange de meilleures performances ou de la capacité d’interfacer avec un autre langage ou du matériel où les garanties de Rust ne s’appliquent pas.

L’encart 20-1 montre comment créer un pointeur brut immuable et un pointeur brut mutable.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;
}
Listing 20-1: Creating raw pointers with the raw borrow operators

Remarquez que nous n’incluons pas le mot-clé unsafe dans ce code. Nous pouvons créer des pointeurs bruts dans du code safe ; nous ne pouvons simplement pas déréférencer des pointeurs bruts en dehors d’un bloc unsafe, comme vous le verrez bientôt.

Nous avons créé des pointeurs bruts en utilisant les opérateurs d’emprunt brut : &raw const num crée un pointeur brut immuable *const i32, et &raw mut num crée un pointeur brut mutable *mut i32. Comme nous les avons créés directement à partir d’une variable locale, nous savons que ces pointeurs bruts particuliers sont valides, mais nous ne pouvons pas faire cette hypothèse pour n’importe quel pointeur brut.

Pour illustrer cela, nous allons ensuite créer un pointeur brut dont nous ne pouvons pas être aussi certains de la validité, en utilisant le mot-clé as pour convertir une valeur au lieu d’utiliser l’opérateur d’emprunt brut. L’encart 20-2 montre comment créer un pointeur brut vers un emplacement arbitraire en mémoire. Essayer d’utiliser de la mémoire arbitraire est un comportement indéfini : il peut y avoir des données à cette adresse ou non, le compilateur peut optimiser le code de sorte qu’il n’y ait pas d’accès mémoire, ou le programme peut se terminer par une erreur de segmentation. En général, il n’y a pas de bonne raison d’écrire du code comme celui-ci, surtout dans les cas où vous pouvez utiliser un opérateur d’emprunt brut à la place, mais c’est possible.

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}
Listing 20-2: Creating a raw pointer to an arbitrary memory address

Rappelez-vous que nous pouvons créer des pointeurs bruts dans du code safe, mais nous ne pouvons pas déréférencer des pointeurs bruts et lire les données pointées. Dans l’encart 20-3, nous utilisons l’opérateur de déréférencement * sur un pointeur brut, ce qui nécessite un bloc unsafe.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;

    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}
Listing 20-3: Dereferencing raw pointers within an unsafe block

Créer un pointeur ne fait aucun mal ; c’est seulement lorsque nous essayons d’accéder à la valeur vers laquelle il pointe que nous risquons de traiter une valeur invalide.

Notez aussi que dans les encarts 20-1 et 20-3, nous avons créé des pointeurs bruts *const i32 et *mut i32 qui pointaient tous les deux vers le même emplacement mémoire, où num est stocké. Si nous avions essayé de créer une référence immuable et une référence mutable vers num, le code n’aurait pas compilé car les règles de possession de Rust n’autorisent pas une référence mutable en même temps que des références immuables. Avec les pointeurs bruts, nous pouvons créer un pointeur mutable et un pointeur immuable vers le même emplacement et modifier les données via le pointeur mutable, créant potentiellement une situation de compétition de données. Soyez prudent !

Avec tous ces dangers, pourquoi utiliseriez-vous des pointeurs bruts ? Un cas d’utilisation majeur est l’interfaçage avec du code C, comme vous le verrez dans la section suivante. Un autre cas est la construction d’abstractions sûres que le vérificateur d’emprunt ne comprend pas. Nous allons présenter les fonctions unsafe puis examiner un exemple d’abstraction sûre utilisant du code unsafe.

Appeler une fonction ou méthode unsafe

Le deuxième type d’opération que vous pouvez effectuer dans un bloc unsafe est l’appel de fonctions unsafe. Les fonctions et méthodes unsafe ressemblent exactement aux fonctions et méthodes normales, mais elles ont un unsafe supplémentaire avant le reste de la définition. Le mot-clé unsafe dans ce contexte indique que la fonction à des exigences que nous devons respecter lorsque nous l’appelons, car Rust ne peut pas garantir que nous les avons satisfaites. En appelant une fonction unsafe dans un bloc unsafe, nous disons que nous avons lu la documentation de cette fonction et que nous prenons la responsabilité de respecter les contrats de la fonction.

Voici une fonction unsafe nommée dangerous qui ne fait rien dans son corps : rust {{#rustdoc_include ../listings/ch20-advanced-features/no-listing-01-unsafe-fn/src/main.rs:here}}

fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

Nous devons appeler la fonction dangerous dans un bloc unsafe séparé. Si nous essayons d’appeler dangerous sans le bloc unsafe, nous obtiendrons une erreur : console {{#include ../listings/ch20-advanced-features/output-only-01-missing-unsafe/output.txt}}

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on how to avoid undefined behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Avec le bloc unsafe, nous affirmons à Rust que nous avons lu la documentation de la fonction, que nous comprenons comment l’utiliser correctement, et que nous avons vérifié que nous respectons le contrat de la fonction.

Pour effectuer des opérations unsafe dans le corps d’une fonction unsafe, vous devez toujours utiliser un bloc unsafe, comme dans une fonction normale, et le compilateur vous avertira si vous oubliez. Cela nous aide à garder les blocs unsafe aussi petits que possible, car les opérations unsafe peuvent ne pas être nécessaires dans tout le corps de la fonction.

Créer une abstraction sûre par-dessus du code unsafe

Ce n’est pas parce qu’une fonction contient du code unsafe que nous devons marquer toute la fonction comme unsafe. En fait, encapsuler du code unsafe dans une fonction sûre est une abstraction courante. À titre d’exemple, étudions la fonction split_at_mut de la bibliothèque standard, qui nécessite du code unsafe. Nous allons explorer comment nous pourrions l’implémenter. Cette méthode sûre est définie sur les slices mutables : elle prend une slice et la divise en deux en la scindant à l’indice donné en argument. L’encart 20-4 montre comment utiliser split_at_mut.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];

    let r = &mut v[..];

    let (a, b) = r.split_at_mut(3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}
Listing 20-4: Using the safe split_at_mut function

Nous ne pouvons pas implémenter cette fonction en utilisant uniquement du Rust safe. Une tentative pourrait ressembler à l’encart 20-5, qui ne compilera pas. Par souci de simplicité, nous allons implémenter split_at_mut comme une fonction plutôt qu’une méthode et uniquement pour des slices de valeurs i32 plutôt que pour un type générique T.

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-5: An attempted implementation of split_at_mut using only safe Rust

Cette fonction obtient d’abord la longueur totale de la slice. Ensuite, elle vérifie par une assertion que l’indice donné en paramètre se trouve dans la slice en vérifiant s’il est inférieur ou égal à la longueur. L’assertion signifie que si nous passons un indice supérieur à la longueur pour scinder la slice, la fonction paniquera avant de tenter d’utiliser cet indice.

Ensuite, nous retournons deux slices mutables dans un tuple : une du début de la slice originale jusqu’à l’indice mid et une autre de mid jusqu’à la fin de la slice.

Lorsque nous essayons de compiler le code de l’encart 20-5, nous obtenons une erreur : console {{#include ../listings/ch20-advanced-features/listing-20-05/output.txt}}

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:6:31
  |
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |                         - let's call the lifetime of this reference `'1`
...
6 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Le vérificateur d’emprunt de Rust ne peut pas comprendre que nous empruntons des parties différentes de la slice ; il sait seulement que nous empruntons la même slice deux fois. Emprunter des parties différentes d’une slice est fondamentalement correct car les deux slices ne se chevauchent pas, mais Rust n’est pas assez intelligent pour le savoir. Quand nous savons que le code est correct, mais que Rust ne le sait pas, il est temps de recourir au code unsafe.

L’encart 20-6 montre comment utiliser un bloc unsafe, un pointeur brut et quelques appels à des fonctions unsafe pour faire fonctionner l’implémentation de split_at_mut.

use std::slice;

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-6: Using unsafe code in the implementation of the split_at_mut function

Rappelez-vous dans la section « Le type slice » du chapitre 4 que le compilateur garantit qu’une slice est toujours une référence valide vers un tableau. Lorsque nous utilisons des pointeurs bruts, nous n’avons pas cette garantie.

Nous conservons l’assertion que l’indice mid se trouve dans la slice. Ensuite, nous passons au code unsafe : la fonction slice::from_raw_parts_mut prend un pointeur brut et une longueur, et crée une slice. Nous utilisons cette fonction pour créer une slice qui commence à ptr et fait mid éléments de long. Ensuite, nous appelons la méthode add sur ptr avec mid en argument pour obtenir un pointeur brut commençant à mid, et nous créons une slice utilisant ce pointeur et le nombre restant d’éléments après mid comme longueur.

La fonction slice::from_raw_parts_mut est unsafe car elle prend un pointeur brut et doit faire confiance au fait que ce pointeur est valide. La méthode add sur les pointeurs bruts est aussi unsafe car elle doit faire confiance au fait que l’emplacement décalé est aussi un pointeur valide. C’est pourquoi nous avons dû placer un bloc unsafe autour de nos appels à slice::from_raw_parts_mut et add pour pouvoir les appeler. En examinant le code et en ajoutant l’assertion que mid doit être inférieur ou égal à len, nous pouvons affirmer que tous les pointeurs bruts utilisés dans le bloc unsafe seront des pointeurs valides vers des données dans la slice. C’est une utilisation acceptable et appropriée d’unsafe.

Notez que nous n’avons pas besoin de marquer la fonction résultante split_at_mut comme unsafe, et nous pouvons appeler cette fonction depuis du Rust safe. Nous avons créé une abstraction sûre pour le code unsafe avec une implémentation de la fonction qui utilise du code unsafe de manière sûre, car elle ne crée que des pointeurs valides à partir des données auxquelles cette fonction a accès.

En revanche, l’utilisation de slice::from_raw_parts_mut dans l’encart 20-7 provoquerait probablement un plantage lorsque la slice est utilisée. Ce code prend un emplacement mémoire arbitraire et crée une slice de 10 000 éléments.

fn main() {
    use std::slice;

    let address = 0x01234usize;
    let r = address as *mut i32;

    let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}
Listing 20-7: Creating a slice from an arbitrary memory location

Nous ne possédons pas la mémoire à cet emplacement arbitraire, et il n’y à aucune garantie que la slice créée par ce code contienne des valeurs i32 valides. Tenter d’utiliser values comme s’il s’agissait d’une slice valide entraîne un comportement indéfini.

Utiliser les fonctions extern pour appeler du code externe

Parfois, votre code Rust peut avoir besoin d’interagir avec du code écrit dans un autre langage. Pour cela, Rust possède le mot-clé extern qui facilite la création et l’utilisation d’une interface de fonctions étrangères (FFI), qui est un moyen pour un langage de programmation de définir des fonctions et de permettre à un autre langage de programmation (étranger) d’appeler ces fonctions.

L’encart 20-8 montre comment mettre en place une intégration avec la fonction abs de la bibliothèque standard C. Les fonctions déclarées dans les blocs extern sont généralement unsafe à appeler depuis du code Rust, donc les blocs extern doivent aussi être marqués unsafe. La raison est que les autres langages n’appliquent pas les règles et garanties de Rust, et Rust ne peut pas les vérifier, donc la responsabilité incombe au programmeur d’assurer la sécurité.

Filename: src/main.rs
unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Listing 20-8: Declaring and calling an extern function defined in another language

Dans le bloc unsafe extern "C", nous listons les noms et signatures des fonctions externes d’un autre langage que nous voulons appeler. La partie "C" définit quelle interface binaire d’application (ABI) la fonction externe utilise : l’ABI définit comment appeler la fonction au niveau de l’assembleur. L’ABI "C" est la plus courante et suit l’ABI du langage de programmation C. Des informations sur toutes les ABI prises en charge par Rust sont disponibles dans [la Référence Rust][ABI].

Chaque élément déclaré dans un bloc unsafe extern est implicitement unsafe. Cependant, certaines fonctions FFI sont sûres à appeler. Par exemple, la fonction abs de la bibliothèque standard C n’à aucune considération de sécurité mémoire, et nous savons qu’elle peut être appelée avec n’importe quel i32. Dans de tels cas, nous pouvons utiliser le mot-clé safe pour indiquer que cette fonction spécifique est sûre à appeler même si elle se trouve dans un bloc unsafe extern. Une fois ce changement effectué, l’appeler ne nécessite plus de bloc unsafe, comme montré dans l’encart 20-9.

Filename: src/main.rs
unsafe extern "C" {
    safe fn abs(input: i32) -> i32;
}

fn main() {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}
Listing 20-9: Explicitly marking a function as safe within an unsafe extern block and calling it safely

Marquer une fonction comme safe ne la rend pas intrinsèquement sûre ! C’est plutôt une promesse que vous faites à Rust qu’elle est sûre. Il reste de votre responsabilité de vous assurer que cette promesse est tenue !

Appeler des fonctions Rust depuis d’autres langages

Nous pouvons aussi utiliser extern pour créer une interface permettant à d’autres langages d’appeler des fonctions Rust. Au lieu de créer un bloc extern complet, nous ajoutons le mot-clé extern et spécifions l’ABI à utiliser juste avant le mot-clé fn de la fonction concernée. Nous devons aussi ajouter une annotation #[unsafe(no_mangle)] pour dire au compilateur Rust de ne pas modifier le nom de cette fonction. Le name mangling est le processus par lequel un compilateur change le nom que nous avons donné à une fonction en un nom différent contenant plus d’informations pour d’autres parties du processus de compilation, mais moins lisible par l’humain. Chaque compilateur de langage de programmation modifié les noms légèrement différemment, donc pour qu’une fonction Rust soit nommable par d’autres langages, nous devons désactiver le name mangling du compilateur Rust. C’est unsafe car il pourrait y avoir des collisions de noms entre bibliothèques sans le mangling intégré, donc c’est notre responsabilité de s’assurer que le nom choisi peut être exporté sans mangling en toute sécurité.

Dans l’exemple suivant, nous rendons la fonction call_from_c accessible depuis du code C, après compilation en bibliothèque partagée et liaison depuis C :

#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

Cette utilisation d’extern ne nécessite unsafe que dans l’attribut, pas sur le bloc extern.

Accéder ou modifier une variable statique mutable

Dans ce livre, nous n’avons pas encore parlé des variables globales, que Rust prend en charge mais qui peuvent être problématiques avec les règles de possession de Rust. Si deux threads accèdent à la même variable globale mutable, cela peut provoquer une situation de compétition de données.

En Rust, les variables globales sont appelées variables statiques. L’encart 20-10 montre un exemple de déclaration et d’utilisation d’une variable statique avec une slice de chaîne de caractères comme valeur.

Filename: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}
Listing 20-10: Defining and using an immutable static variable

Les variables statiques sont similaires aux constantes, que nous avons abordées dans la section « Déclarer des constantes » du chapitre 3. Les noms des variables statiques sont en SCREAMING_SNAKE_CASE par convention. Les variables statiques ne peuvent stocker que des références avec la durée de vie 'static, ce qui signifie que le compilateur Rust peut déterminer la durée de vie et que nous n’avons pas besoin de l’annoter explicitement. Accéder à une variable statique immuable est sûr.

Une différence subtile entre les constantes et les variables statiques immuables est que les valeurs dans une variable statique ont une adresse fixe en mémoire. Utiliser la valeur accédera toujours aux mêmes données. Les constantes, en revanche, sont autorisées à dupliquer leurs données chaque fois qu’elles sont utilisées. Une autre différence est que les variables statiques peuvent être mutables. Accéder et modifier des variables statiques mutables est unsafe. L’encart 20-11 montre comment déclarer, accéder et modifier une variable statique mutable nommée COUNTER.

Filename: src/main.rs
static mut COUNTER: u32 = 0;

/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // SAFETY: This is only called from a single thread in `main`.
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}
Listing 20-11: Reading from or writing to a mutable static variable is unsafe.

Comme pour les variables normales, nous spécifions la mutabilité avec le mot-clé mut. Tout code qui lit ou écrit depuis COUNTER doit se trouver dans un bloc unsafe. Le code de l’encart 20-11 compilé et affiche COUNTER: 3 comme nous l’attendrions car il est mono-thread. Avoir plusieurs threads accédant à COUNTER entraînerait probablement des situations de compétition de données, ce qui est un comportement indéfini. Par conséquent, nous devons marquer la fonction entière comme unsafe et documenter la limitation de sécurité afin que quiconque appelle la fonction sache ce qu’il est autorisé ou non à faire de manière sûre.

Chaque fois que nous écrivons une fonction unsafe, il est idiomatique d’écrire un commentaire commençant par SAFETY et expliquant ce que l’appelant doit faire pour appeler la fonction de manière sûre. De même, chaque fois que nous effectuons une opération unsafe, il est idiomatique d’écrire un commentaire commençant par SAFETY pour expliquer comment les règles de sécurité sont respectées.

De plus, le compilateur refusera par défaut toute tentative de créer des références vers une variable statique mutable via un lint du compilateur. Vous devez soit explicitement désactiver les protections de ce lint en ajoutant une annotation #[allow(static_mut_refs)], soit accéder à la variable statique mutable via un pointeur brut créé avec l’un des opérateurs d’emprunt brut. Cela inclut les cas où la référence est créée de manière invisible, comme lorsqu’elle est utilisée dans le println! de cet encart de code. Exiger que les références aux variables statiques mutables soient créées via des pointeurs bruts aide à rendre les exigences de sécurité pour leur utilisation plus évidentes.

Avec des données mutables accessibles globalement, il est difficile de s’assurer qu’il n’y a pas de situations de compétition de données, c’est pourquoi Rust considère les variables statiques mutables comme unsafe. Dans la mesure du possible, il est préférable d’utiliser les techniques de concurrence et les pointeurs intelligents thread-safe que nous avons vus au chapitre 16 afin que le compilateur vérifie que l’accès aux données depuis différents threads se fait de manière sûre.

Implémenter un trait unsafe

Nous pouvons utiliser unsafe pour implémenter un trait unsafe. Un trait est unsafe lorsqu’au moins une de ses méthodes possède un invariant que le compilateur ne peut pas vérifier. Nous déclarons qu’un trait est unsafe en ajoutant le mot-clé unsafe avant trait et en marquant l’implémentation du trait comme unsafe également, comme montré dans l’encart 20-12.

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implémentations go here
}

fn main() {}
Listing 20-12: Defining and implementing an unsafe trait

En utilisant unsafe impl, nous promettons de respecter les invariants que le compilateur ne peut pas vérifier.

section in Chapter 16: The compiler implements these traits automatically if our types are composed entirely of other types that implement Send and Sync. If we implement a type that contains a type that does not implement Send or Sync, such as raw pointers, and we want to mark that type as Send or Sync, we must use unsafe. Rust can’t verify that our type upholds the guarantees that it can be safely sent across threads or accessed from multiple threads; therefore, we need to do those checks manually and indicate as such with unsafe. –> À titre d’exemple, rappelez-vous les traits marqueurs Send et Sync que nous avons vus dans la section [“La concurrence extensible avec Send et Sync”][send-and-sync] du chapitre 16 : le compilateur implémente ces traits automatiquement si nos types sont entièrement composés d’autres types qui implémentent Send et Sync. Si nous implémentons un type contenant un type qui n’implémente pas Send ou Sync, comme les pointeurs bruts, et que nous voulons marquer ce type comme Send ou Sync, nous devons utiliser unsafe. Rust ne peut pas vérifier que notre type respecte les garanties qu’il peut être envoyé entre les threads de manière sûre ou accédé depuis plusieurs threads ; par conséquent, nous devons effectuer ces vérifications manuellement et l’indiquer avec unsafe.

Accéder aux champs d’une union

L’action finale qui ne fonctionne qu’avec unsafe est d’accéder aux champs d’une union. Une union est similaire à une struct, mais une seule instance de champ déclarée est utilisée dans une instance particulière à la fois. Les unions servent principalement à s’interfacer avec les unions en code C. Accéder aux champs d’une union est unsafe car Rust ne peut pas garantir le type des données actuellement stockées dans l’instance de l’union. Vous pouvez en apprendre davantage sur les unions dans la référence Rust.

Utiliser Miri pour vérifier le code unsafe

Lorsque vous écrivez du code unsafe, vous voudrez peut-être vérifier que ce que vous avez écrit est réellement sûr et correct. L’un des meilleurs moyens de le faire est d’utiliser Miri, un outil officiel de Rust pour détecter les comportements indéfinis. Alors que le vérificateur d’emprunt est un outil statique qui fonctionne au moment de la compilation, Miri est un outil dynamique qui fonctionne à l’exécution. Il vérifie votre code en exécutant votre programme, ou sa suite de tests, et en détectant quand vous violez les règles qu’il comprend sur le fonctionnement de Rust.

L’utilisation de Miri nécessite une version nightly de Rust (dont nous parlons davantage dans [l’Annexe G : Comment Rust est fait et “Nightly Rust”][nightly]). Vous pouvez installer à la fois une version nightly de Rust et l’outil Miri en tapant rustup +nightly component add miri. Cela ne change pas la version de Rust utilisée par votre projet ; cela ajouté seulement l’outil à votre système pour que vous puissiez l’utiliser quand vous le souhaitez. Vous pouvez exécuter Miri sur un projet en tapant cargo +nightly miri run ou cargo +nightly miri test.

Pour un exemple de l’utilité de cet outil, voyons ce qui se passe lorsque nous l’exécutons sur l’encart 20-7. console {{#include ../listings/ch20-advanced-features/listing-20-07/output.txt}}

$ cargo +nightly miri run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
 --> src/main.rs:5:13
  |
5 |     let r = address as *mut i32;
  |             ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
  |
  = help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
  = help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
  = help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
  = help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
  = help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
  = note: BACKTRACE:
  = note: inside `main` at src/main.rs:5:13: 5:32

error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it has no provenance)
 --> src/main.rs:7:35
  |
7 |     let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
  |                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
  |
  = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
  = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
  = note: BACKTRACE:
  = note: inside `main` at src/main.rs:7:35: 7:70

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error; 1 warning emitted

Miri nous avertit correctement que nous convertissons un entier en pointeur, ce qui pourrait être un problème, mais Miri ne peut pas déterminer si un problème existe car il ne sait pas d’où provient le pointeur. Ensuite, Miri renvoie une erreur là où l’encart 20-7 à un comportement indéfini car nous avons un pointeur pendant. Grâce à Miri, nous savons maintenant qu’il y à un risque de comportement indéfini, et nous pouvons réfléchir à comment rendre le code sûr. Dans certains cas, Miri peut même faire des recommandations sur la façon de corriger les erreurs.

Miri n’attrape pas tout ce que vous pourriez mal faire en écrivant du code unsafe. Miri est un outil d’analyse dynamique, donc il ne détecte que les problèmes avec du code qui est réellement exécuté. Cela signifie que vous devrez l’utiliser en conjonction avec de bonnes techniques de test pour augmenter votre confiance dans le code unsafe que vous avez écrit. Miri ne couvre pas non plus toutes les manières possibles dont votre code peut être incorrect.

Autrement dit : si Miri détecte un problème, vous savez qu’il y à un bogue, mais ce n’est pas parce que Miri ne détecte pas un bogue qu’il n’y a pas de problème. Il peut cependant en attraper beaucoup. Essayez de l’exécuter sur les autres exemples de code unsafe de ce chapitre et voyez ce qu’il dit !

Vous pouvez en apprendre plus sur Miri dans [son dépôt GitHub][miri].

Utiliser le code unsafe correctement

Utiliser unsafe pour utiliser l’un des cinq super-pouvoirs que nous venons de voir n’est ni incorrect ni mal vu, mais il est plus délicat d’obtenir un code unsafe correct car le compilateur ne peut pas aider à maintenir la sécurité mémoire. Lorsque vous avez une raison d’utiliser du code unsafe, vous pouvez le faire, et avoir l’annotation explicite unsafe facilite la recherche de la source des problèmes lorsqu’ils surviennent. Chaque fois que vous écrivez du code unsafe, vous pouvez utiliser Miri pour vous aider à être plus confiant que le code que vous avez écrit respecte les règles de Rust.

Pour une exploration beaucoup plus approfondie de la manière de travailler efficacement avec le Rust unsafe, lisez le guide officiel de Rust pour unsafe, [le Rustonomicon][nomicon].