Rust: Problème de suivi pour les API Pin (RFC 2349)

Créé le 18 mars 2018  ·  211Commentaires  ·  Source: rust-lang/rust

Problème de suivi pour rust-lang / rfcs # 2349

Blocage de la stabilisation:

  • [x] Mise en œuvre (PR # 49058)
  • [ ] Documentation

Questions non résolues:

  • [] Devrions-nous offrir des garanties plus solides contre la fuite de données !Unpin ?

Edit : Commentaire de synthèse: https://github.com/rust-lang/rust/issues/49150#issuecomment -409939585 (dans la partie cachée par défaut)

B-RFC-approved C-tracking-issue T-lang T-libs

Commentaire le plus utile

@rfcbot concerne api-refactor

Un peu de struct d'inspiration et la nuit dernière, j'ai compris comment nous pourrions refactoriser cette API afin qu'il n'y ait qu'un seul type Pin , qui enveloppe un pointeur, plutôt que d'avoir à créer une version épinglée de chaque pointeur. Ce n'est en aucun cas une refonte fondamentale de l'API, mais il est préférable de tirer le composant «pins the memory» en un morceau composable.

Tous les 211 commentaires

Je remarque maintenant que l'épinglage de pile ne fait pas partie de la RFC, @withoutboats prévoyez -vous de libérer une caisse pour cela, ou devrais-je simplement copier l'exemple de code dans ma caisse qui en a besoin?

@ Nemo157 Vous devriez le copier et rapporter votre expérience!

La question non résolue concernant la fuite de données Unpin rapporte à cela. Cette API est défectueuse si nous disons que vous ne pouvez pas écraser les données Unpin dans un Pin moins que le destructeur ne s'exécute, comme @cramertj l'a demandé. Il existe d'autres API d'épinglage de pile moins ergonomiques qui fonctionnent pour cela. On ne sait pas quel est le bon choix ici - l'API ergonomique d'épinglage de pile est-elle plus utile ou la garantie supplémentaire sur les fuites est-elle plus utile?

Une chose que je noterai est que l'épinglage de pile n'était pas suffisant pour des choses comme Future::poll à l'intérieur de la macro await! , car cela ne nous permettait pas d'interroger en boucle. Je serais intéressé si vous rencontrez ces problèmes, et comment / si vous les résolvez si vous le faites.

Mon utilisation actuelle est assez triviale, un exécuteur monothread exécutant un seul StableFuture sans prise en charge du spawning . Passer à une API comme @cramertj le suggère fonctionnerait bien avec cela. Je me suis demandé comment étendre cela pour permettre la création de plusieurs StableFuture s, mais au moins avec mon projet actuel, ce n'est pas nécessaire.

J'ai juste essayé d'expérimenter avec l'API. La définition suivante (suggérée par RFC) de Future n'est plus sécurisée pour les objets?

trait Future {
    type Item;
    type Error;

    fn poll(self: Pin<Self>, cx: &mut task::Context) -> Poll<Self::Item, Self::Error>;
}

Ça ne fait rien. J'ai trouvé une note sur un plan pour rendre arbitrary_self_types sûr

@sans bateaux

Une chose que je noterai est que l'épinglage de la pile n'était pas suffisant pour des choses comme Future :: poll dans le fichier await! macro, car elle ne nous permettait pas d'interroger en boucle.

Pouvez-vous développer sur ce sujet?

@RalfJung Vous avez besoin de Pin pour prendre en charge les réemprunts en tant que Pin , ce qui n'est pas le cas actuellement.

@cramertj Cela ressemble à une restriction de l'API Pin , pas de l'API d'épinglage de pile?

@RalfJung Oui, c'est exact. Cependant, PinBox peut être réemprunté comme Pin , tandis que le type épinglé par pile ne le peut pas (un emprunt comme Pin crée un emprunt pour toute la durée de vie du type pile).

Étant donné un Pin , je peux l'emprunter en tant que &mut Pin puis utiliser Pin::borrow - c'est une forme de réemprunt. Je suppose que ce n'est pas le genre de réemprunt dont vous parlez?

@RalfJung Non - des méthodes comme Future::poll étaient prévues pour prendre self: Pin<Self> , plutôt que self: &mut Pin<Self> (qui n'est pas un type self valide car il n'est pas t Deref<item = Self> - c'est Deref<Item = Pin<Self>> ).

Il se peut que nous puissions faire fonctionner cela avec Pin::borrow fait. Je ne suis pas sûr.

@cramertj Je n'ai pas suggéré d'appeler poll sur x: &mut Pin<Self> ; J'ai pensé à x.borrow().poll() .

@RalfJung Oh, je vois. Oui, utiliser une méthode pour réemprunter manuellement pourrait fonctionner.

Je vais essayer de me souvenir de publier un exemple de certaines des choses que je fais avec les Pin s la semaine prochaine, pour autant que je sache, le réemprunt fonctionne parfaitement. J'ai une version épinglée du trait futures::io::AsyncRead ainsi que des adaptateurs fonctionnels comme fn read_exact<'a, 'b, R: PinRead + 'a>(read: Pin<'a, R>, buf: &'b [u8]) -> impl StableFuture + 'a + 'b et je suis capable de travailler cela dans un StableFuture relativement complexe qui vient juste d'épingler en haut niveau.

Voici l'exemple complet de ce que j'utilise pour lire:

pub trait Read {
    type Error;

    fn poll_read(
        self: Pin<Self>,
        cx: &mut task::Context,
        buf: &mut [u8],
    ) -> Poll<usize, Self::Error>;
}

pub fn read_exact<'a, 'b: 'a, R: Read + 'a>(
    mut this: Pin<'a, R>,
    buf: &'b mut [u8],
) -> impl StableFuture<Item = (), Error = Error<R::Error>>
         + Captures<'a>
         + Captures<'b> {
    async_block_pinned! {
        let mut position = 0;
        while position < buf.len() {
            let amount = await!(poll_fn(|cx| {
                Pin::borrow(&mut this).poll_read(cx, &mut buf[position..])
            }))?;
            position += amount;
            if amount == 0 {
                Err(Error::UnexpectedEof)?;
            }
        }
        Ok(())
    }
}

C'est un peu ennuyeux car vous devez passer des instances partout en tant que Pin et utiliser Pin::borrow chaque fois que vous appelez des fonctions dessus.

#[async]
fn foo<'a, R>(source: Pin<'a, R>) -> Result<!, Error> where R: Read + 'a {
    loop {
        let mut buffer = [0; 8];
        await!(read_exact(Pin::borrow(&mut source), &mut buffer[..]));
        // do something with buffer
    }
}

J'ai juste pensé que je pourrais impl<'a, R> Read for Pin<'a R> where R: Read + 'a pour contourner le problème en passant des valeurs comme Pin<'a, R> partout, à la place je pourrais utiliser fn foo<R>(source: R) where R: Read + Unpin . Malheureusement, cela échoue car Pin<'a, R>: !Unpin , je pense qu'il est prudent d'ajouter un unsafe impl<'a, T> Unpin for Pin<'a, T> {} car la broche elle-même n'est qu'une référence et les données derrière elle sont toujours épinglées.

Préoccupation: Il semble probable que nous souhaitons que la plupart des types de libstd implémentent Unpin sans condition, même si leurs paramètres de type ne sont pas Pin . Les exemples sont Vec , VecDeque , Box , Cell , RefCell , Mutex , RwLock , Rc , Arc . Je m'attends à ce que la plupart des caisses ne pensent pas du tout à l'épinglage et que leurs types ne soient donc que Unpin si tous leurs champs sont Unpin . C'est un bon choix, mais cela conduit à des interfaces inutilement faibles.

Cela se résoudra-t-il si nous nous assurons d'implémenter Unpin pour tous les types de pointeurs libstd (peut-être même y compris les pointeurs bruts) et UnsafeCell ? Est-ce quelque chose que nous voudrons faire?

Cela se résoudra-t-il si nous nous assurons d'implémenter Unpin pour tous les types de pointeurs libstd (peut-être même y compris les pointeurs bruts) et UnsafeCell? Est-ce quelque chose que nous voudrons faire?

Oui, cela me semble être la même situation que Send .

Une nouvelle question m'est venue à l'esprit: Quand sont Pin et PinBox Send ? À l'heure actuelle, le mécanisme de trait automatique les rend Send chaque fois que T est Send . Il n'y a aucune raison a priori de faire cela; tout comme les types dans le type partagé ont leur propre trait de marqueur pour l'envoyabilité (appelé Sync ), nous pourrions créer un trait de marqueur indiquant quand Pin<T> est Send , par exemple PinSend . En principe, il est possible d'écrire des types qui sont Send mais pas PinSend et vice versa.

@RalfJung Pin est Send quand &mut T est Send. PinBox est Envoyer lorsque Box<T> est Envoyer. Je ne vois aucune raison pour eux d'être différents.

Eh bien, tout comme certains types sont Send mais pas Sync , vous pourriez avoir un type reposant sur "Une fois que cette méthode est appelée avec Pin<Self> , je peux compter sur ne jamais être déplacée à un autre fil ". Par exemple, cela pourrait donner lieu à des contrats à terme qui peuvent être envoyés avant d'être démarrés pour la première fois, mais doivent ensuite rester dans un thread (un peu comme ils peuvent être déplacés avant d'être démarrés, mais doivent ensuite rester épinglés). Je ne sais pas si je peux trouver un exemple convaincant, peut-être quelque chose sur un avenir qui utilise le stockage local des threads?

Je viens de frapper le problème de la vie mentionné par @Diggsey . Je pense que Pin<Option<T>> -> Option<Pin<T>> devrait être une opération sûre, mais il ne semble pas possible de l'implémenter même en utilisant les API non sûres actuelles, et encore moins quel type d'API serait nécessaire pour créer ce code sûr:

trait OptionAsPin<T> {
    fn as_pin<'a>(self: Pin<'a, Self>) -> Option<Pin<'a, T>>;
}

impl<T> OptionAsPin<T> for Option<T> {
    fn as_pin<'a>(self: Pin<'a, Self>) -> Option<Pin<'a, T>> {
        match *unsafe { Pin::get_mut(&mut self) } {
            Some(ref mut item) => Some(unsafe { Pin::new_unchecked(item) }),
            None => None,
        }
    }
}

(Il est possible de contourner le problème en utilisant la transmutation pour forcer la durée de vie, mais cela me fait me sentir beaucoup trop dégueulasse).

Je voudrais ajouter une question non résolue: devrions-nous rajouter un type de référence épinglé partagé? Je pense que la réponse est oui. Voir cet article avec discussion pour plus de détails.

Je viens de lire que futures 0.2 n'est pas aussi définitif que je le pensais , alors peut-être qu'il est encore possible après tout de renommer Pin en PinMut et d'ajouter une version partagée.

@RalfJung J'ai relu votre article de blog plus en détail pour comprendre les changements que vous proposez.

Je pense que vous avez trouvé un cas d'utilisation potentiellement convaincant pour avoir une variante de Pin immuable, mais je ne comprends pas vos commentaires sur Deref et &Pin<T> <=> &&T . Même si Pin<T> peut être converti en &T toute sécurité, cela ne les rend pas équivalents, car &T ne peut pas être converti en Pin<T> . Je ne vois pas de raison de rendre la conversion sécurisée dangereuse (en éliminant le coffre-fort Deref impl).

Actuellement, la méthode map sur Pin a la signature

pub unsafe fn map<U, F>(this: &'b mut Pin<'a, T>, f: F) -> Pin<'b, U>

Quelle est la raison pour laquelle ce n'est pas la suivante?

pub unsafe fn map<U, F>(this: Pin<'a, T>, f: F) -> Pin<'a, U>

Dans l'état actuel des choses, je ne peux pas transformer un Pin d'un type en un Pin d'un de ses champs sans raccourcir inutilement la durée de vie.

Un autre problème avec la méthode map est qu'il semble impossible de transformer un Pin d'une structure en deux Pin s, chacun d'un champ différent de la structure. Quelle est la bonne façon d'y parvenir?

J'utilise cette macro pour cela:

macro_rules! pin_fields {
    ($pin:expr, ($($field:ident),+ $(,)?)) => {
        unsafe {
            let s = Pin::get_mut(&mut $pin);
            ($(Pin::new_unchecked(&mut s.$field),)+)
        }
    };
}

Dans une grande partie de la discussion autour de Pin il semble y avoir une hypothèse selon laquelle "projeter" une épingle sur un champ privé devrait être considéré comme sûr. Je ne pense pas que ce soit tout à fait vrai. Par exemple, le commentaire doc sur map lit actuellement:

Vous devez garantir que les données que vous renvoyez ne bougeront pas tant que la valeur de l'argument ne bougera pas (par exemple, parce que c'est l'un des champs de cette valeur), et aussi que vous ne sortez pas de l'argument que vous recevez. la fonction intérieure.

La garantie que "les données que vous renvoyez ne bougeront pas tant que la valeur de l'argument ne bougera pas" est la description correcte du contrat qu'un appelant de map doit respecter. La parenthèse «(par exemple, parce que c'est l'un des champs de cette valeur)» semble impliquer que tant que vous ne renvoyez qu'une référence à votre propre champ privé, vous êtes assuré d'être en sécurité. Mais ce n'est pas vrai si vous implémentez Drop . Un Drop impl verra un &mut self , même si un autre code a déjà vu un Pin<Self> . Il peut continuer à utiliser mem::replace ou mem::swap pour sortir de ses champs, violant la promesse faite par une utilisation "correcte" antérieure de map .

En d'autres termes: en utilisant un appel "projection correcte de la broche" à Pin::map (l'appel ressemble à unsafe { Pin::map(&mut self, |x| &mut x.p) } ), sans autre invocations de unsafe , on peut produire un son comportement. Voici un lien de terrain de jeu démontrant cela.

Cela n'implique pas que quelque chose ne va pas avec l'API actuelle. Cela démontre que Pin::map doit être marqué comme unsafe , ce qui est déjà le cas. Je ne pense pas non plus qu'il y ait beaucoup de risque que des gens trébuchent accidentellement à ce sujet lors de la mise en œuvre de contrats à terme ou autres - vous devez vraiment faire tout votre possible pour sortir de vos propres champs dans un Drop impl, et Je doute qu'il y ait de nombreuses raisons pratiques de vouloir le faire.

Mais je pense que le commentaire doc pour map pourrait vouloir le mentionner, et je pense aussi que cela invalide certaines idées d'extensions que je vois dans la RFC et les discussions environnantes:

  • Il ne devrait unsafe chaque fois que c'est fait.
  • La RFC mentionne que si Pin était transformé en une fonctionnalité de langage &'a pin T , il serait «trivial de projeter à travers des champs». Je crois avoir montré que cela devrait toujours nécessiter unsafe , même s'il est limité à la projection de champs privés.

@sans bateaux

Même si Pinpeuvent être convertis en & T en toute sécurité, cela ne les rend pas équivalents, car & T ne peut pas être casté en Pin.

En effet, ce n'est pas une condition suffisante. Ils sont égaux dans mon modèle car dans mon article précédent , nous avons fait la définition suivante:

Définition 5: PinBox<T>.shr . Un pointeur ptr et une durée de vie 'a satisfont le type partagé de PinBox<T> si ptr est un pointeur en lecture seule vers un autre pointeur inner_ptr tel que T.shr('a, inner_ptr)

(Et, en quelque sorte implicitement, la définition correspondante pour Pin<'a, T>.shr .)
Remarquez comment PinBox<T>.shr dépend de T.shr et de rien d'autre. Cela rend PinBox<T>.shr exactement le même invariant que Box<T>.shr , ce qui implique que &Box<T> = &PinBox<T> . Un raisonnement similaire montre que &&T = &Pin<T> .

Donc, ce n'est pas une conséquence de l'API ou du contrat écrit dans la RFC. C'est une conséquence du modèle qui n'a que trois états types: possédé, partagé, épinglé. Si vous voulez argumenter contre &&T = &Pin<T> , vous devez plaider pour l'introduction d'un quatrième état type, "shared pinned".

@MicahChalmer

Dans une grande partie de la discussion autour de Pin, il semble y avoir une supposition que "projeter" une épingle sur un champ privé devrait être considéré comme sûr. Je ne pense pas que ce soit tout à fait vrai.

C'est un très bon point! Pour être clair, il n'y a aucun problème à rendre public le champ p de Shenanigans , n'est-ce pas? À ce stade, n'importe quel client pourrait écrire do_something_that_needs_pinning , et l'intention de la RFC est de rendre cela sûr. (Je ne sais pas pourquoi le RFC mentionne spécifiquement les champs privés, mon interprétation a toujours été qu'il était censé fonctionner avec tous les champs.)

Il est intéressant de voir comment cela interagit avec l' interprétation de drop dans mon modèle . Là, j'ai écrit que drop(ptr) a une condition préalable de T.pin(ptr) . Avec cette interprétation, le code réel en faute dans votre exemple serait l'implémentation drop ! (Maintenant, je me demande pourquoi je ne l'ai pas déjà remarqué lors de la rédaction de l'article ...) Je pense que nous voulons finalement permettre des projections sûres sur les champs, et nous ne devrions vraiment pas fournir drop avec un &mut si le type est épinglé. C'est clairement faux.

Y a-t-il un moyen que nous puissions soit (a) rendre impl Drop dangereux si le type est !Unpin , ou (b) changer sa signature en drop(self: Pin<Self>) si T: !Unpin ? Les deux semblent extrêmement tirés par les cheveux, mais d'un autre côté, les deux résoudraient le point de Drop::drop pour toujours prendre Pin<Self> ;) Mais bien sûr, à ce stade, ce n'est plus une solution réservée aux bibliothèques. Le plus triste, c'est que si nous nous stabilisons tel quel, nous ne pourrons jamais avoir de projections de terrain sûres.

@RalfJung Je suis donc plus intéressé par les questions pratiques (comme vous vous en doutez probablement: wink :). Je pense qu'il y en a deux:

  • Pin<T: !Unpin> devrait-il implémenter Deref<Target =T> ?
  • Devrait-il y avoir à la fois un Pin<T> et un PinMut<T> (le premier étant un pin partagé)?

Je ne vois aucune raison de répondre par la négative à la première question. Je pense que vous avez rouvert la deuxième question; Je suis enclin à revenir à deux types de broches différents (mais pas complètement convaincu). Si jamais nous mettons à niveau cela vers un type de référence au niveau du langage, nous aurions &pin T et &pin mut T .

Il semble que peu importe ce que nous faisons, votre modèle a probablement besoin d'un quatrième état de type pour refléter avec précision les invariants de l'API. Je ne pense pas que la conversion d'un &&T en &Pin<T> devrait être sûre.

Je suis donc plus intéressé par les questions pratiques (comme vous vous attendez probablement à un clin d'œil).

C'est suffisant. ;) Cependant, je pense qu'il est important que nous ayons au moins une compréhension de base de ce qui est et n'est pas garanti autour de Pin une fois que le code non sécurisé entre dans l'image. Basic Rust a eu plusieurs années de stabilisation pour comprendre cela, maintenant nous le répétons pour Pin dans quelques mois. Alors que Pin est un ajout uniquement à la bibliothèque en ce qui concerne la syntaxe, c'est un ajout significatif en ce qui concerne le modèle. Je pense qu'il est prudent que nous documentions de manière aussi exhaustive que possible ce qu'est le code dangereux et qu'il n'est pas autorisé à assumer ou à faire autour de Pin .

Bien que ces préoccupations soient théoriques pour le moment, elles deviendront soudainement très pratiques une fois que nous aurons le premier défaut dû à un code non sûr faisant des hypothèses incompatibles.


Concernant votre deuxième question:

Il semble que peu importe ce que nous faisons, votre modèle a probablement besoin d'un quatrième état de type pour refléter avec précision les invariants de l'API. Je ne pense pas à convertir un && T en un & Pindevrait être sûr.

C'est ce à quoi je m'attendais.

Si nous voulons être conservateurs, nous pourrions renommer Pin en PinMut mais pas ajouter PinShr (probablement appelé Pin mais j'essaye de lever l'ambiguïté ici ) pour le moment, et déclarons qu'un code non sécurisé peut supposer qu'un &PinMut<T> pointe en fait vers quelque chose d'épinglé. Ensuite, nous aurions la possibilité d'ajouter Pin comme référence épinglée partagée plus tard.

Une raison pratique pour avoir PinShr a été évoquée par PinShr<'a, Struct> à PinShr<'a, Field> ; si nous utilisons &PinMut pour les broches partagées qui ne fonctionnent pas. Je ne sais pas combien de tels getters seront nécessaires.


Pour votre première question, il semble y avoir des arguments forts en faveur: (a) les plans actuels pour les types d'auto arbitraires sécurisés pour les objets, et (b) pouvoir utiliser en toute sécurité la grande quantité d'API existantes sur des références partagées lors de la détention d'un PinShr (ou PinMut ).

Il est malheureux que nous ne semblions pas avoir un moyen facile de fournir de la même manière des opérations qui peuvent fonctionner à la fois sur &mut et PinMut ; après tout beaucoup de code travaillant sur &mut n'a pas l'intention d'utiliser mem::swap . (Tel que je le vois, ce serait le principal avantage d'une solution basée sur !DynSized ou quelque chose de comparable: &mut se transformerait en un type de références qui pourraient ou non être épinglées. Nous pourrait avoir cela comme un autre type de bibliothèque dans l'API Pin , mais c'est plutôt inutile étant donné que nous avons déjà toutes ces méthodes &mut là-bas.)

Il y a un léger argument contre, à savoir les types qui veulent faire des choses "intéressantes" à la fois pour &T et PinMut<T> . Ce sera difficile à faire, tout n'est pas possible et il faut être très prudent. Mais je ne pense pas que cela l'emporte sur les bons arguments en faveur.

Dans ce cas (ie, avec ce impl Deref ), PinShr<'a, T> devrait venir avec une méthode sûre qui le transforme en &'a T (préservant la durée de vie).


Et il y a une autre préoccupation que je pense que nous devons résoudre: les règles pour Pin::map et / ou drop , à la lumière de ce que @MicahChalmer a remarqué ci-dessus. Nous avons deux choix:

  • Déclarez que l'utilisation de Pin::map avec une projection vers un champ public (pas de déréfs, même implicites) est toujours sûre. C'est mon interprétation de la RFC actuelle. Cela correspondrait à &mut et & . Cependant, nous avons alors un problème autour de Drop : nous pouvons maintenant écrire du code sûr qui provoque UB étant donné un type !Unpin ailleurs bien formé.
  • Ne faites rien de drôle autour de Drop . Ensuite, nous devons ajouter des avertissements bruyants à Pin::map que même son utilisation pour des champs publics pourrait entraîner des problèmes. Même si jamais nous avons &pin T , nous ne pourrons pas l'utiliser pour accéder à un champ en code sécurisé.

Le seul argument possible pour la deuxième option que je peux voir est que c'est peut-être le seul que nous pouvons réellement implémenter. ;) Je pense qu'il est inférieur de toutes les manières possibles - cela rend &pin plutôt étrange et non ergonomique même si cela devient un jour intégré, c'est une arme à pied, cela entrave la compositionnalité.

Il y a peut-être un moyen de réaliser la première option, mais ce n'est pas trivial et je n'ai aucune idée de comment la rendre rétrocompatible: nous pourrions ajouter un Unpin lié à Drop , et ajouter un DropPinned qui n'a pas de borne et où drop prend Pin<Self> . Peut-être que la Unpin liée à Drop pourrait être appliquée d'une manière étrange où vous pouvez écrire impl Drop for S , mais cela ajoute une liaison implicite à S disant que il doit être Unpin . Probablement pas réaliste. : / (Je suppose que c'est aussi un point où l'approche basée sur !DynSized fonctionne mieux - elle transforme &mut T en "peut ou non être épinglé", en gardant drop du son.)

@RalfJung @MicahChalmer Je pense qu'il vaut mieux simplement documenter que si vous sortez du champ dans le Drop impl, projeter sur une épingle de ce champ ailleurs n'est pas sain.

En effet, c'est déjà le cas aujourd'hui (en utilisant un code non sécurisé), vous pourriez sortir d'un champ de type !Unpin , et c'est sûr et bien défini tant que vous ne projetez jamais sur une épingle de ce champ ailleurs . La seule différence avec Drop est que la partie de déménagement ne contient que du code sûr. Il me semble que les notes sur Pin::map doivent changer pour noter qu'il n'est pas sûr de sortir de ce champ, quel que soit le paramètre Drop.

Il doit être prudent de sortir des champs de type !Unpin dans certains cas, car les générateurs sortiront très probablement de l'un de leurs champs lorsqu'ils reviendront.

Je pense qu'il vaut mieux simplement documenter que si vous sortez du champ dans l'implément Drop, projeter sur une épingle de ce champ ailleurs n'est pas sain.

C'est donc la deuxième option, celle qui rend &pin à un champ de façon permanente une opération dangereuse.
Je pense que ce n'est pas un petit changement. Cela change fondamentalement ce que signifie pub sur un champ. Lors de l'utilisation d'un type de bibliothèque, je ne peux pas savoir ce qu'il fait dans sa drop impl, donc je n'ai fondamentalement aucun moyen d'obtenir une référence épinglée à ce champ.

Par exemple, je ne serais même pas autorisé à passer de Pin<Option<T>> à Option<Pin<T>> moins que Option n'indique explicitement qu'il n'y aura jamais de Drop faisant quoi que ce soit " drôle". Le compilateur ne peut pas comprendre cette instruction, donc bien que Option puisse fournir une méthode appropriée pour ce faire, faire de même avec match doit rester une opération non sécurisée.

La seule différence avec Drop est que la partie mobile ne contient que du code sécurisé.

Mais c'est une énorme différence, n'est-ce pas? Nous pouvons mettre des règles arbitraires sur ce que le code dangereux peut ou ne peut pas faire, mais pas pour le code sûr.

Il doit être prudent de sortir des champs de type! Unpin dans certains cas, car les générateurs sortiront très probablement de l'un de leurs champs à leur retour.

Je suppose que dans ce cas, le champ sera Unpin ? Nous pourrions donc probablement avoir une règle disant que Pin::mut pour un champ public d'une structure étrangère est bien si ce champ a le type Unpin . Je ne sais pas à quel point cela est utile, mais c'est probablement mieux que rien.

Je veux rappeler rapidement ma confusion à propos de &Pin<T> ne fournissant pas plus de garanties que &&T . & , &mut et &pin fournissent respectivement "accès partagé", "accès unique" et "accès unique à une valeur qui ne sera pas déplacée". Comprendre &&pin comme "accès partagé à un accès unique à un type qui ne sera pas déplacé" vous indique que la mémoire est partagée (la garantie d'unicité de &pin est annulée par le partage de & ), mais vous conservez toujours la propriété que le type ne sera pas déplacé, non?

Je ne suis pas sûr de ce que vous demandez ou dites. Êtes-vous confus pourquoi je pense que "partagé épinglé" est un mode / type fondamental en soi?

Le fait est que «l'accès partagé» n'est pas quelque chose que je sais définir seul. Il existe de nombreuses façons différentes de partager et de coordonner le partage, comme en témoignent les différentes manières dont, par exemple, Cell , RefCell et Mutex part.

Vous ne pouvez pas simplement dire que vous partagez quelque chose ("annulant la garantie d'unicité" de quelque chose) que vous possédez et vous attendez à ce que cette déclaration ait un sens. Vous devez dire comment vous partagez et comment vous vous assurez que cela ne peut pas causer de ravages. Vous pouvez "partager en le rendant en lecture seule", ou "partager en donnant uniquement un accès atomique via des chargements / magasins synchronisés", ou "partager [dans un seul thread] en faisant en sorte que ce drapeau d'emprunt coordonne le type d'accès à distribuer" . L'un des points clés de RustBelt était de réaliser l'importance de laisser chaque type définir par lui-même ce qui se passe lorsqu'il est partagé.

Je ne peux pas penser à un moyen de faire apparaître «l'épinglage partagé» comme la composition orthogonale du partage et de l'épinglage. Peut-être existe-t-il un moyen de définir la notion de «mécanisme de partage» qui pourrait ensuite être appliqué à l'invariant possédé ou épinglé pour donner lieu à «partagé (normal)» et «partagé épinglé», mais j'en doute sérieusement. De plus, comme nous l'avons vu, cela tombe à plat pour RefCell - si RefCell fait pour l'invariant épinglé partagé quelque chose de similaire à ce qu'il fait pour l'invariant juste partagé, nous ne pouvons certainement pas justifier cela à partir de & &pin RefCell<T> via &RefCell<T> (en utilisant Deref ) via borrow_mut nous pouvons obtenir une référence &mut qui indique qu'aucun épinglage n'a eu lieu.

@RalfJung

Nous pourrions ajouter un Unpin lié à Drop, et ajouter un DropPinned qui n'a pas de lié et où drop prend Pin.

La définition de Drop vraiment le problème ici? Une autre façon d'y penser est de rejeter la faute sur mem::swap et mem::replace . Ce sont ces opérations qui vous permettent de déplacer quelque chose que vous ne possédez pas. Supposons qu'une liaison T: Unpin été ajoutée à _les_?

Pour commencer, cela corrigerait le trou drop que j'ai signalé - mon Shenanigans échouerait à se compiler, et je ne pense pas que je pourrais le faire pour violer les promesses d'épingle sans un autre unsafe . Mais cela permettrait plus que ça! S'il est devenu sûr d'obtenir une référence &mut à une valeur précédemment épinglée dans drop , pourquoi la limiter à seulement drop ?

À moins que je ne manque quelque chose, je pense que cela permettrait d'emprunter une référence &mut à un PinMut<T> tout moment. (J'utilise PinMut pour faire référence à ce qui, dans la nuit actuelle, est appelé Pin , pour éviter toute confusion avec la discussion sur les broches partagées.) PinMut<T> pourrait implémenter DerefMut inconditionnellement, plutôt que juste pour T: Unpin .

Il est malheureux que nous ne semblions pas avoir un moyen simple de fournir de la même manière des opérations qui peuvent fonctionner à la fois sur & mut et PinMut; après tout, beaucoup de code travaillant sur & mut n'a pas l'intention d'utiliser mem::swap .

Un DerefMut impl sur PinMut réglerait ce problème, non? Un code qui se soucie de l'épinglage et nécessite PinMut , pourrait appeler un code qui fonctionne sur &mut toute sécurité et facilement. Le fardeau serait plutôt placé sur les fonctions génériques que _do_ veut utiliser mem::swap celles-ci devraient avoir une liaison Unpin ajoutée, ou utiliser unsafe et faire attention pour ne pas violer les conditions des broches.

Ajouter de telles limites à swap et replace maintenant briserait la rétrocompatibilité en remontant à la toute première version stable. Je ne vois pas de moyen réaliste d'y arriver à partir d'ici. Mais est-ce que je manque un autre trou en pensant que cela aurait été la chose à faire, si seulement cela avait été connu dans les jours pré-1.0?

Dans l'état actuel des choses, je ne vois pas de meilleure solution que ce que map dangereux et mettez un message dans sa documentation avertissant les gens de ne pas quitter un champ qui était précédemment épinglé dans un drop impl.

Nous pouvons mettre des règles arbitraires sur ce que le code dangereux peut ou ne peut pas faire, mais pas pour le code sûr.

L'utilisation de unsafe impose toujours des règles sur le code sécurisé environnant. La bonne nouvelle ici est que pour autant que nous le sachions, si une structure pinnable a une méthode pour projeter une épingle dans un champ privé, seul son propre drop impl peut l'utiliser pour violer le contrat en code sécurisé. Il est donc toujours possible d'ajouter une telle projection et de présenter les _utilisateurs_ de cette structure avec une API entièrement sûre.

La définition de Drop est-elle vraiment le problème ici?

Attribuer le blâme est quelque peu arbitraire, il y a différentes choses que l'on pourrait changer pour boucher le trou de solidité. Mais sommes-nous d'accord pour dire que changer Drop comme je l'ai suggéré résoudrait le problème?

Une autre façon d'y penser est de rejeter la faute sur mem :: swap et mem :: replace. Ce sont ces opérations qui vous permettent de déplacer quelque chose que vous ne possédez pas. Supposons qu'une liaison T: Unpin leur ait été ajoutée?

Eh bien, cela rendrait &mut T généralement sûr à utiliser pour les types !Unpin . Comme vous l'avez observé, nous n'aurions même plus besoin de PinMut . PinMut<'a, T> dans votre proposition pourrait être défini comme &'a mut T , n'est-ce pas?
Il s'agit essentiellement de la proposition ?Move qui a été précédemment rejetée en raison de problèmes de compatibilité descendante et de complexité linguistique.

L'utilisation de unsafe impose toujours des règles sur le code sécurisé environnant.

Je ne suis pas sûr de ce que tu veux dire. Au-delà de la limite de confidentialité, cela ne doit pas être le cas; le code unsafe ne peut rien imposer à ses clients.

La bonne nouvelle ici est que pour autant que nous le sachions, si une structure pinnable a une méthode pour projeter une épingle dans un champ privé, seul son propre impl drop peut l'utiliser pour violer le contrat en code sécurisé. Il est donc toujours possible d'ajouter une telle projection et de présenter aux utilisateurs de cette structure une API entièrement sûre.

Oui, les types peuvent choisir de déclarer la projection sûre. Mais par exemple, le vérificateur d'emprunt ne comprendra pas qu'il s'agit d'un accès à un champ, donc étant donné un PinMut<Struct> vous ne pouvez pas utiliser de telles méthodes pour obtenir PinMut à deux champs différents en même temps.

Mais sommes-nous d'accord pour dire que changer Drop comme je l'ai suggéré résoudrait le problème?

Je suis d'accord, cela réglerait le problème.

Nous n'aurions même plus besoin de PinMut. PinMut <'a, T> dans votre proposition pourrait être défini comme &' a mut T, non?

Non, PinMut<'a, T> est toujours tenu de promettre que le référent ne bougera plus jamais. Avec &'a mut T vous ne pouviez que lui faire confiance pour ne pas bouger à vie 'a . Cela serait toujours autorisé, comme c'est le cas aujourd'hui:

`` `` rouille
struct X;
impl! Détacher pour X {}
fn takes_a_mut_ref (_: & mut X) {}

fn borrow_and_move_and_borrow_again () {
soit mut x = X;
prend_a_mut_ref (& mut x);
let mut b = Box :: new (x);
prend_a_mut_ref (& mut * b);
}
`` ``

Il serait prudent de passer de PinMut<'a, T> à &'a mut T mais pas l'inverse - PinMut::new_unchecked existerait toujours, et serait toujours unsafe .

Il s'agit essentiellement de la proposition ?Move qui a été précédemment rejetée en raison de problèmes de compatibilité descendante et de complexité linguistique.

Si je comprends bien, les propositions ?Move essayaient de ne pas avoir besoin de PinMut , en modifiant les règles de langage fondamentales pour interdire l'extrait de code ci-dessus (en faisant Unpin be a Move trait.) Je ne propose rien de tel - ma proposition est de commencer exactement comme c'est le soir maintenant, plus:

  • Ajouter un Unpin lié aux fonctions std::mem::swap et std::mem::replace
  • Supprimez la Unpin liée de la DerefMut impl de Pin ( PinMut si ce changement de nom se produit)

C'est tout - aucun changement de langage fondamental dans le fonctionnement des mouvements, ou quoi que ce soit du genre. Ma revendication est la suivante: oui, ce serait un changement radical, mais cela aurait moins d'impact que le changement de Drop (ce qui à son tour est encore moins radical que les propositions ?Move ), tout en conservant beaucoup des avantages. En particulier, cela permettrait une projection sécurisée (au moins pour les champs privés, et je pense même pour le public? Pas tout à fait sûr) et éviterait les situations où du code parallèle doit être écrit pour fonctionner avec PinMut et &mut .

Non, PinMut <'a, T> est toujours tenu de promettre que le référent ne bougera plus jamais. Avec «un mut T, vous ne pouviez que lui faire confiance pour ne pas bouger à vie» a.

Je vois. Logique.

Si je comprends bien, les propositions? Move essayaient de ne pas avoir besoin de PinMut, en modifiant les règles de langage fondamentales pour interdire l'extrait de code ci-dessus (en faisant de Unpin un trait Move.)

Compris.

Ma revendication est: oui, ce serait un changement radical, mais cela aurait moins d'impact que le changement de Drop

De quel changement voulez-vous dire? Ajouter une liaison Unpin ? Vous avez peut-être raison, mais je ne sais pas à quel point mem::swap et mem::replace sont largement utilisés.

En particulier, cela permettrait une projection en toute sécurité (au moins pour les champs privés, et je pense même pour le public? Pas tout à fait sûr)

Je ne vois pas comment le privé par rapport au public peut même faire une différence ici. Comment un champ public autoriserait-il moins qu'un champ privé?

Mais oui, cela semble tenir ensemble dans l'ensemble. Future prendrait toujours un PinMut car il doit s'appuyer sur des choses qui ne bougent jamais, mais il disposerait d'un plus large éventail de méthodes disponibles.

Cependant , l'aspect de la compatibilité est important. Je ne pense pas que ce soit réaliste, cela briserait tout le code générique qui appelle mem::swap / mem::replace . De plus, pour le moment, le code non sécurisé est libre d'implémenter ces méthodes lui-même en utilisant ptr::read / ptr::write ; cela pourrait conduire à une rupture

Pendant que nous sommes sur le sujet de l'introduction de Unpin lié sur mem::swap et mem::replace (et ne pas être préoccupé par la casse). Si nous supposons que la route "compilateur intégré" est prise. Serait-il possible d'introduire également la même borne sur mem::forget pour garantir que les destructeurs sont exécutés pour les variables épinglées par pile faisant le son de thread::scoped et éviter de "pré-faire caca" dans certains cas?

Notez que mem::forget sur un PinBox est toujours autorisé. La nouvelle garantie proposée relative à la baisse ne dit PAS "les choses ne fuient pas". Il dit "les choses ne sont pas désallouées sans que drop soit d'abord appelé". C'est une déclaration très différente. Cette garantie n'aide pas thread::scoped .

Pour ajouter du contexte, déplacer des données hors d'une structure qui implémente Future est quelque chose que je fais couramment. Il arrive assez souvent d'avoir besoin d'effectuer un travail de nettoyage si un futur n'a jamais été interrogé jusqu'à son terme (abandonné avant la fin de l'interrogation).

Donc, j'aurais très certainement touché cette mine lors du portage du code existant vers les futures 0.3 même avec la documentation ajoutée à map .

@carllerche la fonction map dit clairement que vous ne devez pas l'utiliser pour déplacer quoi que ce soit. Nous ne pouvons pas et ne voulons pas nous protéger contre les personnes qui quittent délibérément un Pin<T> , mais vous devez sortir de votre chemin (en utilisant un code non sécurisé) pour ce faire. Je n'appellerais pas cela une mine terrestre.

Alors, de quelle mine parlez-vous?

@RalfJung J'ai moi-même essayé de comprendre les limites de la cartographie des références épinglées et je pense que cela va être un énorme footgun si ce n'est pas résolu bientôt. Je pense que la première solution est préférable, malgré sa complexité; ne pas être en mesure de projeter en toute sécurité sur des champs épinglés rend pratiquement impossible pour les consommateurs d'utiliser réellement des API qui reposent sur l'épinglage sans écrire de code non sécurisé.

Si cela ne peut pas être fait, je pense qu'en pratique, la plupart des API utilisables qui utilisent l'épinglage devront utiliser PinShare. Ce n'est peut-être pas un si gros handicap, je suppose, mais je ne suis toujours pas clair sur la relation avec Unpin dans ce cas. Plus précisément: disons que je prends une référence partagée par pins et obtient une référence à un champ sur le type (pour une certaine durée de vie). Puis-je vraiment me fier à ce qu'il ne bouge pas une fois cette vie terminée? Je le peux probablement si le champ est !Unpin alors peut-être que c'est bien, tant que Pin ne peut pas se projeter - je suis surtout inquiet pour les énumérations. À moins que vous ne disiez que même l'épinglage partagé ne peut pas être sûr sans réparer la chute - dans ce cas, je pense que la correction de la chute pour fonctionner avec l'épinglage doit essentiellement se produire; sinon, cela devient une fonctionnalité de bibliothèque de niche qui ne peut pas vraiment être utilisée en toute sécurité et qui ne mérite pas (IMO) une place de choix dans le langage de base, même si cela se trouve être très utile pour Futures.

Je devrais également mentionner que la seule API pratique que j'ai pour les collections intrusives jusqu'à présent (j'ai encore besoin de résoudre les problèmes) a besoin de garanties encore plus fortes que cela; il doit être en mesure de garantir que drop n'est pas appelé tant qu'il y a un emprunt dans la collection. Je peux le faire en utilisant une technique de style GhostCell, mais cela semble très gênant et oblige l'utilisateur à faire une gestion manuelle de la mémoire (car nous devons fuir si la mémoire de sauvegarde pour quelque chose dans la collection est supprimée sans recevoir le jeton). Je suis donc un peu inquiet que la suppression automatique elle-même semble difficile à faire fonctionner avec des types qui utilisent l'épinglage de manière intéressante.

Par curiosité: quels sont les arguments contre l' ajout d'un Unpin lié à Drop ? Vous avez cité la compatibilité ascendante, l'alternative étant que vous auriez besoin d'une manière ou d'une autre de lier automatiquement la chose qui était abandonnée, mais supprimez déjà des restrictions au niveau du système de type déjà étranges qui n'existent pas pour d'autres traits - pourquoi celui-ci est-il si différent? Ce n'est certainement pas aussi élégant que de faire en sorte que drop prenne un Pin<T> mais nous ne pouvons pas réellement faire ce changement à ce stade. La question que nous ne savons pas vraiment quoi faire si vous faites goutte d'appel sur un type qui a seulement Unpin mise en œuvre, lorsque le type lui - même est !Unpin ? Je suppose que lancer une exception dans l'implémentation de la suppression dans ce cas pourrait être la bonne approche, car toute personne qui s'appuie sur drop fonctionnant pour des types génériques doit déjà gérer le cas de panique. Cela signifierait qu'il serait très difficile d'utiliser les types !Unpin dans la pratique sans qu'un groupe de personnes ne mette à jour leur code pour utiliser le nouveau trait Drop (donc l'écosystème serait obligé de tout déplacer vers thew nouvelle version), mais je pense que je serais d'accord avec cela car cela préserverait toujours la solidité et ne casserait pas du tout le code qui n'utilisait pas du tout !Unpin . De plus, la chose "votre code panique si la bibliothèque ne se met pas à jour" inciterait vraiment les gens à passer à autre chose!

En fait, voici ma conception proposée:

Étendez le trait Drop avec une deuxième méthode qui prend Pin, comme vous l'avez proposé. Faire une implémentation par défaut spécialisée where T: Unpin call drop (je suppose que cela passerait les règles de spécialisation actuelles? Mais même si ce n'est pas le cas, Drop peut toujours être avec une casse spéciale; plus, Drop sont généralement générées automatiquement). Vous avez maintenant exactement le même comportement que celui proposé ci-dessus, sans problème de compatibilité descendante et sans tentative de dériver automatiquement une borne.

Cela pose le problème que les bibliothèques tierces devront être mises à niveau pour qu'elles soient pratiquement utiles avec les types !Unpin , mais comme je l'ai dit, c'est sans doute une bonne chose. Dans tous les cas, je n'ai aucune idée du nombre d'implémentations drop qui font muter leurs champs d'une manière qui nécessite &mut plupart de celles auxquelles je peux penser font quelque chose d'intéressant soit utilisent unsafe ou utilisez la mutabilité intérieure, mais je ne pense probablement pas à certains cas d'utilisation courants.

La principale chose à propos de cette approche est que si elle devait être adoptée, elle devrait être prise avant que Pin soit stabilisé. C'est une autre raison pour laquelle j'espère vraiment que Pin ne sera pas précipité. Je ne pense pas que nous ayons suffisamment exploré les conséquences de la conception.

(Je vois un problème potentiel plus: ManuallyDrop et les œuvre dans il pourrait ne pas paniquer , simplement parce qu'il n'a jamais pu s'exécuter (il ne serait pas non plus autorisé à appeler d'autres fonctions &mut sur le T générique, à l'exception de celles implémentées sur des traits dangereux qui promettaient de ne pas paniquer). Depuis Pin ing un type doit déjà garantir que son implémentation drop s'exécute avant que sa mémoire ne soit détruite [selon la nouvelle sémantique], je crois que le type !Unpin dans un ManuallyDrop utilisé à cette fin dans le code existant ne pouvait pas être considéré comme épinglé en premier lieu, donc le code existant devrait toujours être correct s'il fait cette hypothèse; certainement, vous ne devriez pas être en mesure de projeter en toute sécurité une épingle dans un ManuallyDrop , car il ne peut être sûr que si vous garantissez que son destructeur est appelé avant le drop. Je ne sais simplement pas comment on pourrait communiquer ce cas à la comp Est-ce que cela peut profiter du truc du "cache-œil", car il semble que ce soit destiné à un but similaire?]. Je ne suis pas vraiment sûr que cela vole sémantiquement, mais cela fonctionne probablement pour les besoins de l'implémentation Drop pour le code existant.

Au sujet du cache-œil, cependant ... Je ne suis toujours pas certain de la définition formelle exacte qu'il a, mais si l'idée générale est qu'aucune fonction générique intéressante n'est appelée sur T, peut-être pourrait-on l'exploiter davantage? Cela signifie que si l'implémentation drop_pinned pour le type !Unpin était implémentée, le conteneur respectant le cache-œil se comporterait correctement. Il me semble possible que l'on puisse alors compiler des échecs de temps pour les types !Unpin qui implémentent Drop , ne pas implémenter drop_pinned , et ne pas observer leurs paramètres génériques, en de la même manière que nous le faisons lorsque vous utilisez des conteneurs sans cache-œil avec des durées de vie autoréférentielles.

Les existentiels posent cependant un sérieux risque de compatibilité ascendante avec toute stratégie de compilation; C'est pourquoi je pense qu'une solution qui échoue à l'exécution est plus réaliste. cela n'aurait pas d'échec d'exécution et n'aurait pas de faux positifs.

Edit: En fait, effacez tout ce qui précède: nous n'avons vraiment besoin de nous soucier que des champs accessibles dans les destructeurs, pas &mut accès &mut self .

Je réinvente peut-être simplement la proposition !DynSized , mais essentiellement: les conteneurs génériques doivent déjà être eux-mêmes !Unpin s'ils exposent des méthodes qui se soucient du type de pin (je me rends compte que cela ressemble étrangement à une paramétrie incorrecte argument, mais écoutez-moi!). Les existentiels comme Box<Trait> et &mut Trait ne reflètent pas nécessairement leur statut Unpin , mais (au moins en regardant les API actuelles?) Je ne pense pas Pin<Box<T>> et Pin<&mut T> doivent nécessairement être coercibles à Pin<T>T: !Unpin ; ne pas implémenter cela signifierait que les références épinglées à ces types ne fourniront pas en toute sécurité un accès épinglé à leur intérieur (notez qu'il existe un précédent pour cela avec la relation de & mut et &: & mut & T n'est pas coercible à & mut T, seulement & T, et La boîte <& mut T> n'est pas coercible à Box<T> , seulement & mut T; en général, lorsque différents types de types entrent en collision, ils ne doivent pas être automatiquement propagés). Je reconnais que généralement &mut T , Box<T> et T sont considérés comme totalement interchangeables d'un point de vue typestate, et cet argument ne s'étend pas aux existentiels en ligne hypothétiques, mais c'est peut-être la substance de la proposition DynSize (n'autorisez pas les swaps sûrs ou mem::replace s pour les valeurs d'objet de trait, je suppose? En fait, c'est déjà interdit pour eux ... mais je suppose qu'il y a une raison pour laquelle cela pourrait changer à l'avenir). Cela rend une solution de compilation très simple: pour toute structure qui (de manière transitoire) possède directement (no & mut, &, Box , ou des pointeurs bruts - dont aucun ne propagerait transitivement en toute sécurité l'accès Pin , sauf & pour le pin partagé, mais & ne peut pas être déplacé de toute façon - ou si vous avez décidé d'utiliser la solution "Impossible de sortir des objets de trait", vous pourrait également vérifier les champs de manière transitoire le long de &mut et Box je pense) et a accès (dans un sens de visibilité) à un type !Unpin connu (y compris lui-même), il devrait implémentez le deuxième type de drop . Cela me semble être tout à fait correct et ne pas être du tout un footgun de compatibilité ascendante, car aucun type stable n'est !Unpin gens pourraient devoir réimplémenter les destructeurs après la mise à jour d'une bibliothèque, mais c'est tout, non? De plus, les détails internes de la mise en œuvre resteraient internes; ce n'est que si un champ !Unpin était exposé qu'il y aurait des problèmes. Enfin, tous les types de conteneurs génériques (pas seulement Vec et les éléments de bibliothèque standard, mais essentiellement tout l'écosystème crates.io) continueraient à fonctionner correctement. Quelle est la chose catastrophique qui me manque dans cette solution?

(En fait, il me semble que même si vous ne pouvez pas appliquer cela au moment de la définition drop , vous devriez au moins pouvoir le faire au moment de l'instanciation de type, comme le fait dropck , puisque vous n'avez qu'à vous soucier des types entièrement instanciés).

Relecture de la proposition Dynsized : j'observe qu'un argument contre cela exigeait que les types immobiles soient toujours DynSized avant même qu'ils ne soient épinglés. Je soutiens plus haut que nous n'avons vraiment besoin de nous en préoccuper que dans le cas des objets de trait; il pourrait être possible (bien que difficile à justifier) ​​d'imposer que la contrainte d'un type !Unpin à un trait nécessite de le lier explicitement avec + ?DynSized (ou autre chose; cela pourrait être fait automatiquement, je suppose). Bien qu'il puisse y avoir de nombreux cas où la taille doit être connue pour les types !Unpin (en effet, j'ai un tel cas d'utilisation!), Ou ils doivent pouvoir être échangés avant d'être épinglés, j'espère qu'il y en a très peu de cas de ce type pour les intérieurs d'objets de trait fabriqués à partir de types immobiles (les seuls cas que nous avons actuellement concernent des trucs comme la conversion Box -> Rc que nous voulons explicitement interdire, non? En fait, il n'est même pas clair pour moi que l'exposition de size_of_val soit vraiment un problème ici ou non, car je ne sais toujours pas si vous êtes censé pouvoir transformer Pin<'a, Box<Trait> en Pin<'a, Trait> , mais si vous ne pouvez pas, nous pouvons simplement compter sur Sized bien sûr). On veut vraiment pouvoir les lier avec !Unpin dans tous les cas mais comme je l'ai dit, je pense que les gens veulent éviter d'ajouter plus de traits négatifs que ce dont nous avons déjà besoin (personnellement, j'espère que !Unpin les objets de trait seront suffisamment rares et spécialisés pour que les objets de trait de frontière avec ?Unpin plutôt que Unpin soient totalement raisonnables et n'infectent pas trop le système de types; la plupart des utilisations pour !Unpin Je pense que cela n'aurait pas d'implémentations utiles pour la plupart des traits existants, car ils voudraient généralement effectuer des actions sur Pin<self> . Vous voudriez aussi généralement les utiliser avec PinBox ou PinTypedArena ou peu importe à quel moment les limites ?Unpin semblent assez naturelles).

J'ai un nouveau design, qui à mon avis n'est pas aussi horrible que l'ancien: https://github.com/pythonesque/pintrusive/blob/master/src/main.rs. Au lieu d'essayer de faire fonctionner les projections de broches partout, cette conception demande comment nous pouvons interagir avec le code Rust "hérité" qui ne sait rien de l'épinglage, du code Rust "moderne" qui veut toujours soutenir l'épinglage. La réponse évidente semble être d'utiliser le trait PinDrop @RalfJung proposé, mais uniquement pour les types qui ont tous deux une suppression personnalisée et qui souhaitent projeter des champs.

Les types optent explicitement pour les champs de projection (correspondant à la dérivation du trait PinFields ), ce qui est analogue au code écrit en Rust "moderne"; cependant, il ne fait aucune exigence supplémentaire sur le code dans "legacy" Rust, optant à la place de prendre en charge uniquement la projection en profondeur 1 pour toute dérivation donnée de PinFields . Il n'essaye pas non plus de déplacer Pin travers des références, ce que je pense est probablement une bonne idée de ne pas faire de toute façon. Il prend en charge les structures et les énumérations, y compris toute analyse de disjonction que Rust devrait être en mesure de fournir, en générant une structure avec des champs et des variantes identiques, mais avec les types remplacés par Pin 'd références aux types (cela devrait être trivial pour étendre cela à Pin et PinMut lorsque ce changement est effectué). Évidemment, ce n'est pas idéal (bien que l'optimiseur puisse s'en débarrasser en grande partie), mais il a l'avantage de fonctionner avec borrowck et NLL et fonctionne facilement avec des énumérations (par opposition aux accesseurs générés).

L'argument de sécurité fonctionne en s'assurant que Drop n'est pas du tout implémenté pour la structure, ou en s'assurant que si Drop est implémenté pour elle, il s'agit d'une implémentation triviale qui n'appelle que PinDrop (une version de Drop qui prend Pin). Je crois que cela exclut tous les problèmes de solidité avec la projection de champs épinglés, avec un point d'interrogation: ma principale préoccupation est de trouver un bon argument pour expliquer pourquoi des champs disjoints sur le même conteneur exactement (c'est-à-dire les champs à la profondeur 1) qui pourraient invalider les uns des autres broches dans leurs destructeurs, seraient déjà invalides sans projections de broches. Je pense que je peux justifier cela si nous pouvons montrer que vous pourriez aussi faire la même chose s'ils étaient détenus dans des PinBox distinctes, ce qui implique que l' endroit où ils vivent fait partie de leur contrat de sécurité; cela signifie que leurs destructeurs ne sont pas sûrs de manière isolée et leur construction en dehors du module nécessiterait un code non sécurisé. En d'autres termes, leur exactitude dépend de l'implémentation du type de conteneur, ce qui signifie qu'il devrait être acceptable de demander plus à son destructeur que nous le ferions pour du code de sécurité arbitraire.

@pythonesque Je n'ai pas vraiment suivi ce que vous avez écrit ci-dessus à propos de DynSize , mais je suppose que tout est obsolète maintenant de toute façon? Donc, je vais seulement commenter votre dernier message.

Pour résumer, vous dites que projeter sur un champ d'un struct / enum (y compris des champs pub ) n'est pas sûr en général , mais un type peut opter pour des projections de champ sûres en n'implémentant pas Drop . Si le type veut un destructeur, il doit PinDrop au lieu de Drop :

trait PinDrop {
  fn pin_drop(self: PinMut<Self>);
}

Nous avons déjà une recherche de typecheck pour Drop pour refuser de sortir d'un champ, il ne semble donc pas irréaliste de vérifier également Drop pour rejeter la projection via un &pin . Bien sûr, la vérification "sortir du champ" rejetterait toujours s'il y a un PinDrop , alors que la projection serait autorisée dans ce cas.

Le compilateur appliquerait les mêmes restrictions à PinDrop qu'à Drop , plus il garantirait qu'un type n'implémente pas à la fois Drop et PinDrop . Lors de la génération de drop glue, il appelle le type de Drop implémenté par le type.

Cela résume-t-il la proposition? La partie que je ne comprends pas est votre dernier paragraphe, pourriez-vous donner un exemple montrant ce qui vous inquiète?


Maintenant, bien sûr, je me demande quelles sont les obligations de preuve ici. Je pense que le moyen le plus simple de voir cela est de dire que PinDrop est en fait le principal et le seul type de destructeur qui existe formellement, et impl Drop for T est en fait du sucre syntaxique pour impl PinDrop qui appelle la méthode unsafe PinMut::get_mut puis appelle Drop::drop . Cependant , il s'agit d'un code non sécurisé généré automatiquement, car l'épinglage est une "extension locale" (c'est-à-dire qu'il est rétrocompatible avec le code unsafe existant), ce code unsafe est toujours sûr si le type donné ne se soucie pas de l'épinglage.

Un peu plus formellement, il existe un "invariant d'épinglage par défaut" que les types ont si leur auteur ne se soucie pas de l'épinglage. Les types utilisant cet invariant d'épinglage par défaut sont automatiquement Unpin . L'écriture de impl Drop pour un type qui a un invariant personnalisé affirme que le type utilise «l'invariant d'épinglage par défaut». C'est un peu louche car on a l'impression qu'il y avait une obligation de preuve ici, mais il n'y a pas de unsafe pour avertir à ce sujet - et en effet ce n'est pas parfait, mais la rétrocompatibilité est importante alors que pouvez-vous faire. Ce n'est pas non plus une catastrophe, car on peut affirmer que cela signifie vraiment que cela change l'obligation de preuve qui est encourue sur un code non sécurisé ailleurs, dans la mesure où ce code non sécurisé doit fonctionner avec «l'invariant d'épinglage par défaut». S'il n'y a pas de code dangereux ailleurs dans ce module, tout va bien.

Je pourrais même imaginer que nous pourrions ajouter une charpie contre impl Drop for T moins que T: Unpin , peut-être limité au cas où le module définissant le type contient du code non sécurisé. Ce serait un endroit pour éduquer les gens sur le problème et les encourager à soit unsafe impl Unpin (affirmant formellement qu'ils utilisent l'invariant d'épinglage par défaut), soit impl PinDrop .

@RalfJung

Cela résume-t-il la proposition?

Oui, plus ou moins (je pense que votre proposition est en fait beaucoup plus ambitieuse que la mienne car elle semble proposer automatiquement des projections si Drop n'est pas implémenté, ce qui, je pense, est probablement un problème de compatibilité ascendante pour les bibliothèques; mais peut-être qu'il y a un moyen de contourner cette).

La partie que je ne comprends pas est votre dernier paragraphe, pourriez-vous donner un exemple montrant ce qui vous inquiète?

De manière générale: je m'inquiète pour deux champs qui vivent directement sur la même structure, dont l'un mute l'autre lorsque son Drop est appelé (peut-être en s'appuyant sur des choses comme l'ordre de dépôt pour la sécurité) d'une manière qui viole les invariants d'épinglage du autre champ, mais préserve son intégrité structurelle (vous permettant ainsi d'être témoin de violations de l'invariant d'épinglage). Cela ne peut évidemment pas utiliser un code complètement sûr (ou bien il serait rejeté par dropck, entre autres), donc ma préoccupation ne concerne que certains cas hypothétiques où les destructeurs sur les types de champs étaient sûrs à exécuter lorsque les champs sont épinglés séparément structures, mais pas sûrs quand ils sont épinglés dans la même structure. J'espère qu'il n'y a pas de tels cas à moins qu'il y ait un invariant partagé qui inclut la structure dans laquelle les champs sont contenus; s'il existe un tel invariant, on sait qu'il doit être conscient que ses composants ne respectent pas correctement l'invariant personnalisé et on peut donc blâmer le code quelque part dans le module.

Je pense que le moyen le plus simple de voir cela est de dire que PinDrop est en fait le principal et le seul type de destructeur qui existe formellement, et impl Drop for T est en fait du sucre syntaxique pour impl PinDrop qui appelle la méthode unsafe PinMut :: get_mut puis appelle Drop :: drop.

D'accord. Malheureusement, cette avenue n'était pas ouverte à la solution actuelle puisqu'elle tente de faire des choses dans une bibliothèque.

Un peu plus formellement, il existe un "invariant d'épinglage par défaut" que les types ont si leur auteur ne se soucie pas de l'épinglage. Les types utilisant cet invariant d'épinglage par défaut sont automatiquement désépinglés. L'écriture de impl Drop pour un type qui a un invariant personnalisé affirme que le type utilise «l'invariant d'épinglage par défaut». C'est un peu louche car on a l'impression qu'il y avait une obligation de preuve ici, mais il n'y a pas de danger à avertir à ce sujet - et en effet ce n'est pas parfait, mais la rétrocompatibilité est importante alors que pouvez-vous faire.

Oui, c'est à peu près ce que j'ai en tête ... Je suis juste inquiet de ne pas avoir une bonne intuition pour ce que cela signifierait d'officialiser cette garantie "code non sécurisé dans le module". J'aime votre idée d'une charpie (j'aime tout ce qui incite plus de gens à utiliser PinDrop, en fait!) Mais je pense que unsafe impl Unpin serait probablement trop souvent faux pour que ce soit une bonne chose à suggérer, à moins pour les types avec des champs publics génériques (mais là encore, pour les structures qui n'ont pas de tels champs, ce serait en fait assez souvent vrai ... c'est largement vrai pour la bibliothèque standard, par exemple).

J'ai écrit un exemple de la façon dont #[derive(PinnedFields)] pourrait fonctionner: https://github.com/withoutboats/derive_pinned

J'ai vu des gens affirmer que des dérivés comme celui-ci ne sont "pas sains" mais que ce n'est pas vrai. Vous auriez besoin d'utiliser du code non sécurisé pour faire quelque chose qui entrerait en conflit avec le code généré par ce dérivé - c'est-à-dire que cela rendrait un autre code non sécurisé (et je pense que le code - se déplacer dans ?Unpin data - est quelque chose qui vous pouvez / devriez toujours éviter).

EDIT: D'accord, lisez en fait les derniers articles sur les destructeurs. Traitera.

@withoutboats Ouais, je pense que vous avez déjà vu cela, mais le problème était que le code non sûr dans un type !Unpin correct pouvait être invalidé par un destructeur sûr sur un type qui dérivait PinFields (donc il n'y avait pas de code non sécurisé dans le module pour le type qui a dérivé PinFields sauf pour les éléments générés automatiquement). C'est la partie problématique. Jetez un coup d'œil au design que j'ai lié, cependant (en ignorant le choix stylistique de créer une structure séparée plutôt que de dériver des accesseurs individuels - c'était juste moi qui essayais de le faire prendre en charge autant de cas d'utilisation que possible avec le vérificateur d'emprunt). J'étais inquiet pendant un moment, mais je suis maintenant presque sûr que #[derive(PinFields)] peut toujours fonctionner, il faut juste veiller à ce que Drop ne soit pas directement implémenté.

Je veux aussi évoquer un autre point auquel j'ai réfléchi (que je n'ai encore vu directement résolu nulle part?): Je pense que quelque chose qui rendrait Pin beaucoup plus utilisable et s'intégrerait mieux dans le code existant, serait de se positionner fermement du côté de pratiquement tous les types de pointeurs étant Unpin . Autrement dit, faire &mut T , &T , *const T , *mut T , Box<T> , et ainsi de suite, tous seront considérés Unpin pour tout T . Bien qu'il puisse sembler louche d'autoriser quelque chose comme, disons, Box<T> à être Unpin , cela a du sens quand on considère que vous ne pouvez pas obtenir un Pin à l'intérieur de le Box sur un Pin<Box<T>> . Je pense que le fait de permettre uniquement à !Unpin d'infecter des choses "en ligne" est une approche très raisonnable - je n'ai pas un seul cas d'utilisation pour permettre à l'épinglage de devenir viral entre les références, et cela rend la sémantique de tout éventuel type &pin très agréable (j'ai élaboré un tableau pour savoir comment il interagirait avec les autres types de pointeurs dans ce scénario, et essentiellement si vous ignorez les mouvements, cela fait que &mut pin agit de la même manière que &mut , &pin agissent de la même manière que & , et box pin agissent de la même manière que box qui concerne la relation avec les autres pointeurs). Ce n'est pas non plus important sur le plan opérationnel, pour autant que je sache: en général, déplacer une valeur de type A contenant un pointeur vers une valeur de type B ne déplace pas la valeur pointée de type B , sauf si la valeur de type B est en ligne dans la valeur de type A mais si c'est le cas, alors A est automatiquement !Unpin car il contient B sans indirection de pointeur. Peut-être plus important encore, cela signifierait qu'un grand pourcentage des types qui auraient actuellement besoin d'une implémentation manuelle non sécurisée de !Unpin n'en auraient pas besoin, car la plupart des collections ne contiennent qu'un T derrière une indirection de pointeur . Cela permettrait à la fois à l'actuel Drop de continuer à fonctionner et à ces types d'implémenter PinDrop sans changer leur sémantique (car si un type est Unpin il peut traiter un Pin 'd argument comme &mut toute façon).

Il me manque peut-être une raison pour laquelle ce type d'épinglage transitif serait une bonne idée, mais jusqu'à présent, je n'en ai trouvé aucune. La seule chose opérationnelle qui pourrait m'inquiéter est de savoir s'il est réellement possible d'implémenter ce comportement avec des traits automatiques - je pense que ce serait probablement le cas, mais peut-être dans certains cas où les gens utilisent PhantomData<T> mais en possèdent un pointeur vers T , il serait bon pour eux de le changer en PhantomData<Box<T>> . Généralement, nous pensons que ceux-ci sont exactement les mêmes sémantiquement, mais avec l'épinglage, ce n'est pas tout à fait vrai.

@pythonesque La terminologie de "un nouveau langage" et tel est un peu bizarre la situation pour moi. Mon impression de ce que vous faites est:

  • Par défaut, #[derive(PinFields)] génère un impl no-op Drop . Cela garantit que vous n'accédez jamais au champ épinglé dans le destructeur.
  • Un attribut optionnel change ce Drop impl pour appeler PinDrop::pin_drop , et vous êtes censé implémenter PinDrop .

Est-ce correct?


Je crois aussi que tout cela n'a d'importance que si nous étendons les garanties de Pin pour prendre en charge les collections intrusives. Est-ce conforme à votre compréhension de @RalfJung et @pythonesque?


Tout cela est assez frustrant, car ce qui semble clair, c'est que Drop devrait juste Pin ! Faire un changement plus radical (peut-être d'époque) semble attrayant, mais je ne vois pas de moyen qui ne soit pas extrêmement perturbateur.

Voici un exemple du type d'accès défectueux aux champs épinglés + suppression peut provoquer: https://play.rust-lang.org/?gist=8e17d664a5285e941fe1565ce0eca1ea&version=nightly&mode=debug

Le type Foo passe un tampon interne à une sorte d'API externe qui nécessite que le tampon reste où il est jusqu'à ce qu'il soit explicitement dissocié. Autant que je sache, cela semble conforme aux contraintes proposées par @cramertj , après la Pin<Foo> vous avez la garantie qu'il ne sera pas déplacé avant que Drop ait été appelé la condition qu'il puisse à la place être divulgué et que Drop ne soit jamais appelé, mais dans ce cas, vous êtes assuré qu'il ne sera jamais déplacé).

Le type Bar rompt alors ceci en déplaçant le Foo lors de son implémentation Drop .

J'utilise une structure très similaire à Foo pour prendre en charge un périphérique radio qui communique via DMA, je peux avoir un StableStream avec un tampon interne qui est écrit par la radio.

@sans bateaux

Est-ce correct?

Oui, sauf qu'il ne génère pas réellement un no-op Drop impl (car les types qui n'implémentent pas Drop dans Rust fonctionnent mieux en général). Au lieu de cela, il essaie d'affirmer que Drop n'a pas été implémenté en utilisant certaines fonctionnalités de bibliothèque louche (cela fonctionne sur stable mais se brise sous la spécialisation - je pense qu'il existe une variante qui devrait fonctionner sous spécialisation mais ne le fait pas pour le moment car problèmes avec les constantes associées). Si cela devenait une fonctionnalité de langage, il serait assez facile de l'appliquer.

Je pense également que tout cela n'a d'importance que si nous étendons les garanties de Pin pour soutenir les collections intrusives. Est-ce conforme à votre compréhension de @ralfj et @pythonesque?

Non, ce n'est malheureusement pas le cas. Le contre-exemple lié ci-dessus n'a rien à voir avec les garanties supplémentaires pour les collections intrusives. Un type Pin 'd doit déjà être capable de supposer qu'il ne bougera pas avant d'être réutilisé même sans cette garantie, car si une méthode est appelée deux fois sur une valeur derrière une référence épinglée, la valeur a aucun moyen de savoir s'il s'est déplacé entre les deux appels. Les garanties supplémentaires nécessaires pour le rendre utile pour les collections intrusives ajoutent une chose supplémentaire au fait d'avoir à appeler drop avant que la mémoire ne soit libérée, mais même sans cette garantie, drop peut toujours être appelé sur quelque chose qui est actuellement épinglé (derrière une PinBox, par exemple). Si la chose qui a été abandonnée inclut des champs en ligne et que nous autorisons la projection de la broche de la chose qui a été déposée vers ces champs, alors le destructeur du type externe peut toujours déplacer le champ intérieur, l'épingler à nouveau (en mettant le valeur de champ dans un PinBox , par exemple), puis appelez des méthodes sur celui-ci qui attendent des références à partir du moment où il a été épinglé avant d'être toujours valides. Je ne vois vraiment aucun moyen de contourner cela; tant que vous pouvez implémenter drop , vous pouvez avoir ce problème pour n'importe quel champ !Unpin . C'est pourquoi je pense que la solution la moins mauvaise est de ne pas laisser les gens implémenter drop s'ils veulent que l'épinglage fonctionne correctement.

Tout cela est plutôt frustrant, car ce qui semble clair, c'est que Drop devrait se prendre par Pin! Faire un changement plus radical (peut-être d'époque) semble attrayant, mais je ne vois pas de moyen qui ne soit pas extrêmement perturbateur.

Ouais, c'est vraiment ennuyeux ... J'ai été très boudeur à ce sujet pendant environ une semaine. Mais comme Ralf l'a souligné, nous aurions dû attendre trois ans de plus pour la version 1.0 avant que quiconque ne comprenne cela ... et il y aura toujours quelque chose de plus.

Si la chose qui a été abandonnée inclut des champs en ligne et que nous autorisons la projection de la broche de la chose qui a été déposée vers ces champs, le destructeur du type externe peut toujours déplacer le champ interne puis appeler des méthodes qui attendent des références de quand il a été épinglé pour être toujours valide.

La partie accentuée me semble très importante; en fait, cela semble être le nœud du problème.

Au départ, j'ai imaginé ce code, qui utilise notre seule méthode qui se soucie réellement des adresses internes:

struct TwoFutures<F>(F, F);

impl Drop for TwoFutures {
     fn drop(&mut self) {
          mem::swap(&mut self.0, &mut self.1);
          unsafe { Pin::new_unchecked(&mut self.0).poll() }
     }
}

Mais cela implique un code non sécurisé pour revenir au Pin ! Ce code devrait simplement être considéré comme défectueux.

Pouvons-nous exclure la possibilité pour les méthodes qui prennent &self et &mut self de s'appuyer sur la validité des adresses internes pour la solidité? A quoi cela ressemble-t-il?

@withoutboats Même si seules les méthodes qui prennent Pin<Self> reposent sur la validité des adresses internes pour la solidité, vous pouvez toujours avoir le problème. Le destructeur du type externe peut mem::replace le champ du type interne (en utilisant sa propre référence &mut self ), puis PinBox::new la valeur, puis appeler une méthode épinglée dessus . Aucun danger requis. La seule raison pour laquelle ce n'est pas un problème sans pouvoir projeter des champs épinglés est qu'il est considéré comme acceptable pour un type qui implémente en fait des méthodes !Unpin étranges en utilisant unsafe (ou partage un invariant avec un type qui le fait ) pour avoir des obligations de preuve sur son implémentation drop même s'il n'utilise que du code sécurisé. Mais vous ne pouvez pas demander cela aux types qui contiennent simplement un type !Unpin et n'ont pas de code non sécurisé.

@pythonesque

Je pense que votre proposition est en fait beaucoup plus ambitieuse que la mienne puisqu'elle semble proposer automatiquement des projections si Drop n'est pas implémenté, ce qui, je pense, est probablement un problème de compatibilité en aval pour les bibliothèques; mais peut-être qu'il y a un moyen de contourner cela

Eh bien, je pense que nous voulons le faire éventuellement. En quoi est-ce un problème de compatibilité?

Je m'inquiète pour deux champs qui vivent directement sur la même structure, dont l'un mute l'autre lorsque son Drop est appelé (peut-être en s'appuyant sur des choses comme l'ordre de dépôt pour la sécurité) d'une manière qui viole les invariants d'épinglage de l'autre champ, mais préserve son intégrité structurelle (vous permettant ainsi d'être témoin de violations de l'invariant d'épinglage).

Mais ce serait déjà illégal actuellement. Vous ne devez pas violer les invariants des autres.

mais je pense que l'impl Unpin dangereux serait probablement trop souvent faux pour que ce soit une bonne chose à suggérer, au moins pour les types avec des champs publics génériques

Je pense que ce sera en fait correct la plupart du temps - je pense que la plupart des gens ne fourniront aucun accesseur pour Pin<Self> , et dans ce cas, ils utilisent probablement l'invariant d'épinglage par défaut et par conséquent, il est correct de unsafe impl Unpin pour eux.

Je pense que quelque chose qui rendrait Pin beaucoup plus utilisable et s'intégrerait mieux dans le code existant serait de se positionner fermement du côté de l'essentiel de tous les types de pointeurs étant Unpin. Autrement dit, créer & mut T, & T, * const T, * mut T, Box, et ainsi de suite, tous peuvent être considérés comme Unpin pour tout T. Bien qu'il puisse sembler louche d'autoriser quelque chose comme, par exemple, Boxpour être Unpin, cela a du sens lorsque vous considérez que vous ne pouvez pas obtenir une épingle à l'intérieur de la boîte à partir d'une épingle>.

Je conviens que cela se produira probablement (voir également mon commentaire sur https://github.com/rust-lang/rust/pull/49621#issuecomment-378286959). Actuellement, je pense que nous devrions attendre un peu jusqu'à ce que nous nous sentions plus confiants dans l'ensemble de l'épinglage, mais en fait je ne vois pas l'intérêt d'appliquer l'épinglage au-delà des indirections de pointeur.
Je ne suis pas sûr de ce que je pense de faire cela avec des pointeurs bruts, cependant, nous sommes généralement extrêmement conservateurs sur les traits automatiques car ils sont utilisés de toutes sortes de façons folles.


@sans bateaux

J'ai écrit un exemple de la façon dont # [derive (PinnedFields)] pourrait fonctionner: https://github.com/withoutboats/derive_pinned

@pythonesque a déjà dit ceci, mais juste pour être clair: cette bibliothèque n'est pas saine. Son utilisation avec l » @MicahChalmer problème de chute je peux briser tout type épinglé qui repose en fait sur épingler. Ceci est indépendant de la garantie supplémentaire concernant l'appel de drop. Faites-moi savoir si un exemple est encore nécessaire. ;)

@RalfJung

Cette bibliothèque est malsaine.

Pour clarifier, le dérivé est juste unsafe , et ne peut pas être utilisé en combinaison avec un manuel Drop impl. En utilisant le dérivé, vous promettez de ne faire aucune des mauvaises choses répertoriées dans une implémentation de Drop .

https://github.com/rust-lang/rust/pull/50497 change l'API d'épinglage, plus important encore, il renomme Pin en PinMut pour faire de la place pour l'ajout d'un Pin partagé


Pour clarifier, le dérivé est simplement dangereux et ne peut pas être utilisé en combinaison avec un impl Drop manuel.

D'accord, le rendre dangereux de cette façon fonctionnerait. Bien que le test donne l'impression qu'il n'est actuellement pas marqué comme dangereux?

@RalfJung Oui , je ne pense pas que ce soit le cas pour le moment. Une autre option à laquelle je viens de penser serait que la version "sûre" crée une implémentation de Drop pour le type, bloquant les autres implémentations manuelles de Drop . Il pourrait y avoir un indicateur unsafe pour désactiver cette option. WDYT?

@cramertj oui cela devrait aussi fonctionner. Cela aurait cependant des effets secondaires comme un dropck plus restrictif et une incapacité à sortir des champs.

@pythonesque et moi avons eu un appel lundi pour en discuter. Voici un résumé.

Nous avons conclu que le comportement "correct" aurait probablement été que Drop se prenne par épingle. Cependant, la transition vers cela est intimidante. Bien que cela soit possible avec un changement d'édition sous une forme ou une autre, je crois, ce serait extrêmement perturbateur.

Un changement rétrocompatible consiste simplement à le rendre incohérent pour les types qui peuvent être "projetés par épingle" pour implémenter Drop . Dans son référentiel, @pythonesque l' a implémenté en générant des impls à partir d'un ensemble alambiqué de traits.

On pourrait imaginer un formulaire intégré un peu plus simple:

  • Un trait de marqueur, appelons-le PinProjection , contrôle si un type peut être projeté ou non. Il est incohérent (grâce à la magie intégrée du compilateur) d'implémenter à la fois PinProjection et Drop .
  • Un autre trait, PinDrop , étend PinProjection pour fournir un destructeur alternatif à Drop .

Les dérivés pour générer des méthodes de projection de broches comme celles @pythonesque et que j'ai montrées généreront également des impls de PinProjection pour le type.

@sans bateaux

Nous avons conclu que le comportement «correct» aurait probablement été pour Drop de se prendre en main. Cependant, la transition vers cela est intimidante. Bien que cela soit possible avec un changement d'édition sous une forme ou une autre, je crois, ce serait extrêmement perturbateur.

Je me demande simplement, si nous voulions faire une telle chose un jour, est-ce que ce que vous proposez est compatible avec l'avenir?

Par exemple, disons que nous avons décidé de ...

  • faire Drop accepter comme par magie les formulaires épinglés et non épinglés, OU
  • réécrire les signatures Drop de chacun pour eux dans le cadre de la mise à niveau automatique vers Rust 2021 :)

(Je ne propose ni l'un ni l'autre, mais il semble difficile de répondre à la question sans exemples concrets.)

@tmandry C'est essentiellement l'idée.

Le gros problème est une implémentation générique Drop qui déplace le paramètre de type:

struct MyType<T>(Option<T>);

impl<T> Drop for MyType<T> {
    fn drop(&mut self) {
        let moved = self.0.take();
    }
}

Le type de mise à jour automatique dont vous parlez ne ferait que casser cet impl Drop, qui n'est valide que si nous ajoutons une borne que T: Unpin .

Je ne connais aucun cas d'utilisation valide pour déplacer réellement des données ?Unpin dans un destructeur, donc c'est juste ce cas, dans lequel il pourrait techniquement mais n'est pas censé le faire, c'est vraiment un problème.

@sans bateaux

Un autre trait, PinDrop, étend PinProjection pour fournir un destructeur alternatif à Drop.

Pourquoi PinDrop étend PinProjection ? Cela semble inutile.

Cela pourrait également être rendu légèrement plus intelligent en disant que PinProjection et Drop sont incompatibles à moins que le type soit Unpin (ou en trouvant un autre moyen de supprimer toutes ces nouvelles distinctions / restrictions pour les types Unpin ).

@RalfJung nous voudrions que PinDrop et Drop s'excluent mutuellement d'une manière ou d'une autre. En fin de compte, il existe plusieurs façons de mettre en œuvre cela avec différents compromis; Je pense que nous avons besoin d'un RFC pour cela, car ce n'est pas un petit changement.

@withoutboats d' accord. J'ai pensé que l'exclusivité pouvait résulter d'un traitement spécial de PinDrop , mais il y a évidemment plusieurs façons de le faire. Je pense que ce serait très utile si les types Unpin n'ont pas à s'en soucier; ainsi que la création inconditionnelle de tous les types de pointeurs Unpin cela aidera probablement à réduire le nombre de cas où les gens doivent même savoir à ce sujet.

@withoutboats Il convient également de noter que les principales instances auxquelles je peux penser où les gens veulent, par exemple, déplacer des données génériques dans un Vec dans un destructeur (j'ai choisi Vec parce que les gens de l'IIRC aimeraient implémenter des méthodes sur Vec qui se soucient réellement de Pin , ce qui signifie qu'il ne peut pas implémenter inconditionnellement Unpin ), c'est en fait un &mut Vec<T> ou Vec<&mut T> ou quelque chose. J'en parle principalement parce que ce sont des cas qui seraient probablement déclenchés par la charpie suggérée par @RalfJung , et j'espère que les gens pourront donc passer facilement à PinDrop dans ces cas plutôt que unsafe impl Unpin (théoriquement, ils peuvent toujours faire le premier, bien sûr, en délimitant T avec Unpin dans le type, mais ce sera un changement radical pour les clients de la bibliothèque).

En outre, il convient de noter que même si cela ajoute quelques traits de plus que nous le souhaiterions, les traits sont ceux qui n'apparaîtront pratiquement jamais dans la signature de type de quiconque. En particulier, PinProjection est une borne complètement inutile sauf si vous écrivez une macro #[derive(PinFields)] car le compilateur sera toujours (je crois) capable de déterminer si PinProjection tient s'il le peut déterminer quels sont les champs du type, et la seule chose pour laquelle il est utile est de projeter des champs. De même, PinDrop ne devrait fondamentalement jamais avoir besoin d'être une borne pour quoi que ce soit, pour la même raison que Drop n'est presque jamais utilisé comme borne. Même les objets de trait ne devraient être en grande partie pas affectés (à moins qu'un jour nous obtenions des "champs associés", je suppose, mais avec une nouvelle fonctionnalité comme celle-ci, nous pourrions exiger que les traits avec des champs associés ne soient implémentables que sur les types PinProjection , ce qui résoudrait parfaitement ce problème).

@RalfJung

Eh bien, je pense que nous voulons le faire éventuellement. En quoi est-ce un problème de compatibilité?

Je suppose que ce n'est pas plus qu'un ajout d'un type qui désimplémente Send ou Sync , la différence étant qu'aucun de ces deux n'affecte le langage de base dans la même mesure que cela. Le faire complètement automatiquement semble analogue à la façon dont Rust traitait Copy (les types étaient toujours Copy s'ils ne contenaient que des types Copy et n'implémentaient pas Drop ) qui a finalement été changé pour le rendre explicite parce que, vraisemblablement, les gens n'aimaient pas le fait que l' ajout d' un trait ( Drop ) pouvait briser le code générique sans qu'ils s'en rendent compte (puisque tout le point de cohérence est que les implémentations de traits ne doivent pas casser les caisses en aval). Cela semble presque identique, juste avec PinProjection au lieu de Copy . J'aimais en fait l'ancien comportement, je pense juste qu'il serait difficile de justifier le fonctionnement de PinProjection cette façon alors que Copy ne le fait pas.

Mais ce serait déjà illégal actuellement. Vous ne devez pas violer les invariants des autres.

Ouais, plus j'y pense, moins il semble plausible que cela puisse être un problème.

Je pense que ce sera en fait correct la plupart du temps

Eh bien, oui, mais uniquement parce que la plupart des types n'implémentent aucune méthode nécessitant Pin ou exposent leurs champs génériques comme publics. Bien que ce dernier soit peu susceptible de changer, le premier ne l'est pas - je m'attends à ce qu'au moins certains des types stdlib qui sont actuellement !Unpin ajoutent des méthodes de projection de broches, à quel point l'implémentation unsafe serait pas Ce n'est plus valable. Cela me semble donc assez fragile. De plus, je m'inquiète de l'augmentation de la quantité de code non sécurisé «standard» que les gens doivent écrire; Je pense que Send et Sync limites sont unsafe impl « d correctement à peu près aussi souvent qu'ils sont mis en œuvre de manière incorrecte, et Unpin serait encore plus insidieuse parce que la généralement la version correcte n'a pas de limites sur T . Il semble donc préférable d'orienter les gens vers PinDrop (je comprends pourquoi vous vous méfiez de le faire pour les types de pointeurs bruts. Je crains juste que déjà - unsafe code être encore plus susceptible de faire un unsafe impl sans réfléchir, mais plus j'y pense, plus il semble que la valeur par défaut pour les pointeurs bruts est probablement correcte et le signaler avec votre charpie serait utile).

Je pense que ce serait très utile si les types Unpin n'ont pas à s'en soucier.

Je suis en quelque sorte d'accord, mais comme je l'ai dit, les gens n'utiliseront pas PinProjection comme limite réelle, je ne sais pas à quel point cela importerait en pratique. Il existe déjà une implémentation DerefMut pour PinMutT: Unpin pour que vous n'en tiriez aucun avantage aujourd'hui. Une règle implémentée dans le compilateur (pour les projections) transformerait vraisemblablement PinMut::new en une sorte de &pin reborrow pour les types Unpin , mais cela n'a vraiment rien à voir avec les projections de champ. Et puisque dériver PinProjection ne nécessite pas PinProjection pour ses champs, vous n'en auriez pas besoin simplement pour satisfaire les limites d'un derive sur un autre type. Alors vraiment, quel est le gain? La seule chose qu'il vous laisserait vraiment faire est d'implémenter Drop et de dériver PinProjections en même temps, mais nous voudrions toujours que les gens implémentent PinDrop si possible de toute façon. être une OMI nette négative.

J'ai choisi Vec parce que les gens de l'IIRC aimeraient implémenter des méthodes sur Vec qui se soucient réellement de Pin, ce qui signifie qu'il ne peut pas implémenter sans condition Unpin

Hm, je ne pense pas que j'aime ça. Si Box est inconditionnellement Unpin , je pense que Vec devrait être la même chose. Les deux types sont généralement à peu près équivalents dans leur propriété.

Nous devons également faire attention à la garantie drop ; Vec::drain par exemple peut entraîner des fuites en cas de panique.

Cela semble presque identique, juste avec PinProjection au lieu de Copy

Oh, maintenant je comprends votre question. Je ne parlais pas en fait de la dérivation automatique de PinProjections car ma proposition n'avait pas un tel trait - mais en fait une conséquence de ma proposition serait que l'ajout de Drop est un changement radical si vous avoir des champs publics.

La seule chose qu'il vous laisserait vraiment faire est d'implémenter Drop et de dériver PinProjections en même temps, mais nous voudrions toujours que les gens implémentent PinDrop si possible de toute façon, ce serait une IMO nette négative.

C'était en fait le point. Moins les gens doivent se soucier de tout ce truc d'épinglage, mieux c'est.

Question de clarification: Dans votre proposition et PinDrop un élément lang et traité par le compilateur comme un remplacement de Drop , ou est-ce juste le trait utilisé par derive(PinProjections) pour implémenter Drop ?

Nous devons également faire attention à la garantie de chute; Vec :: drain par exemple peut conduire à des fuites en cas de panique.

Eh bien, c'est vrai, mais cela ne désalloue aucune mémoire, n'est-ce pas? Pour être clair, je fait préférerais que Vec a mis en oeuvre sans condition Unpin car il serait plus facile pour les gens à passer juste PinDrop , mais il a parlé sans aucun doute dans une partie de la discussion d'épinglage sur la possibilité d'épingler des éléments de support. Une fois que vous commencez à parler de l'épinglage des champs , il devient clair que vous ne pouvez pas toujours simplement transformer le Vec en un Box<[T]> en place, puis l'épingler, il pourrait donc avoir une valeur à ce point (bien que vous puissiez évidemment aussi ajouter un type PinVec au lieu d'un type Vec , comme cela a été fait pour Box ).

C'était en fait le point. Moins les gens doivent se soucier de tout ce truc d'épinglage, mieux c'est.

Hm, de mon point de vue, ce serait vrai au départ, mais à long terme, nous voudrions migrer autant de personnes que possible vers une valeur par défaut de PinDrop , surtout si le type dérangeait réellement vers #[derive(PinProjections)] (qui, encore une fois, ne serait même pas nécessaire si le type était Unpin cela ne se produirait probablement que dans des trucs comme le code généré). Ensuite (peut-être qu'après que &pin ait été dans la langue pendant un certain temps) vous pourriez avoir un changement d'époque qui changerait Drop en DeprecatedDrop ou quelque chose. Pour les types Unpin changer simplement la signature de type de &mut à &mut pin résoudrait à peu près tout, donc peut-être que ce n'est pas vraiment nécessaire.

Question de clarification: Dans votre proposition et celle de

L'ancien.

Toute cette discussion sur Drop semble être une oubli assez drastique de la part de la RFC originale. Serait-il utile d'ouvrir une nouvelle RFC pour en discuter? Il est un peu difficile de comprendre quelles sont les préoccupations, quelles sont les préoccupations plus pernicieuses que d'autres, combien de machines nous allons devoir ajouter au langage que nous ne le pensions initialement, à quel point nous devrions nous attendre (et si il peut être atténué par l'édition) dans le pire et le meilleur des scénarios, comment nous pourrions passer à tout ce qui réussit Drop, et si nous pourrions lancer tout cela tout en atteignant nos objectifs pour 2018 via d'autres moyens (comme https: //github.com/rust-lang/rfcs/pull/2418 suggère de masquer Pin de toutes les API publiques afin de ne pas bloquer la stabilisation).

@bstrie Je pense qu'il n'y a qu'une seule préoccupation majeure, mais c'est une préoccupation assez importante. La RFC a été approuvée avec la compréhension qu'un éventuel type &pin mut pourrait à un moment donné rendre ce type de code sûr:

struct Foo<T> { foo : T }

fn bar<T>(x: &pin mut Foo<T>) -> &pin mut T {
  &pin mut x.foo
}

Il est important que ce type de code soit sûr car dans la pratique, les références épinglées ne sont pas vraiment composables autrement dans un code sûr, par exemple, l'implémentation de l'un des combinateurs futurs nécessiterait l'utilisation de unsafe . À l'époque, on pensait que l'utilisation de unsafe pouvait être levée dans la plupart des cas et que ce ne serait donc pas un gros problème.

Malheureusement, la sécurité dépend de l'implémentation de Foo Drop pour Foo . Donc, si nous voulons prendre en charge des projections de champ sûres (que ce soit via une fonctionnalité de langage, un #[derive] ou tout autre mécanisme), nous devons être en mesure de garantir que de telles implémentations de Drop peuvent ' t arriver.

L'alternative qui semble fonctionner le mieux à la fois permet au code ci-dessus de fonctionner sans l'utilisation de unsafe (avec seulement l'ajout d'un #[derive(PinProjections)] au-dessus de la structure) et ne casse aucun code existant . Il peut être ajouté de manière rétrocompatible même si Pin est déjà stabilisé et peut même être ajouté en tant que bibliothèque pure (avec un sérieux coup ergonomique). Il est compatible à la fois avec les macros #[derive] pour générer des accesseurs et un éventuel type de référence natif &pin . Bien que cela nécessite l'ajout d'au moins un nouveau trait (et peut-être deux, selon la façon dont la version épinglée de Drop est implémentée), ils n'ont jamais besoin d'apparaître n'importe où dans des clauses ou des objets de trait where , et la nouvelle version de drop ne doit être implémentée que pour les types qui ont actuellement une implémentation Drop et qui souhaitent opter pour les projections de broches.

Ce n'est pas parce qu'une solution semble qu'elle n'a pas beaucoup d'inconvénients que ce n'est pas un changement substantiel, donc je pense que nous allons presque certainement créer un RFC pour cette proposition ( @withoutboats et j'ai discuté de le faire lors de l'appel) . Ce serait bien de le lancer isolément, car il est entièrement rétrocompatible. Personnellement, cependant, je pense qu'il est plus logique de faire passer ce changement plutôt que de se débarrasser de l'épinglage ailleurs.

La plupart des gens s'inquiètent de PinMut dans les API publiques, c'est précisément qu'il faudra soit unsafe ou passer par des limites Unpin partout, et cette proposition résout ce problème. Les alternatives discutées dans la rouille lang / RFCs # 2418 semblent beaucoup plus controversée, à la fois dans la mécanique même de la façon dont il veut éviter de traiter avec épinglant (qui implique la prolifération de divers autres traits qui apparaissent dans les API et la documentation publique) et dans la complexité globale de la solution. En effet, même si l'épinglage était complètement résolu, je pense qu'il y a un certain nombre de questions que les gens ne pensent pas avoir été résolues de manière adéquate avec cette RFC, donc je pense qu'il y a au moins une chance décente qu'une RFC ajoutant des projections de broches sûres puisse finir être accepté avant lui.

Il est vrai que l'épinglage en est à ses débuts (et je sais que je me suis plaint qu'il semblait être stabilisé beaucoup trop rapidement), mais je pense que le manque de projection de champ sûr est la dernière chose majeure qui décourage les gens de l'utiliser. . Par souci d'exhaustivité, voici tous les problèmes que j'ai vu des gens soulever avec l'épinglage, leurs solutions proposées et si la solution est rétrocompatible avec le type Pin existant (dans un ordre très approximatif et biaisé de la façon dont controversé, je perçois la question comme étant pour le moment):

  • Celui-ci (pouvons-nous faire en sorte que la projection des champs se fasse en code sécurisé?). À résoudre par la prochaine RFC (où les stratégies de mise en œuvre possibles seront énoncées, ainsi que d'autres alternatives que nous avons envisagées et pourquoi elles ont dû être écartées). Toutes les variations de la résolution proposée sont rétrocompatibles.
  • L'épinglage d'une valeur de type !Unpin avec un destructeur manuel devrait-il impliquer une garantie supplémentaire (que la mémoire de sauvegarde de la valeur n'est pas invalidée avant l'appel du destructeur), c'est-à-dire "changements pour les collections intrusives"?

    Toujours non résolu, principalement parce qu'il pourrait casser l'API d'épinglage de pile proposée; si une API d'épinglage de pile qui préserve cette garantie peut être amenée à fonctionner avec async / wait, la plupart des parties prenantes semblent disposées à l'accepter (quelqu'un de l'IIRC a déjà essayé de résoudre ce problème en utilisant des générateurs, mais a rouillé ICE'd).

    Non rétrocompatible avec les anciennes garanties; exiger un code non sécurisé pour appliquer la garantie pour le moment est compatible avec les deux résultats possibles, mais uniquement si le code non sécurisé n'est pas autorisé à se fier à son application pour l'exactitude. Ainsi, cela nécessite certainement une résolution dans un sens ou dans l'autre avant de stabiliser l'épinglage.

    Heureusement, les utilisateurs d'épinglage pour Future s peuvent ignorer cela en plus de la forme de l'API d'épinglage de pile. L'API d'épinglage de pile basée sur la fermeture est compatible avec l'une ou l'autre résolution, donc les utilisateurs Future n'utilisant pas async / await peuvent utiliser l'API basée sur la fermeture aujourd'hui sans attendre que cela soit décidé. La décision a le plus d'impact sur les personnes qui souhaitent utiliser l'épinglage à d'autres fins (comme des collections intrusives).

  • Les pointeurs bruts doivent-ils être inconditionnellement Unpin ? Toujours non résolu (je pense que je suis le seul à l'avoir proposé et j'en suis toujours à peu près 50-50). Ne serait pas rétrocompatible; Je marque cela comme controversé principalement pour cette raison.
  • Les types de bibliothèques standard comme Vec devraient-ils être créés sans condition Unpin , ou devraient-ils avoir des accesseurs de champ Pin ajoutés? Toujours non résolu, et peut nécessiter une résolution au cas par cas. Pour tout type de wrapper sécurisé donné, ajouter les accesseurs ou rendre le type inconditionnel Unpin est rétrocompatible.
  • Devrait-on supprimer PinMut::deref ? La réponse semble fondamentalement être «non» car les avantages de la conserver semblent l'emporter de loin sur les inconvénients, et il semble y avoir des solutions de contournement pour les cas qui ont initialement incité les gens à le vouloir (en particulier, Pin<RefCell<T>> ). Le changer serait incompatible avec le passé.
  • Comment devrions-nous fournir des accesseurs de terrain à court terme (en ignorant les problèmes de chute)? Toujours non résolu: les deux options présentées jusqu'à présent sont https://github.com/withoutboats/derive_pinned et https://github.com/pythonesque/pintrusive. La résolution est entièrement rétrocompatible, car après les modifications de la suppression, cela peut être fait correctement dans une macro uniquement dans le code de la bibliothèque.
  • Comment devrions-nous fournir des accesseurs de champ à long terme (par exemple, devrait-il y avoir des types Rust personnalisés &mut pin et &pin Rust? Comment le réemprunt devrait-il fonctionner?). Toujours non résolu, mais rétrocompatible avec tous les autres changements proposés (au meilleur de ma connaissance) et peut évidemment être punté indéfiniment.
  • Les types de référence sûrs devraient-ils être inconditionnellement Unpin ? Semble être résolu (oui, ils devraient). La résolution est entièrement rétrocompatible.
  • Devrions-nous avoir un type Pin partagé en plus d'un unique Pin , afin de rendre les accesseurs de champ réalisables (cela ne fonctionne pas avec &Pin car c'est une référence à une référence)? La résolution changeait Pin en PinMut et ajoutait un type Pin pour le cas partagé. Ce n'était pas rétrocompatible, mais le changement de rupture ( Pin à PinMut ) était déjà effectué, et je suis presque sûr que cela a déjà été accepté.

Je pense que cela couvre presque tout. Comme vous pouvez le voir, tous (à part les pointeurs bruts et la décision de déréf, dont cette dernière semble être en grande partie résolue à ce stade) ont un chemin vers l'avant rétrocompatible, même si l'épinglage était stabilisé aujourd'hui; plus important encore, le fait que nous puissions résoudre les projections de champ signifie que l'utilisation d'un type épinglé dans votre API n'est pas destinée à être une décision que vous regretterez plus tard.

En plus des questions ci-dessus, il y a eu d'autres propositions qui visent à repenser l'épinglage d'une manière beaucoup plus fondamentale. Les deux que je pense comprendre le mieux sont la proposition @comex de faire des types !Unpin !DynSized , et la proposition steven099 (sur les internes; désolé, je ne connais pas le nom de Github) pour avoir un nouveau type Pinned wrapper

L'option !DynSized est une fonctionnalité assez conservatrice (dans le sens où Rust a déjà un trait similaire disponible) qui a l'avantage qu'elle peut déjà être nécessaire pour traiter les types opaques. En ce sens, il peut être encore moins invasif que les modifications proposées à Drop . Il a également un avantage élevé: il résout automatiquement le problème avec Drop car les types !Unpin seraient !DynSized et par conséquent, on ne pourrait pas en sortir. Cela ferait que &mut T et &T fonctionneraient automatiquement comme PinMut et Pin partout où T était !DynSized , donc vous ne le feriez pas 'pas besoin d'une prolifération de versions épinglées de types et de méthodes fonctionnant sur &mut T pourraient souvent fonctionner normalement (si elles étaient modifiées pour ne pas exiger une liaison DynSized alors qu'elles n'en avaient pas besoin ).

L'inconvénient majeur (outre les préoccupations habituelles autour de ?Trait ) semble être qu'un type !Unpin ne pourrait &move , mais je ne sais pas quelle est la sémantique voulue. Je ne comprends pas non plus (d'après la proposition) comment vous pourriez avoir des projections de champ sûres avec elle, car elle repose sur des types opaques; il semble que vous deviez utiliser beaucoup de code non sécurisé en général pour l'utiliser.

Le type Pinned<T> dimensionné est un peu similaire dans l'esprit, mais veut contourner le problème ci-dessus en vous permettant d'envelopper un type dans un ZST qui le rend inamovible (agissant effectivement non dimensionné). Rust n'a rien de comparable pour le moment: PhantomData n'inclut pas réellement une instance du type, et les autres types de taille dynamique génèrent des pointeurs gras et autorisent toujours les déplacements (en utilisant des API reposant sur size_of_val ; c'est ce que ?DynSized était destiné à corriger, donc cette proposition se superpose probablement à nouveau à ce trait). Il ne me semble pas que cette proposition corrige réellement le problème Drop si vous autorisez des projections sûres, et cela semble également incompatible avec Deref , donc pour moi les avantages par rapport à Pin ne sont pas si clairs.

si une API d'épinglage de pile qui préserve cette garantie peut être amenée à fonctionner avec async / wait, la plupart des parties prenantes semblent disposées à l'accepter (quelqu'un de l'IIRC a déjà essayé de résoudre cela en utilisant des générateurs mais rustc ICE'd)

Pour référence, cela fait probablement référence à https://github.com/rust-lang/rust/issues/49537 qui provient de cette tentative d'une API d'épinglage de pile basée sur un générateur imbriqué. Je ne suis pas certain que les durées de vie là-bas fonctionneraient même si l'ICE est résolu.

Toujours non résolu, principalement parce qu'il pourrait casser l'API d'épinglage de pile proposée; si une API d'épinglage de pile qui préserve cette garantie peut être amenée à fonctionner avec async / wait, la plupart des parties prenantes semblent disposées à l'accepter (quelqu'un de l'IIRC a déjà essayé de résoudre ce problème en utilisant des générateurs, mais a rouillé ICE'd).

Je vois l'API d'épinglage de pile basée sur la fermeture comme une solution à cela, avec une avenue future (une fois que &pin est une primitive de langage) pour quelque chose de plus ergonomique et vérifié par le compilateur. Il y a aussi cette solution macro-basée proposée par @cramertj.

Les pointeurs bruts doivent-ils être décrochés sans condition? [...] Ne serait pas rétrocompatible; Je marque cela comme controversé principalement pour cette raison.

Pourquoi pensez-vous que l'ajout d'un impl Unpin for Vec<T> est rétrocompatible, mais faire la même chose pour les pointeurs bruts ne l'est pas?

L'inconvénient majeur (outre les préoccupations habituelles autour de? Trait) semble être qu'un type! Unpin ne pourrait jamais être déplacé, ce qui est assez différent de la situation avec les types épinglés actuellement. Cela signifie que composer deux types épinglés sans utiliser de référence ne serait pas vraiment possible (pour autant que je sache, de toute façon) et je ne sais pas comment ou s'il y a une solution proposée à cela; IIRC a été conçu pour fonctionner avec une proposition & move, mais je ne suis pas sûr de la sémantique voulue.

Eh bien, sous la proposition de variante de @ steven099 (que j'ai préférée), la plupart des utilisateurs utiliseraient (pour l'instant) un Pinned<T> , qui contient T par valeur mais est !Sized ( et peut-être !DynSized ; la conception exacte de la hiérarchie des traits est ouverte au bikeshedding). Cela finit par ressembler beaucoup à la proposition existante, sauf avec &'a mut Pinned<T> représentant Pin<'a, T> . Mais il est plus composable avec le code actuel et futur générique sur &mut T (pour T: ?Sized ), et il est plus rétrocompatible avec une conception future de vrais types d'immeubles natifs qui n'auraient pas à le faire utilisez Pinned .

Pour entrer plus en détail, Pinned pourrait ressembler à ceci:

extern { type MakeMeUnsized; }

#[fundamental]
#[repr(C)]
struct Pinned<T> {
    val: T,
    _make_me_unsized: MakeMeUnsized,
}

Vous ne construisez généralement pas directement un Pinned<T> . Au lieu de cela, vous pourrez lancer de Foo<T> vers Foo<Pinned<T>> , où Foo est n'importe quel pointeur intelligent qui garantit qu'il ne déplacera pas son contenu:

// This would actually be a method on Box:
fn pin_box<T>(b: Box<T>) -> Box<Pinned<T>> {
    unsafe { transmute(b) }
}

(Il pourrait également y avoir une API d'épinglage de pile, probablement basée sur une macro, mais c'est un peu plus compliqué à implémenter.)

Dans cet exemple, FakeGenerator représente un type de générateur généré par le compilateur:

enum FakeGenerator {
    Initial,
    SelfBorrowing { val: i32, reference_to_val: *const i32 },
    Finished,
}

Une fois qu'une FakeGenerator est épinglée, le code utilisateur ne doit plus pouvoir y accéder directement par valeur ( foo: FakeGenerator ) ou même par référence ( foo: &mut FakeGenerator ), car ce dernier serait autorise l'utilisation de swap ou replace . Au lieu de cela, le code utilisateur fonctionne directement avec, par exemple, &mut Pinned<FakeGenerator> . Encore une fois, cela est très similaire aux règles pour les PinMut<'a, FakeGenerator> de la proposition existante. Mais comme exemple de meilleure composabilité, l'impl généré par le compilateur peut utiliser le trait Iterator existant, plutôt que d'avoir besoin d'un nouveau qui prend Pin<Self> :

impl Iterator for Pinned<FakeGenerator> {
    type Item = i32;
    fn next(&mut self) -> Option<Self::Item> {
        /* elided */
    }
}

D'autre part, pour la construction initiale, nous pouvons distribuer directement les valeurs FakeGenerator et leur permettre d'être déplacées, à condition de garantir que seuls les états non auto-emprunteurs sont accessibles avant d'être épinglés:

impl FakeGenerator {
    fn new() -> Self { FakeGenerator::Initial }
}

Donc, si nous voulons composer deux FakeGenerator s par valeur, la construction est simple:

struct TwoGenerators {
    a: FakeGenerator,
    b: FakeGenerator,
}

impl TwoGenerators {
    fn new() -> Self {
        TwoGenerators {
            a: FakeGenerator::new(),
            b: FakeGenerator::new(),
        }
    }
}

Ensuite, nous pouvons épingler l'objet TwoGenerators dans son ensemble:

let generator = pin_box(Box::new(TwoGenerators::new()));

Mais, comme vous l'avez mentionné, nous avons besoin de projections de terrain: un moyen de passer de &mut Pinned<TwoGenerators> à &mut Pinned<FakeGenerator> (en accédant à a ou b ). Ici aussi, cela finit par ressembler beaucoup au design existant Pin . Pour l'instant, nous utiliserions une macro pour générer des accesseurs:

// Some helper methods:
impl<T> Pinned<T> {
    // Only call this if you can promise not to call swap/replace/etc.:
    unsafe fn unpin_mut(&mut self) -> &mut T {
        &mut self.val
    }
    // Only call this if you promise not to call swap/replace/etc. on the
    // *input*, after the borrow is over.
    unsafe fn with_mut(ptr: &mut T) -> &mut Pinned<T> {
        &mut *(ptr as *mut T as *mut Pinned<T>)
    }
}

// These accessors would be macro-generated:
impl Pinned<TwoGenerators> {
    fn a(&mut self) -> &mut Pinned<FakeGenerator> {
        unsafe { Pinned::with_mut(&mut self.unpin_mut().a) }
    }
    fn b(&mut self) -> &mut Pinned<FakeGenerator> {
        unsafe { Pinned::with_mut(&mut self.unpin_mut().b) }
    }
}

Cependant, tout comme la conception existante, la macro devrait empêcher l'utilisateur d'implémenter Drop pour TwoGenerators . D'un autre côté, idéalement, le compilateur nous permettrait de impl Drop for Pinned<TwoGenerators> . Il le rejette actuellement avec une erreur selon laquelle «les implémentations de Drop ne peuvent pas être spécialisées», mais cela pourrait être modifié. IMO, ce serait un peu plus agréable que PinDrop , car nous n'aurions pas besoin d'un nouveau trait.

Désormais, en tant qu'extension future, le compilateur pourrait en principe prendre en charge l'utilisation de la syntaxe de champ native pour passer de Pinned<Struct> à Pinned<Field> , semblable à la suggestion sous la conception existante que le compilateur pourrait ajouter un &pin T natif

Cependant, mon "endgame" idéal n'est pas cela, mais quelque chose de plus dramatique (et plus complexe à implémenter) où le compilateur prendrait un jour en charge les types immobiles "natifs", y compris les durées de vie existentielles. Quelque chose comme:

struct SelfReferential {
    foo: i32,
    ref_to_foo: &'foo i32,
}

Ces types se comporteraient comme Pinned<T> , en étant !Sized etc. [1] Cependant, contrairement à Pinned , ils n'auraient pas besoin d'un état déplaçable initial. Au lieu de cela, ils fonctionneraient sur la base d'une «élision de copie garantie», où le compilateur autoriserait l'utilisation de types immobiles dans les valeurs de retour de fonction, les littéraux de structure et divers autres endroits, mais garantirait que sous le capot, ils seraient construits en place. Donc non seulement pourrions-nous écrire:

let sr = SelfReferential { foo: 5, ref_to_foo: &sr.foo };

(ce que vous pouvez déjà faire un peu ) ... nous pourrions aussi écrire des choses comme:

fn make_self_referential() -> SelfReferential {
    let sr = SelfReferential { foo: 5, ref_to_foo: &sr.foo };
    sr
}

ou

let sr: Box<SelfReferential> = box SelfReferential { foo: 5, ref_to_foo: &sr.foo };

Bien sûr, ce qui précède n'est qu'une esquisse très approximative de ce à quoi ressemblerait la fonctionnalité; la syntaxe que j'ai utilisée a des problèmes, et la syntaxe serait la moindre des nombreuses complexités impliquées dans la conception de la fonctionnalité. (Cela dit, j'y ai suffisamment réfléchi pour que je sois assez convaincu que les complexités peuvent être résolues - que ce n'est pas seulement une idée incohérente qui ne peut pas fonctionner.)

Je le mentionne simplement dans le cadre de la motivation, de retour dans le présent, d'utiliser un design !DynSized -ish au lieu du design Pin existant. Je pense que &'a Pinned<T> est déjà meilleur que Pin<'a, T> car cela évite une explosion combinatoire de types de pointeurs. Mais il hérite de certains des mêmes problèmes:

  1. Il ne prend fondamentalement pas en charge les types qui ne disposent pas d'un état mobile initial, et
  2. C'est bruyant, déroutant (la différence entre les références épinglées et non épinglées du même type est difficile à expliquer), et cela donne aux types immobiles un sentiment de seconde classe. Je veux que les types immobiles et auto-référentiels se sentent de première classe - à la fois parce qu'ils sont intrinsèquement utiles, et pour que Rust ressemble mieux aux personnes venant d'autres langues, où la création de valeurs autoréférentielles est facile et courante.

Dans le futur que j'envisage, les types immobiles natifs résoudraient les deux problèmes, et la plupart des codes n'auraient jamais besoin d'utiliser le mot «pin». (Bien que Pinned puisse encore avoir des cas d'utilisation, pour les cas où l'élision de la copie n'est pas assez bonne et où vous avez vraiment besoin d'un état mobile initial.) En revanche, Pin crée le concept d'un séparer l'état "pas encore épinglé" dans la conception de chaque trait qui l'utilise. Et un &pin intégré le ferait cuire dans la langue.

Quoi qu'il en soit, voici un lien de terrain de jeu pour le design Pinned ci-dessus.

[1] ... bien que nous puissions vouloir un nouveau trait ReallySized (avec un nom moins idiot), pour les types qui sont de taille statique mais qui peuvent ou non être déplaçables. Le fait est qu'il va y avoir un peu de désabonnement ici de toute façon, car avec la prise en charge des valeurs non dimensionnées, de nombreuses fonctions qui prennent actuellement des arguments Sized pourraient tout aussi bien fonctionner avec des arguments non dimensionnés mais déplaçables. Nous allons modifier à la fois les limites des fonctions existantes et les recommandations sur les limites à utiliser pour les fonctions futures. Je prétends qu'il pourrait même valoir la peine qu'une future édition modifie la limite par défaut (implicite) pour les génériques de fonction, bien que cela ait quelques inconvénients.

[modifier: exemple de code fixe]

@RalfJung

Je vois l'API d'épinglage de pile basée sur la fermeture comme une solution à cela, avec une voie future (once & pin est une primitive de langage) pour quelque chose de plus ergonomique et vérifié par le compilateur. Il y a aussi cette solution macro-basée proposée par @cramertj.

Celui basé sur la fermeture ne fonctionne pas correctement avec async / await, je crois, car vous ne pouvez pas céder à partir d'une fermeture. La macro-basée est intéressante, cependant; si c'est vraiment sûr, c'est assez ingénieux. Je ne pensais pas que cela pourrait fonctionner au début, car une panique lors de l'une des baisses de portée pourrait entraîner des fuites des autres, mais apparemment, cela a été corrigé avec MIR?

Je n'étais pas non plus sûr de l'interaction entre la garantie "les destructeurs s'exécutent avant que la mémoire ne soit libérée" et la possibilité de projeter des champs épinglés; si la baisse de niveau supérieur paniquait, j'avais pensé que les champs épinglés projetés dans la valeur ne seraient pas exécutés. Cependant, sur le terrain de jeu de Rust, il semble en fait que les champs de ce type ont de toute façon leurs gouttes, même après les paniques de type de haut niveau, ce qui est assez excitant! Cette garantie est-elle documentée quelque part? Cela semble nécessaire si l'épinglage de pile doit fonctionner avec des projections de broches (cela, ou quelque chose de plus lourd, comme PinDrop qui abandonne toujours en cas de panique, ce qui semble indésirable car cela introduirait une différence de fonctionnalité entre Drop et PinDrop).

Pourquoi pensez-vous que l'ajout d'un impl Unpin pour Vecest rétrocompatible mais faire la même chose pour les pointeurs bruts n'est pas?

Je vois votre argument: quelqu'un pourrait se fier à l'implémentation automatique !Unpin d'un champ Vec<T> et implémenter ses propres accesseurs qui supposaient que l'épinglage était transitif pour un certain !Unpin T . S'il est vrai que PinMut<Vec<T>> ne vous fournit en fait aucun moyen sûr d'obtenir un PinMut<T> , un code non sécurisé pourrait toujours exploiter PinMut::deref pour obtenir des pointeurs bruts et faire des hypothèses sur les pointeurs étant stables. Donc, je suppose que c'est une autre situation où il est rétrocompatible uniquement si le code non sécurisé ne repose pas sur le fait que !Unpin est transitif via Vec (ou autre). Cependant, ce genre de forte dépendance au raisonnement négatif me semble de toute façon louche; si vous voulez vous assurer que vous êtes !UnpinT n'est pas, vous pouvez toujours ajouter un PhantomData<T> (je suppose que cet argument s'applique également aux pointeurs bruts). Une déclaration générale comme "c'est UB de supposer que les types dans la bibliothèque standard sont! Unpin dans le code unsafe, quels que soient leurs paramètres de type, à moins que le type ne soit explicitement exclu ou que sa documentation déclare que cela peut être utilisé" suffirait probablement.

La macro-basée est intéressante, cependant; si c'est vraiment sûr, c'est assez ingénieux. Je ne pensais pas que cela pourrait fonctionner au début, car une panique lors de l'une des baisses de portée pourrait entraîner des fuites des autres, mais apparemment, cela a été corrigé avec MIR?

Ce serait un bogue dans MIR si la panique pendant la chute conduisait à sauter la suppression des variables locales restantes. Cela devrait simplement passer de "chute normale" à "chute de déroulement". Une autre panique interrompra alors le programme.

@RalfJung

Ce serait un bogue dans MIR si la panique pendant la chute conduisait à sauter la suppression des variables locales restantes. Cela devrait simplement passer de "chute normale" à "chute de déroulement". Une autre panique interrompra alors le programme.

Les autres champs de la structure supprimés sont-ils considérés comme des variables locales dans ce contexte? Ce n'est vraiment pas clair pour moi dans la documentation des utilisateurs (en fait, cela est vrai pour toute la garantie dont vous parlez - j'ai seulement découvert qu'il était en fait considéré comme un bogue qui devait être corrigé à partir du suivi des problèmes).

@comex

Le truc à propos de Pinned (ce que vous proposez) est que je pense que si nous voulions l'implémenter (une fois que Rust avait toutes les fonctionnalités en place), nous n'aurions pas à faire beaucoup plus que cela pour le faire. rétrocompatible avec le code existant:

type PinMut<'a, T> = &'a mut Pinned<T>;
type Pin<'a, T> = &'a Pinned<T>;

(Je pense qu'avoir une implémentation deref de Pin à Pinned a également été proposé). Il est instructif de regarder les endroits où il semble que cela ne fonctionnerait pas. Par exemple:

impl Drop for Pinned<TwoGenerators>

ne fonctionne pas (du moins, pas directement) avec PinMut . Même si nous supposons que cela remplace le Drop pour TwoGenerators lui-même lorsque le type Pinned est construit (je ne sais pas comment cela fonctionnerait?), Rust ne le ferait toujours pas sachez appeler la version Pinned du constructeur pour tous les champs qui ont été projetés, car les champs seraient simplement détenus par valeur. Si la version épinglée d'un destructeur était juste toujours utilisée si elle existait, c'est effectivement identique à PinDrop , juste avec une syntaxe étrange, et je ne vois pas comment c'est mieux.

Cependant, on est tenté d'envisager de choisir son destructeur en analysant récursivement si une valeur était enracinée à un Pinned . Observez que si jamais nous voulons autoriser les objets de trait par valeur, nous ne pouvons pas nécessairement nous fier à la capacité de décider d'utiliser ou non la baisse Pinned<T> ou la baisse T au moment de la compilation; Je suppose que vous pensez qu'il pourrait y avoir une entrée vtable séparée pour la version Pinned dans de tels cas? Cette idée m'intrigue en quelque sorte. Cela nécessite certainement un support substantiel du compilateur (bien plus que le PinDrop proposé), mais cela pourrait être globalement plus agréable à certains égards.

Cependant, en relisant le fil, je me souviens qu'il y a d'autres problèmes: l'implémentation deref pour les types épinglés ne fonctionne Pinned<T> (je suppose que c'est parce que l'implémentation deref sur PinMut est logiquement faux d'une certaine manière, c'est pourquoi cela continue de causer des problèmes, mais il est vraiment difficile de justifier de le perdre étant donné sa commodité - à moins que vous ne fassiez tout un tas de types inconditionnellement Unpin toute façon). Plus précisément, je trouve l'exemple de RefCell assez troublant car en présence de Pinned::deref cela signifie que nous ne pourrions même pas appliquer le pinning dynamiquement avec le type existant (je ne sais pas si la spécialisation suffirait). Cela suggère en outre que si nous gardons l'implémentation deref , nous devrons finir par dupliquer la surface de l'API presque autant avec Pinned qu'avec Pin ; et si nous ne le gardons pas, Pinned<T> devient incroyablement difficile à utiliser. Il ne semble pas non plus que Box<Pinned<T>> fonctionnerait à moins que nous ajoutions ?Move séparément de ?DynSized (comme indiqué dans le fil de discussion).

Tout cela peut être une bonne idée, mais dans le contexte de Rust actuel, tout cela me semble un peu peu attrayant, surtout si vous considérez que fondamentalement aucune méthode dans Rust actuel n'aurait ?Move limites deref ferait vraiment mal, sauf si le type est Unpin auquel cas Pinned n'offre aucun avantage). Je soupçonne qu'il modélise de plus près ce qui se passe vraiment en faisant épingler une propriété propriétaire, ce qui rend plus difficile de s'en sortir avec des décisions ad hoc comme PinMut::deref et crée une interface beaucoup plus agréable (subjectivement de toute façon) , mais cela ajoute beaucoup de machines de langage pour ce faire et il ne semble pas que vous pensiez que tout cela est particulièrement utile (par rapport aux types d'immeubles natifs que vous proposez). Je ne sais pas non plus si ce que nous obtenons que nous ne pourrions pas obtenir complètement de manière rétrocompatible (principalement, appeler une implémentation différente en fonction de l'état de la broche) est vraiment utile, même si cela peut être fait (peut-être que vous pourriez enregistrer un branchez-vous parfois dans le destructeur si vous saviez que le type était épinglé?). Je ne sais donc pas si cela vaut la peine de modifier la proposition PinMut à ce stade. Mais il me manque peut-être un cas d'utilisation concret vraiment convaincant.

Le truc à propos de Pinned (ce que vous proposez) est que je pense que si nous voulions l'implémenter (une fois que Rust avait toutes les fonctionnalités en place), nous n'aurions pas à faire beaucoup plus que cela pour le faire. rétrocompatible avec le code existant:

type PinMut<'a, T> = &'a mut Pinned<T>;
type Pin<'a, T> = &'a Pinned<T>;

Tout d'abord, Pinned lui-même nécessiterait un support minimal ou nul du compilateur, si c'est ce que vous entendez par «fonctionnalités». Si vous parlez de conception de bibliothèque, comme DynSized et les traits associés, alors c'est valide, mais…

Ce que vous suggérez ne serait pas vraiment rétrocompatible, puisque par exemple, vous pourriez essayer d'implémenter le même trait pour Pin<'a, T> et &'a T , ce qui deviendrait soudainement conflictuel.

Plus généralement, il existe une différence significative dans les conceptions d'API. Avec Pin , les traits supposés être implémentés par des types immobiles doivent avoir leurs méthodes prenant PinMut<Self> , les fonctions génériques qui veulent prendre des références à des types immobiles doivent ressembler à fn foo<T>(p: PinMut<T>) , etc. D'un autre côté, le design Pinned évite le besoin de nouveaux traits dans la plupart des cas, puisque vous pouvez impliquer des traits pour Pinned<MyStruct> . Donc:

  1. Les types immobiles seraient incompatibles avec les traits existants dont les méthodes prennent &self ou &mut self : par exemple, les générateurs ne pourraient pas impliquer Iterator . Nous aurions donc besoin d'un tas de nouveaux traits équivalents à ceux existants mais prenant PinMut<Self> place. Si nous changeons de cap et faisons de PinMut<T> un alias pour &mut Pinned<T> , nous pourrions alors revenir en arrière et désapprouver tous ces traits en double, mais ce serait assez idiot. Mieux vaut ne pas avoir besoin des doublons en premier lieu.

  2. D'un autre côté, les traits nouvellement conçus ou spécifiques au générateur prendraient probablement PinMut<Self> comme seule option, au prix d'ajouter du bruit pour les types qui veulent implémenter les traits mais ne sont pas immobiles et n'en ont pas besoin. être épinglé. (Plus précisément, les appelants devraient appeler PinMut::new pour passer de &mut self à PinMut<Self> , en supposant que Self: Unpin .) Même si Pin<T> devenait un alias pour &mut Pinned<T> , il n'y aurait aucun moyen de se débarrasser de ce bruit. Et les futurs types immobiles natifs que j'envisage seraient dans la même situation que les types mobiles, nécessitant inutilement d'être enveloppés dans Pinned lorsqu'ils sont toujours considérés comme épinglés.

Je répondrai au reste de votre message dans un deuxième post.

Concernant Drop

Je suis un peu confus par ce que vous dites à propos de Drop , mais dans la mesure où vous essayez de le rendre rétrocompatible avec PinMut , je ne vais pas y penser parce que je ne pense pas que ce soit une bonne approche.

Je pense que la meilleure approche est que si vous avez une structure comme TwoGenerators , vous avez deux options:

  1. Pas de manuel Drop impl pour TwoGenerators ou Pinned<TwoGenerators> ;
  2. Vous impliquez Drop pour Pinned<TwoGenerators> ; pendant ce temps, la même macro qui vous donne des accesseurs générera un Drop impl pour TwoGenerators elle-même qui se convertit simplement en &mut Pinned<TwoGenerators> et le supprime. (C'est sûr: l'invariant requis pour convertir &mut T en &mut Pinned<T> est que vous ne déplacerez pas T après la fin de l'emprunt, et dans le cas de Drop , vous avez le dernier emprunt qui sera jamais créé pour cette valeur.)

La seule raison d'avoir deux options est que, comme mentionné précédemment, vous ne voudrez peut-être pas que votre structure implique Drop parce que les structures qui ne l'impliquent pas sont traitées de manière plus lâche par le vérificateur d'emprunt.

Je ne vois pas pourquoi vous voudriez avoir un destructeur distinct pour l'état épinglé par rapport à l'état non épinglé, il n'est donc pas nécessaire de jouer des tours avec des vtables pour les différencier.

Concernant RefCell

Je ne pense pas que Pinned::deref devrait exister. Les accesseurs de champ sûrs générés par macro devraient être suffisants; Je ne vois pas comment c'est "incroyablement difficile à utiliser". C'est un peu moins agréable que de pouvoir utiliser la syntaxe de champ native, mais un jour, cela sera corrigé par des structures immobiles natives. Quoi qu'il en soit, s'il est difficile à utiliser, le même problème s'applique à Pin .

Cela évite le problème avec RefCell .

surtout si l'on considère que pratiquement aucune méthode dans Rust actuel n'aurait ?Move limites

Au contraire, tout ce qui a une ?Sized lié est implicitement ?Move .

Cela a du sens car en général, il est impossible pour le code avec un ?Sized lié d'assumer la mobilité. La seule exception est le code unsafe qui appelle size_of_val et ensuite memcpy autant d'octets, c'est pourquoi nous avons besoin du hack où size_of_val paniquerait pour les types immobiles (et serait obsolète en faveur d'une nouvelle fonction avec une bonne borne).

Je suis un peu confus par ce que vous dites à propos de Drop, mais dans la mesure où vous essayez de le rendre rétrocompatible avec PinMut, je ne vais pas y penser car je ne pense pas que ce soit une bonne approche .

Je disais que si quelque chose n'est pas rétrocompatible avec PinMut , il devrait y avoir une bonne raison à cela. Cependant, la chose que vous proposez est fonctionnellement identique à PinDrop en tout, sauf que vous voulez l'implémenter sur Pinned<T> (qui ne fonctionne pas dans Rust actuel). Personnellement, je pense que la spécialisation Drop crée un précédent vraiment douteux et est presque certainement indésirable pour des raisons qui n'ont rien à voir avec l'épinglage, donc je ne considère pas cela comme un avantage intrinsèque. Dans tous les cas, je pense que PinDrop peut être principalement séparé du reste de votre proposition.

Quoi qu'il en soit, s'il est difficile à utiliser, le même problème s'applique à Pin.

Bien sûr, et si nous étions prêts à nous débarrasser de PinMut::deref , cela composerait également bien avec des types comme RefCell ; la différence est que nous pouvons encore concocter une solution avec PinMut tout en prenant en charge deref , ce qui semble ne pas fonctionner avec Pinned . Si nous devions nous débarrasser de l'implémentation deref , je pense que je serais beaucoup plus susceptible de convenir que Pinned offre un avantage significatif.

Mais je ne suis vraiment pas sûr que je suis d' accord que de ne pas avoir deref est juste un petit problème dans la pratique: par exemple, cela signifie que dans son état actuel , vous ne pouvez rien faire avec &Pinned<Vec<T>>T: !Unpin , et la même chose s'appliquerait à pratiquement tous les types de bibliothèques existants. C'est un problème qui vient du fonctionnement de Unpin , pas du type particulier de référence dont vous disposez. L'écosystème devrait décider collectivement de faire des choses du type impl Deref for Pinned<Vec<T>> { type Target = Pinned<[T]>; } ou quelque chose du genre, ce qui, je pense que je suis d'accord, serait préférable à un impl PinDeref<Vec<T>> si cela peut être fait pour fonctionner, mais c'est en un monde sans deref . Dans le monde avec deref , presque toutes les bibliothèques peuvent s'en tirer sans aucun accesseur lié aux broches, et ont toujours un support à moitié décent pour les types !Unpin .

Au contraire, tout ce qui a une borne "dimensionnée" est implicitement "Move".

Ah oui, c'est un bon point. Malheureusement, tout un tas de code Rust ne fonctionne pas avec les types avec une liaison !Sized , car ce n'est pas la valeur par défaut, mais au moins une partie le fait. Je ne pense pas que ce soit un avantage convaincant car la plupart des choses que je peux faire sur des valeurs non dimensionnées sont d'appeler des méthodes & ou &mut sur elles (par exemple pour les tranches ou les objets de trait), ni ce que je pourrais faire avec votre proposition (sauf pour les types Unpin ) puisque vous ne voulez pas Pinned::deref . Peut-être que les cas courants pourraient être traités en faisant de #[derive] implémentations générant également des instances distinctes pour Pinned<T> ou quelque chose?

Drop

Personnellement, je pense que la spécialisation de Drop crée un précédent vraiment douteux et est presque certainement indésirable pour des raisons qui n'ont rien à voir avec l'épinglage, donc je ne considère pas cela comme un avantage intrinsèque. Dans tous les cas, je pense que PinDrop peut être principalement séparé du reste de votre proposition.

Je conviens que c'est séparable, mais je ne pense pas que ce soit douteux. Au moins… ce que vous avez dit est juste, c'est une forme de spécialisation. Il ne spécialise pas littéralement un impl de couverture parent de Drop , mais le drop glue généré par le compilateur fait l'équivalent de la spécialisation en appelant Drop uniquement s'il est implémenté. Une implémentation 'userland' ressemblerait à ceci (en ignorant le fait que vous ne pouvez pas appeler manuellement drop ):

trait DropIfImplemented {
    fn maybe_drop(&mut self);
}
impl<T: ?Sized> DropIfImplemented for T {
    default fn maybe_drop(&mut self) {}
}
impl<T: ?Sized + Drop> DropIfImplemented for T {
    fn maybe_drop(&mut self) { self.drop() }
}

Donc, j'imagine que la raison pour laquelle vous ne pouvez pas actuellement écrire des impls Drop `` spécialisés '' est la même raison pour laquelle la spécialisation elle-même est actuellement mal fondée: l'incohérence entre trans (qui efface les paramètres de durée de vie) et typeck (qui ne le fait pas). En d'autres termes, si nous pouvions écrire, disons, impl Drop for Foo<'static> , il serait en fait appelé pour tout Foo<'a> , pas seulement Foo<'static> , car codegen suppose que les deux types sont identiques.

La bonne nouvelle est, comme vous le savez probablement, qu'il y a eu des tentatives pour trouver un moyen de limiter les impls spécialisés afin qu'ils ne puissent pas créer ce type d'incohérence. Et on s'attend à ce que la spécialisation soit éventuellement livrée avec une telle limite. Une fois que cela se produit, je ne vois aucune raison pour laquelle nous ne pourrions pas appliquer les mêmes règles à Drop impls - et pour rendre le langage aussi cohérent que possible, nous devrions le faire.

Maintenant, nous ne voulons pas bloquer l'épinglage sur la spécialisation. Cependant, je prétends qu'autoriser impl Drop for Pinned<MyStruct> - ou plus généralement, autoriser impl<params> Drop for Pinned<MyStruct<params>> dans les mêmes conditions que le compilateur autorise actuellement impl<params> Drop for MyStruct<params> - est garanti comme un sous-ensemble de la spécialisation. permettre, donc si nous en faisons un cas particulier aujourd'hui, il finira par disparaître dans une règle plus générale.

Mais encore une fois, c'est séparable; si les gens n'aiment pas cela, nous pourrions avoir un trait distinct à la place.

Unpin

Mais je ne suis vraiment pas sûr d'être d'accord que ne pas avoir de deref n'est qu'un petit problème en pratique: par exemple, cela signifie que dans son état actuel, vous ne pouvez rien faire avec &Pinned<Vec<T>>T: !Unpin , et la même chose s'appliquerait à pratiquement tous les types de bibliothèques existants. C'est un problème qui vient du fonctionnement de Unpin , pas du type particulier de référence dont vous disposez.

Euh… d'accord, permettez-moi de corriger ma déclaration. Pinned::deref devrait exister, mais il devrait être borné sur Unpin - même si je vais l'appeler Move .

La seule raison pour laquelle Deref impl pour PinMut pose des problèmes avec RefCell est que (contrairement au DerefMut impl) il n'est pas borné sur Unpin . Et la raison de ne pas avoir de limite est le désir de permettre aux utilisateurs d'obtenir &MyImmovableType , permettant aux types immobiles d'implémenter des traits avec des méthodes qui prennent &self , à passer à des fonctions génériques qui prennent &T , etc. C'est fondamentalement impossible pour &mut self , mais cela fonctionne principalement avec &self car vous ne pouvez pas en sortir avec mem::swap ou mem::replace - c'est-à-dire, sauf lorsque vous utilisez RefCell . Mais, selon le raisonnement, la compatibilité avec les références existantes est suffisamment précieuse pour être prise en charge, même si la limitation aux références immuables semble arbitraire, même si elle provoque des kludges.

Avec Pinned , nous pouvons prendre en charge les références immuables et mutables: vous implémentez simplement vos traits sur Pinned<MyStruct> plutôt que directement sur MyStruct . L'inconvénient est qu'il n'est pas compatible avec les traits ou fonctions qui prennent &T mais qui ont séparément une limite Self: Sized ; mais ceux-ci sont relativement rares et souvent involontaires.

Fait intéressant, Pinned lui-même n'a pas vraiment besoin de Unpin pour exister. Après tout, pourquoi quelqu'un créerait-il un &Pinned<Vec<T>> ? Avec PinMut , divers traits prendraient PinMut<Self> , donc même les impls de ces traits pour les types mobiles devraient recevoir un PinMut . Avec Pinned , comme je l'ai dit, les traits continueraient à prendre &self ou &mut self , et vous les impliqueriez pour Pinned<MyStruct> . Si vous voulez impliquer le même trait pour Vec<T> , Pinned n'a pas besoin d'entrer dans l'image.

Cependant, une source potentielle serait la macro d'accesseur de champ. Si tu as

struct SomePinnable {
    gen: FakeGenerator,
    also_a_vec: Vec<Foo>,
}

alors la conception la plus simple générerait toujours des accesseurs en utilisant Pinned :

impl Pinned<SomePinnable> {
    fn gen(&self) -> &Pinned<FakeGenerator> { … }
    fn gen_mut(&mut self) -> &mut Pinned<FakeGenerator> { … }
    fn also_a_vec(&self) -> &Pinned<Vec<Foo>> { … }
    fn also_a_vec_mut(&mut self) -> &mut Pinned<Vec<Foo>> { … }
}

… Et c'est à vous de gérer les Pinned si vous ne le vouliez pas. En fait, je pense que c'est très bien, car Unpin / Move devrait effectivement exister, voir ci-dessous. Mais si cela n'existait pas, une alternative serait d'avoir un moyen d'accepter par champ pour recevoir une référence directe plutôt qu'une Pinned un. Autrement dit, vous auriez

    fn also_a_vec(&self) -> &Vec<Foo> { … }
    fn also_a_vec_mut(&mut self) -> &mut Vec<Foo> { … }

Il ne serait pas judicieux d'avoir à la fois des accesseurs Pinned et non- Pinned , mais l'un ou l'autre devrait suffire.

… Mais oui, nous devons avoir un trait Move , mais pas pour Pinned . Par exemple, cela ferait partie de la liaison pour la nouvelle version de size_of_val (correction: ce ne serait pas le cas, mais on s'attendrait à ce qu'un code unsafe le vérifie avant d'essayer de mémoriser des types arbitraires en fonction du résultat sur size_of_val ); dans le futur avec des valeurs non dimensionnées, ce sera la borne (relâchée à partir de Sized ) pour ptr::read et mem::swap et mem::replace ; si jamais nous obtenons &move , ce serait la limite pour vous laisser, eh bien, sortir de l'un d'eux; et quelque chose de similaire s'applique à l'élision de la copie garantie.

Donc, tant que nous avons le trait, il n'y a aucune raison de ne pas avoir Pinned::deref (et deref_mut ) avec une liaison T: Move .

[modifier: comme me l'a rappelé pythonesque, cela a une sémantique différente de Unpin , alors tant pis.]

(Et puis les types comme Vec et Box voudront implémenter manuellement Move afin qu'il s'applique indépendamment du fait que le type d'élément soit Move .)

Euh… d'accord, permettez-moi de corriger ma déclaration. Pinned :: deref devrait exister, mais il devrait être limité sur Unpin - bien que je vais l'appeler Move.

D'accord, attendez. Est-ce ?Move ou Move ? Le premier signifierait que les types !Unpin ne peuvent même pas être construits dans de nombreux cas; ce dernier me fait me demander comment, exactement, nous faisons référence à des types comme Pinned<T> (puisque ?DynSized n'est pas vraiment la bonne liaison pour eux). J'espère certainement qu'ils ne sont pas identiques - sinon, traiter Move comme Unpin à nouveau exactement ce que nous essayons d'éviter et rend le type inamovible au moment où il est construit.

L'inconvénient est qu'il n'est pas compatible avec les traits ou fonctions qui prennent & T mais qui ont séparément une borne Self: Sized; mais ceux-ci sont relativement rares et souvent involontaires.

Il y a un inconvénient pratique beaucoup plus important, à savoir que peu de ces traits ou fonctions fonctionnent réellement avec & épingléaujourd'hui. Ils pourraient être faits pour fonctionner avec mais cela nécessiterait un grand nombre d'implémentations de traits supplémentaires et (comme je l'ai dit) probablement une refonte significative des implémentations existantes de #[derive] . C'est aussi un coût qui devrait également être payé pour de nouvelles choses - vous devrez tout implémenter pour &Pinned<Self> si vous voulez que cela fonctionne sur les types !Unpin . C'est une (bien) meilleure situation pour les traits qui prennent &mut self qu'avec PinMut , mais pire pour &self , ce qui est (je suppose) beaucoup plus courant. C'est pourquoi je dis que je pense que c'est la solution la plus correcte (en ce sens que si nous n'avions pas beaucoup de bibliothèques Rust existantes, la version Pinned serait meilleure) mais peut-être pas la plus utilisable.

Avec Pinned, comme je l'ai dit, les traits continueraient de prendre & self ou & mut self, et vous les impliqueriez pour Pinned

Réimplémenter chaque trait dans la surface API de Vec , juste avec Pinned cette fois, ne me semble pas très bien (d'autant plus que certains traits ne fonctionnent même pas avec). Je suis à peu près sûr de mettre en œuvre de manière sélective Deref au cas par cas (en laissant &Pinned<Vec<T>> passer à &[Pinned<T>] , par exemple), ou simplement de laisser le tout de Vec be Unpin (et ne pas autoriser la projection de broches), est beaucoup plus sensé. Dans tous les cas, les deux sont plus de travail que de ne rien faire, et il faudrait le reproduire sur tout un tas de types et de traits existants pour que des choses immobiles fonctionnent avec eux.

Je pourrais être convaincu du contraire - j'aime en fait la solution Pinned plus j'y pense - mais je ne sais tout simplement pas où se trouvent toutes ces nouvelles implémentations de traits sur Pinned<T> va venir de; il me semble plus probable que les gens ne prendront tout simplement pas la peine de les mettre en œuvre.

Fait intéressant, Pinned lui-même n'a pas vraiment besoin de Unpin pour exister. Après tout, pourquoi quelqu'un créerait-il un & épinglé>?

Il y a de très bonnes raisons de vouloir faire cela (par exemple: votre type épinglé contient un Vec ). Les collections intrusives se heurteront très fréquemment à ce type de scénario. Je pense que toute proposition basée sur l'idée que les gens ne voudront jamais de Pinned références à des conteneurs existants, ou que vous devez opter pour que Unpin fonctionne, ne fonctionnera probablement pas bien. Ne pas pouvoir opter pour l'écosystème régulier de Rust en ajoutant une limite de Unpin serait incroyablement perturbateur (en fait, presque tous les cas d'utilisation que j'ai pour les types immobiliers deviendraient beaucoup plus difficiles).

Avec PinMut, divers traits prendraient PinMut, donc même les impls de ces traits pour les types mobiles devraient recevoir un PinMut.

Sûr! Le gros avantage de la version Pinned est que vous n'aurez pas besoin de traits distincts pour les références épinglées mutables. Cependant, il est pire ou neutre que PinMut avec deref pour presque tous les autres scénarios.

Il ne serait pas judicieux d'avoir à la fois des accesseurs épinglés et non épinglés, mais l'un ou l'autre devrait suffire.

Les accesseurs manuels nécessitant un code dangereux pour implémenter le son semblent être une mauvaise idée pour moi; Je ne vois pas comment une telle proposition permettrait à la génération d'accesseurs d'être sûre (comment empêcher quelqu'un de fournir des accesseurs unsafe pour affirmer qu'il ne le fera pas?). Cependant, comme vous le notez, utiliser Move (en supposant que cela signifie réellement Unpin ) fonctionnera bien.

Donc, tant que nous avons le trait, il n'y a aucune raison de ne pas avoir Pinned :: deref (et deref_mut) avec un T: Move lié.

Sûr. Je parle spécifiquement des types !Unpin ici. Unpin types PinMut , donc ils ne sont pas si pertinents de mon point de vue. Cependant, les limites de Unpin (ou Move ) sur les génériques ne sont pas agréables et, idéalement, vous devriez pouvoir les éviter autant que possible. Encore une fois, comme je l'ai dit plus tôt: Unpin et tout ce qui est impliqué par !Sized ne sont

… Mais oui, nous devons avoir un trait Déplacer, mais pas pour Épinglé. Par exemple, cela ferait partie de la borne pour la nouvelle version de size_of_val; dans le futur avec des rvalues ​​non dimensionnées, ce sera la borne (relâchée de Sized) pour ptr :: read et mem :: swap et mem :: replace; si jamais nous bougeons et bougeons, ce serait la limite pour vous laisser, eh bien, sortir de l'un d'eux; et quelque chose de similaire s'applique à l'élision de la copie garantie.

Je pense que cela confond à nouveau !Unpin (qui dit qu'un type peut avoir un invariant d'épinglage non par défaut) et le !DynSized -like !Move ; ils ne peuvent pas vraiment être les mêmes sans provoquer le comportement de congélation indésirable.

Oups, vous avez tout à fait raison. Unpin ne peut pas être identique à Move .

Donc, je pense que je suis de retour à croire que Unpin et donc Pinned::deref ne devrait pas exister du tout, et à la place, nous devrions éviter toutes les situations (comme avec la macro générant des accesseurs) où vous obtenir un type comme &Pinned<MovableType> . Mais peut-être y a-t-il un argument selon lequel il devrait exister en tant que trait distinct.

Réimplémenter chaque trait dans la surface API de Vec , juste avec Pinned cette fois, ne me semble pas très bien (d'autant plus que certains traits ne fonctionnent même pas avec). Je suis à peu près sûr de mettre en œuvre sélectivement Deref au cas par cas (en laissant &Pinned<Vec<T>> passer à &[Pinned<T>] , par exemple), ou simplement de laisser le tout de Vec be Unpin (et ne pas autoriser la projection de broches), est bien plus sensé.

Ouais, je ne voulais absolument pas proposer de réimplémenter la totalité de la surface API de Vec ou quelque chose comme ça.

Je conviens que ce serait une bonne idée de ne pas autoriser la "projection d'épingle" sur Vec , puisque &Pinned<Vec<T>> semble être un invariant étranger - vous devriez être autorisé à déplacer le Vec sans invalidation de la broche du contenu.

Comme alternative, que diriez-vous d'autoriser la transmutation de Vec<T> à Vec<Pinned<T>> , ce qui aurait la plupart des Vec API mais omettrait les méthodes qui peuvent provoquer une réallocation? Autrement dit, changez la définition de Vec de l'actuel struct Vec<T> à struct Vec<T: ?Sized + ActuallySized> , pour un nom moins idiot, où en gros Sized devient un alias pour ActuallySized + Move ; puis ajoutez un Sized lié aux méthodes qui peuvent provoquer une réallocation, et une méthode pour faire la transmutation.

Il serait également nécessaire de changer la limite du type d'élément du type de tranche intégré de Sized à ActuallySized . Penser à cela me rappelle la maladresse de changer Sized pour signifier autre chose que Sized , mais d'un autre côté, il est toujours vrai que la grande majorité des Sized limitée dans le code existant nécessitent intrinsèquement Move . Besoin de connaître size_of::<T>() pour indexer dans une tranche est un peu une exception…

Sûr! Le gros avantage de la version Pinned est que vous n'avez pas besoin de traits distincts pour les références épinglées mutables. Cependant, il est pire ou neutre que PinMut avec deref pour presque tous les autres scénarios.

Il a également l'avantage d'éviter les conflits avec RefCell , certes au prix d'un conflit avec d'autres choses (limites Sized ).

Les accesseurs manuels nécessitant un code dangereux pour implémenter le son semblent être une mauvaise idée pour moi; Je ne vois pas comment une telle proposition permettrait à la génération d'accesseurs d'être sûre (comment empêcher quelqu'un de fournir des accesseurs non épinglés sans les rendre non sécurisés pour affirmer qu'ils ne le feront pas?).

Parce que les accesseurs sont impliqués sur Pinned<MyStruct> , pas directement sur MyStruct . Si vous avez &mut MyStruct , vous pouvez toujours accéder manuellement à un champ pour obtenir &mut MyField , mais vous êtes toujours dans un état mobile. Si vous avez &mut Pinned<MyStruct> , vous ne pouvez pas obtenir &mut MyStruct (en supposant que MyStruct est !Unpin ou Unpin n'existe pas), vous devez donc utiliser un accesseur pour accéder aux champs. L'accesseur prend &mut Pinned<MyStruct> (c'est-à-dire qu'il prend &mut self et est impliqué sur Pinned<MyStruct> ) et vous donne soit &mut Pinned<MyField> ou &mut MyField , selon l'option que vous avez choisie. Mais vous ne pouvez avoir qu'un seul type d'accesseur ou l'autre, car l'invariant critique est que vous ne devez pas pouvoir obtenir un &mut Pinned<MyField> , y écrire, libérer l'emprunt, puis obtenir un &mut MyField (et déplacez-le).

Il y a de très bonnes raisons de vouloir faire cela (par exemple: votre type épinglé contient un Vec ). Les collections intrusives se heurteront très fréquemment à ce type de scénario. Je pense que toute proposition basée sur l'idée que les gens ne voudront jamais Pinned références Unpin fonctionne, ne fonctionnera probablement pas bien. Ne pas pouvoir opter pour l'écosystème régulier de Rust en ajoutant une limite Unpin serait incroyablement perturbateur (en fait, presque tous les cas d'utilisation que j'ai pour les types immobiliers deviendraient beaucoup plus difficiles).

Je ne comprends pas tout à fait ce que vous voulez dire ici, mais c'est peut-être parce que vous avez réagi à ma propre erreur wrt Unpin contre Move :)

Maintenant que je suis corrigé ... Si Unpin existe, alors Vec devrait l'impliquer. Mais en supposant qu'il n'existe pas, quels sont exactement les scénarios auxquels vous faites allusion?

Pour une structure qui a un Vec comme l'un de ses champs, j'ai expliqué ci-dessus comment vous obtiendriez une référence non épinglée au champ (au prix de ne pas pouvoir en obtenir une référence épinglée, ce qui c'est bien).

Je suppose que ce serait problématique si vous voulez une structure générique avec un champ qui pourrait contenir un Vec , ou pourrait contenir un type immobile, selon le paramètre de type. Cependant, il peut y avoir une manière différente de résoudre ce problème sans avoir besoin d'un trait Unpin que tout doit penser à implémenter.

@comex

Comme alternative, que diriez-vous d'autoriser la transmutation à partir de Vecà Vec>, qui aurait la plupart de l'API Vec mais omettrait les méthodes qui peuvent provoquer une réallocation?

Car...

Autrement dit, changez la définition de Vec de la structure actuelle Vecpour structurer Vec, pour un nom moins idiot, où fondamentalement Sized devient un alias pour ActuallySized + Move; puis ajoutez une Sized liée aux méthodes qui peuvent provoquer une réallocation et une méthode pour effectuer la transmutation.

... cela semble vraiment très compliqué et cela ne fait pas vraiment ce que vous voulez (c'est-à-dire obtenir un Vec<T> normal sur &mut Pinned<Vec<T>> ou autre). Il est un peu fraîche qu'il vous permet d' épingler un vecteur après le fait, vous obtenez un analogue agréable à Box<Pinned<T>> , mais c'est une préoccupation orthogonale; c'est juste une autre illustration du fait que l'épinglage étant une propriété du type possédé est probablement correct. Je pense que tout ce qui concerne Unpin n'a presque aucun rapport avec la question de savoir comment le type de référence est construit.

Maintenant que je suis corrigé ... Si Unpin existe, alors Vec devrait l'impliquer. Mais en supposant qu'il n'existe pas, quels sont exactement les scénarios auxquels vous faites allusion?

Je vais simplement répondre à cela parce que je pense que cela illustrera mon propos: je ne peux même pas muter un champ i32 via un PinMut sans Unpin . À un moment donné, si vous voulez faire quelque chose avec votre structure, vous devez souvent déplacer quelque chose à l'intérieur (à moins que ce soit complètement immuable). Avoir besoin de personnes pour implémenter explicitement des accesseurs de champ sur Pinned<MyType> semble vraiment ennuyeux, surtout si le champ en question peut toujours être déplacé en toute sécurité. Cette approche semble également être très déroutante à utiliser avec un type intégré Pinned car les projections légales varieraient en quelque sorte d'un champ à l'autre d'une manière qui ne dépend pas du type de champ, ce qui a déjà été rejeté dans Rust quand les champs mut ont été supprimés (et IMO, si nous allons ajouter une telle annotation unsafe est un meilleur choix, car les champs non sécurisés sont un énorme footgun en pratique). Étant donné que les types épinglés intégrés sont à peu près le seul moyen de rendre les énumérations épinglées agréables à utiliser, je tiens à ce qu'ils puissent être quelque peu cohérents avec le reste du langage.

Mais plus important...

Je suppose que ce serait problématique si vous voulez une structure générique avec un champ qui pourrait contenir un Vec, ou pourrait contenir un type immobile, selon le paramètre de type

C'est à peu près le cas d'utilisation qui tue pour Unpin , et (pour moi) le fait que cela semble fonctionner est assez fantastique et valide tout le modèle d'épinglage (au point que je pense même si nous partions de avant Rust 1.0, nous voudrions probablement garder Unpin tel quel). Les paramètres génériques vivant en ligne dans la structure sont également à peu près le seul moment où vous devriez avoir besoin de se lier par Unpin si les plans actuels (pour que presque tous les types de référence sûrs implémentent Unpin sans condition) traverser.

Mais ce que je n'obtiens vraiment pas, c'est: pourquoi voulez-vous supprimer Unpin ? L'éliminer ne vous achète pratiquement rien; toutes les belles choses que vous obtenez de Pinned sur PinMut (ou vice versa) n'ont pratiquement aucun rapport avec sa présence. Son ajout vous permet une compatibilité facile avec le reste de l'écosystème Rust. FWIW, Unpin et l'implémentation inconditionnelle de deref sur Pin sont pas non plus liés les uns aux autres, sauf que sans Unpin tous les types ressentent la même chose la douleur que font les types !Unpin (ce qui signifie que cela rendrait probablement l'implémentation inconditionnelle deref plus utile). Je ne peux pas m'empêcher de sentir que quelque chose me manque.

Les autres champs de la structure supprimés sont-ils considérés comme des variables locales dans ce contexte? Ce n'est vraiment pas clair pour moi de la documentation des utilisateurs

Assez juste, j'ai ouvert https://github.com/rust-lang/rust/issues/50765 pour suivre cela.


@pythonesque

Plus précisément, je trouve l'exemple RefCell assez troublant car en présence de Pinned :: deref cela signifie que nous ne pourrions même pas appliquer le pinning dynamiquement avec le type existant (je ne sais pas si la spécialisation suffirait). Cela suggère en outre que si nous conservons l'implémentation de deref, nous devrons finir par dupliquer la surface de l'API presque autant avec Pinned que nous le faisons avec Pin; et si on ne le garde pas, épinglédevient incroyablement difficile à utiliser.

La solution pour RefCell est de fournir des méthodes supplémentaires borrow_pin et borrow_pin_mut (en prenant Pin<RefCell<T>> ), et de garder une trace de l'état épinglé de l'intérieur du RefCell au moment de l'exécution. Cela devrait fonctionner à la fois pour PinMut et Pinned . Alors, est-ce que votre argument ici est que Pinned n'aide pas? Cela ne devrait pas non plus empirer les choses pour RefCell .

Mais ensuite tu écris

la différence est que nous pouvons encore concocter une solution avec PinMut tout en prenant en charge deref, ce qui semble ne pas fonctionner avec Pinned

et je ne sais pas à quoi vous faites référence, pourquoi cela ne fonctionnerait-il pas avec Pinned ?

@comex

Je ne pense pas que Pinned :: deref devrait exister. Les accesseurs de champ sûrs générés par macro devraient être suffisants; Je ne vois pas comment c'est "incroyablement difficile à utiliser".

Je ne vois pas la connexion entre les deref et les accesseurs de champ. Mais je ne vois pas non plus comment le deref devient plus problématique avec Pinned<T> , donc je suppose que j'attendrai d'abord une réponse à ma question ci-dessus.

Tout comme @pythonesque, je pense que le suivi de l'état épinglé sur le type derrière la référence ("sur le type possédé") est fondamentalement plus correct. Cependant, je doute qu'elle puisse réellement être transformée en une API globale plus ergonomique, en particulier compte tenu de la contrainte de travailler avec l'écosystème Rust existant.

Si nous allons délibérément adopter l'approche que nous pensons être moins "fondamentalement correcte", nous devrions au moins nous laisser beaucoup de temps pour l'expérimentation avant de la stabiliser, afin que nous puissions être aussi convaincus que raisonnablement possible que nous ne le sommes pas. va finir par le regretter.

@pythonesque , wow, merci beaucoup pour le résumé complet! Heureux d'apprendre qu'une RFC est en préparation. :)

C'est enregistrer pour appeler une fonction qui prend une référence mutable à une valeur épinglée tant que cette fonction n'utilise pas quelque chose comme mem::swap ou mem::replace . À cause de cela, il semble plus naturel que ces fonctions utilisent la liaison Unpin que de rendre dangereux chaque déréf mutable d'une Pin à une Unpin .

Si une fonction devait plus tard être mise à jour pour utiliser swap il ne serait plus sûr de l'appeler sur une référence mutable à une valeur épinglée. Lorsque swap et replace ont cette limite, la fonction mise à jour doit faire aussi bien avec, il est bien plus évident que ce n'est pas un changement rétrocompatible.

Alors quelques pensées que j'avais:

  1. Fondamentalement, Drop offre le même privilège que Unpin - vous pouvez obtenir un &mut pour quelque chose qui était auparavant dans un PinMut . Et Drop est sûr, ce qui signifie que Unpin devrait être sûr (c'est mem::forget et leakpocalypse encore une fois).
  2. C'est bien, car cela signifie que des choses comme les API actuelles basées sur les contrats à terme, qui ne gèrent pas les générateurs de désépinglage, sont toutes 100% sûres à mettre en œuvre même si elles prennent self: PinMut<Self> (non unsafe impl Unpin ).
  3. L'API est-elle saine si Unpin est sûr? La réponse est oui: tant que les générateurs n'implémentent pas Unpin , et qu'il n'est pas sûr d'épingler un projet à un type !Unpin , tout est sûr.
  4. Mais cela signifie que les projections de broches ne sont pas sûres! Pas idéal.
  5. Les projections de broches sont sûres si Self n'implémente pas Unpin ou Drop [edit: true ou false?] Pouvons-nous automatiser cette vérification?

J'ai quelques idées pour une alternative plus à cette API bibliothèque langage pris en charge, ce qui implique l' abandon Unpin entièrement et plutôt en feuilletant la polarité - un Pin trait que vous optez dans l' obtention de ces garanties, au lieu de opting out. Mais cela nécessiterait une prise en charge significative du langage, alors que l'implémentation actuelle est entièrement basée sur une bibliothèque. Je ferai un autre article après y avoir réfléchi davantage.


une autre note parce que j'oublie sans cesse:

la sécurité de la projection des broches dépend uniquement du type Self, et non du type de champ, car le type de champ doit garantir la sécurité de son API publique, qui est inconditionnelle. Donc, ce n'est pas une vérification récursive - si Self ne déplace jamais rien de son Pin , la projection sur un champ de n'importe quel type est sûre.

@withoutboats FWIW cela correspond exactement aux conclusions auxquelles @cramertj et moi sommes parvenus lors de notre précédente série de discussions. Et je pense que nous pouvons automatiser la vérification d'exclusion mutuelle, au départ grâce à certains attributs spécifiques émis par le dérivé.

@sans bateaux

Fondamentalement, Drop fournit le même privilège que Unpin - vous pouvez obtenir un & mut sur quelque chose qui était auparavant dans un PinMut. Et Drop est sûr, ce qui signifie que Unpin devrait être sûr (c'est encore une fois mem :: forget et leakpocalypse).

Je ne vois pas la connexion à leakpocalypse mais je suis d'accord autrement. La seule raison pour laquelle je suis (était?) Un peu hésitant, c'est que tant que ce n'est que Drop , cela me semblait plus être un cas de coin dont peu de gens doivent se soucier. Je ne sais pas si c'est un avantage ou non. Et de toute façon, un Unpin sûr n'augmente pas seulement la cohérence ici et "résout" le problème Drop en n'en faisant plus un cas spécial (au lieu de cela, nous pouvons penser à chaque impl Drop comme venant avec un impl Unpin ) implicite; d'après ce que vous dites, il est également plus facile à utiliser du côté Future des choses. Donc, cela semble être une victoire globale.

@pythonesque sauf si je manque quelque chose, le coffre-fort Unpin ne pose pas non plus de nouveaux problèmes pour les collections intrusives, n'est-ce pas? S'ils ont fonctionné malgré la sécurité Drop , ils devraient toujours fonctionner.


@sans bateaux

Les projections de broches sont sûres si Self n'implémente pas Unpin ou Drop [modifier: vrai ou faux?] Pouvons-nous automatiser cette vérification?

Au départ, vous avez également mentionné le type de champ ici et j'étais sur le point de vous demander pourquoi vous pensez que c'est pertinent. Maintenant, je vois que vous modifiez le message. :) Je reconnais qu'il s'agit uniquement du type Self . Dans ce qui suit, je répète à peu près votre argument dans mes termes.

Essentiellement, la question est: comment Self choisit-il son invariant d'épinglage? Par défaut, nous supposons (même s'il y a du code non sécurisé!) Que l'invariant d'épinglage est exactement le même que l'invariant possédé, c'est-à-dire que l'invariant est indépendant de l'emplacement et que ce type ne fait pas d'épinglage. Tant que je ne peux rien faire avec un PinMut<T> autre que le transformer en &mut T , c'est une hypothèse sûre.

Pour activer les projections de champ, l'invariant d'épinglage devrait plutôt être "tous mes champs sont épinglés à l'invariant de leur type respectif". Cet invariant justifie facilement les projections d'épinglage, indépendamment des types de champ (c'est-à-dire qu'ils peuvent choisir n'importe quel invariant d'épinglage qu'ils veulent). Bien sûr, cet invariant est incompatible avec la transformation de PinMut<T> en &mut T , donc nous ferions mieux de nous assurer que ces types ne sont pas Drop ou Unpin .

Je ne vois pas la connexion à leakpocalypse mais je suis d'accord autrement.

juste une analogie - Unpin est à Drop comme mem :: forget est aux cycles Rc. mem :: forget était à l'origine marqué comme dangereux, mais il n'y avait aucune justification à cela. (Et le même argument selon lequel les cycles Rc sont un cas de bord a été fait contre le marquage de mem :: forget safe.)

Copier-coller (spirituellement) à partir de la Discord, j'aimerais vraiment voir des preuves que nous n'avons pas simplement renversé le problème: rendre Unpin sûr à mettre en œuvre, en rendant les accesseurs de broches structurelles dangereux à implémenter (cela serait également vrai avec tout trait dangereux qui a été introduit, n'est-ce pas? Vous devrez toujours écrire du code dangereux). Cela m'ennuie parce que la grande majorité du temps, ils sont complètement sûrs - en gros, tant qu'il n'y a pas d'implément Unpin explicite pour le type, tout comme nous sommes toujours en sécurité s'il n'y a pas d'implément Drop pour le type. Avec le plan actuel, nous avons besoin de quelque chose de beaucoup plus fort - il devrait y avoir un impl explicite! Unpin pour le type - qui sera vrai pour beaucoup moins de types (c'est à peu près tout ce que nous pouvons faire dans une bibliothèque).

Malheureusement, je n'ai aucune idée de comment ou si le compilateur peut vérifier s'il y a un implément Unpin manuel pour un type particulier ou non, par opposition à "has any impl", et je ne suis pas sûr s'il a de mauvaises interactions avec la spécialisation. Si nous avons un moyen précis d'effectuer cette vérification, de sorte que le créateur d'un type n'ait pas à écrire de code dangereux pour obtenir un épinglage structurel, je serais beaucoup plus heureux avec impl Unpin étant en sécurité, je pense ... est-ce quelque chose qui semble réalisable?

J'ai une question simple que j'essaie de comprendre maintenant. Dans les génériques, unpin sera-t-il une limite implicite comme size à moins que votre API pour tous les paramètres génériques?

cela doit être correct pour que vecpour continuer à être en sécurité.

détachera une borne implicite comme size

Non.

cela doit être correct pour que vec continue d'être sûr.

Pourquoi penses-tu ça?

Chose ennuyeuse que @MajorBreakfast a frappé aujourd'hui: PinMut::get_mut vous donne un &mut avec la durée de vie de l'original PinMut , mais il n'y a pas de moyen sûr de faire la même chose, depuis DerefMut vous donne un emprunt avec la durée de vie de la référence mutable à PinMut . Peut-être devrions-nous faire de get_mut le plus sûr et ajouter get_mut_unchecked ?

Tu veux dire comme cela?

fn get_mut(this: PinMut<'a, T>) -> &'a mut T where T : Unpin

Ouais, on devrait avoir ça.

@RalfJung Exactement.

La méthode de la caisse à terme dans laquelle je voudrais utiliser ceci ressemble à ceci:

fn next(&mut self) -> Option<&'a mut F> {
    self.0.next().map(|f| unsafe { PinMut::get_mut(f) })
}

J'ai dû utiliser un bloc unsafe même si F a une liaison Unpin et c'est complètement sûr. Il n'existe actuellement aucun moyen sûr de produire une référence qui conserve la durée PinMut vie de

Ouais, ce n'est qu'une omission dans l'API.

Voulez-vous préparer un PR ou devrais-je?

Je peux le faire. Voulons-nous l'appeler get_mut et get_mut_unchecked ?

J'ajouterai un coffre-fort map et renommerai également celui actuel en map_unchecked . Principalement pour la cohérence. Ensuite, toutes les fonctions non sécurisées de PinMut terminent par _unchecked et ont un équivalent sûr

Voulons-nous l'appeler get_mut et get_mut_unchecked?

Semble raisonnable.

J'ajouterai un coffre-fort map

Attention là-bas. À quoi voulez-vous qu'il ressemble? La raison pour laquelle la carte est dangereuse est que nous ne savons pas comment en créer une.

Attention là-bas. À quoi voulez-vous qu'il ressemble?

Vous avez raison. Il doit exiger une liaison Unpin sur la valeur de retour.

J'ai juste une idée: qu'en est-il de PinArc ? Nous pourrions utiliser une telle chose dans le BiLock impl (https://github.com/rust-lang-nursery/futures-rs/pull/1044) dans la caisse à terme.

@MajorBreakfast PinArc (et PinRc , etc.) dépend de Pin (la version non- Mut ). Je serais prêt à l'ajouter, mais je ne savais pas s'il y avait un consensus sur les garanties exactes qu'il offrirait.

Je ne sais plus si l'ajout de PinArc ajoute quelque chose ^^ ' Arc ne permet déjà pas de "sortir du contenu emprunté"

@MajorBreakfast Vous pouvez sortir de Arc utilisant Arc::try_unwrap . Ce serait une modification incompatible vers l'arrière pour supprimer cela ou introduire une liaison T: Unpin .

Salut!
J'ai réfléchi un peu à la manière de faire fonctionner les accesseurs de broches en toute sécurité. Pour autant que je sache, le problème avec les accesseurs de broches se produit _uniquement_ lorsque vous avez la combinaison T: !Unpin + Drop . À cette fin, j'ai vu une discussion essayant d'empêcher la possibilité d'un tel type T existant - par exemple, rendre les traits !Unpin et Drop s'excluant mutuellement de diverses manières, mais là n'était pas un moyen clair de le faire sans rompre la compatibilité ascendante. En regardant de plus près ce problème, nous pouvons obtenir des accesseurs Pin sûrs sans empêcher qu'un type soit !Unpin et Drop , c'est juste qu'un tel type ne peut pas être mis dans un Pin<T> . Empêcher _que_ de se produire est quelque chose que je pense être beaucoup plus faisable, et plus dans l'esprit du fonctionnement du trait Unpin toute façon.

Actuellement, j'ai une "solution" pour empêcher un type d'entrer en toute sécurité un Pin<T> qui est à la fois !Unpin et Drop . Cela nécessite en quelque sorte des fonctionnalités que nous n'avons pas encore dans la rouille, ce qui est un problème, mais j'espère que c'est un début. Bref, voici le code ...

/// This is an empty trait, it is used solely in the bounds of `Pin`
unsafe trait Pinnable {}

/// Add the extra trait bounds here, and add it to the various impls of Pin as well
struct Pin<T: Pinnable> {
    ...
}

/// Then we impl Pinnable for all the types we want to be able to put into pins
unsafe impl<T: Unpin> Pinnable for T {}
unsafe impl<T: !Unpin + !Drop> Pinnable for T {}

Les types Unpin et les types !Unpin mais aussi !Drop , n'ont aucun problème (à ma connaissance) avec les accesseurs de broches. Cela ne casse aucun code existant qui utilise Drop , cela limite simplement ce qui peut être mis dans une structure Pin . Dans le cas peu probable * où quelqu'un a besoin d'un type pour être à la fois !Unpin et Drop (et capable d'être effectivement placé dans un Pin ), il pourrait unsafe impl Pinnable pour leur type.

* Je n'ai pas d'expérience avec Pin , mais j'espère que la plupart des cas où quelqu'un a besoin de impl Drop pour un type !Unpin ne sont pas causés par le chose qui doit être abandonnée étant intrinsèquement !Unpin , mais plutôt causée par une structure ayant des champs Drop champs !Unpin . Dans ce cas, les champs Drop pourraient être séparés dans leur propre structure qui ne contient pas les champs !Unpin . Je peux entrer plus en détail là-dessus si nécessaire, car j'ai l'impression que cette explication était douteuse, mais ce n'était pas destiné à être le corps principal du commentaire, je vais donc le laisser court pour le moment.

Pour autant que je sache, le problème avec les accesseurs de broches ne se produit que lorsque vous avez la combinaison T:! Unpin + Drop.

C'est plus un "ou" qu'un "et".

Les accesseurs Pin impliquent une interprétation "structurelle" de l'épinglage: Lorsque T est épinglé, tous ses champs le sont aussi. impl Unpin et Drop , d'autre part, sont sûrs car ils supposent que le type ne se soucie pas du tout de l'épinglage - T être épinglé ou non ne fait aucune différence; en particulier, les champs de T ne sont pas épinglés dans les deux cas.

Cependant, vous avez bien compris plus tard lorsque vous avez dit que les types !Unpin + !Drop sont ceux pour lesquels les accesseurs peuvent être ajoutés en toute sécurité. (Le type doit toujours faire attention à ne rien faire de mal dans son code non sécurisé , mais Unpin ad Drop sont les deux moyens sûrs de briser les accesseurs d'épinglage.)

En regardant ce problème de plus près, nous pouvons obtenir des accesseurs Pin sûrs sans empêcher qu'un type ne soit! Détachez et déposez, c'est juste qu'un tel type ne peut pas être mis dans une épingle.

Je ne suis pas. Pourquoi pensez-vous que cela suffit? Et si c'est le cas, pourquoi serions-nous même intéressés par des accesseurs épinglés si un type ne peut pas être épinglé ...?

C'est plus un "ou" qu'un "et".

Parlons simplement de cela pour le moment, car toute mon idée était basée sur le contraire de cela, donc si j'ai mal compris cela, le reste de l'idée n'aide pas.

À ma connaissance (actuelle), les accesseurs de broches avec un type qui est Unpin sont complètement sains, indépendamment de la chute, car Pin<T> est effectivement &mut T toute façon. De plus, les accesseurs pin sur un type qui est !Drop sont complètement sains, même pour un type !Unpin , car drop est la seule fonction qui pourrait obtenir &mut self sur le type !Unpin une fois qu'il entre dans un Pin , donc rien ne pourrait déplacer les choses.

Si j'ai fait une erreur, faites-le moi savoir. Mais sinon, alors les types Unpin + Drop ou les types !Unpin + !Drop conviendraient parfaitement avec les accesseurs de broches, et les seuls types de problèmes seraient ceux qui sont !Unpin + Drop .

@ashfordneil

De plus, les accesseurs de broches sur un type qui est! Drop sont complètement sains, même pour un type! Unpin, car drop est la seule fonction qui pourrait se mettre en place sur le type! Unpin une fois qu'il entre dans un Pin, donc rien ne pourrait déplacer les choses.

Si j'écris struct Foo(InnerNotUnpin); impl Unpin for Foo {} il n'est pas judicieux de passer de PinMut<Foo> à PinMut<InnerNotUnpin> . Pour que la projection soit saine, vous devez (a) interdire les drop impls et (b) vous assurer que la structure est Unpin uniquement lorsque le champ est Unpin .

(a) devrait simplement interdire les drop impls sur des choses qui sont !Unpin non?
(b) doit être gérée par le fait que la mise en œuvre de Unpin n'est pas sûre - il est sûr qu'il est possible de se tirer une balle dans le pied en affirmant de manière non sécurisée qu'un type peut être détaché alors qu'il ne le peut pas - mais il est tout aussi possible d'étiqueter quelque chose comme Sync quand cela ne devrait pas être et se retrouver avec un comportement malsain de cette façon

(a) devrait simplement interdire de laisser tomber les impls sur les choses qui le sont!

Ouais mais c'est impossible pour des raisons de rétrocompatibilité.

Et cela arrive à mon point - interdire les impls de baisse sur les choses qui sont! Unpin est impossible pour des raisons de compatibilité descendante. Mais nous n'avons pas besoin d'empêcher la suppression des impls sur les choses qui sont! Le marqueur! Unpin sur un type n'a aucune signification et ne fait aucune promesse tant que ce type n'est pas à l'intérieur d'un Pin_, donc le drop impl sur un type! Unpin est complètement son _ tant que ce type n'est pas à l'intérieur d'un Pin_. Ce que je propose, c'est de changer le Pin de Pin<T> à Pin<T: Pinnable> où le trait Pinnable agit comme un gardien pour empêcher les types Drop + !Unpin (ou plus généralement, les types pour lesquels les accesseurs de broches sont problématiques) d'être placés dans le Pin .

À ce stade, vous rendez le trait Unpin totalement inutile, n'est-ce pas? De plus, vous ne pouvez toujours pas exprimer les limites nécessaires avec Rust actuel. Si nous pouvions dire " Unpin et Drop sont incompatibles", il n'y aurait pas de problème. Vous semblez avoir besoin de quelque chose de similaire avec Pinnable . Je ne vois pas comment cela aide.

Le trait Unpin - à lui seul - est déjà «inutile». Le trait _only_ signifie quelque chose une fois que le type a été épinglé, le type peut être déplacé avant d'entrer dans l'épingle.

@RalfJung et moi venons d'avoir une réunion pour parler de ces API, et nous avons convenu que nous sommes prêts à les stabiliser sans changement majeur d'API.

Résolution des questions en suspens

Ma conviction à propos des API pin est la suivante:

  1. Les concepts API de base de PinMut et Unpin constituent la meilleure solution possible qui n'implique pas de prise en charge intégrée du langage.
  2. Ces API n'empêchent pas la prise en charge future du langage intégré si nous le décidons nécessaire.

Il y a cependant quelques questions en suspens concernant leurs limites et leurs compromis, pour lesquelles je veux enregistrer notre décision.

Le problème de Drop

Un problème que nous n'avons découvert qu'après la fusion de la RFC était le problème du trait Drop . La méthode Drop::drop a une API qui prend self &mut self . Il s'agit essentiellement de "désépingler" Self , violant les garanties que les types épinglés sont censés fournir.

Heureusement, cela n'affecte pas la solidité des types de "générateurs immobiles" de base que nous essayons de projeter avec le pin: ils n'implémentent pas de destructeur, donc leur implémentation !Unpin signifie en fait .

Cependant, cela signifie que pour les types que vous définissez vous-même, il est forcément sûr de les "détacher", tout ce que vous avez à faire est d'implémenter Drop . Cela signifie que Unpin étant un unsafe trait ne nous apportait aucune sécurité supplémentaire: implémenter Drop est aussi bien que mettre en œuvre Unpin en ce qui concerne la solidité. concerné.

Pour cette raison, nous avons fait du trait Unpin un trait sûr à implémenter.

Projection de broches

La principale limitation de l'API actuelle est qu'il n'est pas possible de déterminer automatiquement qu'il est sûr d'effectuer une projection pin lorsque le champ n'implémente pas Unpin . Une projection d'épingle est:

Étant donné un type T avec un champ a avec le type U , je peux en toute sécurité "projet" de PinMut<'a, T> à un montant de PinMut<'a, U> en accédant le champ a .

Ou, dans le code:

struct Foo {
    bar: Bar,
}

impl Foo {
    fn bar(self: PinMut<'_, Foo>) -> PinMut<'_, Bar> {
        unsafe { PinMut::map_unchecked(self, |foo| &mut foo.bar) }
    }
}

Pour le moment, à moins que le type de champ n'implémente Unpin nous n'avons pas de rustc pour _prouver_ que c'est sûr. Autrement dit, l'implémentation de ce type de méthode de projection nécessite actuellement un code non sécurisé. Heureusement, vous pouvez prouver que c'est sûr. Si le type de champ ne peut pas implémenter Unpin , cela n'est sûr que si tout cela est valable:

  1. Le type Self n'implémente pas Unpin , ou n'implémente que conditionnellement Unpin lorsque le type de champ le fait.
  2. Le type Self n'implémente pas Drop , ou bien le destructeur du type Self ne déplace jamais rien de ce champ.

Avec les extensions de langage, nous pourrions demander au compilateur de vérifier que certaines de ces conditions tiennent (par exemple, que Self n'implémente pas Drop et n'implémente que conditionnellement Unpin ).

En raison du problème de Drop , un trait stable, ce problème préexistait à toute décision que nous pouvions prendre sur ce problème, et il n'y a aucun moyen sans changement de langue de rendre la projection de broches automatiquement sûre. Un jour, nous pourrions apporter ces modifications, mais nous ne bloquerons pas la stabilisation de ces API à ce sujet.

Spécification de la garantie "pin"

À la fin du processus RFC, @cramertj s'est ici .

Essentiellement, la garantie de l'épinglage est la suivante:

Si vous avez un PinMut<T> et que T n'implémente pas Unpin , T ne sera pas déplacé et le support mémoire T ne le sera pas être invalidé jusqu'à ce que le destructeur s'exécute pour T .

Ce n'est pas exactement la même chose que "la liberté de fuite" - T peut encore fuir par exemple en étant coincé derrière un cycle Rc, mais même s'il y a une fuite, cela signifie que la mémoire ne sera jamais invalidée (jusqu'à ce que le programme se termine), car le destructeur ne fonctionnera jamais.

Cette garantie exclut certaines API pour l'épinglage de pile, nous n'étions donc pas certains au début d'étendre l'épinglage pour l'inclure. Cependant, nous devrions finalement opter pour cette garantie pour plusieurs raisons:

  1. D'autres API d'épinglage de pile, encore plus ergonomiques, ont été découvertes, comme cette macro , éliminant l'inconvénient.
  2. L'avantage est assez grand! Ces types de listes intrusives pourraient avoir un impact très important sur les bibliothèques comme josephine qui intègrent Rust avec des langages récupérés.
  3. Ralf n'a jamais été certain que certaines de ces API étaient vraiment saines au départ (en particulier celles basées sur «l'emprunt permanent»).

Réorganisation de l'API

Cependant, je souhaite apporter une dernière modification proposée à l'API, qui consiste à déplacer les choses. En examinant les API existantes, Ralf et moi avons réalisé qu'il n'y avait pas de bon endroit pour décrire les garanties de haut niveau et les invariants relatifs à Drop et ainsi de suite. Par conséquent, je propose

  1. Nous créons un nouveau module std::pin . La documentation du module fournira une vue d'ensemble de haut niveau sur l'épinglage, les garanties qu'il offre et les invariants que les utilisateurs de ses parties non sécurisées devraient respecter.
  2. Nous déplaçons PinMut et PinBox de std::mem et std::boxed dans le module pin . Nous laissons Unpin dans le module std::marker , avec les autres traits marqueurs.

Nous devrons également écrire cette documentation plus complète avant de terminer la stabilisation de ces types.

Ajout d'implémentations Unpin

Ralf et moi avons également discuté de l'ajout de plus d'implémentations de Unpin à la bibliothèque standard. Lors de l'implémentation de Unpin , il y a toujours un compromis: si Unpin est implémenté sans condition en ce qui concerne le type d'un champ, vous ne pouvez pas épingler le projet sur ce champ. Pour cette raison, nous n'avons pas été très agressifs dans l'implémentation de Unpin jusqu'à présent.

Cependant, nous pensons qu'en règle générale:

  1. La projection de broches à travers un pointeur ne doit jamais être considérée comme sûre (voir l'encadré ci-dessous).
  2. La projection de broches à travers la mutabilité intérieure ne doit jamais être considérée comme sûre.

En général, les types faisant l'une de ces choses font quelque chose (comme la réallocation du magasin de support comme dans Vec ), ce qui rend la projection de broches intenable. Pour cette raison, nous pensons qu'il serait approprié d'ajouter une implémentation inconditionnelle de Unpin à tous les types de std qui contiennent un paramètre générique derrière un pointeur ou dans un UnsafeCell; cela inclut tous les std::collections par exemple.

Je n'ai pas enquêté de trop près, mais je pense que ce serait suffisant pour la plupart de ces types si nous ajoutons simplement ces impls:

impl<T: ?Sized> Unpin for *const T { }
impl<T: ?Sized> Unpin for *mut T { }
impl<T: ?Sized> Unpin for UnsafeCell<T> { }

Il y a cependant un problème: techniquement, nous pensons que la projection d'épingle via Box pourrait être techniquement sûre: c'est-à-dire que passer de PinMut<Box<T>> à PinMut<T> peut être fait en toute sécurité. C'est parce que Box est un pointeur en propriété exclusive qui ne se réalloue jamais.

Il est possible que nous voulions fournir un trou pour que Unpin ne soit implémenté conditionnellement que pour Box<T> quand T: Unpin . Cependant, mon opinion personnelle est qu'il devrait être possible de penser à l'épinglage comme étant un bloc de mémoire qui a été épinglé en place, et donc toute projection à travers l'indirection du pointeur devrait être dangereuse.

Nous pourrions retarder cette décision sans bloquer la stabilisation et ajouter ces impls au fil du temps.

Conclusion

J'adorerais vraiment entendre les opinions d'autres personnes qui ont été très investies dans les API de broches jusqu'à présent, et si ce plan de stabilisation semble raisonnable ou non. Je ferai un résumé plus court avec une proposition fcp dans un prochain article, pour le reste de l'équipe linguistique.

@rfcbot fcp fusionner

Je propose de stabiliser les API de broches existantes après une petite réorganisation. Vous pouvez lire un résumé plus long dans mon précédent post . Le TL; DR de mon avis est que l'API actuelle est:

  • Du son
  • Aussi bon que possible sans changement de langue
  • Transférer compatible avec les changements de langue qui le rendraient meilleur

Nous avons résolu toutes les questions majeures concernant les garanties précises et les invariants de l'épinglage au cours des derniers mois, les décisions auxquelles nous sommes actuellement (je pense que sont les décisions sur lesquelles nous devrions nous stabiliser) sont documentées dans le post le plus long.

Je propose quelques changements avant de stabiliser les choses:

  1. Réorganisez PinMut et PinBox sous un nouveau module std::pin , qui contiendra une documentation de haut niveau concernant les invariants et les garanties d'épinglage en général.
  2. Ajoutez des implémentations inconditionnelles de Unpin pour les types dans std qui contiennent des paramètres génériques derrière une indirection de pointeur ou à l'intérieur d'un UnsafeCell; la justification de la pertinence de ces implémentations se trouve dans le résumé plus long.

Le membre de l'équipe @withoutboats a proposé de fusionner cela. L'étape suivante est l'examen par le reste des équipes marquées:

  • [] @Kimundi
  • [] @SimonSapin
  • [] @alexcrichton
  • [] @dtolnay
  • [] @sfackler
  • [x] @sans bateaux

Aucun problème actuellement répertorié.

Une fois que la majorité des réviseurs approuvent (et aucun ne s'y oppose), cela entrera dans sa période de commentaires finale. Si vous repérez un problème majeur qui n'a été soulevé à aucun moment de ce processus, veuillez en parler!

Consultez ce document pour plus d'informations sur les commandes que les membres de l'équipe marqués peuvent me donner.

@rfcbot fcp annuler

Je marque aussi lang en raison des décisions invariantes fondamentales que nous avons dû prendre, donc j'annule et redémarre le FCP

Proposition @withoutboats annulée.

@rfcbot fcp merge Voir le message de fusion précédent et le long résumé, désolé!

Le membre de l'équipe @withoutboats a proposé de fusionner cela. L'étape suivante est l'examen par le reste des équipes marquées:

  • [x] @Kimundi
  • [] @SimonSapin
  • [x] @alexcrichton
  • [] @aturon
  • [x] @cramertj
  • [x] @dtolnay
  • [x] @eddyb
  • [] @joshtriplett
  • [] @nikomatsakis
  • [x] @nrc
  • [] @pnkfelix
  • [x] @scottmcm
  • [] @sfackler
  • [x] @sans bateaux

Préoccupations:

Une fois que la majorité des réviseurs approuvent (et aucun ne s'y oppose), cela entrera dans sa période de commentaires finale. Si vous repérez un problème majeur qui n'a été soulevé à aucun moment de ce processus, veuillez en parler!

Consultez ce document pour plus d'informations sur les commandes que les membres de l'équipe marqués peuvent me donner.

Je suis heureux de voir une certaine confiance dans le son. Cependant, il est difficile de comprendre tout ce qui a changé depuis la RFC d'origine; le texte de la RFC pourrait-il être mis à jour pour refléter la conception proposée pour la stabilisation? (En général, c'est un problème que beaucoup de RFC rencontrent, où ils changent tellement pendant la période d'implémentation qu'il est vain d'essayer de le comprendre en lisant le RFC. Nous devrions trouver un moyen de faire mieux.)

Les RFC ne sont pas une spécification. La lecture de la RFC ne remplace pas la documentation réelle.

@bstrie le changement significatif de la RFC ( Unpin est sûr) est couvert dans mon article de synthèse. À long terme, les gens devraient acquérir une compréhension de l'API d'épinglage en lisant les documents dans std::pin (un bloqueur de stabilisation), et non en déterrant le RFC d'origine.

Si l'intention est que la documentation bloque la stabilisation, mais que la documentation n'a pas encore été écrite, alors ce FCP n'est-il pas prématuré? S'attendre à ce que les commentateurs potentiels reconstruisent eux-mêmes la documentation provisoire en rassemblant des sources disparates est une barrière à l'entrée plus élevée qu'on ne le souhaiterait.

@bstrie c'est notre procédure standard pour proposer FCP avant documentation. J'ai écrit un commentaire de synthèse très détaillé sur l'état des lieux qui devrait fournir suffisamment d'informations pour quiconque n'a pas suivi le problème de suivi, ainsi qu'un commentaire plus court pour les personnes qui ne veulent pas beaucoup de contexte

Pour élaborer un peu plus, FCP est la question «voulons-nous stabiliser cela?» Si cette réponse s'avère «non», alors un tas de travail sur les documents a été gaspillé. L'achèvement du FCP ne signifie pas que la fonctionnalité devient instantanément stable; cela signifie que la décision a été prise de la stabiliser et que le moment est donc venu de faire le travail nécessaire pour ce faire; qui inclut le travail de compilation et de documentation.

Le 2 août 2018, à 12 h 56, bateaux [email protected] a écrit:

@bstrie c'est notre procédure standard pour proposer FCP avant documentation. J'ai écrit un commentaire de synthèse très détaillé sur l'état du jeu qui devrait fournir suffisamment d'informations pour quiconque n'a pas suivi le problème de suivi

-
Vous recevez ceci parce que vous avez commenté.
Répondez directement à cet e-mail, affichez-le sur GitHub ou désactivez le fil de discussion.

@sans bateaux

La projection de broches à travers un pointeur ne doit jamais être considérée comme sûre (voir l'encadré ci-dessous).
La projection de broches à travers la mutabilité intérieure ne doit jamais être considérée comme sûre.

Pouvez-vous clarifier un peu ce point? Faites-vous spécifiquement référence aux types de la bibliothèque standard ? Semblable à Box , il existe des types comme Mutex qui pourraient projeter l'épinglage car ils possèdent uniquement leur contenu (bien qu'ils soient hors bande). Au moins en dehors de la bibliothèque standard, je suppose que nous voudrions avoir la possibilité d'avoir des primitives de concurrence qui projettent PinRef<InPlaceMux<T>> à PinMut<T> lors d'un appel à .lock() , ce qui semble comme un cas de «projection par mutabilité intérieure».

@cramertj Projection dans un Mutex semble dangereux; on pourrait utiliser la conversion sûre en &Mutex puis Mutex::lock pour obtenir un &mut et ensuite déplacer les choses.

EDIT: Oh, vous voulez dire un Mutex toujours épinglé. Comme, PinMutex . Oui, cela fonctionnerait. La mutabilité intérieure est bonne si vous ne distribuez &mut . Mais il semble peu probable actuellement que nous ayons de telles structures de données dans libstd.

Mais oui, pour une précision totale, la déclaration @withoutboats devrait être modifiée pour dire que "la projection de broches à travers la mutabilité intérieure n'est sûre que si le type épingle toujours , c'est-à-dire ne distribue jamais &mut ."

@RalfJung Exactement, ouais.

Mais il semble peu probable actuellement que nous ayons de telles structures de données dans libstd.

Bien, je voulais m'assurer que les exigences énoncées pour ne pas projeter à travers des pointeurs ou la mutabilité intérieure concernaient spécifiquement libstd, pas des règles générales.

@cramertj ce n'est certainement pas une règle dure et rapide (ce n'est pas UB de la violer par exemple), mais je dirais que c'est une bonne directive pour les personnes incertaines

Je suis déçu de la décision de stabiliser cela. Je ne suis pas trop préoccupé par ce qui se passe à court terme, car sans le support du compilateur, toute conception potentielle aura l'air un peu hackish. Cependant, à long terme, si Rust gagne des types immobiliers natifs, ils ne se sentiront pas vraiment de première classe à moins qu'ils ne puissent être utilisés avec des méthodes de trait acceptant &self et &mut self . Par conséquent, l'épinglage doit être une propriété du type référent, et non du type référence. Cela peut encore se produire, bien sûr, dans une conception future pour les types d'immeubles natifs. Mais il en résulterait que Pin<T> deviendrait redondant - pas seulement, disons, un alias obsolète pour certains &pin T natifs, mais totalement inutile. À leur tour, des traits tels que Future (et potentiellement beaucoup d'autres) qui acceptent Pin<Self> devront être réorganisés ou obsolètes, ce qui nécessiterait une période de transition désordonnée.

Désordonné, pas impossible. Mais au risque d'être trop pessimiste, je crains que le désir d'éviter une telle transition ne biaise les décisions futures en faveur de l'adoption de &pin place comme base des types d'immeubles natifs - qui, à mon avis, laisseront eux en permanence de seconde classe. Et je pense toujours que parmi les conceptions à court terme, l'approche &Pinned<T> aurait été faisable, évitant ces problèmes de compatibilité ascendante et présentant également d'autres avantages (par exemple, contournant complètement le problème de la mutabilité intérieure).

Tant pis. Dans l'état actuel des choses, j'ai toujours hâte d'utiliser Pin , mais j'aurais préféré qu'il reste sur crates.io plutôt que d'entrer dans la bibliothèque standard.

Je conviens qu'il est trop tôt pour stabiliser cela. Actuellement, c'est un peu gênant dans certaines situations, par exemple lors de l'accès aux champs de type !Unpin dans une méthode appelée sur PinMut<Self> . Je pense que des changements

@Thomasdezeeuw Quels changements pensez-vous amélioreraient la situation? Les changements que j'ai proposés ne semblent pas être liés à cet inconvénient.

Il n'y a aucun moyen d'écrire une solution générique - c'est-à-dire d'ajouter une méthode à std - mais cette variation de la macro à partir des futures est sûre:

macro_rules! get_mut {
    ($f:tt: $t:ty) => (
        fn $f<'a>(
            self: &'a mut std::mem::PinMut<Self>
        ) -> &'a mut $t
            where $t: Unpin
        {
            unsafe {
                 &mut std::mem::PinMut::get_mut_unchecked(self.reborrow()).$f
            }
        }
    )
}

struct Foo {
    bar: Bar,
}

impl Foo {
     get_mut!(bar: Bar);
}

@withoutboats C'est sûr car il ne donne accès qu'aux données Unpin , non?

@RalfJung exactement! la ligne de clause where le rend sûr

@withoutboats c'est une belle macro mais ne fonctionne que pour les types Unpin .

Mais ne serait-il pas possible d'ajouter une méthode / macro qui prendrait un PinMut<Self> et retournerait PinMut<Self.field> , de préférence en code sécurisé. Je peux me tromper, mais je pense que si l'appelant garantit que Self est épinglé, alors Self.field , non? Cela devrait également fonctionner pour les types qui n'implémentent pas Unpin .

@Thomasdezeeuw Ce problème est discuté dans mon résumé sous la section "Pin projection"; il n'est pas sûr de projeter sur des champs !Unpin moins que vous ne garantissiez également que Self ne fait pas certaines autres choses, pour lesquelles nous ne pouvons pas générer de chèque. Nous ne pouvons que garantir automatiquement la possibilité de projeter sur des champs qui implémentent Unpin (car c'est toujours sûr, et nous pouvons le vérifier avec une clause where ).

@withoutboats J'ai lu le résumé, mais j'ai peut-être mal compris. Mais si un type T est !Unpin alors PinMut<T> n'implémentera pas DerefMut for T , donc déplacer la valeur ne serait pas possible sans code non sécurisé. Vous avez toujours le problème avec le trait Drop , mais c'est plus gros que d'accéder au champ d'un type. Donc je ne vois pas ce qui rend le mappage de PinMut<T> à PinMut<T.field> dangereux, même si T.field est !Unpin .

@Thomasdezeeuw Dans futures-rs, nous avons des situations où nous avons un type dans un PinMut , mais l'un de ses champs n'est pas considéré comme épinglé

L'inconvénient de la macro de @withoutboats est qu'elle ne peut donner accès qu'à un champ à la fois car elle contient une référence mutable à self . Les accès aux champs Struct n'ont pas cette limitation.

Je pense que décider de stabiliser l'API d'épinglage est la bonne décision. Cependant, je suggérerais d'implémenter d'abord le module core::pin proposé.

Vous avez toujours le problème avec le trait Drop, mais c'est plus gros que d'accéder au champ d'un type.

Non, ce n'est pas le cas. Drop est en fait très bien tant que vous ne faites pas de mappage de champ . C'est pourquoi la cartographie des champs n'est pas sûre.

Vous avez toujours le problème avec le trait Drop, mais c'est plus gros que d'accéder au champ d'un type. Donc je ne vois pas ce qui rend la cartographie de PinMutà PinMutdangereux, même si T.field l'est!

Pour être clair, nous ne pouvons pas non plus générer automatiquement une preuve que T: !Unpin . Mais même si ce n'est pas le cas, voici un exemple d'utilisation de Drop impl:

struct Foo {
    field: UnpinFuture,
}

impl Foo {
     fn field(self: PinMut<Self>) -> PinMut<UnpinFuture> { ... }

     fn poll_field(self: PinMut<Self>, ctx: &mut Context) {
         self.field().poll(ctx);
     }
}

impl Drop {
    fn drop(&mut self) {
        // ...
        let moved_field = mem::replace(&mut self.field, UnpinFuture::new());

        // polling after move! violated the guarantee!
        PinBox::new(moved_field).as_pin().poll();
    }
}

Comme le dit @RalfJung , c'est exactement le problème avec Drop - si vous ne faites pas de projections de broches dans les champs !Unpin , tout ce que vous faites dans Drop va bien.

L'inconvénient de la macro de @withoutboats est qu'elle ne peut donner accès qu'à un champ à la fois car elle contient une référence mutable à soi. Les accès aux champs Struct n'ont pas cette limitation.

Vous pouvez écrire la méthode qui donne accès à plusieurs champs, mais vous devez le faire pour chaque combinaison qui vous intéresse.

Par curiosité, existe-t-il d'autres cas d'utilisation concrets en plus des futurs, ou du moins quelque chose d'autre motivant le désir de les stabiliser maintenant (par opposition à plus tard avec plus d'expérience)?

Il ne semble y avoir aucune autre discussion ici concernant les points soulevés par @comex. Ils m'ont rendu assez curieux, car je ne me souviens pas de l'idée de faire de pin une propriété du type au lieu de la référence. Cela a-t-il déjà été discuté ailleurs? Ce n'est pas facile de tout suivre de "l'extérieur", je fais de mon mieux ;-)

À leur tour, des traits tels que Future (et potentiellement beaucoup d'autres) qui acceptent Pindevra être réorganisé ou obsolète, ce qui nécessiterait une période de transition désordonnée.

Hmmm. Une certaine magie liée à l'édition pourrait-elle nous permettre d'envelopper et de dérouler silencieusement ces Pin s, et de rendre les modifications entièrement rétrocompatibles? Un peu dégoûtant, mais pas trop, et cela garderait le langage et les API propres pendant une telle transition.

Peut-être que l'idée aurait besoin d'être étoffée davantage pour répondre à cette question (je me souviens que cela avait déjà été évoqué dans le fil, mais j'ai perdu la trace de l'endroit.)

@yasammez J'ai pu reprendre la discussion sur l'épinglage axé sur la valeur ici

Juste pour confirmer, la voie de la stabilisation n'inclut pas un plan de projections sûres sur le terrain? Il me semble que cela va introduire beaucoup de code dangereux dans toutes les bibliothèques futures. D'après mon expérience, les impls manuels Future ne sont pas rares, et je sais que tous les miens nécessitent un sondage des champs.

Je ne pense pas qu'il soit important qu'une grande partie de ce code dangereux soit prouvé de manière triviale. La simple existence du code non sécurisé dilue la capacité de l'audit d' un code plus

@tikue à court terme, vous pouvez soit:

  1. utilisez une macro comme la macro unsafe_pinned que la bibliothèque de futurs a développée, en vous limitant à un seul dangereux pour chaque sondage interne, que vous devez vérifier par rapport aux contraintes décrites dans mon long résumé (vous ne pouvez pas non plus utiliser une macro et écrivez simplement l'accesseur à la main, c'est aussi un seul unsafe ).
  2. exigent que vos champs implémentent Unpin , ce qui rend ces projections trivialement sûres et ne nécessitant aucun code dangereux, au prix d'un tas allouant tout futur !Unpin .

@withoutboats Je comprends les options mais je les trouve difficiles à palper. Exiger Unpin limitera la compatibilité avec toute une série de futurs, comme tout ce qui s'auto-emprunte, par exemple toute fn asynchrone utilisant des canaux. Et la pollution dangereuse est une réelle préoccupation.

En pratique, j'ai essayé de migrer une bibliothèque vers Futures 0.3 et j'ai trouvé que c'était une lutte pour ces raisons.

@tikue Ceci est compatible avec l'

Cependant, je ne comprends pas vraiment cette préoccupation:

Et la pollution dangereuse est une réelle préoccupation.

Le "problème de l'insécurité" est trop souvent laissé à ce niveau que je ressens - juste une interdiction générale sur unsafe .

Pour la plupart des futurs imbriqués, il devrait être très facile de déterminer si vous pouvez utiliser en toute sécurité la macro unsafe_pinned! . La liste de contrôle va généralement comme ceci:

  1. Je n'ai pas implémenté manuellement Unpin pour mon avenir, à la place je me fie simplement au trait automatique impl.
  2. Je n'ai pas implémenté manuellement Drop pour mon avenir, car je n'ai pas de destructeur personnalisé.
  3. Le champ sur lequel je veux projeter est privé.

Dans ce cas: tu es bon! unsafe_pinned est sûr à utiliser.

Si vous avez à mettre en œuvre manuellement Unpin ou Drop , vous devez penser réellement à ce sujet, mais même dans ce cas , ce ne est pas nécessairement un problème difficile:

  1. Si j'ai implémenté Unpin , il est contraint sur le futur que j'abstrait en étant Unpin .
  2. Si j'ai implémenté Drop , je ne déplace jamais le champ futur pendant mon destructeur.

@tikue pour référence, j'ai rédigé un résumé de la proposition de base pour une solution basée sur les attributs . Cela nécessite le support du compilateur, mais pas d'extensions significatives du langage - juste un nouveau trait et un nouvel attribut intégré.

Il existe également une solution plus "intrusive" dans laquelle nous ajoutons des types de référence &pin appropriés, qui auraient la même forme en termes de contraintes, mais qui auraient plus d'impact sur le langage dans son ensemble.

Et pour PinMut::replace ?

impl<'a, T> PinMut<'a, T> {
  pub fn replace(&mut self, x: T) { unsafe {
    ptr::drop_in_place(self.inner as *mut T);
    ptr::write(self.inner as *mut T, x);
  } }
}

Même si nous ne voulons pas ajouter une telle méthode, nous devrions avoir une réponse à la question de savoir si c'est sûr - pour PinMut , et aussi pour PinBox qui pourrait avoir quelque chose de similaire.

Je pense que c'est certainement sans danger pour PinBox , car je prends soin d'appeler le destructeur "en place" et il ne s'agit que de réutiliser la mémoire. Cependant, je suis un peu inquiet avec PinMut sujet de la valeur supprimée tôt, c'est-à-dire avant la fin de sa durée de vie. Il ne me semble pas clair que ce sera toujours bien, mais je ne peux pas non plus proposer un contre-exemple. @cramertj @withoutboats des pensées?

(J'ajouterais ceci à @rfcbot si je pouvais ...)

@rfcbot concerne remplacer

@RalfJung Je T panique en baisse?

@RalfJung Nous avons déjà PinMut::set .

@cramertj D'oh. Ouais d'accord, j'aurais pu être un peu plus minutieux en vérifiant cela.

Désolé pour le bruit, mon souci est résolu.

@rfcbot résoudre remplacer

C'est du vrai code que j'écris en ce moment pour les futures 0.1 => 0.3 compatibilité avec tokio_timer::Deadline . Il doit faire un PinMut<Future01CompatExt<Delay>> , où Delay est un champ de Deadline .

        let mut compat;
        let mut delay = unsafe {
            let me = PinMut::get_mut_unchecked(self);
            compat = Future01CompatExt::compat(&mut me.delay);
            PinMut::new_unchecked(&mut compat)
        };

Outre l'utilisation non triviale de unsafe, il m'a également fallu environ 5 itérations sur ce code pour trouver une version compilée. Ecrire et réfléchir à ce qui se passe ici est assez peu ergonomique.

Je pense qu'il y a beaucoup de place entre une interdiction générale des produits dangereux et ce code. Par exemple, map_unchecked est trop restrictif pour aider avec ce code car il nécessite le renvoi d'une référence, alors que ce code nécessite le retour de Future01CompatExt<&mut Delay> .

Edit: Une autre source d'unergonomie est que vous ne pouvez pas emprunter mutuellement dans les gardes de match, alors PinMutne peut pas travailler avec un code comme celui-ci:

`` rouille
correspondre à self.poll_listener (cx)? {
// était si self.open_connections> 0
Poll :: Ready (Aucun) si self.open_connections ()> 0 => {
^^^^ emprunté mutuellement dans Pattern Guard
return Poll :: Pending;
}
Sondage :: Prêt (Aucun) => {
return Poll :: Ready (Aucun);
}
}
`` ``

Votre commentaire sur map_unchecked est sur le point, peut-être qu'il y a une signature plus générique qui peut également permettre de renvoyer des temporaires autour des pointeurs.

Existe-t-il des directives pour savoir quand il est sûr d'obtenir une référence &mut partir d'un champ de PinMut<Type> ? Je pense que tant que Type ne suppose jamais que le champ est épinglé, ça va? Doit-il être privé?

Oui, soit le champ implémente Unpin ou vous ne construisez jamais un PinMut du champ.

@rfcbot variance

J'écris un montant décent de code basé sur PinMut et une chose qui s'est souvent produite est que j'ai une opération qui ne repose pas sur l'épinglage, mais elle se prend de PInMut plutôt que &mut pour dire "Je promets que je ne sortirai pas de cette mémoire". Cependant, cela oblige tous les utilisateurs à avoir une valeur épinglée, ce qui n'est pas nécessaire. J'y ai réfléchi et je n'ai pas pu trouver une bonne API pour cela, mais ce serait merveilleux s'il y avait un moyen de séparer "J'ai besoin que mon argument soit épinglé" de "Je peux travailler avec un épinglé argument".

Je pense que «variance» n'est pas le bon terme ici; ce que vous voulez, ce sont des constructeurs de types associés pour abstraire sur différents types de référence: D

@RalfJung Oui, c'est vrai. Cependant, je me contenterais d'un type comme pourrait provenir d'un PinMut ou d'un & mut mais ne peut être utilisé que comme un PinMut, même si je sais que cela ne s'adapte pas particulièrement bien. Je pense que c'est plus réalisable que les types génériques arbitrary_self_types: smile: Il est peut-être préférable de terminer par "nous sommes assez confiants que nous sommes à l'épreuve du temps pour faire fonctionner des signatures comme celle-ci":

impl MyType {
    fn foo(self: impl MutableRef<Self>, ...) { ... }
}

@rfcbot concerne api-refactor

Un peu de struct d'inspiration et la nuit dernière, j'ai compris comment nous pourrions refactoriser cette API afin qu'il n'y ait qu'un seul type Pin , qui enveloppe un pointeur, plutôt que d'avoir à créer une version épinglée de chaque pointeur. Ce n'est en aucun cas une refonte fondamentale de l'API, mais il est préférable de tirer le composant «pins the memory» en un morceau composable.

Quand je travaillais sur la pile réseau pour nt-rs , on m'a dit que les références épinglées aideraient avec la "danse de la propriété" que je résous actuellement avec fold comme vu ici (tx.send déplace tx dans un futur Send<<type of tx>> , ce qui rend difficile la boucle sur les données entrantes pour envoyer des paquets.). En quoi l'épinglage aiderait-il à ce genre de chose?

@Redrield send() prend &mut à terme 0.3.

@withoutboats J'ai hâte d'essayer cette nouvelle API dans Futures-rs!

Étant donné que la nouvelle API utilise des identifiants différents, je pense qu'il devrait être possible d'avoir les deux API disponibles simultanément. Je pense qu'il serait préférable d'avoir une courte période de transition pendant laquelle les deux API sont disponibles, ce qui nous permet d'expérimenter, de préparer un PR et de porter l'API du futur dans libcore vers le nouveau style.

@MajorBreakfast, il semble que nous pourrions potentiellement alias type PinMut<T> = Pin<&mut T>; et proposer un certain nombre des mêmes méthodes, ce qui réduirait l'ampleur et l'immédiateté de la casse.

@sans bateaux

Donc avec la nouvelle API:

  1. Pin<&T> et Pin<&mut T> ont l'invariant "d'épinglage standard", où la valeur derrière eux:
    1.a. n'est plus un &T / &mut T , à moins que Unpin
    1.b. ne sera pas utilisé comme Pin depuis une autre adresse mémoire.
    1.c. sera supprimé avant d'être invalidé en mémoire.
  2. Pin<Smaht<T>> n'a aucune garantie "spéciale", sauf qu'elle renvoie des Pin<&mut T> valides et Pin<&T> quand on le lui demande (ce qui est juste la garantie de sécurité standard, étant donné que les API sont sûres ).
    2.a. Voulons- nous une garantie DerefPure , où Pin<Smaht<T>> est nécessaire pour renvoyer la même valeur à moins qu'elle ne soit mutée? Quelqu'un veut-il ça?

Correction sur l'invariant.

L'invariant pour Pin<Smaht<T>> est:

  1. Appeler Deref::deref(&self.inner) donnera un Pin<&T::Target> valide (notez que &self.inner n'a pas besoin d'être un coffre-fort &Smaht<T> ).
  2. Si Smaht<T>: DerefMut , appeler DerefMut::deref_mut(&mut self.inner) donnera un Pin<&mut T::Target> valide (notez que &mut self.inner n'a pas besoin d'être un coffre-fort &mut Smaht<T> ).
  3. Appeler le destructeur de self.inner afin de le détruire est OK tant qu'il est sans danger pour la destruction.
  4. self.inner n'a pas besoin d'être un coffre-fort Smaht<T> - il n'a pas besoin de prendre en charge d'autres fonctions.

Il y a eu des commentaires publiés dans le post de reddit à propos de la proposition @withoutboats :

  • (multiple) Own est un nom étrange, voir ci-dessous les moyens possibles de résoudre ce problème.

  • ryani demande pourquoi les implémentations qui contraignent Deref<Target = T> ont ce générique supplémentaire T ? Vous ne pouvez pas simplement utiliser P::Target ?

  • jnicklas demande quel est le but du trait Own , qu'en est-il d'ajouter pinned méthodes inhérentes par convention? Cela signifie que les utilisateurs de l'API n'ont pas besoin d'importer le trait, mais vous perdez la possibilité d'être générique par rapport aux constructeurs pinnables. Est-ce un gros problème? La motivation du trait semble insuffisamment motivée: _ [ils] ont après tout la même forme .'_

  • RustMeUp (moi-même) demande, dans le trait Own , quel est le but de la méthode own ? Pourquoi le trait ne peut-il pas simplement laisser pinned à implémenter par les types qui souhaitent s'inscrire? Cela permet au trait d'être sûr et lui permet d'être nommé Pinned ce qui semble moins gênant que Own . La raison de la propre méthode semble insuffisamment motivée.

J'ajouterai une autre question:

Pourquoi ni Pin<P> ni Pin<P>::new_unchecked limités à P: Deref ?

#[derive(Copy, Clone)]
pub struct Pin<P> {
    pointer: P,
}

impl<P: Deref> Pin<P> { // only change
    pub unsafe fn new_unchecked(pointer: P) -> Pin<P> {
        Pin { pointer }
    }
}

ou

#[derive(Copy, Clone)]
pub struct Pin<P: Deref> { // changed
    pointer: P,
}

impl<P: Deref> Pin<P> { // changed
    pub unsafe fn new_unchecked(pointer: P) -> Pin<P> {
        Pin { pointer }
    }
}

empêcheraient à la fois Pin<P>P: !Deref instances qui sont inutiles car il n'y a rien que vous puissiez faire avec, à moins que des méthodes supplémentaires ne soient ajoutées. Je pense...

Voici un résumé du mien et des commentaires de ryani adressés.

ryani demande pourquoi les implémentations qui contraignent Derefavez ce T générique supplémentaire? Vous ne pouvez pas simplement utiliser P :: Target?

Il n'y a aucune différence entre ces deux impls à part la façon dont ils sont écrits.

Pourquoi aucune des broches

lui-même ni Pin

:: new_unchecked limité à P: Deref?

Je n'ai pas d'opinion; historiquement, la bibliothèque std ne s'est pas liée aux structures, mais je ne suis pas sûr que cette politique soit bien motivée ou soit un accident historique.


Nous prévoyons de l'implémenter après que # 53227 soit débarqué, mais n'implémentera pas le trait Own car c'est controversé. Pour l'instant, juste un constructeur inhérent pinned sur Box , Rc et Arc .

Suite à ce que @ arielb1 a écrit, je considère Pin ici comme agissant réellement sur des "constructeurs de type pointeur" - des choses comme Box ou &'a mut qui ont "kind" * -> * . Bien sûr, nous n'avons pas de syntaxe pour cela, mais c'est une manière dont je peux donner un sens à cela en termes d'invariants. Nous en avons encore 4 (sécurité (invariants par type comme dans mon article de blog : possédé, partagé, épinglé-possédé, épinglé-partagé.

Je pense qu'une formalisation appropriée nécessite cette vue, car l'idée est que Pin<Ptr><T> prend les invariants de T , les transforme (en utilisant les invariants épinglés partout), puis applique les Ptr constructeur à ce type résultant. (Cela nécessite de changer la façon dont nous définissons Owned , mais c'est quelque chose que j'ai prévu à long terme de toute façon.) En termes de formalisme, on préférerait vraiment écrire Ptr<Pin<T>> , mais nous avons essayé cela et cela ne fonctionne pas très bien avec Rust.

La promesse, alors, qu'un constructeur de type pointeur fait en "optant" pour Pin est que l'application de Deref / DerefMut sera sûre: que lorsque l'entrée est en fait Ptr appliqué à la variante appartenant à l'épinglé / partagé du type, la sortie satisfera la variante appartenant à l'épinglé / partagé de ce type.

Ce sont vraiment les deux seules «méthodes non sûres» ici (dans le sens où elles doivent faire attention aux invariants):

impl<P, T> Pin<P> where
    P: Deref<Target = T>,
{
    pub fn as_ref(this: &Pin<P>) -> Pin<&T> {
        Pin { pointer: &*this.pointer }
    }
}

impl<P, T> Pin<P> where
    P: DerefMut<Target = T>,
{
    pub fn as_mut(this: &mut Pin<P>) -> Pin<&mut T> {
        Pin { pointer: &mut *this.pointer }
}

Tout ce qui est sûr à utiliser peut être implémenté en plus de cela avec un code sûr, en exploitant que Pin<&T> -> &T est une conversion sûre, et pour Unpin même pour Pin<&mut T> -> &mut T .

Je préférerais que nous puissions changer les implémentations Deref et DerefMut pour Pin en quelque chose comme

impl<'a, T: Unpin> Pin<&'a mut T> {
    pub fn unpin_mut(this: Pin<&'a mut T>) -> &'a mut T {
        this.pointer
    }
}

impl<'a, T> Pin<&'a T> {
    // You cannot move out of a shared reference, so "unpinning" this
    // is always safe.
    pub fn unpin_shr(this: Pin<&'a T>) -> &'a T {
        this.pointer
    }
}

// The rest is now safe code that could be written by users as well
impl<P, T> Deref for Pin<P> where
    P: Deref<Target = T>,
{
    type Target = T;
    fn deref(&self) -> &T {
        Pin::unpin_shr(Pin::as_ref(self))
    }
}

impl<P, T> DerefMut for Pin<P> where
    P: DerefMut<Target = T>,
    T: Unpin,
{
    fn deref_mut(&mut self) -> &mut T {
        Pin::unpin_mut(Pin::as_mut(self))
    }
}

Cela rappellerait que ces deux -là ne font rien de nouveau , ils composent simplement as_ref / as_mut avec une conversion sûre de Pin<&[mut] T> à &[mut] T - - ce qui laisse clairement as_ref / as_mut comme la seule chose dont les autres constructeurs de type pointeur doivent s'inquiéter.


Le PinMut actuel a une méthode borrow prenant &mut self pour un réemprunt plus facile (puisque cela prend self , nous obtenons un emprunt automatique). Cette méthode fait exactement la même chose que Pin::as_mut . Je suppose que nous voudrions quelque chose comme ça ici aussi?

Je viens de remarquer que les tranches ont get_unchecked_mut , mais la broche a get_mut_unchecked . On dirait que nous devrions nous en tenir à quelque chose de cohérent?

Quelqu'un pourrait-il ajouter ceci aux préoccupations de rfcbot?

@rfcbot concerne get_mut_unchecked_mut_mut

Je viens de réaliser que nous avons oublié une situation où rustc copierait des trucs autour - ce n'est probablement pas un gros problème pour l'instant. Je parle de structures compactées. Si une structure compressée a un champ qui doit être supprimé, rustc émettra du code pour copier les données de ce champ vers un endroit aligné, puis appelera drop dessus. C'est pour s'assurer que le &mut passé à drop est bien aligné.

Pour nous, cela signifie qu'une structure repr(packed) ne doit pas être "structurelle" ou "récursive". pinning - ses champs ne peuvent pas être considérés comme épinglés même si la structure elle-même l'est. En particulier, il n'est pas sûr d'utiliser le pin-accessor-macro sur une telle structure. Cela devrait être ajouté à sa documentation.

Cela ressemble à une arme à pied géante qui devrait être collée sur tous les documents d'épinglage.

@alercah Je ne pense pas que ce soit beaucoup plus une arme à pied que la capacité existante d'impliquer Unpin ou Drop - les deux sont certainement beaucoup plus souvent vus à côté des valeurs épinglées que #[repr(packed)] .

C'est juste. Ma préoccupation est que quelqu'un pourrait penser qu'une projection est sûre pour un type qu'il n'a pas écrit, et ne pas se rendre compte que packed rend cette opération dangereuse, car ce n'est décidément pas évident. Il est vrai que quiconque fait la projection est responsable d'en être conscient, mais je pense que cela doit être documenté en plus partout où une telle projection pourrait se produire.

Hm, cela signifierait-il également que toutes les structures compactées pourraient être Unpin quels que soient les types de champs puisque l'épinglage n'est pas récursif?

@alercah Je ne suis pas super familier avec la façon dont sont mises en œuvre types emballés, mais je crois que son coffre - fort à la broche à un type tassé, mais pas à la broche par un type tassée. Donc, le seul cas où vous ne contrôlez pas le type compressé est si vous projetez sur un champ public du type de quelqu'un d'autre, ce qui pourrait être tout aussi dangereux à cause de Drop ou autre. En général, il ne semble pas conseillé d'épingler le projet sur les champs de quelqu'un d'autre à moins qu'ils ne fournissent une projection de broche indiquant que c'est sûr.

Hm, cela signifierait-il également que toutes les structures compactées pourraient être Unpin indépendamment des types de champ puisque l'épinglage n'est pas récursif?

Un cas d'utilisation que je pourrais imaginer est une liste chaînée intrusive, où vous ne vous souciez que de l'adresse de l'ensemble de la structure, mais où elle contient un tas de données mal alignées que vous souhaitez écraser sans vous soucier de l'adresse de ces données.

J'ai dû apprendre l'API Pin en raison de mon travail avec Futures, et j'ai une question.

  • Box implémente sans condition Unpin .

  • Box implémente sans condition DerefMut .

  • La méthode Pin::get_mut fonctionne toujours sur &mut Box<T> (car Box implémente sans condition Unpin ).

Pris ensemble, cela permet de déplacer une valeur épinglée avec un code entièrement sûr .

Est-ce voulu? Quelle est la raison pour laquelle cela est sécuritaire?

Il semble que vous ayez Pin<&mut Box<...>> , qui épinglent Box , pas le contenu de Box . Il est toujours prudent de déplacer un Box , car Box ne stocke jamais de références à son emplacement sur la pile.

Ce que vous voulez probablement, c'est Pin<Box<...>> . Lien Playground .

EDIT: n'est plus précis

@Pauan Ce Box est épinglé que la chose _à l'intérieur_ est épinglée. Tout code dépendant de cela serait incorrect.

La chose que vous recherchez est probablement PinBox , qui interdit le comportement que vous avez mentionné et vous permet d'obtenir un PinMut<Foo> .

@tikue Il est toujours possible de sortir d'un Pin<Box<...>> , ce que je pense que c'est ce qu'ils essayaient d'éviter.

@tmandry Corrigez-moi si je me trompe, mais Pin<Box<...>> épingle la chose à l'intérieur de Box , pas la boîte elle-même. Dans l'exemple original de @Pauan , ils avaient un Pin<&mut Box<...>> , qui épinglait uniquement le Box . Voir le lien de mon terrain de jeu montrant comment Pin<Box<...>> empêche d'obtenir une référence mutable à l'objet dans la boîte.

Notez que PinBox été récemment supprimé, et Pin<Box<T>> maintenant la même sémantique que PinBox .

@tmandry PinBox<T> a été supprimé et remplacé par Pin<Box<T>> sur Nightly (le lien doc que vous avez donné est pour Stable). Voici le bon lien Nightly.

Oh, les règles ont dû changer depuis ma dernière utilisation. Désolé pour la confusion.

@tmandry Oui, les changements étaient très récents. Puisque les choses sont toujours en mouvement, il est difficile de suivre tous les changements.

Le commentaire de @tikue est correct. Vous devez vous rappeler que les broches n'épinglent qu'un niveau d'indirection vers le bas.

@tikue @tmandry @withoutboats Merci pour les réponses! Cela a été très utile.

Alors, quels sont les états des deux préoccupations maintenant? ( api-refactor & get_mut_unchecked_mut_mut ) En tant que personne qui attend avec impatience la fonctionnalité de série async / await, je me demande quelle version rustc les API Pin cibleront? Y a-t-il une estimation?

@ crlf0710 voir la proposition de stabilisation .

@withoutboats Cela semble terminé? Allons-nous fermer?

Ok, je m'excuse si ce n'est pas le bon endroit pour publier ça, mais j'ai réfléchi au problème Drop + !Unpin et j'ai eu l'idée suivante:

  1. Idéalement, si Drop::drop était fn(self: Pin<&mut Self>) il n'y aurait pas de problème. Appelons un tel Drop PinDrop . Nous ne pouvons pas simplement remplacer Drop par PinDrop raison de problèmes de rétrocompatibilité.
  1. puisque le seul problème avec Drop::drop(&mut self) est pour le cas Drop + !Unpin , nous pourrions dériver une implication par défaut de PinDrop pour Drop + Unpin (depuis Pin<&mut T> : DerefMut<Target = T> ) et faites de PinDrop le trait utilisé automatiquement par rustc (grâce à Pin::new_unchecked(&mut self) , puisque drop est le seul cas d'épinglage de pile quand on y pense).

Voici un PoC sommaire de l'idée: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=9aae40afe732babeafef9dab3d7525a8

Quoi qu'il en soit, à mon humble avis, cela devrait rester dans beta et ne pas encore devenir stable, même si cela doit être une exception. S'il y a un moment où le comportement de Drop peut dépendre de Unpin sans casser la compatibilité, alors c'est maintenant.

@danielhenrymantilla Je ne vois pas comment cela résout le problème de compatibilité avec les impls de drop génériques existants, comme impl<T> Drop for Vec<T> .

Vous avez raison, il faut autre chose:
un T: Unpin implicite lié à tous les génériques, avec un opt-out utilisant ?Unpin de la même manière que Sized

Cela devrait le faire devenir

impl<T> Drop for Vec<T>
where
  T : Unpin, // implicit, which implies that Vec<T> : Unpin which "upgrades" `Drop` into `PinDrop`

un T: Unpin implicite lié à tous les génériques, avec un opt-out utilisant ?Unpin de la même manière que Sized

Cela a un effet énorme sur la conception globale de l'API et a été largement discuté dans le cadre des propositions ?Move . Par exemple, cela signifierait que de nombreuses bibliothèques existantes auraient besoin d'être mises à jour pour fonctionner avec l'épinglage. La conclusion était qu'il était préférable d'utiliser une solution réservée aux bibliothèques, comme ce que nous avons maintenant, car cela n'exige rien de tout cela.

Oui, un coût énorme à court terme puisque toutes les bibliothèques existantes auraient besoin de se mettre à jour pour être compatibles avec !Unpin , mais à long terme nous finirions avec un Drop "plus sûr". Cela ne semblait pas si mal au début, car au moins nous ne cassons rien.

Mais c'est une préoccupation juste (je ne savais pas qu'elle avait été soulevée précédemment; merci de l'avoir signalé, @RalfJung ), et je suppose que les inconvénients pratiques à court terme drop(Pin<&mut Self>) .

Y at - il eu des discussions sur Hash mise en œuvre pour Pin types hachant sur les adresses?

Pin devrait probablement avoir une implémentation de Hash qui délègue simplement au pointeur contenu, il n'y a pas de priorité pour le hachage basé sur les adresses (et je ne vois aucune raison pour laquelle épingler une valeur devrait changer il est haché).

Cette page vous a été utile?
0 / 5 - 0 notes