web-dev-qa-db-fra.com

Quand est-il sûr de retirer une valeur de membre d'un futur épinglé?

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) par None, ou en appelant Vec::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 type fn(Pin<&mut Wrapper<T>>) -> Option<T>, cette opération peut être utilisée pour déplacer un T hors d'un Wrapper<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é?

23
Shepmaster

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'un Pin<P<Wrapper<Field>>>, En "projetant" notre Pin<P<_>> Du Wrapper vers son field?

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.

Il y a une projection structurelle Pin dans ce champ

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

    • aucune implémentation n'est autorisée à utiliser 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.

Il n'y a pas de projection structurelle Pin sur ce champ

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

    • les implémentations sont autorisées à utiliser 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.

Exemple des deux types de projection

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
8
Daniel H-M

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.

4
ecstaticm0rse