Le Rust non sécurisé (unsafe)

Tout le code Rust que nous avons abordé jusqu'à présent a bénéficié des garanties de sécurité de la mémoire, vérifiées à la compilation. Cependant Rust possède un second langage caché en son sein qui n'applique pas ces vérifications de sécurité de la mémoire : il s'appelle le Rust non sécurisé et fonctionne comme le Rust habituel, mais fournit quelques super-pouvoirs supplémentaires.

Le Rust non sécurisé existe car, par nature, l'analyse statique est conservative. Lorsque le compilateur essaye de déterminer si le code respecte ou non les garanties, il vaut mieux rejeter quelques programmes valides plutôt que d'accepter quelques programmes invalides. Bien que le code puisse être correct, si le compilateur Rust n'a pas assez d'information pour être sûr, il va refuser ce code. Dans ce cas, vous pouvez utiliser du code non sécurisé pour dire au compilateur “fais-moi confiance, je sais ce que je fait”. Le prix à payer pour cela est que vous l'utilisez à vos risques et périls : si vous écrivez du code non sécurisé de manière incorrecte, des problèmes liés à la sécurité de la mémoire peuvent se produire, tel qu'un déréférencement d'un pointeur vide.

Une autre raison pour laquelle Rust embarque son alter-ego non sécurisé est que le matériel des ordinateurs sur lequel il repose n'est pas sécurisé par essence. Si Rust ne vous laissait pas procéder à des opérations non sécurisées, vous ne pourriez pas faire certaines choses. Rust doit pouvoir vous permettre de développer du code bas-niveau, comme pouvoir interagir directement avec le système d'exploitation ou même écrire votre propre système d'exploitation. Pouvoir travailler avec des systèmes bas-niveau est un des objectifs du langage. Voyons ce que nous pouvons faire avec le Rust non sécurisé et comment le faire.

Les super-pouvoirs du code non sécurisé

Pour pouvoir utiliser le Rust non sécurisé, il faut utiliser le mot-clé unsafe et ensuite créer un nouveau bloc qui contient le code non sécurisé. Vous pouvez faire cinq actions en Rust non sécurisé, qui s'appellent les super-pouvoirs du non sécurisé, actions que vous ne pourriez pas faire en Rust sécurisé. Ces super-pouvoirs permettent de :

  • Déréférencer un pointeur brut
  • Faire appel à une fonction ou une méthode non sécurisée
  • Lire ou modifier une variable statique mutable
  • Implémenter un trait non sécurisé
  • Accéder aux champs des union

Il est important de comprendre que unsafe ne désactive pas le vérificateur d'emprunt et ne désactive pas les autres vérifications de sécurité de Rust : si vous utilisez une référence dans du code non sécurisé, elle sera toujours vérifiée. Le mot-clé unsafe vous donne seulement accès à ces cinq fonctionnalités qui ne sont alors pas vérifiées par le compilateur en vue de veiller à la sécurité de la mémoire. Vous conservez donc un certain niveau 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 obligatoirement dangereux ou qu'il va forcément présenter des problèmes de sécurité mémoire : l'idée étant qu'en tant que développeur, vous vous assuriez que le code à l'intérieur d'un bloc unsafe va accéder correctement à la mémoire.

Personne n'est parfait, les erreurs arrivent, et en imposant que ces cinq opérations non sécurisés se trouvent dans des blocs marqués d'un unsafe, Rust vous permet de savoir que ces éventuelles erreurs liées à la sécurité de la mémoire se trouveront dans un bloc unsafe. Vous devez donc essayer de minimiser la taille des blocs unsafe ; vous ne le regretterez pas lorsque vous rechercherez des bogues de mémoire.

Pour isoler autant que possible le code non sécurisé, il vaut mieux intégrer du code non sécurisé dans une abstraction et fournir ainsi une API sécurisée, comme nous le verrons plus tard dans ce chapitre lorsque nous examinerons les fonctions et méthodes non sécurisées. Certaines parties de la bibliothèque standard sont implémentées comme étant des abstractions sécurisées et basées sur du code non sécurisé qui a été audité. Encapsuler du code non sécurisé dans une abstraction sécurisée évite que l'utilisation de unsafe ne se propage dans des endroits où vous ou vos utilisateurs souhaiteraient éviter d'utiliser les fonctionnalités du code unsafe, car au final utiliser une abstraction sécurisée doit rester sûr.

Analysons ces cinq super-pouvoirs à tour de rôle. Nous allons aussi découvrir quelques abstractions qui fournissent une interface sécurisée pour faire fonctionner du code non sécurisé.

Déréférencer un pointeur brut

Au chapitre 4, dans la section “Les références pendouillantes”, nous avions mentionné que le compilateur s'assure que les références sont toujours valides. Le Rust non sécurisé offre deux nouveaux types qui s'appellent les pointeurs brut et qui ressemblent 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 un contexte de pointeur brut, immuable signifie que le pointeur ne peut pas être affecté directement après avoir été déréférencé.

Par rapport aux références et aux pointeurs intelligents, les pointeurs bruts peuvent :

  • ignorer les règles d'emprunt en ayant plusieurs pointeurs tant immuables que mutables ou en ayant plusieurs pointeurs mutables qui pointent vers le même endroit.
  • ne pas être obligés de pointer sur un emplacement mémoire valide
  • être autorisés à avoir la valeur nulle
  • ne pas implémenter de fonctionnalité de nettoyage automatique

En renonçant à ce que Rust fasse respecter ces garanties, vous pouvez sacrifier la sécurité garantie pour obtenir de meilleures performances ou avoir la possibilité de vous interfacer avec un autre langage ou matériel pour lesquels les garanties de Rust ne s'appliquent pas.

L'encart 19-1 montre comment créer un pointeur brut immuable et mutable à partir de références.

fn main() {
    let mut nombre = 5;

    let r1 = &nombre as *const i32;
    let r2 = &mut nombre as *mut i32;
}

Encart 19-1 : création de pointeurs bruts à partir de références

Remarquez que nous n'incorporons pas le mot-clé unsafe dans ce code. Nous pouvons créer des pointeurs bruts dans du code sécurisé ; nous ne pouvons simplement pas déréférencer les pointeurs bruts à l'extérieur d'un bloc non sécurisé, comme vous allez le constater d'ici peu.

Nous avons créé des pointeurs bruts en utilisant as pour transformer les références immuables et mutables en leur type de pointeur brut correspondant. Comme nous les avons créés directement à partir de références qui sont garanties d'être valides, nous savons que ces pointeurs bruts seront valides, mais nous ne pouvons pas faire cette supposition sur tous les pointeurs bruts.

Ensuite, nous allons créer un pointeur brut dont la validité n'est pas certaine. L'encart 19-2 montre comment créer un pointeur brut vers un emplacement arbitraire de la mémoire. Essayer d'utiliser de la mémoire arbitraire va engendrer un comportement incertain : il peut y avoir des données à cette adresse comme il peut ne pas y en avoir, le compilateur pourrait optimiser le code de tel sorte qu'aucun accès mémoire n'aura lieu ou bien le programme pourrait déclencher une erreur de segmentation. Habituellement, il n'y a pas de bonne raison d'écrire du code comme celui-ci, mais c'est possible.

fn main() {
    let addresse = 0x012345usize;
    let r = addresse as *const i32;
}

Encart 19-2 : création d'un pointeur brut vers une adresse mémoire arbitraire

Souvenez-vous que nous pouvons créer des pointeurs bruts dans du code sécurisé, mais que nous ne pouvons pas y déréférencer les pointeurs bruts et lire les données sur lesquelles ils pointent. Dans l'encart 19-3, nous utilisons l'opérateur de déréférencement * sur un pointeur brut qui nécessite un bloc unsafe.

fn main() {
    let mut nombre = 5;

    let r1 = &nombre as *const i32;
    let r2 = &mut nombre as *mut i32;

    unsafe {
        println!("r1 vaut : {}", *r1);
        println!("r2 vaut : {}", *r2);
    }
}

Encart 19-3 : déréférencement d'un pointeur brut à l'intérieur d'un bloc unsafe

La création de pointeur ne pose pas de problèmes ; c'est seulement lorsque nous essayons d'accéder aux valeurs sur lesquelles ils pointent qu'on risque d'obtenir une valeur invalide.

Remarquez aussi que dans les encarts 19-1 et 19-3, nous avons créé les pointeurs bruts *const i32 et *mut i32 qui pointent tous les deux au même endroit de la mémoire, où nombre est stocké. Si nous avions plutôt tenté de créer une référence immuable et une mutable vers nombre, le code n'aurait pas compilé à cause des règles de possession de Rust qui ne permettent pas d'avoir une référence mutable en même temps qu'une ou plusieurs références immuables. Avec les pointeurs bruts, nous pouvons créer un pointeur mutable et un pointeur immuable vers le même endroit et changer la donnée via le pointeur mutable, en risquant un accès concurrent. Soyez vigilant !

Avec tous ces dangers, pourquoi vous risquer à utiliser les pointeurs bruts ? Une des utilisations principale consiste à s'interfacer avec du code C, comme vous allez le découvrir dans la section suivante. Une autre utilisation est de nous permettre de créer une abstraction sécurisée que le vérificateur d'emprunt ne comprend pas. Nous allons découvrir les fonctions non sécurisées puis voir un exemple d'une abstraction sécurisée qui utilise du code non sécurisé.

Faire appel à une fonction ou une méthode non sécurisée

Le deuxième type d'opération qui nécessite un bloc unsafe est l'appel à des fonctions non sécurisées. Les fonctions et méthodes non sécurisées ressemblent exactement aux méthodes et fonctions habituelles, mais ont un unsafe en plus devant le reste de leur définition. Le mot-clé unsafe dans ce cas signifie que la fonction a des exigences que nous devons respecter pour pouvoir y faire appel, car Rust ne pourra pas garantir de son côté que nous les ayons remplies. En faisant appel à une fonction non sécurisée dans un bloc unsafe, nous reconnaissons que nous avons lu la documentation de cette fonction et pris la responsabilité de respecter les conditions d'utilisation de la fonction.

Voici une fonction non sécurisée dangereux, qui ne fait rien dans son corps :

fn main() {
    unsafe fn dangereux() {}

    unsafe {
        dangereux();
    }
}

Nous devons faire appel à la fonction dangereux dans un bloc unsafe séparé. Si nous essayons d'appeler dangereux sans le bloc unsafe, nous obtenons une erreur :

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function is unsafe and requires unsafe function or block
 --> src/main.rs:4:5
  |
4 |     dangereux();
  |     ^^^^^^^^^^^ 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` due to previous error

En ajoutant le bloc unsafe autour de notre appel à dangereux, nous déclarons à 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 répondons bien aux exigences de la fonction.

Les corps des fonctions non sécurisées sont bel et bien des blocs unsafe, donc pour pouvoir procéder à d'autres opérations non sécurisées dans une fonction non sécurisée, nous n'avons pas besoin d'ajouter un autre bloc unsafe.

Créer une abstraction sécurisée sur du code non sécurisé

Ce n'est pas parce qu'une fonction contient du code non sécurisé que nous devons forcément marquer l'intégralité de cette fonction comme non sécurisée. En fait, envelopper du code non sécurisé dans une fonction sécurisée est une abstraction courante. Par exemple, étudions une fonction de la bibliothèque standard, split_at_mut, qui nécessite du code non sécurisé, et étudions comment nous devrions l'implémenter. Cette méthode sécurisée est définie sur des slices mutables : elle prend une slice en paramètre et en créée deux autres en divisant la slice à l'indice donné en argument. L'encart 19-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]);
}

Encart 19-4 : utilisation de la fonction sécurisée split_at_mut

Nous ne pouvons pas implémenter cette fonction en utilisant uniquement du Rust sécurisé. Une tentative en ce sens ressemblerait à l'encart 19-5, qui ne se compilera pas. Par simplicité, nous allons implémenter split_at_mut comme une fonction plutôt qu'une méthode et seulement pour des slices de valeurs i32 au lieu d'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);
}

Encart 19-5 : une tentative d'implémentation de split_at_mut en utilisant uniquement du Rust sécurisé

Cette fonction commence par obtenir la longueur totale de la slice. Elle vérifie ensuite que l'indice donné en paramètre est bien à l'intérieur de la slice en vérifiant s'il est inférieur ou égal à la longueur. La vérification implique que si nous envoyons un indice qui est plus grand que la longueur de la slice à découper, la fonction va paniquer avant d'essayer d'utiliser cet indice.

Ensuite, nous retournons deux slices mutables dans un tuple : une à partir du début de la slice initiale jusqu'à l'indice mod et une autre à partir de l'indice jusqu'à la fin de la slice.

Lorsque nous essayons de compiler le code de l'encart 19-5, nous allons obtenir une erreur.

$ 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:30
  |
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`

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

Le vérificateur d'emprunt de Rust ne comprend pas que nous empruntons différentes parties de la slice ; il comprend seulement que nous empruntons la même slice à deux reprises. L'emprunt de différentes parties d'une slice ne pose fondamentalement pas de problèmes car les deux slices ne se chevauchent pas, mais Rust n'est pas suffisamment intelligent pour comprendre ceci. Lorsque nous savons que ce code est correct, mais que Rust ne le sait pas, il est approprié d'utiliser du code non sécurisé.

L'encart 19-6 montre comment utiliser un bloc unsafe, un pointeur brut, et quelques appels à des fonctions non sécurisées pour construire une implémentation de split_at_mut qui fonctionne.

use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.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);
}

Encart 19-6 : utilisation de code non sécurisé dans l'implémentation de la fonction split_at_mut

Souvenez-vous de la section “Le type slice” du chapitre 4 dans laquelle nous avions dit qu'une slice est définie par un pointeur vers une donnée ainsi qu'une longueur de la slice. Nous avons utilisé la méthode len pour obtenir la longueur d'une slice ainsi que la méthode as_mut_ptr pour accéder au pointeur brut d'une slice. Dans ce cas, comme nous avons une slice mutable de valeurs i32, as_mut_ptr retourne un pointeur brut avec le type *mut i32 que nous stockons dans la variable ptr.

Nous avons conservé la vérification que l'indice mid soit dans la slice. Ensuite, nous utilisons le code non sécurisé : la fonction slice::from_raw_parts_mut prend en paramètre un pointeur brut et une longueur, et elle créée une slice. Nous utilisons cette fonction pour créer une slice qui débute à ptr et qui est longue de mid éléments. Ensuite nous faisons appel à la méthode add sur ptr avec mid en argument pour obtenir un pointeur brut qui démarre à mid, et nous créons une slice qui utilise ce pointeur et le nombre restant d'éléments après mid comme longueur.

La fonction slice::from_raw_parts_mut est non sécurisée car elle prend en argument un pointeur brut et doit avoir confiance en la validité de ce pointeur. La méthode add sur les pointeurs bruts est aussi non sécurisée, car elle doit croire que l'emplacement décalé est aussi un pointeur valide. Voilà pourquoi nous avons placé un bloc unsafe autour de nos appels à slice::from_raw_parts_mut et add afin que nous puissions les effectuer. En analysant le code et en ayant ajouté la vérification que mid doit être inférieur ou égal à len, nous pouvons affirmer que tous les pointeurs bruts utilisés dans le bloc unsafe sont des pointeurs valides vers les données de la slice. C'est une utilisation acceptable et appropriée de unsafe.

Remarquez que nous n'avons pas eu besoin de marquer la fonction résultante split_at_mut comme étant unsafe, et que nous pouvons faire appel à cette fonction dans du code Rust sécurisé. Nous avons créé une abstraction sécurisée du code non sécurisé avec une implémentation de la fonction qui utilise de manière sécurisée du code non sécurisé, car elle créée uniquement des pointeurs valides à partir des données auxquelles cette fonction a accès.

En contre-partie, l'utilisation de slice::from_raw_parts_mut dans l'encart 19-7 peut planter lorsque la slice sera utilisée. Ce code prend un emplacement arbitraire dans la mémoire et crée un slice de 10 000 éléments.

fn main() {
    use std::slice;

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

    let valeurs: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}

Encart 19-7 : création d'une slice à partir d'un emplacement mémoire arbitraire

Nous ne possédons pas la mémoire à cet emplacement arbitraire, et il n'y a aucune garantie que la slice créée par ce code contiennent des valeurs i32 valides. Toute tentative d'utilisation de valeurs aura un comportement imprévisible bien qu'il s'agisse d'une slice valide.

Utiliser des fonctions extern pour faire appel à du code externe

Parfois, votre code Rust peut avoir besoin d'interagir avec du code écrit dans d'autres langages. Dans ce cas, Rust propose un mot-clé, extern, qui facilite la création et l'utilisation du Foreign Function Interface (FFI). Le FFI est un outil permettant à un langage de programmation de définir des fonctions auxquelles d'autres langages de programmation pourront faire appel.

L'encart 19-8 montre comment configurer l'intégration de la fonction abs de la bibliothèque standard du C. Les fonctions déclarées dans des blocs extern sont toujours non sécurisées lorsqu'on les utilise dans du code Rust. La raison à cela est que les autres langages n'appliquent pas les règles et garanties de Rust, Rust ne peut donc pas les vérifier, si bien que la responsabilité de s'assurer de la sécurité revient au développeur.

Fichier : src/main.rs

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("La valeur absolue de -3 selon le langage C : {}", abs(-3));
    }
}

Encart 19-8 : déclaration et appel à une fonction externe qui est définie dans un autre langage

Au sein du bloc extern "C", nous listons les noms et les signatures des fonctions externes de l'autre langage que nous souhaitons solliciter. La partie "C" définit quelle est l'application binary interface (ABI) que la fonction doit utiliser : l'ABI définit comment faire appel à la fonction au niveau assembleur. L'ABI "C" est la plus courante et respecte l'ABI du langage de programmation C.

Faire appel à des fonctions Rust dans d'autres langages

Nous pouvons aussi utiliser extern pour créer une interface qui permet à d'autres langages de faire appel à des fonctions Rust. Au lieu d'avoir un bloc extern, nous ajoutons le mot-clé extern et nous renseignons l'ABI à utiliser juste avant le mot-clé fn. Nous avons aussi besoin d'ajouter l'annotation #[no_mangle] pour dire au compilateur Rust de ne pas déformer le nom de cette fonction. La déformation s'effectue lorsqu'un compilateur change le nom que nous avons donné à une fonction pour un nom qui contient plus d'informations pour d'autres étapes du processus de compilation, mais qui est moins lisible par l'humain. Tous les compilateurs de langages de programmation déforment les noms de façon légèrement différente, donc pour que le nom d'une fonction Rust soit utilisable par d'autres langages, nous devons désactiver la déformation du nom par le compilateur de Rust.

Lire ou modifier une variable statique mutable

Jusqu'à présent, nous n'avons pas parlé des variables globales, que Rust accepte mais qui peuvent poser des problèmes avec les règles de possession de Rust. Si deux tâches accèdent en même temps à la même variable globale, cela peut causer un accès concurrent.

En Rust, les variables globales s'appellent des variables statiques. L'encart 19-9 montre un exemple de déclaration et d'utilisation d'une variable statique avec une slice de chaîne de caractères comme valeur.

Fichier : src/main.rs

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("Cela vaut : {}", HELLO_WORLD);
}

Encart 19-9 : définition et utilisation d'une variable statique immuable

Les variables statiques ressemblent aux constantes, que nous avons vues dans la section “Différences entre les variables et les constantes” du chapitre 3. Les noms des variables statiques sont par convention en SCREAMING_SNAKE_CASE. Les variables statiques peuvent uniquement stocker des références ayant la durée de vie 'static, de façon à ce que le compilateur Rust puisse la déterminer tout seul et que nous n'ayons pas besoin de la renseigner explicitement. L'accès à une variable statique immuable est sécurisé.

Les constantes et les variables statiques immuables se ressemblent, mais leur différence subtile est que les valeurs dans les variables statiques ont une adresse fixe en mémoire. L'utilisation de sa valeur va toujours accéder à la même donnée. Les constantes en revanche, peuvent reproduire leurs données à chaque fois qu'elles sont utilisées.

Une autre différence entre les constantes et les variables statiques est que les variables statiques peuvent être mutables. Lire et modifier des variables statiques mutables est non sécurisé. L'encart 19-10 montre comment déclarer, lire et modifier la variable statique mutable COMPTEUR.

Fichier : src/main.rs

static mut COMPTEUR: u32 = 0;

fn ajouter_au_compteur(valeur: u32) {
    unsafe {
        COMPTEUR += valeur;
    }
}

fn main() {
    ajouter_au_compteur(3);

    unsafe {
        println!("COMPTEUR : {}", COMPTEUR);
    }
}

Encart 19-10 : la lecture et l'écriture d'une variable statique mutable est non sécurisé

Comme avec les variables classiques, nous renseignons la mutabilité en utilisant le mot-clé mut. Tout code qui lit ou modifie COMPTEUR doit se trouver dans un bloc unsafe. Ce code se compile et affiche COMPTEUR : 3 comme nous l'espérions car nous n'avons qu'une seule tâche. Si nous avions plusieurs tâches qui accèdent à COMPTEUR, nous pourrions avoir un accès concurrent.

Avec les données mutables qui sont accessibles globalement, il devient difficile de s'assurer qu'il n'y a pas d'accès concurrent, c'est pourquoi Rust considère les variables statiques mutables comme étant non sécurisées. Lorsque c'est possible, il vaut mieux utiliser les techniques de concurrence et les pointeurs intelligents adaptés au multitâche que nous avons vus au chapitre 16, afin que le compilateur puisse vérifier que les données qu'utilisent les différentes tâches sont sécurisées.

Implémenter un trait non sécurisé

Un autre cas d'usage de unsafe est l'implémentation d'un trait non sécurisé. Un trait n'est pas sécurisé lorsque au moins une de ses méthodes contient une invariante que le compilateur ne peut pas vérifier. Nous pouvons déclarer un trait qui n'est pas sécurisé en ajoutant le mot-clé unsafe devant trait et en marquant aussi l'implémentation du trait comme unsafe, comme dans l'encart 19-11.

unsafe trait Foo {
    // les méthodes vont ici
}

unsafe impl Foo for i32 {
    // les implémentations des méthodes vont ici
}

fn main() {}

Encart 19-11 : définition et implémentation d'un trait non sécurisé

En utilisant unsafe impl, nous promettons que nous veillons aux invariantes que le compilateur ne peut pas vérifier.

Par exemple, souvenez-vous des traits Sync et Send que nous avions découverts dans une section du chapitre 16 : le compilateur implémente automatiquement ces traits si nos types sont entièrement composés des types Send et Sync. Si nous implémentions un type qui contenait un type qui n'était pas Send ou Sync, tel que les pointeurs bruts, et nous souhaitions marquer ce type comme étant Send ou Sync, nous aurions dû utiliser unsafe. Rust ne peut pas vérifier que notre type respecte les garanties pour que ce type puisse être envoyé en toute sécurité entre des tâches ou qu'il puisse être utilisé par plusieurs tâches ; en conséquence, nous avons besoin de faire ces vérifications manuellement et le signaler avec unsafe.

Utiliser des champs d'un Union

La dernière action qui fonctionne uniquement avec unsafe est d'accéder aux champs d'un union. Un union ressemble à une struct, mais un seul champ de ceux déclarés est utilisé dans une instance précise au même moment. Les unions sont principalement utilisés pour s'interfacer avec les unions du code C. L'accès aux champs des unions n'est pas sécurisé car Rust ne peut pas garantir le type de la donnée qui est actuellement stockée dans l'instance de l'union. Vous pouvez en apprendre plus sur les unions dans the Rust Reference.

Quand utiliser du code non sécurisé

L'utilisation de unsafe pour mettre en oeuvre une des cinq actions (ou super-pouvoirs) que nous venons d'aborder n'est pas une mauvaise chose et ne doit pas être mal vu. Mais il est plus difficile de sécuriser du code unsafe car le compilateur ne peut pas aider à garantir la sécurité de la mémoire. Lorsque vous avez une bonne raison d'utiliser du code non sécurisé, vous pouvez le faire, et vous aurez l'annotation explicite unsafe pour faciliter la recherche de la source des problèmes lorsqu'ils surviennent.