🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Vous pouvez contribuer à l'amélioration de cette page sur sa Pull Request.

L'épinglage

Pour piloter les futures, ils doivent être épinglés en utilisant un type spécial qui s'appelle Pin<T>. Si vous lisez l'explication du trait Future dans la section précédente, vous devriez constater la présence du Pin dans le self: Pin<&mut Self> dans la définition de la méthode Future::poll. Mais qu'est-ce que cela signifie, et pourquoi nous en avons besoin ?

Pourquoi épingler ?

Pin fonctionne en binôme avec le marqueur Unpin. L'épinglage permet de garantir qu'un objet qui implémente !Unpin ne sera jamais déplacé. Pour comprendre pourquoi c'est nécessaire, nous devons nous rappeler comment async et await fonctionnent. Imaginons le code suivant :

let premiere_future = /* ... */;
let seconde_future = /* ... */;
async move {
    premiere_future.await;
    seconde_future.await;
}

Sous le capot, cela crée un type anonyme qui implémente Future, ce qui va fournir une méthode poll qui ressemble à ceci :

// Le type `Future` généré pour notre bloc `async { ... }`
struct FutureAsynchrone {
    premiere_future: FutOne,
    seconde_future: FutTwo,
    etat: Etat,
}

// Liste des états dans lesquels notre bloc `async` peut être
enum Etat {
    AttentePremiereFuture,
    AttenteSecondeFuture,
    Termine,
}

impl Future for FutureAsynchrone {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        loop {
            match self.etat {
                Etat::AttentePremiereFuture => match self.premiere_future.poll(..) {
                    Poll::Ready(()) => self.etat = Etat::AttenteSecondeFuture,
                    Poll::Pending => return Poll::Pending,
                }
                Etat::AttenteSecondeFuture => match self.seconde_future.poll(..) {
                    Poll::Ready(()) => self.etat = Etat::Termine,
                    Poll::Pending => return Poll::Pending,
                }
                Etat::Termine => return Poll::Ready(()),
            }
        }
    }
}

Lorsque poll est appelé la première fois, il va appeler premiere_future. Si premiere_future ne peut pas être complété, FutureAsynchrone::poll va retourner sa valeur. Les appels futurs à poll vont reprendre où le précédent s'est arrêté. Ce fonctionnement va continuer jusqu'à ce que la future se termine au complet.

Cependant, que se passe-t-il si nous avons un bloc async qui utilise des références ? Par exemple :

async {
    let mut x = [0; 128];
    let lire_dans_un_tampon = lire_dans_un_tampon(&mut x);
    lire_dans_un_tampon.await;
    println!("{:?}", x);
}

Quelle structure va donner la compilation ?

struct LireDansTampon<'a> {
    tampon: &'a mut [u8], // cela pointe sur le `x` ci-desous
}

struct FutureAsynchrone {
    x: [u8; 128],
    future_lire_dans_un_tampon: LireDansTampon<'quelle_duree_de_vie?>,
}

Ici, la future LireDansTampon contient une référence vers l'autre champ de notre structure, x. Cependant, si FutureAsynchrone est déplacée, l'emplacement de x va aussi être déplacé, ce qui va corrompre le pointeur stocké dans future_lire_dans_un_tampon.tampon.

L'épinglage des futures à un endroit précis de la mémoire évite ce problème, ce qui va sécuriser la création de références vers des valeurs dans des blocs async.

L'épinglage en détail

Essayons de comprendre l'épinglage en utilisant un exemple légèrement plus simple. Le problème que nous allons rencontrer ci-dessous peut se résumer à notre manière de gérer les types auto-référentiels en Rust.

Pour l'instant, notre exemple ressemble à ceci :

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
}

impl Test {
    fn new(texte: &str) -> Self {
        Test {
            a: String::from(texte),
            b: std::ptr::null(),
        }
    }

    fn initialiser(&mut self) {
        let self_ref: *const String = &self.a;
        self.b = self_ref;
    }

    fn a(&self) -> &str {
        &self.a
    }

    fn b(&self) -> &String {
        assert!(!self.b.is_null(), "Test::b est appelé sans appeler avant Test::initialiser");
        unsafe { &*(self.b) }
    }
}

Test propose des méthodes pour obtenir une référence vers la valeur des champs a et b. Comme b est une référence vers a, nous le stockons comme un pointeur puisque les règles d'emprunt de Rust ne nous autorisent pas à définir cette durée de vie. Nous avons désormais ce que l'on appelle une structure auto-référentielle.

Notre exemple fonctionne bien si nous ne déplaçons aucune de nos données, comme vous pouvez le constater en exécutant cet exemple :

fn main() {
    let mut test1 = Test::new("test1");
    test1.initialiser();
    let mut test2 = Test::new("test2");
    test2.initialiser();

    println!("a: {}, b: {}", test1.a(), test1.b());
    println!("a: {}, b: {}", test2.a(), test2.b());

}
#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
}

impl Test {
    fn new(texte: &str) -> Self {
        Test {
            a: String::from(texte),
            b: std::ptr::null(),
        }
    }

    // We need an `init` method to actually set our self-reference
    fn initialiser(&mut self) {
        let self_ref: *const String = &self.a;
        self.b = self_ref;
    }

    fn a(&self) -> &str {
        &self.a
    }

    fn b(&self) -> &String {
        assert!(!self.b.is_null(), "Test::b est appelé sans appeler avant Test::initialiser");
        unsafe { &*(self.b) }
    }
}

Nous obtenons ce que nous attendions :

a: test1, b: test1
a: test2, b: test2

Voyons maintenant ce qui se passe si nous permutions test1 avec test2 et ainsi nous déplaçons les données :

fn main() {
    let mut test1 = Test::new("test1");
    test1.initialiser();
    let mut test2 = Test::new("test2");
    test2.initialiser();

    println!("a: {}, b: {}", test1.a(), test1.b());
    std::mem::swap(&mut test1, &mut test2);
    println!("a: {}, b: {}", test2.a(), test2.b());

}
#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
}

impl Test {
    fn new(texte: &str) -> Self {
        Test {
            a: String::from(texte),
            b: std::ptr::null(),
        }
    }

    fn initialiser(&mut self) {
        let self_ref: *const String = &self.a;
        self.b = self_ref;
    }

    fn a(&self) -> &str {
        &self.a
    }

    fn b(&self) -> &String {
        assert!(!self.b.is_null(), "Test::b est appelé sans appeler avant Test::initialiser");
        unsafe { &*(self.b) }
    }
}

Naïvement, nous pourrions penser que nous devrions obtenir l'écriture de déboguage de test1 deux fois comme ceci :

a: test1, b: test1
a: test1, b: test1

Mais à la place, nous avons ceci :

a: test1, b: test1
a: test1, b: test2

Le pointeur vers test2.b pointe toujours vers l'ancien emplacement qui est maintenant test1. La structure n'est plus auto-référentielle, elle contient un pointeur vers un champ dans un objet différent. Cela signifie que nous ne pouvons plus considérer que la durée de vie de test2.b soit toujours liée à la durée de vie de test2.

Si vous n'êtes pas convaincu, ceci devrait vous convaincre :

fn main() {
    let mut test1 = Test::new("test1");
    test1.initialiser();
    let mut test2 = Test::new("test2");
    test2.initialiser();

    println!("a: {}, b: {}", test1.a(), test1.b());
    std::mem::swap(&mut test1, &mut test2);
    test1.a = "J'ai complètement changé, désormais !".to_string();
    println!("a: {}, b: {}", test2.a(), test2.b());

}
#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
}

impl Test {
    fn new(texte: &str) -> Self {
        Test {
            a: String::from(texte),
            b: std::ptr::null(),
        }
    }

    fn initialiser(&mut self) {
        let self_ref: *const String = &self.a;
        self.b = self_ref;
    }

    fn a(&self) -> &str {
        &self.a
    }

    fn b(&self) -> &String {
        assert!(!self.b.is_null(), "Test::b est appelé sans appeler avant Test::initialiser");
        unsafe { &*(self.b) }
    }
}

Le schéma ci-dessous peut vous aider à voir ce qui se passe :

Figure 1 : avant et après l'échange probleme_echange

C'est ainsi facile d'avoir un fonctionnement indéfini et aussi de provoquer une autre défaillance spectaculaire.

L'épinglage dans la pratique

Voyons voir comment l'épinglage et le type Pin peut nous aider à résoudre ce problème.

Le type Pin enveloppe les types de pointeurs, ce qui garantit que les valeurs derrière ce pointeur ne seront pas déplacées. Par exemple, Pin<&mut T>, Pin<&T>, Pin<Box<T>> garantissent tous que T ne sera pas déplacé même si T: !Unpin.

La plupart des types n'ont pas de problème lorsqu'ils sont déplacés. Ces types implémentent le trait Unpin. Les pointeurs vers des types Unpin peuvent être librement logés à l'intérieur d'un Pin, ou en être retiré. Par exemple, u8 implémente Unpin, donc Pin<&mut u8> se comporte exactement comme un &mut u8 normal.

Cependant, les types qui ne peuvent pas être déplacés après avoir été épinglés ont un marqueur !Unpin. Les futures créées par async et await en sont un exemple.

L'épinglage sur la pile

Retournons à notre exemple. Nous pouvons résoudre notre problème en utilisant Pin. Voyons ce à quoi notre exemple ressemblerait si nous avions utilisé un pointeur épinglé à la place :

use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marqueur: PhantomPinned,
}

impl Test {
    fn new(texte: &str) -> Self {
        Test {
            a: String::from(texte),
            b: std::ptr::null(),
            _marqueur: PhantomPinned, // Cela rends notre type `!Unpin`
        }
    }

    fn initialiser(self: Pin<&mut Self>) {
        let self_pointeur: *const String = &self.a;
        let this = unsafe { self.get_unchecked_mut() };
        this.b = self_pointeur;
    }

    fn a(self: Pin<&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin<&Self>) -> &String {
        assert!(!self.b.is_null(), "Test::b est appelé sans appeler avant Test::initialiser");
        unsafe { &*(self.b) }
    }
}

L'épinglage d'un objet à la pile va toujours être unsafe si notre type implémente !Unpin. Vous pouvez utiliser une crate comme pin_utils pour éviter d'avoir à écrire notre propre unsafe code lorsqu'on épinglera sur la pile.

Ci-dessous, nous épinglons les objets test1 et test2 sur la pile :

pub fn main() {
    // test1 peut être déplacé en sécurité avant que nous l'initialisions :
    let mut test1 = Test::new("test1");
    // Notez que nous masquons `test1` pour l'empêcher d'être toujours
    // accessible :
    let mut test1 = unsafe { Pin::new_unchecked(&mut test1) };
    Test::initialiser(test1.as_mut());

    let mut test2 = Test::new("test2");
    let mut test2 = unsafe { Pin::new_unchecked(&mut test2) };
    Test::initialiser(test2.as_mut());

    println!("a: {}, b: {}", Test::a(test1.as_ref()), Test::b(test1.as_ref()));
    println!("a: {}, b: {}", Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}
use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marqueur: PhantomPinned,
}


impl Test {
    fn new(texte: &str) -> Self {
        Test {
            a: String::from(texte),
            b: std::ptr::null(),
            // Cela rends notre type `!Unpin`
            _marqueur: PhantomPinned,
        }
    }

    fn initialiser(self: Pin<&mut Self>) {
        let self_pointeur: *const String = &self.a;
        let this = unsafe { self.get_unchecked_mut() };
        this.b = self_pointeur;
    }

    fn a(self: Pin<&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin<&Self>) -> &String {
        assert!(!self.b.is_null(), "Test::b est appelé sans appeler avant Test::initialiser");
        unsafe { &*(self.b) }
    }
}

Maintenant, si nous essayons de déplacer nos données, nous avons désormais une erreur de compilation :

pub fn main() {
    let mut test1 = Test::new("test1");
    let mut test1 = unsafe { Pin::new_unchecked(&mut test1) };
    Test::initialiser(test1.as_mut());

    let mut test2 = Test::new("test2");
    let mut test2 = unsafe { Pin::new_unchecked(&mut test2) };
    Test::initialiser(test2.as_mut());

    println!("a: {}, b: {}", Test::a(test1.as_ref()), Test::b(test1.as_ref()));
    std::mem::swap(test1.get_mut(), test2.get_mut());
    println!("a: {}, b: {}", Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}
use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marqueur: PhantomPinned,
}


impl Test {
    fn new(txt: &str) -> Self {
        Test {
            a: String::from(txt),
            b: std::ptr::null(),
            _marqueur: PhantomPinned, // Cela rends notre type `!Unpin`
        }
    }

    fn initialiser(self: Pin<&mut Self>) {
        let self_pointeur: *const String = &self.a;
        let this = unsafe { self.get_unchecked_mut() };
        this.b = self_pointeur;
    }

    fn a(self: Pin<&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin<&Self>) -> &String {
        assert!(!self.b.is_null(), "Test::b est appelé sans appeler avant Test::initialiser");
        unsafe { &*(self.b) }
    }
}

Le système de type nous empêche de déplacer les données.

Il est important que vous compreniez que l'épinglage sur la pile s'appuie toujours sur les garanties que vous écrivez dans votre unsafe. Même si nous savons que ce sur quoi pointe le &'a mut T est épinglé pour la durée de vie de 'a, nous ne pouvons pas savoir si la donnée sur laquelle pointe &'a mut T n'est pas déplacée après que 'a soit terminé. Si c'est ce qui se passe, cela violera le contrat du Pin.

Une erreur courante est d'oublier de masquer la variable originale alors que vous pourriez terminer le Pin et déplacer la donnée après le &'a mut T comme nous le montrons ci-dessous (ce qui viole le contrat du Pin) :

fn main() {
   let mut test1 = Test::new("test1");
   let mut test1_pin = unsafe { Pin::new_unchecked(&mut test1) };
   Test::init(test1_pin.as_mut());

   drop(test1_pin);
   println!(r#"test1.b pointe sur "test1": {:?}..."#, test1.b);

   let mut test2 = Test::new("test2");
   mem::swap(&mut test1, &mut test2);
   println!("... et maintenant il pointe nulle part : {:?}", test1.b);
}
use std::pin::Pin;
use std::marker::PhantomPinned;
use std::mem;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marqueur: PhantomPinned,
}


impl Test {
    fn new(txt: &str) -> Self {
        Test {
            a: String::from(txt),
            b: std::ptr::null(),
            // Cela rends notre type `!Unpin`
            _marqueur: PhantomPinned,
        }
    }

    fn init<'a>(self: Pin<&'a mut Self>) {
        let self_pointeur: *const String = &self.a;
        let this = unsafe { self.get_unchecked_mut() };
        this.b = self_pointeur;
    }

    fn a<'a>(self: Pin<&'a Self>) -> &'a str {
        &self.get_ref().a
    }

    fn b<'a>(self: Pin<&'a Self>) -> &'a String {
        assert!(!self.b.is_null(), "Test::b est appelé sans appeler avant Test::initialiser");
        unsafe { &*(self.b) }
    }
}

Epingler sur le tas

L'épinglage d'un type !Unpin sur le tas donne une adresse stable à vos données donc nous savons que la donnée sur laquelle nous pointons ne peut pas être déplacée après avoir été épinglée. Contrairement à l'épinglage sur la pile, nous savons que la donnée va être épinglée pendant la durée de vie de l'objet.

use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marqueur: PhantomPinned,
}

impl Test {
    fn new(texte: &str) -> Pin<Box<Self>> {
        let t = Test {
            a: String::from(texte),
            b: std::ptr::null(),
            _marqueur: PhantomPinned,
        };
        let mut boxed = Box::pin(t);
        let self_pointeur: *const String = &boxed.as_ref().a;
        unsafe { boxed.as_mut().get_unchecked_mut().b = self_pointeur };

        boxed
    }

    fn a(self: Pin<&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin<&Self>) -> &String {
        unsafe { &*(self.b) }
    }
}

pub fn main() {
    let test1 = Test::new("test1");
    let test2 = Test::new("test2");

    println!("a: {}, b: {}",test1.as_ref().a(), test1.as_ref().b());
    println!("a: {}, b: {}",test2.as_ref().a(), test2.as_ref().b());
}

Certaines fonctions nécessitent que les futures avec lesquelles elles fonctionnent soient des Unpin. Pour utiliser une Future ou un Stream qui n'est pas Unpin avec une fonction qui nécessite des types Unpin, vous devez d'abord épingler la valeur en utilisant soit Box::pin (pour créer un Pin<Box<T>>) ou la macro pin_utils::pin_mut! (pour créer une Pin<&mut T>). Pin<Box<Future>> et Pin<&mut Future> peuvent tous deux être utilisés comme des Futures, et les deux implémentent Unpin.

Par exemple :

use pin_utils::pin_mut; // `pin_utils` est une crate bien pratique,
                        // disponible sur crates.io

// Une fonction qui prend en argument une `Future` qui implémente `Unpin`.
fn executer_une_future_unpin(x: impl Future<Output = ()> + Unpin) { /* ... */ }

let future = async { /* ... */ };
executer_une_future_unpin(future); // Erreur : `future` n'implémente pas
                                   // le trait `Unpin`

// Epingler avec `Box`:
let future = async { /* ... */ };
let future = Box::pin(future);
executer_une_future_unpin(future); // OK

// Epingler avec `pin_mut!`:
let future = async { /* ... */ };
pin_mut!(future);
executer_une_future_unpin(future); // OK

En résumé

  1. Si T: Unpin (ce qu'il est par défaut), alors Pin<'a, T> est strictement équivalent à &'a mut T. Autrement dit : Unpin signifie que ce type peut être déplacé sans problème même lorsqu'il est épinglé, donc Pin n'aura pas d'impact sur ce genre de type.

  2. Obtenir un &mut T à partir d'un T épinglé nécessite du code non sécurisé si T: !Unpin.

  3. La plupart des bibliothèques standard implémentent Unpin. C'est la même chose pour la plupart des types "normaux" que vous utilisez en Rust. Une Future générée par async et await est une exception à cette généralité.

  4. Vous pouvez ajouter un lien !Unpin sur un type avec la version expérimentale de Rust avec un drapeau de fonctionnalité, ou en ajoutant le std::marker::PhantomPinned sur votre type avec la version stable.

  5. Vous pouvez épingler des données soit sur la pile, soit sur le tas.

  6. Epingler un objet !Unpin sur la pile nécessite unsafe

  7. Epingler un objet !Unpin sur le tas ne nécessite pas unsafe. Il existe un raccourci pour faire ceci avec Box::pin.

  8. Pour les données épinglées où T: !Unpin, vous devez maintenir l'invariant dont sa mémoire n'est pas invalidée ou réaffectée à partir du moment où elle est épinglée jusqu'à l'appel à drop. C'est une partie très importante du contrat d'épinglage.