🚧 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.