Acheter version papier

Rust

Fork me on GitHub

II. Spécificités de Rust

9. Unsafe

Le code Rust que l'on a vu jusque là est sûr (sound/safe) : il ne peut pas causer de comportement non-défini (undefined behaviour). Cependant, il est possible que vous ayez besoin d'écrire du code dont la sûreté ne peut pas être assurée par le compilateur de Rust. Par-exemple si vous utilisez une bibliothèque écrite dans un autre langage.

Il est cependant important de noter que même dans un bloc unsafe, les règles d'emprunts et de propriétés sur les variables sont exactement les même ! unsafe n'est donc pas un mot-clé magique qui permet d'ignorer les règles de Rust.

Voici la liste des cas où le mot-clé unsafe doit être utilisé :

  • Déréférencer un pointeur.
  • Implémenter un trait défini comme unsafe.
  • Appeler une fonction définie comme unsafe.
  • Modifier la valeur d'une variable statique.
  • Accéder aux champs d'une union (on revient sur ce type dans le livre juste après).

Si vous tentez de faire une de ces opérations en dehors d'un bloc unsafe, la compilation échouera en indiquant qu'il faut que ce code est unsafe. Par-exemple ce code :

Runfn main() {
    let x = 0u32;
    let y = &x as *const u32;

    println!("{}", *y);
}

donnera cette erreur :

error[E0133]: dereference of raw pointer is unsafe and requires unsafe function or block
 --> src/main.rs:5:20
  |
6 |     println!("{}", *y);
  |                    ^^ dereference of raw pointer

Le mot-clé unsafe a donc deux utilités :

  1. Il indique que le compilateur ne peut pas s'assurer que ce code ne contient pas de comportement non-défini, et donc que cette responsabilité revient au développeur.
  2. Il permet au développeur de rapidement voir que ce code a sans doute besoin de plus d'attention que le reste car il risque d'avoir des comportements non-définis qu'il faudra vérifier soi-même.

Donc le code précédent doit être écrit ainsi :

Runfn main() {
    let x = 0u32;
    let y = &x as *const u32;

    unsafe {
        println!("{}", *y);
    }
}

On déréférence maintenant y dans un bloc unsafe.

Dernier point : toutes les utilisations de unsafe n'ont pas le même sens. On va donc voir ce que chacune signifie.

Blocs unsafe

Les blocs unsafe permettent de déréférencer des pointeurs, mais aussi d'appeler des fonctions/méthodes unsafe, comme vu dans l'exemple précédent.

Ils servent aussi de marqueurs visuels pour nous permettre de voir quel code a besoin de plus d'attention car c'est au développeur de s'assurer que le code n'aura pas de comportement non-défini.

Fonctions/méthodes unsafe

Les fonctions et méthodes unsafe peuvent avoir un comportement non-défini dans certains contextes et/ou selon les arguments qu'elles reçoivent. On définit une fonction unsafe de cette façon :

Rununsafe fn fonction() {
    // code
}

Un bon exemple est la méthode slice::get_unchecked : elle retourne la valeur à l'index donné sans vérifier si cet index est bien inclus dans la slice. Donc si on lui donne un index en dehors de ces limites, le comportement sera non-défini. Cela peut causer une erreur de segmentation (segmentation fault) entrainant le plantage du programme ou bien juste renvoyer une valeur dans la mémoire se trouvant à cet emplacement. C'est donc pour cela qu'elle est définie

Bien qu'il ne soit pas obligatoire d'ajouter des blocs unsafe dans une fonction définie comme unsafe pour pouvoir faire des opérations unsafe, il est cependant recommandé de quand même en ajouter un pour améliorer la lisibilité du code :

Rununsafe fn fonction() -> u32 {
    let x = 12u32;
    let y = &x as *const u32;

    // Cela permet de voir quelle partie de la fonction a besoin d'être unsafe.
    unsafe {
        *y
    }
}

Traits unsafe

Un trait unsafe est un trait avec des pré-requis qui ne peuvent être vérifiés par le compilateur lorsqu'il est implémenté sur un type. Ce sera donc au développeur de s'assurer que l'implémentation respecte bien ces conditions.

On peut déclarer un trait unsafe comme ceci :

Rununsafe trait UnsafeTrait {
    // Les éléments du trait.
}

L'implémentation d'un trait défini comme unsafe utilise aussi ce mot-clé :

Runstruct Structure;

unsafe impl UnsafeTrait for Structure {
    // Implémentation des éléments du trait.
}

Un bon exemple sont les traits Send et Sync qui permettent respectivement de d'indiquer qu'un type peut être transféré dans un autre thread et que la référence d'un type peut être partagée dans un autre thread. Nous reviendrons plus en détail sur ces 2 traits et sur le multi-threading plus tard dans ce livre.

Les blocs externes

Si vous voulez utiliser une bibliothèque codée en langage C, il vous faudra définir les fonctions de cette bibliothèque que vous voulez utiliser. Par-exemple si on veut utiliser la fonction puts de la bibliothèque standard du langage C qui est définie comme ceci :

int puts(const char *s);

On va donc écrire ce code en Rust :

Rununsafe extern "C" {
    fn puts(s: *const i8) -> i32;
}

// Qu'on appelera comme ceci :
fn main() {
    unsafe {
        puts(b"bonjour\n\0".as_ptr() as *const _);
    }
}

Veuillez noter que ce code est incorrect car les types char et int ne correspondent pas nécessairement à un entier signé de 8 bits et à un entier de 32 bits selon la plate-forme. Ne l'utilisez donc surtout pas ! Nous reviendrons sur comment utiliser correctement une bibliothèque C dans la troisième partie de ce livre.

On doit s'assurer que les éléments que l'on importe ont la bonne signature car si ce n'est pas le cas, cela conduira à des comportements non-définis.

Dernier point : il n'est pas obligatoire de définir un bloc externe comme unsafe, cependant je considère que cela rend plus évident que ce code a des risques très élevés de conduire à des comportements non-définis. De plus, que l'on définisse un bloc externe comme unsafe ou non, les éléments qui sont définis dedans sont considérés comme unsafe par le compilateur quoi qu'il arrive.