Unsafe Cell

Lecture 3 min. ‱


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:

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:

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.