🚧 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 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 duPin
.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 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 Future
s, 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 :Unpin
signifie que ce type peut être déplacé sans problème même lorsqu'il est épinglé, doncPin
n'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. UneFuture
générée parasync
etawait
est une exception à cette généralité. -
Vous pouvez ajouter un lien
!Unpin
sur un type avec la version expérimentale de Rust avec un drapeau de fonctionnalité, ou en ajoutant lestd::marker::PhantomPinned
sur votre type avec la version stable. -
Vous pouvez épingler des données soit sur la pile, soit sur le tas.
-
Epingler un objet
!Unpin
sur la pile nécessiteunsafe
-
Epingler un objet
!Unpin
sur 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.