https://lafor.ge/feed.xml

Unsafe Cell

2024-03-14

Bonjour à toutes et à tous 😀

Nous n'avons pas trop fait de Rust unsafe sur ce blog, il est temps de remédier à ce manque.

Ne vous inquiétez pas, on ne va pas faire du trop complexe. J'ai juste besoin d'une brique fondamentale de l'unsafe que l'on nomme une Unsafe Cell.

Voici la structure:

#[repr(transparent)]
struct UnsafeCell<T: ?Sized> {
    value : T
}

Deux choses à remarquer:

  • T: ?Sized : permet de stocker ce que l'on veut au sein de value
  • #[repr(transparent)] : Une structure de 1 seule champ peut avoir la même représentation en mémoire que le type du champ

Les deux cumulés permettent d'avoir un type totalement transparent qui accueille n'importe quoi sans broncher.

Comme d'habitude la pratique vaut mieux que les long discours.

On construit notre UnsafeCell.

fn main() {
    let cell = UnsafeCell::new(0);
}

Si on essaye de dbg!, on obtient un résultat des plus étrange:

dbg!(cell); //  cell = UnsafeCell { .. }

Bon on aura pas grand chose de cette manière.

L'API nous fourni d'autres méthodes.

fn main() {
    let cell = UnsafeCell::new(0);
    dbg!(cell.get()) // 0x000000e33c58f56c
}

Ah c'est pas ce que j'espérai, ça ressemble plus à une adresse,

Et pour cause c'est bien une adresse, celle de la UnsafeCell et donc par extension de la donnée.

La signature de la méthode UnsafeCell::get est

fn get(&self) -> *mut T;

Le type *mut T, on appelle ça un raw pointer, il existe deux types de raw pointer:

  • *const T : les données qui sont derrières ne bougeront pas, ou en tout cas elle ne sont pas sensées bouger
  • *mut T : les données pointées n'ont aucune garantie de ne pas être modifiées

En Rust pour déréférencer, on utilise la syntaxe *x.

Cette syntaxe est également valide pour les raw pointers.

fn main() {
    let cell = UnsafeCell::new(0);
    dbg!(*cell.get()) // Erreur
}

Félicitation, vous avez mis en rogne le compilateur:

error: dereference of raw pointer is unsafe and requires unsafe function or block
   |
   |     dbg!(*cell.get());
   |          ^^^^^^^^^^^ dereference of raw pointer
   |
   = note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior

Pour le paraphrasé, Rust ne fourni aucune garantie sur l'existence ou la cohérence des données à cette adresse mémoire et donc ne vous laisse pas y accéder.

Pour être en capacité de le faire, il va falloir débrayé Rust.

Pour cela, nous allons utiliser une syntaxe qui permet de rendre un bloc de code où le dev devient responsable de ses âneries, en gros on signe une décharge de responsabilité.

Et Rust se lave les mains de ce que vous allez y faire. 😁

Attention

unsafe ne libère pas du Borrow Checker.

unsafe {
    let mut a = 12;
    let ref_a_mut = &mut a;
    let ref_a = &a;
    dbg!(ref_a_mut);
}
error: cannot borrow `a` as immutable because it is also borrowed as mutable

    let ref_a_mut = &mut a;
                    ------ mutable borrow occurs here
    let ref_a = &a;
                ^^ immutable borrow occurs here
    dbg!(ref_a_mut);
        --------- mutable borrow later used here

Cette notion va être cruciale pour implémenter la sécurité du langage tout en permettant de faire des choses qui sont nécessaires.

Bien, entourons d'un bloc unsafe.

fn main() {
    let cell = UnsafeCell::new(0);
    unsafe { 
        dbg!(*cell.get());  // 0
    }
}

Parfait, on accède à la donnée. 😎

Le retour de UnsafeCell::get étant un *mut T, nous avons alors la possibilité de lire et d'écrire les données.

Et être les seuls à le faire grâce aux garanties du BorrowChecker car on a besoin d'une référence non-mutable de &self.

Donc personne n'est censé muter les données en "même temps" que nous.

Nous pouvons alors muter, donc incrémenter notre donnée.

let cell = UnsafeCell::new(0);
unsafe {
    *cell.get() += 1;
    dbg!(*cell.get()); // 1
}

Sympa, non ? 😊

Mais alors, en pratique, à quoi ça sert ?

Voyez le code suivant:

// On déclare une structure qui référence une UnsafeCell
struct Data<'a> {
    shared: &'a UnsafeCell<i32>
}

impl Data<'_> {
    // Cette méthode incrémente de 1 le contenu de la UnsafeCell
    fn inc(&self) {
        unsafe {
            *self.shared.get() += 1
        }
    }
}

fn main() {
    
    let shared = UnsafeCell::new(0);
    // On créé deux instances
    let data1 = Data {shared: &shared};
    let data2 = Data {shared: &shared};

    // On incrémente depuis les deux instances
    data1.inc();
    data2.inc();

    // On consomme la UnsafeCell
    dbg!(shared.into_inner()); // 2
}

Les UnsafeCell sont un moyen de muter une référence non mutable tout en garantissant que les règles du Borrow Checker sont respectées.

Elles vont être extrêmement utiles pour tout un pan de Rust.

Un dernier truc en passant.

Il est également possible de réaliser des mutation sur la UnsafeCell si vous possédez une version mutable de celle-ci.

let mut cell = UnsafeCell::new(0);
*cell.get_mut() += 1;
dbg!(cell.into_inner()); // 1

Dans ce mode safe, vous avez toutes les garanties de Rust sur l'existence et la validité des données, ainsi que l'exclusivité de la mutabilité.

Voilà, c'était un pas timide de ce blog dans le chaos organisé de l'unsafe de Rust! 😇

PS: J'ai réalisé une correction de cette article ici. Merci à leur vigilence.

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.