🚧 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

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 Test épinglé pour la durée de vie de'a, nous ne pouvons pas savoir si la donnée sur laquelle pointe&'a mut Tn'est pas déplacée après que'asoit terminé. Si c'est ce qui se passe, cela violera le contrat duPin.Une erreur courante est d'oublier de masquer la variable originale alors que vous pourriez terminer le
Pinet déplacer la donnée après le&'a mut Tcomme nous le montrons ci-dessous (ce qui viole le contrat duPin) :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é
-
Si
T: Unpin(ce qu'il est par défaut), alorsPin<'a, T>est strictement équivalent à&'a mut T. Autrement dit :Unpinsignifie que ce type peut être déplacé sans problème même lorsqu'il est épinglé, doncPinn'aura pas d'impact sur ce genre de type. -
Obtenir un
&mut Tà partir d'un T épinglé nécessite du code non sécurisé siT: !Unpin. -
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. UneFuturegénérée parasyncetawaitest une exception à cette généralité. -
Vous pouvez ajouter un lien
!Unpinsur un type avec la version expérimentale de Rust avec un drapeau de fonctionnalité, ou en ajoutant lestd::marker::PhantomPinnedsur votre type avec la version stable. -
Vous pouvez épingler des données soit sur la pile, soit sur le tas.
-
Epingler un objet
!Unpinsur la pile nécessiteunsafe -
Epingler un objet
!Unpinsur le tas ne nécessite pasunsafe. Il existe un raccourci pour faire ceci avecBox::pin. -
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.