J'écris un futur combinateur qui doit consommer une valeur qui lui a été fournie. Avec les futures 0.1, Future::poll
a pris self: &mut Self
, Ce qui signifie que mon combinateur contenait un Option
et j'ai appelé Option::take
Dessus lorsque l'avenir sous-jacent se résout.
La méthode Future::poll
dans la bibliothèque standard prend à la place self: Pin<&mut Self>
, J'ai donc lu les garanties requises pour utiliser en toute sécurité Pin
.
De la documentation du module pin
sur la garantie Drop
(accent sur le mien):
Concrètement, pour les données épinglées, vous devez conserver l'invariant selon lequel sa mémoire ne sera pas invalidée à partir du moment où elle est épinglée jusqu'à l'appel de drop. La mémoire peut être invalidée par désallocation, mais aussi en en remplaçant une
Some(v)
parNone
, ou en appelantVec::set_len
Pour "tuer" certains éléments d'un vecteur.
Et Projections et Structural Pinning (accent sur le mien):
Vous ne devez proposer aucune autre opération pouvant entraîner le déplacement de données hors des champs lorsque votre type est épinglé. Par exemple, si l'encapsuleur contient un
Option<T>
Et qu'il y a opération de type prise avec le typefn(Pin<&mut Wrapper<T>>) -> Option<T>
, cette opération peut être utilisée pour déplacer unT
hors d'unWrapper<T>
épinglé - ce qui signifie que l'épinglage ne peut pas être structurel.
Cependant, le combinateur Map
existant appelle Option::take
Sur une valeur membre lorsque l'avenir sous-jacent est résolu:
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
match self.as_mut().future().poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(output) => {
let f = self.f().take()
.expect("Map must not be polled after it returned `Poll::Ready`");
Poll::Ready(f(output))
}
}
}
La méthode f
est générée par la macro unsafe_unpinned
et ressemble à peu près à:
fn f<'a>(self: Pin<&'a mut Self>) -> &'a mut Option<F> {
unsafe { &mut Pin::get_unchecked_mut(self).f }
}
Il apparaît que Map
viole les exigences décrites dans la documentation pin
, mais je crois que les auteurs du combinateur Map
savent ce qu'ils font et que ce code est sûr.
Quelle logique leur permet d'effectuer cette opération en toute sécurité?
Il s'agit de goupillage structurel.
Tout d'abord, j'utiliserai la syntaxe P<T>
Pour signifier quelque chose comme impl Deref<Target = T>
- un type de pointeur (intelligent) P
qui Deref::deref
S en T
. Pin
ne "s'applique" qu'à/est logique sur ces pointeurs (intelligents).
Disons que nous avons:
struct Wrapper<Field> {
field: Field,
}
La question initiale est
Peut-on obtenir un
Pin<P<Field>>
D'unPin<P<Wrapper<Field>>>
, En "projetant" notrePin<P<_>>
DuWrapper
vers sonfield
?
Cela nécessite la projection de base P<Wrapper<Field>> -> P<Field>
, Qui n'est possible que pour:
références partagées (P<T> = &T
). Ce n'est pas un cas très intéressant étant donné que Pin<P<T>>
Toujours deref
s à T
.
références uniques (P<T> = &mut T
).
J'utiliserai la syntaxe &[mut] T
Pour ce type de projection.
La question devient maintenant:
Pouvons-nous passer de
Pin<&[mut] Wrapper<Field>>
ÀPin<&[mut] Field>
?
Le point qui peut ne pas être clair dans la documentation est qu'il appartient au créateur de Wrapper
de décider!
Il existe deux choix possibles pour l'auteur de la bibliothèque pour chaque champ struct.
Pin
dans ce champPar exemple, la macro pin_utils::unsafe_pinned!
est utilisée pour définir une telle projection (Pin<&mut Wrapper<Field>> -> Pin<&mut Field>
).
Pour que la projection Pin
soit saine:
la structure entière ne doit implémenter Unpin
que lorsque tous les champs pour lesquels il existe une structure Pin
projection implémentent Unpin
.
unsafe
pour déplacer ces champs hors d'un Pin<&mut Wrapper<Field>>
(ou Pin<&mut Self>
lorsque Self = Wrapper<Field>
). Par exemple, Option::take()
est interdit .la structure entière ne peut implémenter Drop
que si Drop::drop
ne déplace aucun des champs pour lesquels il existe une projection structurelle.
la structure ne peut pas être #[repr(packed)]
(corollaire de l'élément précédent).
Dans votre exemple future::Map
donné, c'est le cas du champ future
de la structure Map
.
Pin
sur ce champPar exemple, la macro pin_utils::unsafe_unpinned!
est utilisée pour définir une telle projection (Pin<&mut Wrapper<Field>> -> &mut Field
).
Dans ce cas, ce champ n'est pas considéré comme épinglé par un Pin<&mut Wrapper<Field>>
.
que Field
soit Unpin
ou non n'a pas d'importance.
unsafe
pour déplacer ces champs hors d'un Pin<&mut Wrapper<Field>>
. Par exemple, Option::take()
est autorisé .Drop::drop
Est également autorisé à déplacer ces champs,
Dans votre exemple future::Map
donné, c'est le cas du champ f
de la structure Map
.
impl<Fut, F> Map<Fut, F> {
unsafe_pinned!(future: Fut); // pin projection -----+
unsafe_unpinned!(f: Option<F>); // not pinned --+ |
// | |
// ... | |
// | |
fn poll (mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
// | |
match self.as_mut().future().poll(cx) { // <----+ required here
Poll::Pending => Poll::Pending, // |
Poll::Ready(output) => { // |
let f = self.f().take() // <--------+ allows this
edit : Cette réponse est incorrecte. Il reste là pour la postérité.
Commençons par rappeler pourquoi Pin
a été introduit en premier lieu: nous voulons nous assurer statiquement que les futurs auto-référentiels ne peuvent pas être déplacés, invalidant ainsi leurs références internes.
Dans cet esprit, regardons la définition de Map
.
pub struct Map<Fut, F> {
future: Fut,
f: Option<F>,
}
Map
a deux champs, le premier stocke un futur, le second stocke une fermeture qui mappe le résultat de ce futur à une autre valeur. Nous souhaitons prendre en charge le stockage de types auto-référentiels directement dans future
sans les placer derrière un pointeur. Cela signifie que si Fut
est un type auto-référentiel, Map
ne peut pas être déplacé une fois qu'il est construit. C'est pourquoi nous devons utiliser Pin<&mut Map>
Comme récepteur pour Future::poll
. Si une référence mutable normale à un Map
contenant un futur autoréférentiel était déjà exposée à un implémenteur de Future
, les utilisateurs pourraient provoquer UB en utilisant uniquement du code sécurisé en provoquant le Map
à déplacer à l'aide de mem::replace
.
Cependant, nous n'avons pas besoin de prendre en charge le stockage de types auto-référentiels dans f
. Si nous supposons que la partie auto-référentielle d'un Map
est entièrement contenue dans future
, nous pouvons librement modifier f
tant que nous n'autorisons pas future
être déplacé.
Bien qu'une fermeture auto-référentielle soit très inhabituelle, l'hypothèse selon laquelle f
peut être déplacé en toute sécurité (ce qui équivaut à F: Unpin
) N'est explicitement énoncée nulle part. Cependant, nous déplaçons toujours la valeur dans f
dans Future::poll
En appelant take
! Je pense que c'est en effet un bug, mais je ne suis pas sûr à 100%. Je pense que la f()
getter devrait nécessiter F: Unpin
Ce qui signifierait que Map
ne peut implémenter Future
que lorsque l'argument de fermeture peut être déplacé de derrière un Pin
.
Il est très possible que j'ignore certaines subtilités de l'API pin ici, et la mise en œuvre est en effet sûre. J'enroule toujours ma tête aussi.