Rust: Problema de seguimiento para las API de PIN (RFC 2349)

Creado en 18 mar. 2018  ·  211Comentarios  ·  Fuente: rust-lang/rust

Problema de seguimiento para rust-lang / rfcs # 2349

Estabilización de bloqueo:

  • [x] Implementación (PR # 49058)
  • [] Documentación

Preguntas sin resolver:

  • [] ¿Deberíamos ofrecer garantías más sólidas sobre la filtración de datos de !Unpin ?

Editar : comentario resumido: https://github.com/rust-lang/rust/issues/49150#issuecomment -409939585 (en la parte oculta por defecto)

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

Comentario más útil

@rfcbot preocupación api-refactor

Un poco de estructura de inspiración y anoche descubrí cómo podríamos refactorizar esta API para que solo haya un tipo Pin , que envuelve un puntero, en lugar de tener que crear una versión anclada de cada puntero. Esta no es una remodelación fundamental de la API de ninguna manera, pero se siente mejor extraer el componente "fija la memoria" en una pieza componible.

Todos 211 comentarios

Ahora me doy cuenta de que la fijación de pilas no es parte del RFC, @withoutboats ,

@ Nemo157 ¡Deberías copiarlo e informar tu experiencia!

La pregunta no resuelta sobre la filtración de datos de Unpin se relaciona con esto. Esa API no es sólida si decimos que no puede sobrescribir los datos de Unpin en un Pin menos que se ejecute el destructor, como lo solicitó

Una cosa que notaré es que la fijación de la pila no fue suficiente para cosas como Future::poll dentro de la macro await! , porque no nos permitió sondear en un bucle. Me interesaría si se encuentra con esos problemas, y cómo / si los resuelve si lo hace.

Mi uso actual es bastante trivial, un ejecutor de un solo subproceso que ejecuta un solo StableFuture sin soporte de generación . Cambiar a una API como sugiere @cramertj funcionaría bien con esto. Me he preguntado cómo extender esto para permitir la generación de múltiples StableFuture s, pero al menos con mi proyecto actual eso no es necesario.

Intenté experimentar con la API. ¿Parece que la siguiente definición (sugerida por RFC) de Future ya no es segura para objetos?

trait Future {
    type Item;
    type Error;

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

No importa. Encontré una nota sobre un plan para hacer que arbitrary_self_types seguro para objetos.

@sin barcos

Una cosa que notaré es que la fijación de la pila no fue suficiente para cosas como Future :: poll dentro de await! macro, porque no nos permitió sondear en un bucle.

¿Podría dar más detalles sobre eso?

@RalfJung Necesitas Pin para admitir nuevos préstamos como Pin , que actualmente no es así.

@cramertj ¿ Eso suena como una restricción de la API Pin , no de la API de fijación de pilas?

@RalfJung Sí, eso es correcto. Sin embargo, PinBox se puede volver a pedir prestado como Pin , mientras que el tipo de pila fija no puede (uno prestado como Pin crea un préstamo durante toda la vida útil del tipo de pila).

Dado un Pin , puedo pedirlo prestado como &mut Pin y luego usar Pin::borrow ; esa es una forma de volver a pedir prestado. ¿Supongo que ese no es el tipo de reajuste del que estás hablando?

@RalfJung No-- se planeó que métodos como Future::poll tomaran self: Pin<Self> , en lugar de self: &mut Pin<Self> (que no es un tipo self válido ya que no es ' t Deref<item = Self> - es Deref<Item = Pin<Self>> ).

Podría ser el caso de que podamos hacer que esto funcione con Pin::borrow realidad. No estoy seguro.

@cramertj No poll en x: &mut Pin<Self> ; Pensé en x.borrow().poll() .

@RalfJung Oh, ya veo. Sí, usar un método para cambiar el mañana manualmente podría funcionar.

Intentaré recordar publicar un ejemplo de algunas de las cosas que estoy haciendo con Pin s la semana que viene, por lo que puedo decir, el cambio de préstamos funciona perfectamente. Tengo una versión anclada del rasgo futures::io::AsyncRead junto con adaptadores que funcionan como fn read_exact<'a, 'b, R: PinRead + 'a>(read: Pin<'a, R>, buf: &'b [u8]) -> impl StableFuture + 'a + 'b y puedo trabajar esto en un StableFuture relativamente complejo que solo se apila en la parte superior nivel.

Aquí está el ejemplo completo de lo que estoy usando para leer:

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(())
    }
}

Esto es un poco molesto ya que tiene que pasar instancias por todas partes como Pin y usar Pin::borrow siempre que llame a funciones en ellas.

#[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
    }
}

Acabo de pensar que podría impl<'a, R> Read for Pin<'a R> where R: Read + 'a para solucionar tener que pasar valores como Pin<'a, R> todas partes, en su lugar podría usar fn foo<R>(source: R) where R: Read + Unpin . Desafortunadamente, eso falla porque Pin<'a, R>: !Unpin , creo que es seguro agregar un unsafe impl<'a, T> Unpin for Pin<'a, T> {} ya que el pin en sí es solo una referencia y los datos detrás de él todavía están anclados.

Preocupación: Parece probable que queramos que la mayoría de los tipos en libstd implementen Unpin incondicionalmente, incluso si sus parámetros de tipo no son Pin . Algunos ejemplos son Vec , VecDeque , Box , Cell , RefCell , Mutex , RwLock , Rc , Arc . Supongo que la mayoría de las cajas no pensarán en fijar en absoluto y, por lo tanto, solo sus tipos serán Unpin si todos sus campos son Unpin . Esa es una buena elección, pero conduce a interfaces innecesariamente débiles.

¿Se solucionará esto solo si nos aseguramos de implementar Unpin para todos los tipos de punteros libstd (tal vez incluso incluyendo punteros sin formato) y UnsafeCell ? ¿Es eso algo que queremos hacer?

¿Se solucionará esto solo si nos aseguramos de implementar Unpin para todos los tipos de punteros libstd (tal vez incluso incluyendo punteros sin formato) y UnsafeCell? ¿Es eso algo que queremos hacer?

Sí, me parece la misma situación que Send .

Se me acaba de ocurrir una nueva pregunta: ¿Cuándo son Pin y PinBox Send ? En este momento, el mecanismo de rasgo automático los convierte en Send siempre que T sea Send . No hay una razón a priori para hacer eso; al igual que los tipos en el estado de tipo compartido tienen su propio rasgo marcador para la capacidad de envío (llamado Sync ), podríamos hacer un rasgo marcador que diga cuándo Pin<T> es Send , por ejemplo, PinSend . En principio, es posible escribir tipos que sean Send pero no PinSend y viceversa.

@RalfJung Pin es Enviar cuando &mut T es Enviar. PinBox es Enviar cuando Box<T> es Enviar. No veo ninguna razón para que sean diferentes.

Bueno, al igual que algunos tipos son Send pero no Sync , podría tener un tipo confiando en "Una vez que se llame a este método con Pin<Self> , puedo confiar en que nunca me moverán a otro hilo ". Por ejemplo, esto podría dar lugar a futuros que pueden enviarse antes de iniciarse por primera vez, pero luego deben permanecer en un hilo (al igual que pueden moverse antes de iniciarse, pero luego deben permanecer fijos). No estoy seguro de poder dar un ejemplo convincente, ¿tal vez algo sobre un futuro que use almacenamiento local de subprocesos?

Acabo de abordar el problema de por vida mencionado por @Diggsey . Creo que Pin<Option<T>> -> Option<Pin<T>> debería ser una operación segura, pero no parece posible implementarla incluso usando las API inseguras actuales, y mucho menos qué tipo de API se necesitaría para hacer este código seguro:

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,
        }
    }
}

(Es posible solucionar el uso de transmutar para forzar las vidas, pero eso me hace sentir demasiado asqueroso).

Me gustaría agregar una pregunta sin resolver: ¿Deberíamos volver a agregar un tipo de referencia fijo compartido? Creo que la respuesta es sí. Vea esta publicación con discusión para más detalles.

Acabo de leer que los futuros 0.2 no son tan definitivos como pensé , por lo que, después de todo, quizás aún sea posible cambiar el nombre de Pin a PinMut y agregar una versión compartida.

@RalfJung Leí de nuevo la publicación de tu blog con más

Creo que ha encontrado un caso de uso potencialmente convincente para tener una variante Pin inmutable, pero no entiendo sus comentarios sobre Deref y &Pin<T> <=> &&T . Incluso si Pin<T> se puede convertir a &T forma segura, eso no los hace equivalentes, porque &T no se puede convertir en Pin<T> . No veo una razón para hacer que la conversión segura sea insegura (al eliminar la seguridad Deref impl).

Actualmente, el método map en Pin tiene la firma

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

¿Cuál es la razón por la que no es lo siguiente?

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

Tal como está, no puedo transformar un Pin de un tipo en un Pin de uno de sus campos sin acortar la vida útil innecesariamente.

Otro problema con el método map es que parece imposible convertir un Pin de una estructura en dos Pin s, cada uno de un campo diferente de la estructura. ¿Cuál es la forma correcta de lograrlo?

He estado usando esta macro para eso:

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

En gran parte de la discusión en torno a Pin parece haber una suposición de que "proyectar" un pin en un campo privado debe considerarse seguro. No creo que eso sea del todo cierto. Por ejemplo, el comentario del documento en map actualmente dice:

Debe garantizar que los datos que devuelve no se moverán mientras el valor del argumento no se mueva (por ejemplo, porque es uno de los campos de ese valor), y también que no se mueva fuera del argumento que recibe para la función interior.

La garantía de que "los datos que devuelve no se moverán mientras el valor del argumento no se mueva" es la descripción correcta del contrato que debe mantener un llamante de map . El paréntesis "(por ejemplo, porque es uno de los campos de ese valor)" parece implicar que siempre que esté devolviendo una referencia a su propio campo privado, tiene la garantía de estar seguro. Pero eso no es cierto si implementa Drop . Una Drop impl verá un &mut self , incluso cuando otro código ya haya visto un Pin<Self> . Puede proceder a usar mem::replace o mem::swap para salir de sus campos, violando la promesa hecha por un uso "correcto" anterior de map .

En otras palabras: usando una llamada de "proyección de pin correcta" a Pin::map (la llamada parece unsafe { Pin::map(&mut self, |x| &mut x.p) } ), sin otras invocaciones de unsafe , se puede producir una comportamiento. Aquí hay un enlace de juegos que demuestra esto.

Esto no implica que haya algún problema con la API actual. Demuestra que Pin::map debe marcarse como unsafe , que ya está. Tampoco creo que haya mucho peligro de que la gente se tropiece accidentalmente con esto al implementar futuros o similares; realmente tienes que hacer todo lo posible para salir de tus propios campos en una implícita de Drop , y Dudo que haya muchas razones prácticas para querer hacerlo.

Pero creo que el comentario del documento para map podría querer mencionar esto, y también creo que invalida algunas ideas para extensiones que veo en el RFC y discusiones circundantes:

  • No debería haber una macro / derivan que hace "proyección pin" y hace que parezca seguro. La proyección realmente impone un contrato sobre el otro código que lo rodea, que el compilador no puede hacer cumplir por completo. Por lo tanto, debería requerir el uso de la palabra clave unsafe siempre que esté listo.
  • El RFC menciona que si Pin se convirtieran en una característica de idioma &'a pin T , sería "trivial proyectar a través de campos". Creo que he demostrado que esto aún debería requerir unsafe , incluso si se limita a proyectar campos privados.

@sin barcos

Incluso si Pinse pueden lanzar a & T de forma segura, eso no los hace equivalentes, porque & T no se puede lanzar a Pin.

De hecho, esa no es una condición suficiente. Son iguales en mi modelo porque en mi publicación anterior , hicimos la siguiente definición:

Definición 5: PinBox<T>.shr . Un puntero ptr y life 'a satisfacen el estado de tipo compartido de PinBox<T> si ptr es un puntero de solo lectura a otro puntero inner_ptr tal que T.shr('a, inner_ptr)

(Y, implícitamente, la definición correspondiente para Pin<'a, T>.shr .)
Observe cómo PinBox<T>.shr depende de T.shr y nada más. Esto hace que PinBox<T>.shr exactamente el mismo invariante que Box<T>.shr , lo que implica que &Box<T> = &PinBox<T> . Un razonamiento similar muestra que &&T = &Pin<T> .

Entonces, esto no es una consecuencia de la API o el contrato escrito en el RFC. Es una consecuencia del modelo que solo tiene tres estados de tipo: propiedad, compartida, anclada. Si desea argumentar en contra de &&T = &Pin<T> , debe defender la introducción de un cuarto estado de tipo, "pin compartido".

@MicahChalmer

En gran parte de la discusión sobre Pin, parece haber una suposición de que "proyectar" un pin en un campo privado debe considerarse seguro. No creo que eso sea del todo cierto.

¡Ese es un muy buen punto! Para ser claros, no hay problema con hacer público el campo p de Shenanigans , ¿verdad? En ese momento, cualquier cliente podría escribir do_something_that_needs_pinning , y la intención del RFC es hacerlo seguro. (No sé por qué la RFC menciona específicamente los campos privados, mi interpretación siempre fue que se suponía que funcionaba con todos los campos).

Es interesante ver cómo esto interactúa con la interpretación de drop en mi modelo . Allí escribí que drop(ptr) tiene una condición previa de T.pin(ptr) . Con esta interpretación, el código real que falla en su ejemplo sería la implementación drop . (Ahora me pregunto por qué no me di cuenta de esto cuando escribí la publicación ...) Creo que queremos permitir proyecciones seguras a los campos eventualmente, y realmente no deberíamos proporcionar drop con un &mut si el tipo no fija. Eso es claramente falso.

¿Hay alguna forma de que podamos (a) hacer que impl Drop no sea seguro si el tipo es !Unpin , o (b) cambiar su firma a drop(self: Pin<Self>) si T: !Unpin ? Ambos suenan extremadamente inverosímiles, pero por otro lado ambos resolverían el punto de @MicahChalmer , al tiempo que preservarían la seguridad de las proyecciones de campo. (Si solo esto fuera anterior a 1.0 y pudiéramos cambiar Drop::drop para que siempre tome Pin<Self> ;) Pero, por supuesto, en este punto, esta ya no es una solución solo de biblioteca. La parte triste es que si nos estabilizamos como estamos, nunca podremos tener proyecciones de campo seguras.

@RalfJung Así que estoy más interesado en las preguntas prácticas (como probablemente esperarías: guiño :). Creo que hay dos:

  • ¿Debe Pin<T: !Unpin> implementar Deref<Target =T> ?
  • ¿Debería haber un Pin<T> y un PinMut<T> (el primero es un pin compartido)?

No veo ninguna razón para responder negativamente a la primera pregunta. Creo que ha reabierto la segunda pregunta; Me inclino a volver a dos tipos diferentes de pines (pero no estoy completamente convencido). Si alguna vez actualizamos esto a un tipo de referencia de nivel de idioma, tendríamos &pin T y &pin mut T .

Parece que no importa lo que hagamos, su modelo probablemente necesite un cuarto estado de tipo para reflejar con precisión las invariantes de la API. No creo que convertir un &&T en un &Pin<T> deba ser seguro.

Así que estoy más interesado en las preguntas prácticas (como probablemente esperarías un guiño).

Lo suficientemente justo. ;) Sin embargo, creo que es importante que tengamos al menos una comprensión básica de lo que está y no está garantizado alrededor de Pin una vez que el código inseguro entra en escena. Basic Rust tuvo varios años de estabilización para resolver esto, ahora lo repetiremos por Pin en unos pocos meses. Si bien Pin es una adición solo de biblioteca en lo que respecta a la sintaxis, es una adición significativa en lo que respecta al modelo. Creo que es prudente que documentemos lo más exhaustivamente posible qué es el código inseguro y qué código no está permitido asumir o hacer alrededor de Pin .

Si bien estas preocupaciones son teóricas en este momento, de repente serán muy prácticas una vez que tengamos la primera falta de solidez debido a un código inseguro que hace suposiciones incompatibles.


En cuanto a su segunda pregunta:

Parece que no importa lo que hagamos, su modelo probablemente necesite un cuarto estado de tipo para reflejar con precisión las invariantes de la API. No creo que convertir un && T en un & Pindebe ser seguro.

Eso es lo que esperaba.

Si queremos ser conservadores, podríamos cambiar el nombre de Pin a PinMut pero no agregar PinShr (probablemente se llamará Pin pero estoy tratando de eliminar la ambigüedad aquí ) por ahora, y declarar que el código inseguro puede asumir que un &PinMut<T> realidad apunta a algo anclado. Entonces tendríamos la opción de agregar Pin como una referencia fija compartida más adelante.

@Comex ha planteado una razón práctica para tener PinShr : debería ser posible proporcionar un getter que vaya desde PinShr<'a, Struct> a PinShr<'a, Field> ; si usamos &PinMut para pines compartidos, eso no funciona. No sé cuánto se necesitarán tales captadores.


Para su primera pregunta, parece haber algunos argumentos sólidos a favor: (a) los planes actuales para tipos de self arbitrarios seguros para objetos, y (b) poder usar de manera segura la gran cantidad de API existentes en referencias compartidas cuando se mantiene un PinShr (o PinMut ).

Es lamentable que no parezcamos tener una manera fácil de proporcionar operaciones de manera similar que puedan funcionar tanto en &mut como en PinMut ; después de todo, mucho código trabajando en &mut no tiene intención de usar mem::swap . (Esto, como yo lo veo, sería la principal ventaja de una solución basada en !DynSized o algo comparable: &mut se convertiría en un tipo para referencias que pueden o no estar ancladas. podría tener esto como otro tipo de biblioteca en la API Pin , pero eso es bastante inútil dado que ya tenemos todos estos métodos &mut ).

Hay un argumento ligero en contra, que son los tipos que quieren hacer cosas "interesantes" tanto por &T como por PinMut<T> . Eso será difícil de hacer, no todo es posible y hay que tener mucho cuidado. Pero no creo que eso supere los buenos argumentos a favor.

En este caso (es decir, con este impl Deref ), PinShr<'a, T> debería venir con un método seguro que lo convierta en &'a T (preservando la vida útil).


Y hay otra preocupación que creo que tenemos que resolver: las reglas para Pin::map y / o drop , a la luz de lo que @MicahChalmer notó anteriormente. Tenemos dos opciones:

  • Declare que usar Pin::map con una proyección a un campo público (sin derefs, ni siquiera implícito) siempre es seguro. Esta es mi interpretación del RFC actual. Esto coincidiría con &mut y & . Sin embargo, entonces tenemos un problema alrededor de Drop : ahora podemos escribir código seguro que cause UB dado un tipo !Unpin bien formado.
  • No hagas nada divertido alrededor de Drop . Luego, tenemos que agregar advertencias fuertes a Pin::map que incluso usarlo para campos públicos podría provocar errores. Incluso si alguna vez tenemos &pin T , no podremos usarlo para acceder a un campo en un código seguro.

El único argumento posible para la segunda opción que puedo ver es que puede ser el único que realmente podemos implementar. ;) Creo que es inferior en todos los sentidos posibles: hace que &pin bastante extraño y poco ergonómico, incluso si se incorpora un día, es un arma de fuego, dificulta la composición.

Puede haber una forma de lograr la primera opción, pero no es trivial y no tengo idea de cómo hacerla compatible con versiones anteriores: podríamos agregar un Unpin vinculado a Drop , y agregar un DropPinned que no tiene el límite y donde drop toma Pin<Self> . Tal vez el límite de Unpin en Drop podría aplicarse de una manera extraña donde se puede escribir el impl Drop for S , pero esto agrega un límite implícito a S diciendo que tiene que ser Unpin . Probablemente no sea realista. : / (Supongo que este es también un punto en el que el enfoque basado en !DynSized funciona mejor: convierte &mut T en "puede o no estar fijado", manteniendo drop sonido.)

@RalfJung @MicahChalmer Creo que es mejor simplemente documentar que si se mueve fuera del campo en el Drop impl, proyectar a un Pin de ese campo en otro lugar no es correcto.

De hecho, hoy en día ya es el caso de que (usando un código inseguro) podría salir de un campo de tipo !Unpin , y esto es seguro y está bien definido siempre y cuando nunca proyecte a un pin de ese campo en otro lugar . La única diferencia con Drop es que la parte de mudanza solo contiene código seguro. Me parece que las notas sobre Pin::map necesidad de cambiar a la nota que no es seguro si alguna vez se muda fuera de ese campo, independientemente de la impl gota.

Debe ser seguro salir de los campos de tipo !Unpin en algunos casos, porque es muy probable que los generadores salgan de uno de sus campos cuando regresen.

Creo que es mejor simplemente documentar que si se mueve fuera del campo en el impl Drop, proyectar a un Pin de ese campo en otro lugar no es correcto.

Entonces, esta es la segunda opción, la que hace que &pin a un campo permanentemente sea una operación insegura.
Creo que este no es un cambio pequeño. Esto cambia fundamentalmente lo que significa pub en un campo. Cuando utilizo un tipo de biblioteca, no puedo saber qué hace en su implementación de caída, por lo que fundamentalmente no tengo forma de obtener una referencia fija a ese campo.

Por ejemplo, ni siquiera se me permitiría pasar de Pin<Option<T>> a Option<Pin<T>> menos que Option declare explícitamente que nunca habrá un Drop haciendo nada " gracioso". El compilador no puede entender esa declaración, así que aunque Option podría proporcionar un método apropiado para hacer esto, hacer lo mismo con una match tiene que seguir siendo una operación insegura.

La única diferencia con Drop es que la parte de mudanza solo contiene código seguro.

Pero esa es una gran diferencia, ¿no? Podemos poner reglas arbitrarias sobre lo que puede o no hacer el código inseguro, pero no así para el código seguro.

En algunos casos, debe ser seguro salir de los campos de un tipo! Unpin, porque es muy probable que los generadores salgan de uno de sus campos cuando regresen.

Supongo que en este caso el campo será Unpin ? Así que probablemente podríamos tener una regla que diga que Pin::mut para un campo público de una estructura externa está bien si ese campo tiene el tipo Unpin . No estoy seguro de cuán útil sea esto, pero probablemente sea mejor que nada.

Quiero reafirmar rápidamente mi confusión acerca de que &Pin<T> no brinda más garantías que &&T . & , &mut y &pin respectivamente proporcionan "acceso compartido", "acceso único" y "acceso único a un valor que no se moverá". Entender &&pin como "acceso compartido a un acceso único a un tipo que no se moverá" le indica que la memoria es compartida (la garantía de unicidad de &pin se cancela al compartir & ), pero aún conserva la propiedad de que el tipo no se moverá, ¿no?

No estoy seguro de lo que está preguntando o diciendo. ¿Estás confundido por qué creo que "compartidos fijados" es un modo / estado de tipo fundamental en sí mismo?

El punto es que el "acceso compartido" no es algo que yo sepa definir por sí solo. Hay muchas formas diferentes de compartir y coordinar el intercambio, como lo demuestran las muy diferentes formas en que, por ejemplo, Cell , RefCell y Mutex comparten.

No puede simplemente decir que está compartiendo algo ("cancelar la garantía de unicidad" de algo) que posee y esperar que esa declaración tenga sentido. Tienes que decir cómo estás compartiendo y cómo te aseguras de que esto no cause estragos. Puede "compartir haciéndolo de solo lectura", o "compartir solo dando acceso atómico a través de cargas / tiendas sincronizadas", o "compartir [dentro de un solo hilo] haciendo que esta bandera de préstamo coordine qué tipo de acceso entregar" . Uno de los puntos clave en RustBelt fue darse cuenta de la importancia de dejar que cada tipo defina por sí mismo lo que sucede cuando se comparte.

No puedo pensar en una manera de hacer que la "fijación compartida" surja como la composición ortogonal de compartir y fijar. Tal vez haya una manera de definir la noción de un "mecanismo de intercambio" que luego podría aplicarse tanto al invariante de propiedad como al invariante fijo para dar lugar a "compartido (normal)" y "compartido fijo", pero lo dudo seriamente. Además, como hemos visto, eso cae plano para RefCell - si RefCell hace para el invariante fijo compartido algo similar a lo que hace para el invariante recién compartido, ciertamente no podemos justificar eso desde & &pin RefCell<T> través de &RefCell<T> (usando Deref ) a través de borrow_mut podemos obtener una referencia de &mut que dice que no ocurrió ninguna fijación.

@RalfJung

Podríamos agregar un Unpin bound to Drop, y agregar un DropPinned que no tiene el límite y donde drop toma Pin.

¿Es la definición de Drop realmente el problema aquí? Otra forma de pensarlo es echarle la culpa a mem::swap y mem::replace . Estas son las operaciones que le permiten mover algo que no es de su propiedad. Suponga que se agregó un T: Unpin a _ellos_?

Para empezar, eso arreglaría el agujero de drop que señalé: mi Shenanigans no se compilaría y no creo que pueda violar las promesas de los pines sin otro unsafe . ¡Pero permitiría más que eso! Si se ha vuelto seguro obtener una referencia &mut a un valor previamente fijado en drop , ¿por qué limitarlo a solo drop ?

A menos que me falte algo, creo que esto haría que sea seguro tomar prestada una referencia de &mut de una PinMut<T> cualquier momento que desee. (Estoy usando PinMut para referirse al qué, en la corriente de noche, que se llama Pin , para evitar confusiones con la discusión alrededor de los pasadores compartidos.) PinMut<T> podría aplicar DerefMut incondicionalmente, en lugar de solo T: Unpin .

Es lamentable que no parece que tengamos una manera fácil de proporcionar operaciones de manera similar que puedan funcionar tanto en & mut como en PinMut; después de todo, un montón de código trabajando en & mut no tiene intención de usar mem::swap .

Una DerefMut impl en PinMut arreglaría eso, ¿verdad? El código que se preocupa por la fijación y requiere PinMut , podría llamar al código que funciona en &mut forma segura y sencilla. La carga se colocaría en cambio en las funciones genéricas que _do_ quieren usar mem::swap - a las que se les debería agregar Unpin , o usar unsafe y tener cuidado no violar las condiciones de los pines.

Agregar tales límites a swap y replace ahora rompería la compatibilidad con versiones anteriores desde la primera versión estable. No veo una forma realista de llegar allí desde aquí. Pero, ¿me estoy perdiendo algún otro agujero al pensar que habría sido lo mejor si se hubiera sabido esto en los días anteriores a 1.0?

Tal como están las cosas, no veo ninguna solución mejor que la que dijo @withoutboats : mantener map inseguros y poner un mensaje en sus documentos advirtiendo a las personas que no se muevan de ningún campo que se haya anclado previamente en un drop impl.

Podemos poner reglas arbitrarias sobre lo que puede o no hacer el código inseguro, pero no así para el código seguro.

El uso de unsafe siempre impone reglas sobre el código seguro circundante. La buena noticia aquí es que, hasta donde sabemos, si una estructura pinnable tiene un método para proyectar un pin en un campo privado, solo su propio drop impl puede usarlo para violar el contrato en código seguro. Por lo tanto, aún es posible agregar tal proyección y presentar a los _usuarios_ de esa estructura con una API completamente segura.

¿Es la definición de Drop realmente el problema aquí?

Asignar la culpa es algo arbitrario, hay diferentes cosas que uno podría cambiar para tapar el agujero de la solidez. Pero, ¿estamos de acuerdo en que cambiar Drop como sugerí solucionaría el problema?

Otra forma de pensarlo es echarle la culpa a mem :: swap y mem :: replace. Estas son las operaciones que le permiten mover algo que no es de su propiedad. Suponga que se les agregó un enlace T: Unpin.

Bueno, eso haría que &mut T generalmente seguro de usar para los tipos !Unpin . Como ha observado, ya ni siquiera necesitaríamos PinMut . PinMut<'a, T> en su propuesta podría definirse como &'a mut T , ¿verdad?
Esta es esencialmente la propuesta ?Move que se ha descartado anteriormente debido a problemas de compatibilidad con versiones anteriores y complejidad del idioma.

El uso de inseguro siempre impone reglas sobre el código seguro circundante.

No estoy seguro que quieres decir. Más allá del límite de la privacidad, este no debe ser el caso; El código inseguro no puede imponer nada a sus clientes.

La buena noticia aquí es que, hasta donde sabemos, si una estructura pinnable tiene un método para proyectar un pin en un campo privado, solo su propio drop impl puede usarlo para violar el contrato en código seguro. Por lo tanto, aún es posible agregar tal proyección y presentar a los usuarios de esa estructura una API completamente segura.

Sí, los tipos pueden optar por declarar la proyección segura. Pero, por ejemplo, el verificador de préstamos no entenderá que se trata de un acceso de campo, por lo que dado un PinMut<Struct> no puede utilizar dichos métodos para obtener PinMut en dos campos diferentes al mismo tiempo.

Pero, ¿estamos de acuerdo en que cambiar Drop como sugerí solucionaría el problema?

Estoy de acuerdo, eso lo arreglaría.

Ni siquiera necesitaríamos PinMut más. PinMut <'a, T> en su propuesta podría definirse como &' a mut T, ¿verdad?

No, todavía se requiere PinMut<'a, T> para prometer que el referente nunca volverá a moverse. Con &'a mut T solo puede confiar en que no se moverá de por vida 'a . Esto todavía estaría permitido, como lo está hoy:

`` `` óxido
estructura X;
impl! Desanclar para X {}
fn toma_a_mut_ref (_: & mut X) {}

fn pedir prestado_y_movirse_y_borrow_again () {
sea ​​mut x = X;
toma_a_mut_ref (& mut x);
let mut b = Box :: nuevo (x);
toma_a_mut_ref (& mut * b);
}
`` ``

Sería seguro pasar de PinMut<'a, T> a &'a mut T pero no al revés - PinMut::new_unchecked seguiría existiendo y seguiría siendo unsafe .

Esta es esencialmente la propuesta ?Move que se ha descartado anteriormente debido a problemas de compatibilidad con versiones anteriores y complejidad del idioma.

Según tengo entendido, las propuestas ?Move intentaban llegar hasta el final para no necesitar PinMut , cambiando las reglas fundamentales del lenguaje para prohibir el fragmento de código anterior (haciendo Unpin sea ​​un rasgo de Move .) No estoy proponiendo nada de eso; mi propuesta es comenzar exactamente como está ahora todas las noches, más:

  • Agregue un Unpin vinculado a las funciones std::mem::swap y std::mem::replace
  • Elimine el límite Unpin del DerefMut impl de Pin ( PinMut si ese cambio de nombre ocurre)

Eso es todo: ningún lenguaje fundamental cambia la forma en que funcionan los movimientos, ni nada de eso. Mi afirmación es: sí, este sería un cambio rotundo, pero tendría menos impacto que cambiar Drop (que a su vez es menos drástico que las propuestas de ?Move ), pero conserva de los beneficios. En particular, permitiría una proyección de pines segura (al menos para campos privados, y creo que incluso para públicos, no estoy seguro) y evitaría situaciones en las que el código paralelo deba escribirse para que funcione con PinMut y &mut .

No, todavía se requiere PinMut <'a, T> para prometer que el referente nunca volverá a moverse. Con & 'a mut T solo puedes confiar en que no se moverá de por vida' a.

Veo. Tiene sentido.

Según tengo entendido, las propuestas? Move intentaban llegar hasta el final para no necesitar PinMut, cambiando las reglas fundamentales del lenguaje para prohibir el fragmento de código anterior (haciendo que Unpin fuera un rasgo Move).

Entendido.

Mi afirmación es: sí, este sería un cambio radical, pero tendría menos impacto que cambiar Drop

¿A qué cambio te refieres? ¿Agregar un límite de Unpin ? Puede que tengas razón, pero no tengo idea de lo ampliamente utilizados que son mem::swap y mem::replace .

En particular, permitiría una proyección de pines segura (al menos para campos privados, ¿y creo que incluso para públicos? No estoy muy seguro)

No veo cómo lo privado frente a lo público pueden hacer una diferencia aquí. ¿Cómo permitiría un campo público menos que un campo privado?

Pero sí, esto parece mantenerse unido en general. Future aún tomaría un PinMut porque tiene que depender de que las cosas nunca se muevan, pero tendría una gama más amplia de métodos disponibles para su uso.

Sin embargo , el aspecto de la compatibilidad es importante. No creo que esto sea realista, rompería todo el código genérico que llama a mem::swap / mem::replace . Además, en este momento, el código inseguro es libre de implementar estos métodos usando ptr::read / ptr::write ; esto podría conducir a la rotura

Ya que estamos en el tema de presentar Unpin enlazado en mem::swap y mem::replace (y no preocuparnos por la rotura). Si asumimos que se toma la ruta del "compilador integrado". ¿Sería posible introducir también el mismo límite en mem::forget para garantizar que los destructores se ejecuten para las variables fijadas en la pila haciendo que thread::scoped suene y evitar "pre-cacarse los pantalones" en ciertos casos?

Tenga en cuenta que mem::forget en un PinBox todavía está permitido. La nueva garantía propuesta relacionada con la caída NO dice "las cosas no se filtran". Dice "las cosas no se desasignan sin que se llame primero a drop ". Esa es una declaración muy diferente. Esta garantía no ayuda thread::scoped .

Para agregar contexto, mover datos fuera de una estructura que implementa Future es algo que hago comúnmente. Con bastante frecuencia surge la necesidad de realizar un trabajo de limpieza si un futuro nunca se completó (se eliminó antes de que se completara el sondeo).

Por lo tanto, seguramente habría golpeado esta mina terrestre al migrar el código existente a futuros 0.3 incluso con la documentación agregada a map .

@carllerche la función map dice claramente que no debes usar esto para mover nada. No podemos ni queremos protegernos contra las personas que se mueven deliberadamente de un Pin<T> , pero tienes que apartarte de tu camino (usando un código inseguro) para hacerlo. No llamaría a eso una mina terrestre.

Entonces, ¿a qué mina se refiere?

@RalfJung He estado tratando de averiguar los límites en el mapeo de referencias fijadas por mí mismo y creo que esto va a ser una gran pistola si no se resuelve pronto. Creo que la primera solución es preferible, a pesar de su complejidad; no poder proyectar de forma segura en campos anclados hace que sea virtualmente imposible para los consumidores utilizar API que se basan en la fijación sin escribir código inseguro.

Si esto no se puede hacer, creo que, en la práctica, la mayoría de las API utilizables que usan pinning tendrán que usar PinShare. Esto podría no ser una gran desventaja, supongo, pero todavía no tengo clara la relación con Unpin en ese caso. Específicamente: digamos que tomo una referencia compartida y obtengo una referencia a un campo en el tipo (por un tiempo determinado). ¿Puedo confiar realmente en que no se moverá una vez que termine la vida? Probablemente pueda si el campo es !Unpin así que tal vez esté bien, siempre que Pin no se pueda proyectar, lo que más me preocupan son las enumeraciones. A menos que esté diciendo que incluso la fijación compartida no puede ser segura sin reparar la caída; en ese caso, creo que la fijación de la caída para que funcione con la fijación esencialmente tiene que suceder; de lo contrario, se convierte en una característica de la biblioteca de nicho que realmente no se puede usar de manera segura y no merece (IMO) ningún lugar de honor en el lenguaje principal, incluso si resulta ser muy útil para Futures.

También debo mencionar que la única API práctica que tengo para colecciones intrusivas hasta ahora (todavía necesito resolver los problemas) necesita garantías aún más fuertes que esa; debe poder garantizar que no se solicite la entrega siempre que haya un préstamo en la colección. Puedo hacer esto usando una técnica al estilo GhostCell, pero se siente muy incómodo y requiere que el usuario realice una administración manual de la memoria (ya que tenemos que filtrar si la memoria de respaldo para algo en la colección se elimina sin que se le proporcione el token). Así que estoy un poco preocupado de que la caída automática parezca difícil de hacer funcionar con tipos que usan la fijación de formas interesantes.

Por curiosidad: ¿cuáles son los argumentos en contra de agregar un Unpin vinculado a Drop ? Citaste la compatibilidad con versiones anteriores, con la alternativa de que de alguna manera necesitarías unir automáticamente lo que se estaba eliminando, pero las restricciones de nivel de sistema de tipo Drop ya son raras que no existen para otros rasgos: ¿por qué este es tan diferente? Ciertamente no es tan elegante como hacer que la gota se lleve Pin<T> pero en realidad no podemos hacer ese cambio en este momento. Es el tema que en realidad no sabemos qué hacer si lo hace de llamada perdida en un tipo que sólo tiene un Unpin implementación, cuando el tipo en sí es !Unpin ? Supongo que lanzar una excepción en la implementación de caída en ese caso podría ser el enfoque correcto, ya que cualquiera que confíe en la ejecución de drop para tipos genéricos ya necesita manejar el caso de pánico. Eso significaría que sería muy difícil usar los tipos !Unpin en la práctica sin un grupo de personas actualizando su código para usar el nuevo rasgo Drop (por lo que el ecosistema se vería obligado a mover todo a thew nueva versión), pero creo que estaría bien con eso, ya que aún preservaría la solidez y no rompería el código que no usa !Unpin en absoluto. Además, el asunto de "su código entra en pánico si la biblioteca no se actualiza" ¡realmente incentivaría a la gente a seguir adelante!

De hecho, aquí está mi diseño propuesto:

Extienda el rasgo Drop con un segundo método que usa Pin, como propusiste. Realice una implementación predeterminada especializada where T: Unpin caída de llamadas (¿supongo que esto pasaría las reglas de especialización actuales? Pero incluso si no lo hiciera, Drop siempre puede tener un caso especial; más, Drop funciones Ahora tiene exactamente el mismo comportamiento que propuse anteriormente, sin problemas de compatibilidad con versiones anteriores y sin ningún intento de derivar automáticamente un límite de manera incómoda.

Tiene el problema de que las bibliotecas de terceros tendrán que actualizarse para que sean prácticamente útiles con los tipos !Unpin , pero como dije, podría decirse que esto es algo bueno. En cualquier caso, no tengo idea de cuántas implementaciones de drop realmente mutan sus campos de manera que requieren &mut mayoría de las que puedo pensar que hacen algo interesante o usan unsafe o use mutabilidad interior, pero probablemente no estoy pensando en algunos casos de uso comunes.

Lo principal de este enfoque es que si se tomara, tendría que tomarse antes de que Pin se estabilizara. Esta es una razón más por la que realmente espero que Pin no se apresuren. No creo que hayamos explorado lo suficiente las consecuencias del diseño.

(Veo un problema mayor potencial: ManuallyDrop y los sindicatos en la media general de que probablemente alguien podría haber escrito un destructor más de un tipo genérico que no asuma que el drop aplicación dentro del mismo no podía entrar en pánico , simplemente porque nunca fue capaz de correr (que también no se le permitirá llamar a cualquier otro &mut funciones en el T genérica, a excepción de las implantadas en los rasgos inseguras que prometió no entrar en pánico). Desde Pin ing un tipo ya debe garantizar que su implementación de caída se ejecute antes de que se destruya su memoria [de acuerdo con la nueva semántica], creo que el tipo !Unpin dentro de un ManuallyDrop usado para ese propósito en el código existente no podría considerarse anclado en primer lugar, por lo que el código existente debería ser correcto si se hace esa suposición; ciertamente, no debería poder proyectar de forma segura un pin en un ManuallyDrop , ya que solo puede ser seguro si garantiza que se llama a su destructor antes de la caída. Simplemente no sé cómo se comunicaría este caso a la comp. ¿Puede esto aprovechar las cosas del "parche en el ojo", ya que parece que está destinado a un propósito similar?]. No estoy realmente seguro de que eso vuele semánticamente, pero probablemente funcione para los propósitos de la implementación Drop para el código existente.

Sin embargo, sobre el tema del parche ocular ... todavía no estoy seguro de la definición formal exacta que tiene, pero si la idea general es que no se invocan funciones genéricas interesantes en T, ¿tal vez uno podría explotarlo más? Eso significa que si se implementara la implementación drop_pinned para el tipo !Unpin , el contenedor respetuoso con el parche se comportaría correctamente. Me parece posible que luego se pueda compilar el error de tiempo para !Unpin tipos que implementan Drop , no implementan drop_pinned , y no parchean sus parámetros genéricos, en de la misma manera que lo hacemos cuando utiliza contenedores sin parche con una vida útil autorreferencial.

Sin embargo, los existenciales plantean un grave riesgo de compatibilidad con versiones anteriores con cualquier estrategia de tiempo de compilación; Por eso creo que una solución que falla en tiempo de ejecución es más realista. esto no tendría fallas en el tiempo de ejecución y no tendría falsos positivos.

Editar: En realidad, rayar todo lo anterior: que en realidad sólo tiene que preocuparse acerca de los campos accesibles en destructores, no &mut de acceso general, correcto? Porque solo nos preocupan las proyecciones de campo implícitas de &mut self .

Puede que esté reinventando la propuesta !DynSized , pero esencialmente: los contenedores genéricos ya deben ser !Unpin si exponen algún método que se preocupe por el estado de tipo de pin (me doy cuenta de que esto suena sospechosamente a la parametricidad incorrecta argumento, pero escúchame!). Existenciales como Box<Trait> y &mut Trait no reflejan necesariamente su estado Unpin , pero (¿al menos al mirar las API actuales?) No creo que Pin<Box<T>> y Pin<&mut T> necesariamente tienen que ser coercibles a Pin<T> donde T: !Unpin ; no implementar eso significaría que las referencias ancladas a estos tipos no proporcionarán un acceso fijo a su interior (tenga en cuenta que hay un precedente para esto con la relación de & mut y &: & mut & T no es coercible a & mut T, solo & T, y Box <& mut T> no es coercible a Box<T> , solo & mut T; en general, cuando colisionan diferentes estados de tipo, no es necesario que se propaguen automáticamente). Reconozco que generalmente &mut T , Box<T> y T se consideran totalmente intercambiables desde una perspectiva de estado de tipo, y este argumento no se extiende a hipotéticos existenciales en línea, pero tal vez esto sea la sustancia de la propuesta DynSize (supongo que no permite intercambios seguros o mem::replace s para valores de objeto de rasgo? En realidad, ya no está permitido para ellos ... pero supongo que hay alguna razón por la que esto podría cambiar en el futuro). Eso hace que una solución de tiempo de compilación sea muy sencilla: para cualquier estructura que (de manera transitiva) posea directamente (no & mut, &, Box , o punteros sin procesar, ninguno de los cuales propagaría de forma transitiva el acceso de Pin forma segura , excepto & para el pin compartido, pero & no se puede quitar de todos modos, o si decidió optar por la solución "no se puede mover fuera de los objetos de rasgo", también podría verificar los campos de manera transitiva a lo largo de &mut y Box creo) y tiene acceso (en un sentido de visibilidad) a un tipo !Unpin conocido (incluido él mismo), tendría que implementar el segundo tipo de drop . Me parece que eso está totalmente bien y no es una pistola de compatibilidad con versiones anteriores, ya que no hay tipos estables !Unpin ; es posible que la gente tenga que volver a implementar los destructores después de actualizar una biblioteca, pero eso es todo, ¿verdad? Además, los detalles de implementación interna permanecerían internos; solo si un campo !Unpin estuviera expuesto habría algún problema. Finalmente, todos los tipos de contenedores genéricos (no solo Vec y cosas de la biblioteca estándar, sino esencialmente todo el ecosistema crates.io) continuarían funcionando bien. ¿Qué es lo catastrófico que me falta de esta solución?

(De hecho, me parece que incluso si no pudiera hacer cumplir esto en el momento de la definición de drop , al menos debería poder hacerlo en el momento de la instanciación de tipo, como lo hace dropck , ya que solo debe preocuparse por los tipos totalmente instanciados).

Releyendo la propuesta Dynsized : observo que un argumento en contra requería que los tipos inamovibles fueran siempre DynSized incluso antes de que se fijaran. Argumento anteriormente que realmente solo debemos preocuparnos por esto en el caso de los objetos de rasgo; podría ser posible (aunque difícil de justificar) imponer que forzar un tipo !Unpin a un rasgo requiera delimitarlo explícitamente con + ?DynSized (o lo que sea; podría hacerse automáticamente, supongo). Si bien puede haber muchos casos en los que se deba conocer el tamaño de los tipos !Unpin (de hecho, ¡ tengo un caso de uso así!), O deben poder intercambiarse antes de que se fijen, espero que haya muy pocos de esos casos para los interiores de objetos de rasgo hechos de tipos inamovibles (los únicos casos que tenemos ahora son para cosas como la conversión Box -> Rc que queremos prohibir explícitamente, ¿verdad? En realidad, ni siquiera me queda claro que la exposición de size_of_val sea ​​realmente un problema aquí o no, ya que todavía no sé si se espera que pueda convertir Pin<'a, Box<Trait> en Pin<'a, Trait> , pero si no puede, podemos confiar en Sized por supuesto). Uno realmente quiere poder unirlos con !Unpin en cualquier caso, pero como dije, creo que la gente quiere evitar agregar más rasgos negativos de los que ya necesitamos (personalmente, espero que !Unpin Los objetos de rasgo serán lo suficientemente raros y especializados que limitar los objetos de rasgo con ?Unpin lugar de Unpin sería totalmente razonable y no infectaría demasiado el sistema de tipos; la mayoría de los usos para !Unpin me ocurre que Pin<self> . Usualmente también querrá usarlos con PinBox o PinTypedArena o lo que sea, momento en el que los límites de ?Unpin parecen bastante naturales).

Tengo un nuevo diseño, que creo que no es tan horrible como el anterior: https://github.com/pythonesque/pintrusive/blob/master/src/main.rs. En lugar de intentar hacer que las proyecciones de pines funcionen en todas partes, este diseño pregunta cómo podemos interoperar con el código "heredado" de Rust que no sabe nada sobre la fijación, desde el código "moderno" de Rust que siempre quiere admitir la fijación. La respuesta obvia parece ser usar el rasgo PinDrop propuesto por @RalfJung , pero solo para los tipos que tienen una caída personalizada y desean proyectar campos.

Los tipos optan explícitamente por proyectar campos (correspondientes a derivar el rasgo PinFields ), que es análogo al código escrito en Rust "moderno"; sin embargo, no establece requisitos adicionales sobre el código en Rust "heredado", sino que opta por admitir solo la proyección a profundidad 1 para cualquier derivación dada de PinFields . Tampoco intenta mover Pin través de referencias, lo que creo que probablemente sea una buena idea no hacer de todos modos. Admite estructuras y enumeraciones, incluido cualquier análisis de disjunción que Rust debería poder proporcionar, mediante la generación de una estructura con campos y variantes idénticos, pero con los tipos reemplazados por Pin 'd referencias a los tipos (debería ser trivial para extender esto a Pin y PinMut cuando se realice ese cambio). Obviamente, esto no es ideal (aunque es de esperar que el optimizador pueda deshacerse de la mayor parte), pero tiene la ventaja de que funciona con préstamos y NLL y funciona fácilmente con enumeraciones (a diferencia de los accesos generados).

El argumento de seguridad funciona asegurando que Drop no se implemente para la estructura en absoluto, o asegurando que si Drop se implementa para ella, es una implementación trivial que solo llama a PinDrop (una versión de Drop que toma Pin). Creo que esto descarta todos los problemas de solidez con la proyección de campos anclados, con un signo de interrogación: mi principal preocupación es encontrar un buen argumento de por qué campos disjuntos en el mismo contenedor exacto (es decir, campos en profundidad 1) que podrían invalidar los de los demás pines en sus destructores, ya no serían válidos sin proyecciones de pines. Creo que puedo justificar esto si podemos demostrar que también podría hacer lo mismo si estuvieran en PinBoxes separados, lo que implica que el lugar donde viven es parte de su contrato de seguridad; eso significa que sus destructores no son seguros de forma aislada y construirlos fuera del módulo requeriría un código inseguro. En otras palabras, su corrección depende de la implementación del tipo de contenedor, lo que significa que debería estar bien pedir más a su destructor de lo que lo haríamos por un código seguro arbitrario.

@pythonesque Realmente no seguí lo que escribiste arriba sobre DynSize , pero supongo que ahora todo está desactualizado. Entonces, solo voy a comentar tu última publicación.

Para resumir, está diciendo que proyectar en un campo de una estructura / enumeración (incluidos los campos pub ) no es seguro en general , pero un tipo puede optar por las proyecciones de campo seguro al no implementar Drop . Si el tipo quiere un destructor, tiene que PinDrop lugar de Drop :

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

Ya tenemos typecheck busque Drop para rechazar el movimiento fuera de un campo, por lo que no parece poco realista verificar también Drop para rechazar la proyección a través de &pin . Por supuesto, la verificación de "salir del campo" aún se rechazaría si hay un PinDrop , mientras que la proyección se permitiría en ese caso.

El compilador aplicaría las mismas restricciones a PinDrop que aplica a Drop , además de garantizar que un tipo no implemente tanto Drop como PinDrop . Al generar drop pegamento, llama a cualquier tipo de Drop que el tipo haya implementado.

¿Eso resume la propuesta? La parte que no entiendo es tu último párrafo, ¿podrías dar un ejemplo que demuestre lo que te preocupa?


Ahora, por supuesto, me pregunto cuáles son las obligaciones de prueba aquí. Creo que la forma más fácil de ver esto es decir que PinDrop es en realidad el principal y único tipo de destructor que existe formalmente, y impl Drop for T es en realidad azúcar sintáctico por impl PinDrop que llama al método inseguro PinMut::get_mut y luego llama a Drop::drop . Este es un código inseguro generado automáticamente, sin embargo , debido a que la fijación es una "extensión local" (es decir, es compatible con el código inseguro existente), ese código inseguro siempre es seguro si el tipo dado no se preocupa por la fijación.

Hablando un poco más formalmente, existe un "invariante de fijación predeterminado" que los tipos tienen si a su autor no le importa la fijación. Los tipos que utilizan ese invariante de fijación predeterminado son automáticamente Unpin . Escribir impl Drop para un tipo que tiene un invariante personalizado afirma que el tipo usa el "invariante de fijación predeterminado". Esto es un poco sospechoso, ya que se siente un poco como si hubiera una obligación de prueba aquí, pero no hay unsafe para advertir sobre esto, y de hecho esto no es perfecto, pero la compatibilidad con versiones anteriores es importante, así que, ¿qué puedes hacer? hacer. Tampoco es una catástrofe, porque se puede argumentar que lo que esto realmente significa es que cambia la obligación de prueba en la que se incurre con el código inseguro en otro lugar, hasta el punto de que este código inseguro debe funcionar con el "invariante de fijación predeterminado". Si no hay ningún código inseguro en otra parte de este módulo, todo está bien.

Incluso podría imaginar que podríamos agregar una pelusa contra impl Drop for T menos que T: Unpin , tal vez restringido al caso en el que el módulo que define el tipo tiene código inseguro. Ese sería un lugar para educar a las personas sobre el problema y animarlas a unsafe impl Unpin (afirmando formalmente que usan el invariante de fijación predeterminado), o bien impl PinDrop .

@RalfJung

¿Eso resume la propuesta?

Sí, más o menos (creo que su propuesta es en realidad significativamente más ambiciosa que la mía, ya que parece proponer la realización automática de proyecciones si Drop no se implementa, lo que creo que probablemente sea un problema de compatibilidad con versiones posteriores para las bibliotecas; pero tal vez haya alguna forma de evitarlo. ese).

La parte que no entiendo es tu último párrafo, ¿podrías dar un ejemplo que demuestre lo que te preocupa?

En términos generales: me preocupan dos campos que viven directamente en la misma estructura, uno de los cuales muta al otro cuando se llama a su Drop (posiblemente confiando en cosas como el orden de drop por seguridad) de una manera que viola la fijación de invariantes del otro campo, pero conserva su integridad estructural (lo que le permite presenciar violaciones del invariante de fijación). Obviamente, esto no puede usar código completamente seguro (o de lo contrario sería rechazado por dropck, entre otras cosas), por lo que mi preocupación es solo sobre un caso hipotético en el que los destructores en los tipos de los campos eran seguros para ejecutarse cuando los campos se anclan por separado estructuras, pero no seguras cuando están ancladas en la misma estructura. Mi esperanza es que no existan tales casos a menos que haya un invariante compartido que incluya la estructura en la que están contenidos los campos; si existe tal invariante, sabemos que debe ser consciente de que sus componentes no respetan adecuadamente el invariante personalizado y, por lo tanto, podemos culpar al código en algún lugar del módulo.

Creo que la forma más fácil de ver esto es decir que PinDrop es en realidad el principal y único tipo de destructor que existe formalmente, e impl Drop for T es en realidad azúcar sintáctico para impl PinDrop que llama al método inseguro PinMut :: get_mut y luego llama Soltar :: soltar.

Convenido. Desafortunadamente, esta vía no estaba abierta a la solución actual, ya que intenta hacer cosas en una biblioteca.

Hablando un poco más formalmente, existe un "invariante de fijación predeterminado" que los tipos tienen si a su autor no le importa la fijación. Los tipos que utilizan ese invariante de fijación predeterminado son Desanclar automáticamente. Escribir impl Drop para un tipo que tiene un invariante personalizado afirma que el tipo usa el "invariante de fijación predeterminado". Esto es un poco sospechoso, ya que se siente un poco como si hubiera una obligación de prueba aquí, pero no es seguro advertir sobre esto, y de hecho esto no es perfecto, pero la compatibilidad con versiones anteriores es importante, así que, ¿qué puede hacer?

Sí, esto es más o menos lo que tengo en mente ... Solo me preocupa no tener una buena intuición de lo que significaría realmente formalizar esta garantía de "código inseguro en el módulo". Me gusta tu idea de una pelusa (¡me gusta cualquier cosa que haga que más personas usen PinDrop, de hecho!) Pero creo que unsafe impl Unpin probablemente estaría mal con demasiada frecuencia para que sea bueno sugerirlo, en al menos para tipos con campos públicos genéricos (pero, de nuevo, para estructuras que no tienen tales campos, en realidad sería cierto con bastante frecuencia ... es en gran parte cierto para la biblioteca estándar, por ejemplo).

Escribí un ejemplo de cómo podría funcionar #[derive(PinnedFields)] : https://github.com/withoutboats/derive_pinned

He visto a personas afirmar que derivaciones como esta "no son sólidas" pero afaik que no es cierto. Necesitaría usar código inseguro para hacer algo que entraría en conflicto con el código generado por este derivado, es decir, esto hace que otro código inseguro sea incorrecto (y creo que el código, moviéndose alrededor de ?Unpin datos, es algo que puede / debe evitar siempre).

EDITAR: Está bien, en realidad lea las últimas publicaciones sobre destructores. Procesará.

@withoutboats Sí, creo que ya has visto esto, pero el problema era que el código inseguro en un tipo !Unpin correcto podía ser invalidado por un destructor seguro en un tipo que derivaba PinFields (entonces no había ningún código inseguro en el módulo para el tipo que derivó PinFields excepto por las cosas autogeneradas). Esa es la parte problemática. Sin embargo, eche un vistazo al diseño que vinculé (ignorando la elección estilística de crear una estructura separada en lugar de derivar accesos individuales; solo estaba yo tratando de que admita tantos casos de uso como sea posible con el verificador de préstamos). Estuve preocupado por un tiempo, pero ahora estoy bastante seguro de que #[derive(PinFields)] aún puede funcionar, solo requiere asegurarse de que Drop no se implemente directamente.

También quiero mencionar otro punto en el que he estado pensando (¿que aún no he visto resuelto directamente en ninguna parte?): Creo que algo que haría que Pin mucho más utilizable y se integre mejor en el código existente, sería estar firmemente del lado de que esencialmente todos los tipos de punteros sean Unpin . Es decir, hacer que &mut T , &T , *const T , *mut T , Box<T> , y así sucesivamente se consideren Unpin por cualquier T . Si bien puede parecer sospechoso permitir que algo como, digamos, Box<T> sea Unpin , tiene sentido si considera que no puede obtener un Pin en el interior de el Box de un Pin<Box<T>> . Creo que esto solo permitir que !Unpin infecte cosas que están "en línea" es un enfoque muy razonable: no tengo un solo caso de uso para permitir que la fijación se convierta en viral en todas las referencias, y hace que la semántica de cualquier tipo &pin eventual es muy agradable (elaboré una tabla sobre cómo interactuaría con los otros tipos de punteros en ese escenario, y esencialmente si ignora los movimientos hace que &mut pin actúe igual que &mut , &pin actúan igual que & , y box pin actúan igual que box con respecto a la relación con otros indicadores). Tampoco es importante desde el punto de vista operativo, hasta donde yo sé: en general, mover un valor de tipo A contiene un puntero a un valor de tipo B no mueve el valor apuntado de escriba B , a menos que el valor del tipo B esté en línea en el valor del tipo A pero si ese es el caso, entonces A es automáticamente !Unpin ya que contiene B sin una indirección de puntero. Quizás lo más importante es que significaría que un gran porcentaje de los tipos que actualmente necesitarían una implementación manual insegura de !Unpin no necesitarían una, ya que la mayoría de las colecciones solo contienen T detrás de una indirección de puntero . Eso permitiría que el actual Drop continúe funcionando, y permitiría que estos tipos implementen PinDrop sin cambiar su semántica (ya que si un tipo es Unpin puede tratar un Pin 'd argumento como &mut todos modos).

Puede que me esté perdiendo alguna razón por la que este tipo de fijación transitiva sería una buena idea, pero hasta ahora no he encontrado ninguna. Lo único operativo que podría preocuparme es si realmente es posible implementar este comportamiento con rasgos automáticos; creo que probablemente lo haría, pero tal vez en algunos casos en los que las personas usan PhantomData<T> pero en realidad tienen un puntero a T , sería bueno que lo cambiaran a PhantomData<Box<T>> . Por lo general, pensamos que son exactamente iguales semánticamente, pero con la fijación no es del todo cierto.

@pythonesque La terminología de "un nuevo idioma" y tal es una especie de novatada para mí. Mi impresión de lo que haces es:

  • De forma predeterminada, #[derive(PinFields)] genera una Drop implícita sin operación. Esto garantiza que nunca accederá al campo anclado en el destructor.
  • Un atributo opcional cambia este Drop impl para llamar a PinDrop::pin_drop , y se supone que debes implementar PinDrop .

¿Es esto correcto?


También creo que todo esto solo importaba si ampliamos las garantías de Pin para admitir colecciones intrusivas. ¿Es esto consistente con su comprensión de @RalfJung y @pythonesque?


Todo esto es bastante frustrante, porque lo que parece claro es que Drop debería tomarse a sí mismo por Pin ! Hacer un cambio más radical (posiblemente de época) parece atractivo, pero no veo una forma que no sea enormemente disruptiva.

A continuación, se muestra un ejemplo del tipo de error que puede causar el acceso a los campos fijados + drop: https://play.rust-lang.org/?gist=8e17d664a5285e941fe1565ce0eca1ea&version=nightly&mode=debug

El tipo Foo pasa un búfer interno a algún tipo de API externa que requiere que el búfer permanezca donde está hasta que se desvincule explícitamente. Hasta donde yo sé, esto es sólido bajo las restricciones que propuso @cramertj , después de que se Pin<Foo> tiene la garantía de que no se moverá hasta después de que se haya llamado a Drop (con con la condición de que, en cambio, podría filtrarse y nunca se llamará a Drop , pero en ese caso se le garantiza que nunca se moverá).

El tipo Bar luego rompe esto moviendo el Foo durante su implementación Drop .

Estoy usando una estructura muy similar a Foo para admitir un periférico de radio que se comunica a través de DMA, puedo tener un StableStream con un búfer interno en el que se escribe la radio.

@sin barcos

¿Es esto correcto?

Sí, excepto que en realidad no genera una Drop impl (porque los tipos que no implementan Drop en Rust funcionan mejor en general). En su lugar, intenta afirmar que Drop no se ha implementado utilizando algunas características de biblioteca sospechosas (funciona en estable pero se rompe en la especialización; creo que hay una variante que debería funcionar en la especialización, pero no en este momento porque de problemas con constantes asociadas). Si se convirtiera en una característica del idioma, sería bastante fácil hacer cumplir esto.

También creo que todo esto solo importaba si ampliamos las garantías de Pin para admitir colecciones intrusivas. ¿Es esto consistente con su comprensión de @ralfj y @pythonesque?

No, lamentablemente ese no es el caso. El contraejemplo vinculado anteriormente no tiene nada que ver con las garantías adicionales para colecciones intrusivas. Un tipo Pin 'd ya tiene que ser capaz de asumir que no se moverá antes de que se vuelva a usar incluso sin esa garantía, ya que si un método se llama dos veces en un valor desde detrás de una referencia fijada, el valor tiene no hay forma de saber si se movió entre las dos llamadas. Las garantías adicionales que se necesitan para que sea útil para las colecciones intrusivos añadir una cosa adicional de tener que llamar a drop antes de que se liberó a la memoria, pero incluso sin esa garantía drop todavía pueden ser llamados en algo que está anclado actualmente (detrás de un PinBox, por ejemplo). Si lo que se soltó incluye campos en línea, y permitimos proyectar el pin de lo que se colocó en esos campos, entonces el destructor del tipo externo aún puede mover el campo interno, anclarlo nuevamente (colocando el elemento movido fuera valor de campo en un PinBox , por ejemplo), y luego llamar a métodos en él que esperan referencias de cuando se fijó antes para que sigan siendo válidos. Realmente no veo ninguna forma de evitarlo; Siempre que pueda implementar drop , puede tener este problema para cualquier campo !Unpin . Es por eso que creo que la solución menos mala es no permitir que las personas implementen drop si quieren que el anclaje funcione correctamente.

Todo esto es bastante frustrante, porque lo que parece claro es que ¡Drop debería tomarse yo mismo por Pin! Hacer un cambio más radical (posiblemente de época) parece atractivo, pero no veo una forma que no sea enormemente disruptiva.

Sí, realmente es molesto ... Estuve muy malhumorado por eso durante una semana. Pero como señaló Ralf, hubiéramos tenido que esperar tres años más para obtener la versión 1.0 antes de que alguien se diera cuenta de esto ... y siempre habrá algo más.

Si lo que se soltó incluye campos en línea, y permitimos proyectar el pin de lo que se colocó en esos campos, entonces el destructor del tipo externo aún puede mover el campo interno y luego llamar a métodos en él que esperan referencias de cuando se fijó para seguir siendo válido.

La parte enfatizada me parece muy importante; de hecho, parece ser el meollo del problema.

Inicialmente imaginé este código, que usa nuestro único método que tenemos que realmente se preocupa por las direcciones internas:

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() }
     }
}

¡Pero implica un código inseguro para volver al Pin ! Ese código debería considerarse poco sólido.

¿Podemos descartar la capacidad de los métodos que requieren &self y &mut self para depender de la validez de las direcciones internas para su solidez? ¿Cómo se ve eso?

@withoutboats Incluso si solo los métodos que toman Pin<Self> dependen de la validez de las direcciones internas para su solidez, aún puede tener el problema. El destructor del tipo externo puede mem::replace el campo del tipo interno (usando su propia referencia &mut self ), luego PinBox::new el valor, y luego llamar a un método fijo en él . No se requiere seguridad. La única razón por la que no es un problema sin poder proyectar campos anclados es que se considera aceptable para un tipo que realmente implementa métodos !Unpin extraños usando unsafe (o comparte un invariante con un tipo que no ) para tener obligaciones de prueba en su implementación drop incluso si solo usa código seguro allí. Pero no puede preguntar eso a los tipos que simplemente contienen un tipo que es !Unpin y no tienen un código inseguro propio.

@pythonesque

Creo que su propuesta es en realidad significativamente más ambiciosa que la mía, ya que parece proponer la realización automática de proyecciones si no se implementa Drop, que creo que probablemente es un problema de compatibilidad con versiones posteriores para las bibliotecas; pero tal vez haya alguna forma de evitarlo

Bueno, creo que queremos hacerlo eventualmente. ¿Cómo es eso un problema de compatibilidad?

Me preocupan dos campos que viven directamente en la misma estructura, uno de los cuales muta al otro cuando se llama a su Drop (posiblemente dependiendo de cosas como el orden de drop por seguridad) de una manera que viola la fijación de invariantes del otro campo, pero conserva su integridad estructural (lo que le permite presenciar violaciones del invariante de fijación).

Pero eso ya sería ilegal actualmente. No debes violar las invariantes de otros.

pero creo que la implícita insegura probablemente estaría mal con demasiada frecuencia para que sea una buena sugerencia, al menos para tipos con campos públicos genéricos

Creo que en realidad será correcto la mayor parte del tiempo; espero que la mayoría de las personas no proporcionen ningún acceso por Pin<Self> , y en ese caso es probable que estén usando el invariante de fijación predeterminado y, por lo tanto, está bien para unsafe impl Unpin por ellos.

Creo que algo que haría que Pin sea mucho más utilizable y que se integre mejor en el código existente, sería estar firmemente del lado de que esencialmente todos los tipos de punteros sean Despin. Es decir, haciendo & mut T, & T, * const T, * mut T, Box, y así sucesivamente, todos se considerarán Desanclar para cualquier T. Si bien puede parecer sospechoso permitir algo como, por ejemplo, Boxpara ser Unpin, tiene sentido cuando considera que no puede sacar un Pin al interior de la Caja de un Pin>.

Estoy de acuerdo en que esto probablemente sucederá (vea también mi comentario en https://github.com/rust-lang/rust/pull/49621#issuecomment-378286959). Actualmente creo que deberíamos esperar un poco hasta que nos sintamos más confiados en todo el asunto de la fijación, pero de hecho veo muy poco sentido en imponer la fijación más allá de las indirecciones del puntero.
Sin embargo, no estoy seguro de cómo me siento al hacer esto con los punteros en bruto, generalmente somos extremadamente conservadores sobre los rasgos automáticos para ellos, ya que se usan de muchas maneras locas.


@sin barcos

Escribí un ejemplo de cómo podría funcionar # [derive (PinnedFields)]: https://github.com/withoutboats/derive_pinned

@pythonesque ya dijo esto, esto pero solo para ser claro: esa biblioteca no es sólida. Al usarlo con el caída de @MicahChalmer , puedo romper cualquier tipo de

@RalfJung

Esa biblioteca no es sólida.

Para aclarar, la derivación es solo unsafe , y no se puede usar en combinación con una Drop implícita manual. Al usar la derivación, promete no hacer ninguna de las cosas malas enumeradas en una implementación de Drop .

https://github.com/rust-lang/rust/pull/50497 cambia la API de fijación, lo más importante es que cambia el nombre de Pin a PinMut para dejar espacio para agregar un Pin compartido


Para aclarar, la derivación es simplemente insegura y no se puede usar en combinación con una impl.

De acuerdo, hacerlo inseguro de esa manera funcionaría. ¿Aunque la prueba hace que parezca que actualmente no está marcado como inseguro?

@RalfJung Correcto, no creo que lo sea en este momento. Otra opción que acabo de pensar sería hacer que la versión "segura" cree una implementación de Drop para el tipo, bloqueando otras implicaciones manuales de Drop . Podría haber una bandera unsafe para desactivar esto. WDYT?

@cramertj sí, eso también debería funcionar. Sin embargo, tendría efectos secundarios como dropck más restrictivo y no poder moverse fuera de los campos.

@pythonesque y yo tuvimos una llamada el lunes para discutir esto. He aquí un resumen.

Concluimos que probablemente el comportamiento "correcto" hubiera sido que Drop tomaran uno mismo por pin. Sin embargo, la transición a eso es abrumadora. Aunque creo que es posible con un cambio de edición de alguna forma, esto sería extremadamente perturbador.

Un cambio compatible con versiones anteriores es simplemente hacerlo de alguna manera incoherente para los tipos que se pueden "proyectar con pin" para implementar Drop . En su repositorio, @pythonesque ha implementado esto generando implicaciones a partir de un intrincado conjunto de rasgos.

Uno podría imaginar un formulario integrado que es un poco más simple:

  • Un rasgo de marcador, llamémoslo PinProjection , controla si un tipo se puede proyectar con pin o no. Es incoherente (a través de la magia incorporada del compilador) implementar tanto PinProjection como Drop .
  • Otro rasgo, PinDrop , extiende PinProjection para proporcionar un destructor alternativo a Drop .

Las derivadas para generar métodos de proyección de pines como las que @pythonesque y yo hemos mostrado también generarán implicaciones de PinProjection para el tipo.

@sin barcos

Llegamos a la conclusión de que probablemente el comportamiento "correcto" hubiera sido que Drop se tomara a sí mismo por alfiler. Sin embargo, la transición a eso es abrumadora. Aunque creo que es posible con un cambio de edición de alguna forma, esto sería extremadamente perturbador.

Solo me preguntaba, SI quisiéramos hacer algo así algún día, ¿lo que propones es compatible con el futuro?

Por ejemplo, digamos que decidimos ...

  • hacer que Drop acepte mágicamente tanto formularios fijados como no fijados, O
  • reescriba las firmas Drop de todos para ellos como parte de la actualización automática a Rust 2021 :)

(No propongo ninguno de estos, pero parece difícil responder la pregunta sin ejemplos concretos).

@tmandry Esa es esencialmente la idea.

El gran problema es una implementación genérica Drop que mueve el parámetro de tipo:

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

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

El tipo de actualización automática de la que está hablando rompería esta implicación de caída, que solo es válida si agregamos un límite que T: Unpin .

No conozco ningún caso de uso válido para mover realmente ?Unpin datos en un destructor, por lo que es solo este caso, en el que técnicamente podría, pero no está destinado a hacerlo, eso es realmente un problema.

@sin barcos

Otro rasgo, PinDrop, extiende PinProjection para proporcionar un destructor alternativo a Drop.

¿Por qué PinDrop extiende PinProjection ? Eso parece innecesario.

Además, esto podría hacerse un poco más inteligente diciendo que PinProjection y Drop son incompatibles a menos que el tipo sea Unpin (o encontrando alguna otra forma de eliminar todas estas nuevas distinciones / restricciones por Unpin tipos).

@RalfJung queremos que PinDrop y Drop sean mutuamente excluyentes de alguna manera. En última instancia, hay algunas formas de implementar esto con diferentes compensaciones; Creo que necesitamos un RFC para ello independientemente porque no es un cambio pequeño.

@withoutboats De acuerdo. Pensé que la exclusividad podría surgir de un tratamiento especial de PinDrop , pero obviamente hay varias formas de hacerlo. Creo que sería muy útil si los tipos Unpin no tuvieran que preocuparse; junto con hacer que todos los tipos de punteros sean Unpin incondicionalmente, esto probablemente ayudará a reducir el número de casos en los que la gente tiene que saberlo.

@withoutboats También vale la pena señalar que las instancias principales en las que puedo pensar dónde la gente quiere, digamos, mover datos genéricos en un Vec en un destructor (elegí Vec porque a la gente del IIRC le gustaría implementar métodos en Vec que realmente se preocupan por Pin , lo que significa que no podría implementar incondicionalmente Unpin ), en realidad es un &mut Vec<T> o Vec<&mut T> o algo así. Lo menciono principalmente porque estos son casos que probablemente se tropezarían con la pelusa que sugirió PinDrop fácilmente en esos casos en lugar de unsafe impl Unpin (en teoría, siempre pueden hacer lo primero, por supuesto, limitando T con Unpin en el tipo, pero eso será un cambio importante para los clientes de la biblioteca).

Además, vale la pena señalar que si bien esto agrega algunos rasgos más de los que nos gustaría, los rasgos son los que casi nunca aparecerán en la firma de alguien. En particular, PinProjection es un límite completamente inútil a menos que esté escribiendo una macro #[derive(PinFields)] porque el compilador siempre (creo) podrá determinar si PinProjection mantiene si puede averigüe cuáles son los campos en el tipo, y lo único para lo que es útil es proyectar campos. De manera similar, PinDrop básicamente nunca debería necesitar ser un límite para nada, por la misma razón que Drop casi nunca se usa como límite. Incluso los objetos de rasgos no deberían verse afectados en gran medida (a menos que algún día obtengamos "campos asociados", supongo, pero con una nueva característica como esa podríamos exigir que los rasgos con campos asociados solo se pudieran implementar en los tipos PinProjection , lo que resolvería perfectamente ese problema).

@RalfJung

Bueno, creo que queremos hacerlo eventualmente. ¿Cómo es eso un problema de compatibilidad?

Supongo que no es más de uno que agregar un tipo que desactive Send o Sync , la diferencia es que ninguno de ellos afecta el lenguaje central en la misma medida que esto. Hacerlo de forma completamente automática parece análogo a cómo Rust solía tratar Copy (los tipos eran siempre Copy si solo contenían Copy tipos y no implementaban Drop ) que finalmente se cambió para hacerlo explícito porque, presumiblemente, a la gente no le gustó el hecho de que agregar un rasgo ( Drop ) podría romper el código genérico sin que se dieran cuenta (ya que el punto de coherencia es que adicional las implementaciones de rasgos no deberían romper las cajas posteriores). Esto parece casi idéntico, solo con PinProjection lugar de Copy . De hecho, me gustó el comportamiento anterior, solo creo que sería difícil justificar que PinProjection funcione de esta manera cuando Copy no lo hace.

Pero eso ya sería ilegal actualmente. No debes violar las invariantes de otros.

Sí, cuanto más pienso en esto, menos plausible parece que pueda ser un problema.

Creo que en realidad será correcto la mayor parte del tiempo.

Bueno, sí, pero solo porque la mayoría de los tipos no implementan ningún método que requiera Pin o exponga sus campos genéricos como públicos. Si bien es poco probable que el último cambie, el primero no lo es; espero que al menos algunos de los tipos de stdlib que actualmente son !Unpin agreguen métodos de proyección de pines, en cuyo punto la implementación de unsafe no ya no será válido. Así que me parece algo bastante frágil. Además, me preocupa aumentar la cantidad de código inseguro "repetitivo" que la gente tiene que escribir; Creo que los límites de Send y Sync son unsafe impl 'd correctamente tan a menudo como se implementan incorrectamente, y Unpin sería aún más insidioso porque el normalmente, la versión correcta no tiene límites en T . Por lo tanto, parece muy preferible orientar a las personas hacia PinDrop (aunque entiendo por qué desconfías de hacerlo con tipos de punteros sin procesar. Me preocupa que ya- unsafe código ser aún más probable que haga un unsafe impl sin pensar, pero cuanto más lo pienso, más parece que el valor predeterminado para los punteros en bruto es probablemente correcto y marcarlo con su lint sería útil).

Creo que sería muy útil si los tipos Unpin no tuvieran que preocuparse.

Estoy de acuerdo, pero como dije, la gente no usará PinProjection como límite real, no estoy seguro de cuánto importaría en la práctica. Ya existe una implementación de DerefMut para PinMut donde T: Unpin por lo que hoy no obtendría ningún beneficio. Una regla implementada en el compilador (para proyecciones) presumiblemente convertiría PinMut::new en una especie de &pin reborrow por Unpin tipos, pero eso no tiene nada que ver con las proyecciones de campo. Y dado que derivar PinProjection no requiere PinProjection para sus campos, no lo necesitaría solo para satisfacer los límites en un derive en otro tipo. Entonces, realmente, ¿cuál es la ganancia? Lo único que realmente le permitiría hacer es implementar Drop y derivar PinProjections al mismo tiempo, pero siempre queremos que la gente implemente PinDrop si es posible de todos modos, de modo que ser una OMI neta negativa.

Elegí Vec porque a la gente del IIRC le gustaría implementar métodos en Vec que realmente se preocupan por Pin, lo que significa que no podría implementar incondicionalmente Unpin

Hm, no creo que me guste eso. Si Box es incondicionalmente Unpin , creo que Vec debería ser el mismo. Los dos tipos suelen ser bastante equivalentes en cuanto a propiedad.

También debemos tener cuidado con la garantía drop ; Vec::drain por ejemplo, puede provocar que se filtren cosas en caso de pánico.

Esto parece casi idéntico, solo con PinProjection en lugar de Copy

Oh, ahora entiendo tu pregunta. En realidad, no estaba hablando de derivar automáticamente PinProjections ya que mi propuesta no tenía un rasgo como ese, pero de hecho una consecuencia de mi propuesta sería que agregar Drop es un cambio radical si tener campos públicos.

Lo único que realmente le permitiría hacer es implementar Drop y derivar PinProjections al mismo tiempo, pero siempre queremos que las personas implementen PinDrop si es posible de todos modos, por lo que sería una OMI netamente negativa.

Ese era realmente el punto. Mientras menos personas tengan que preocuparse por todas estas cosas de fijar, mejor.

Pregunta de aclaración: en su propuesta y en la de @withoutboats , es PinDrop un elemento de idioma y el compilador lo trata como un reemplazo de Drop , o es solo el rasgo utilizado por derive(PinProjections) para implementar Drop ?

También tenemos que tener cuidado con la garantía de caída; Vec :: Drain, por ejemplo, puede provocar que se filtren cosas en caso de pánico.

Bueno, eso es cierto, pero en realidad no desasigna ningún recuerdo, ¿correcto? Para que quede claro, yo realmente preferiría si Vec implementó incondicionalmente Unpin , ya que haría más fácil para las personas a parámetro sólo por PinDrop , pero no había duda hablar de parte de la discusión sobre la fijación de elementos de respaldo. Una vez que comienzas a hablar sobre los campos que se están anclando, queda claro que no siempre puedes convertir el Vec en un Box<[T]> en su lugar y luego fijarlo, por lo que en realidad podría tener algún valor en ese punto (aunque obviamente también podría agregar un tipo PinVec lugar de un tipo Vec , como se hizo para Box ).

Ese era realmente el punto. Mientras menos personas tengan que preocuparse por todas estas cosas de fijar, mejor.

Hm, desde mi perspectiva, eso sería cierto inicialmente, pero a largo plazo querríamos migrar a la mayor cantidad de personas posible a un valor predeterminado de PinDrop , especialmente si el tipo realmente molesta a #[derive(PinProjections)] (que, de nuevo, ni siquiera sería necesario para ningún propósito si el tipo fuera Unpin probablemente solo sucedería en cosas como el código generado). Entonces (tal vez después de que &pin haya estado en el idioma por un tiempo) podría tener un cambio de época que cambiara Drop a DeprecatedDrop o algo así. Para los tipos Unpin simplemente cambiar la firma de tipo de &mut a &mut pin prácticamente resolvería todo, así que tal vez esto no sea realmente necesario.

Pregunta de aclaración: en su propuesta y en la de @withoutboats , ¿PinDrop es un elemento lang y el compilador lo trata como un reemplazo de Drop, o es solo el rasgo utilizado por derive (PinProjections) para implementar Drop?

El primero.

Toda esta discusión de Drop parece un descuido bastante drástico en nombre del RFC original. ¿Sería valioso abrir un nuevo RFC para discutir esto? Es algo difícil entender cuáles son las preocupaciones, qué preocupaciones son más perniciosas que otras, exactamente cuánta más maquinaria vamos a necesitar agregar al lenguaje de lo que pensamos originalmente, cuántas roturas debemos esperar (y si puede ser mitigado por la edición) en el peor y mejor escenario de casos, cómo podríamos hacer la transición a cualquier cosa que tenga éxito en Drop, y si podríamos apostar en todo esto y aún cumplir con nuestros objetivos para 2018 a través de otros medios (como https: //github.com/rust-lang/rfcs/pull/2418 sugiere ocultar Pin de todas las API públicas para no bloquear la estabilización).

@bstrie Creo que solo hay una preocupación importante, pero es bastante sustancial. El RFC fue aprobado con el entendimiento de que un eventual tipo &pin mut podría en algún momento hacer que este tipo de código sea seguro:

struct Foo<T> { foo : T }

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

Es importante que este tipo de código sea seguro porque, en la práctica, las referencias ancladas no se pueden componer de otra manera en código seguro, por lo que, por ejemplo, implementar cualquiera de los combinadores de futuros requeriría el uso de unsafe . En ese momento, se pensó que el uso de unsafe podría eliminarse en la mayoría de los casos y, por lo tanto, no sería un gran problema.

Desafortunadamente, si esto es seguro depende de la implementación de Foo Drop para Foo . Entonces, si queremos admitir proyecciones de campo seguras (ya sea a través de una función de idioma, un #[derive] o cualquier otro mecanismo), tenemos que poder garantizar que esas implementaciones de Drop tan malas puedan ' no suceda.

La alternativa que parece funcionar mejor permite que el código anterior funcione sin el uso de unsafe (con solo la adición de #[derive(PinProjections)] en la parte superior de la estructura) y no rompe ningún código existente . Se puede agregar de manera compatible con versiones anteriores incluso si Pin ya está estabilizado e incluso se puede agregar como una biblioteca pura (con un impacto ergonómico severo). Es compatible tanto con las macros #[derive] para generar accesos como con un eventual tipo de referencia nativo &pin . Si bien requiere agregar al menos un rasgo nuevo (y posiblemente dos, dependiendo de cómo se implemente la versión anclada de Drop ), nunca es necesario que aparezcan en ninguna parte de las cláusulas u objetos de rasgo where , y la nueva versión de drop solo debe implementarse para los tipos que actualmente tienen una implementación de Drop y desean optar por las proyecciones de pines.

El hecho de que una solución parezca que no tiene muchas desventajas no significa que no sea un cambio sustancial, por lo que creo que es casi seguro que crearemos un RFC para esta propuesta ( @withoutboats y yo discutimos hacer esto en la llamada) . Estaría bien apuntarlo de forma aislada, ya que es totalmente compatible con versiones anteriores. Personalmente, sin embargo, creo que tiene más sentido impulsar este cambio en lugar de deshacerse de la fijación en otro lugar.

La preocupación de la mayoría de la gente con PinMut en las API públicas es precisamente que requerirá unsafe o pasar por los límites Unpin todas partes, y esta propuesta resuelve ese problema. Las alternativas discutidas en óxido-lang / RFC # 2418 parecen mucho más controvertido, tanto en la mecánica real de la forma en que quiere evitar el trato con la fijación (que implica la proliferación de varias otras características que aparecerán en API públicas y documentación) y en la complejidad general de la solución. De hecho, incluso si la fijación se resolviera por completo, creo que hay una serie de preguntas que la gente no cree que se hayan resuelto adecuadamente con ese RFC, por lo que creo que hay al menos una posibilidad decente de que una RFC que agregue proyecciones de clavijas seguras pueda terminar siendo aceptado ante él.

Es cierto que la fijación está en sus inicios (y sé que me quejé de que parecía que se estaba estabilizando demasiado rápido), pero creo que la falta de proyección de campo segura es la última cosa importante que desalienta a las personas a usarla. . En aras de la exhaustividad, aquí están todos los problemas que he visto a la gente plantear con el anclaje, sus soluciones propuestas y si la solución es compatible con el tipo existente Pin (en un orden muy aproximado y sesgado de cómo controvertido, percibo que el problema es en este momento):

  • Este (¿podemos hacer que los campos de proyección se realicen en código seguro?). Se resolverá en el próximo RFC (donde se detallarán las posibles estrategias de implementación, así como otras alternativas que consideramos y por qué tuvieron que descartarse). Todas las variaciones de la resolución propuesta son compatibles con versiones anteriores.
  • ¿Fijar un valor de tipo !Unpin con un destructor manual implica una garantía adicional (de que la memoria de respaldo del valor no se invalida hasta después de que se llama al destructor), también conocida como "cambios para colecciones intrusivas"?

    Aún sin resolver, principalmente porque podría romper la API de fijación de pila propuesta; si se puede hacer que una API de fijación de pila que conserve esta garantía funcione con async / await, la mayoría de las partes interesadas parecen estar dispuestas a aceptar esto (IIRC, alguien ya intentó resolver esto usando generadores pero rustc ICE).

    No retrocompatible con las antiguas garantías; requerir un código inseguro para hacer cumplir la garantía por ahora es compatible con ambos resultados posibles, pero solo si el código inseguro no puede depender de que se aplique para su corrección. Por lo tanto, esto ciertamente requiere resolución de una forma u otra antes de que se estabilice la fijación.

    Afortunadamente, los usuarios de fijación por Future s pueden ignorar esto además de la forma de la API de fijación de pila. La API de fijación de pila basada en cierres es compatible con cualquier resolución, por lo que los usuarios de Future que no usen async / await pueden usar la API basada en cierres hoy sin esperar a que se decida. La decisión afecta más a las personas que desean usar la fijación para otros fines (como colecciones intrusivas).

  • ¿Deberían los punteros en bruto ser incondicionalmente Unpin ? Aún sin resolver (creo que soy el único que lo propuso y todavía tengo un 50-50 en él). No sería compatible con versiones anteriores; Estoy marcando esto como controvertido principalmente por esa razón.
  • ¿Deberían los tipos de biblioteca estándar como Vec hacerse incondicionalmente Unpin , o deberían agregarse los accesos de campo Pin ? Aún no se ha resuelto y es posible que necesite una resolución caso por caso. Para cualquier tipo de contenedor seguro dado, agregar los accesores o hacer que el tipo sea incondicionalmente Unpin es compatible con versiones anteriores.
  • ¿Deben eliminarse PinMut::deref ? Básicamente, la respuesta parece ser "no", ya que las ventajas de mantenerlo parecen superar con creces las desventajas, y parece haber soluciones para los casos que originalmente hicieron que la gente lo quisiera (específicamente, Pin<RefCell<T>> ). Cambiarlo sería incompatible al revés.
  • ¿Cómo deberíamos proporcionar accesos de campo a corto plazo (ignorando los problemas de caída)? Aún sin resolver: dos opciones presentadas hasta ahora son https://github.com/withoutboats/derive_pinned y https://github.com/pythonesque/pintrusive. La resolución es totalmente compatible con versiones anteriores, ya que después de los cambios de caída, esto se puede hacer correctamente en una macro puramente en el código de la biblioteca.
  • ¿Cómo deberíamos proporcionar accesos de campo a largo plazo (es decir, debería haber tipos personalizados &mut pin y &pin Rust? ¿Cómo debería funcionar el représtamo?). Aún sin resolver, pero compatible con todos los demás cambios propuestos (a mi leal saber y entender) y, obviamente, se puede puntuar indefinidamente.
  • ¿Deberían los tipos de referencia seguros ser incondicionalmente Unpin ? Parece estar resuelto (sí, deberían). La resolución es totalmente compatible con versiones anteriores.
  • ¿Deberíamos tener un tipo Pin compartido además de uno único Pin , para que los accesos de campo sean factibles (no funciona con &Pin porque es una referencia a una referencia)? La resolución fue cambiar Pin a PinMut y agregar un tipo Pin para el caso compartido. Esto no era compatible con versiones anteriores, pero el cambio radical ( Pin a PinMut ) ya se realizó, y estoy bastante seguro de que ya se ha aceptado efectivamente.

Creo que eso lo cubre bastante. Como puede ver, todos ellos (además de los punteros en bruto y la decisión incorrecta, la última de las cuales parece estar resuelta en gran medida en este punto) tienen algún camino hacia adelante compatible con versiones anteriores, incluso si la fijación se estabilizó hoy; Más importante aún, el hecho de que podamos resolver las proyecciones de campo significa que el uso de un tipo fijo en su API no está destinado a ser una decisión de la que luego se arrepienta.

Además de las preguntas anteriores, ha habido otras propuestas que apuntan a repensar el pinning de una manera mucho más fundamental. Los dos que creo que entiendo mejor son la propuesta de @comex para hacer !Unpin tipos !DynSized , y la propuesta de steven099 (en aspectos internos; lo siento, no sé el nombre de Github) para tener un nuevo tipo de envoltorio Pinned tamaño que haga que las partes internas sean inamovibles (algo así como un envoltorio ZST)

La opción !DynSized es una característica bastante conservadora (en el sentido de que Rust ya tiene una característica similar disponible) que tiene la ventaja de que puede que ya sea necesaria para tratar con tipos opacos. En este sentido, puede ser incluso menos invasivo que los cambios propuestos a Drop . También tiene una gran ventaja: resuelve automáticamente el problema con Drop porque los tipos de !Unpin serían !DynSized y, por lo tanto, uno no podría salir de ellos. Esto haría que &mut T y &T funcionen automáticamente como PinMut y Pin donde T fuera !DynSized , por lo que no No se necesita una proliferación de versiones ancladas de tipos y métodos que funcionan en &mut T menudo se podrían hacer que funcionen normalmente (si se modificaran para que no requieran un límite de DynSized cuando no lo necesitaban ).

La principal desventaja (además de las preocupaciones habituales en torno a ?Trait ) parece ser que un tipo !Unpin nunca podría moverse, lo cual es bastante diferente de la situación con los tipos anclados actualmente. Esto significa que componer dos tipos anclados sin usar una referencia no sería realmente posible (por lo que puedo decir, de todos modos) y no estoy seguro de cómo o si hay alguna resolución propuesta para esto; IIRC estaba destinado a funcionar junto con una propuesta de &move , pero no estoy seguro de cuál es la semántica prevista de eso. Tampoco entiendo (de la propuesta) cómo podría tener proyecciones de campo seguras con él, ya que se basa en tipos opacos; parece que tendrías que usar mucho código inseguro en general para usarlo.

El tipo Pinned<T> tamaño es algo similar en espíritu, pero quiere solucionar el problema anterior permitiéndole envolver un tipo en un ZST que lo haga inamovible (actuando efectivamente sin tamaño). Rust no tiene nada comparable en este momento: PhantomData realidad no incluye una instancia del tipo, y los otros tipos de tamaño dinámico generan punteros gordos y aún permiten movimientos (usando API que dependen de size_of_val ; esto es lo que se pretendía arreglar con ?DynSized , por lo que esta propuesta probablemente vuelva a aprovecharse de ese rasgo). No me parece que esta propuesta realmente solucione el problema de Drop si permite proyecciones seguras, y también parece incompatible con Deref , así que para mí las ventajas sobre Pin no son tan claros.

si se puede hacer que una API de fijación de pila que preserve esta garantía funcione con async / await, la mayoría de las partes interesadas parecen estar dispuestas a aceptar esto (IIRC, alguien ya intentó resolver esto usando generadores pero rustc ICE'd)

Como referencia, esto probablemente se refiera a https://github.com/rust-lang/rust/issues/49537 que es de este intento de una API de fijación de pila basada en un generador anidado. Sin embargo, no estoy seguro de si las vidas útiles allí funcionarían incluso si se resuelve el ICE.

Aún sin resolver, principalmente porque podría romper la API de fijación de pila propuesta; si se puede hacer que una API de fijación de pila que conserve esta garantía funcione con async / await, la mayoría de las partes interesadas parecen estar dispuestas a aceptar esto (IIRC, alguien ya intentó resolver esto usando generadores pero rustc ICE).

Veo la API de fijación de pila basada en cierres como una solución a esto, con una vía futura (una vez que &pin es una primitiva de lenguaje) para algo más ergonómico y verificado por el compilador. También existe esta solución basada en macros propuesta por @cramertj.

¿Los punteros en bruto deben ser Desanclar incondicionalmente? [...] No sería compatible con versiones anteriores; Estoy marcando esto como controvertido principalmente por esa razón.

¿Por qué crees que agregar un impl Unpin for Vec<T> es compatible con versiones anteriores, pero hacer lo mismo con los punteros sin formato no lo es?

La principal desventaja (además de las preocupaciones habituales en torno a? Rasgo) parece ser que un tipo! Unpin nunca podría moverse, lo cual es bastante diferente de la situación con los tipos anclados actualmente. Esto significa que componer dos tipos anclados sin usar una referencia no sería realmente posible (por lo que puedo decir, de todos modos) y no estoy seguro de cómo o si hay alguna resolución propuesta para esto; IIRC tenía la intención de trabajar junto con una propuesta de & move, pero no estoy seguro de cuál es la semántica deseada.

Bueno, según la propuesta de variante de @ steven099 (que yo prefiero), la mayoría de los usuarios (por ahora) usarían Pinned<T> , que contiene T por valor pero es !Sized ( y tal vez !DynSized ; el diseño exacto de la jerarquía de rasgos está abierto a la eliminación de bicicletas). En realidad, esto termina siendo muy similar a la propuesta existente, excepto que &'a mut Pinned<T> representa Pin<'a, T> . Pero es más componible con código actual y futuro que es genérico en &mut T (por T: ?Sized ), y es más compatible con versiones anteriores con un diseño futuro para tipos inamovibles nativos verdaderos que no tendrían que serlo. use Pinned .

Para entrar en más detalles, Pinned podría verse así:

extern { type MakeMeUnsized; }

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

Por lo general, no construiría un Pinned<T> directamente. En su lugar, podría enviar desde Foo<T> a Foo<Pinned<T>> , donde Foo es cualquier puntero inteligente que garantice que no moverá su contenido:

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

(También podría haber una API de fijación de pila, probablemente basada en una macro, pero eso es un poco más complicado de implementar).

En este ejemplo, FakeGenerator representa un tipo de generador generado por el compilador:

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

Una vez que se fija un FakeGenerator , el código de usuario ya no debe poder acceder a él directamente por valor ( foo: FakeGenerator ) o incluso por referencia ( foo: &mut FakeGenerator ), ya que este último permitir el uso de swap o replace . En cambio, el código de usuario trabaja directamente con, por ejemplo, &mut Pinned<FakeGenerator> . Nuevamente, esto es muy similar a las reglas para los PinMut<'a, FakeGenerator> de la propuesta existente. Pero como un ejemplo de una mejor capacidad de compilación, el impl generado por el compilador puede usar el rasgo Iterator , en lugar de necesitar uno nuevo que requiera Pin<Self> :

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

Por otro lado, para la construcción inicial, podemos entregar los valores FakeGenerator directamente y permitir que se muevan, siempre y cuando garanticemos que solo los estados que no son autoprestados sean accesibles antes de ser anclados:

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

Entonces, si queremos componer dos FakeGenerator s por valor, la construcción es sencilla:

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

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

Luego podemos anclar el objeto TwoGenerators como un todo:

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

Pero, como mencionaste, necesitamos proyecciones de campo: una manera de pasar de &mut Pinned<TwoGenerators> a &mut Pinned<FakeGenerator> (accediendo a a o b ). Aquí también, esto termina luciendo muy similar al diseño existente Pin . Por ahora, usaríamos una macro para generar accesos:

// 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) }
    }
}

Sin embargo, al igual que el diseño existente, la macro debería evitar que el usuario implemente Drop por TwoGenerators . Por otro lado, idealmente el compilador nos permitiría impl Drop for Pinned<TwoGenerators> . Actualmente lo rechaza con un error de que "Las implementaciones de Drop no se pueden especializar", pero eso se podría cambiar. En mi opinión, esto sería un poco mejor que PinDrop , ya que no necesitaríamos un nuevo rasgo.

Ahora, como una extensión futura, en principio, el compilador podría admitir el uso de la sintaxis de campo nativa para pasar de Pinned<Struct> a Pinned<Field> , similar a la sugerencia bajo el diseño existente de que el compilador podría agregar un &pin T nativo

Sin embargo, mi "final" ideal no es eso, sino algo más dramático (y más complejo de implementar) donde el compilador algún día soportaría tipos inamovibles "nativos", incluyendo vidas existenciales. Algo como:

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

Estos tipos se comportarían como Pinned<T> , siendo !Sized etc. [1] Sin embargo, a diferencia de Pinned , no requerirían un estado móvil inicial. En cambio, trabajarían basados ​​en "elisión de copia garantizada", donde el compilador permitiría usar tipos inamovibles en valores de retorno de funciones, literales de estructura y varios otros lugares, pero garantizaría que bajo el capó, se construirían en su lugar. Entonces no solo podríamos escribir:

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

(que ya puedes hacer ) ... también podríamos escribir cosas como:

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

o

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

Por supuesto, lo anterior es solo un bosquejo muy aproximado de cómo se vería la función; la sintaxis que utilicé tiene problemas, y la sintaxis sería la menor de las muchas complejidades involucradas en el diseño de la función. (Dicho esto, lo he pensado lo suficiente como para estar bastante seguro de que se pueden resolver las complejidades, que no es solo una idea incoherente que no puede funcionar).

Solo lo menciono como parte de la motivación, en el presente, por usar un diseño !DynSized -ish en lugar del diseño Pin . Creo que &'a Pinned<T> ya es mejor que Pin<'a, T> porque evita una explosión combinatoria de tipos de punteros. Pero hereda algunos de los mismos problemas:

  1. Básicamente, no admite tipos que carecen de un estado móvil inicial y
  2. Es ruidoso, confuso (la diferencia entre referencias ancladas y no ancladas al mismo tipo es difícil de explicar) y hace que los tipos inamovibles se sientan de segunda clase. Quiero que los tipos inamovibles y autorreferenciales se sientan de primera clase, tanto porque son inherentemente útiles como para hacer que Rust se vea mejor para las personas que provienen de otros idiomas, donde hacer valores autorreferenciales es fácil y común.

En el futuro, estoy imaginando que los tipos inamovibles nativos resolverían ambos problemas, y la mayoría del código nunca necesitaría usar la palabra "pin". (Aunque Pinned todavía puede tener algunos casos de uso, para los casos en que la elisión de copia no es lo suficientemente buena y realmente necesita un estado móvil inicial). En contraste, Pin hornea el concepto de un separar el estado "aún no fijado" en el diseño de cada rasgo que lo use. Y un &pin incorporado lo integraría al idioma.

De todos modos, aquí hay un enlace del patio de recreo para el diseño Pinned anterior.

[1] ... aunque es posible que queramos un nuevo rasgo ReallySized (con un nombre menos tonto), para tipos que tienen un tamaño estático pero pueden o no ser móviles. La cosa es que, de todos modos, habrá algo de abandono aquí, porque con soporte para rvalues ​​sin tamaño, muchas funciones que actualmente toman Sized argumentos podrían funcionar igual de bien con los que no tienen tamaño pero que se pueden mover. Cambiaremos los límites de las funciones existentes y las recomendaciones sobre qué límites usar para funciones futuras. Afirmo que incluso podría valer la pena que una edición futura cambie el límite predeterminado (implícito) para los genéricos de funciones, aunque esto tendría algunas desventajas.

[editar: ejemplo de código fijo]

@RalfJung

Veo la API de fijación de pila basada en cierres como una solución a esto, con una vía futura (una vez & pin es un lenguaje primitivo) para algo más ergonómico y verificado por el compilador. También existe esta solución basada en macros propuesta por @cramertj.

El basado en el cierre no funciona correctamente con async / await, creo, porque no se puede ceder desde dentro de un cierre. Sin embargo, el basado en macros es interesante; si eso es realmente seguro, es bastante ingenioso. No pensé que pudiera funcionar al principio porque un pánico durante una de las caídas en el alcance podría provocar que las otras fugas, pero aparentemente eso se ha solucionado con MIR.

Tampoco estaba seguro de la interacción entre la garantía de "ejecución de destructores antes de que se desasigne la memoria" y la capacidad de proyectar campos anclados; si la caída del nivel superior entraba en pánico, había pensado que los campos fijados proyectados en el valor no se ejecutarían. Sin embargo, en el patio de juegos de Rust, en realidad parece que los campos de este tipo tienen sus gotas de todos modos incluso después de que el tipo de nivel superior entra en pánico, ¡lo cual es bastante emocionante! ¿Esa garantía está realmente documentada en alguna parte? Parece necesario si la fijación de pilas va a funcionar con proyecciones de pines (eso, o algo más pesado, como PinDrop siempre abortando en pánico, lo que parece indeseable ya que introduciría una diferencia de funcionalidad entre Drop y PinDrop).

¿Por qué crees que agregar un impl Unpin para Vec¿Es compatible con versiones anteriores, pero hacer lo mismo con los punteros en bruto no?

Veo su punto: alguien podría estar confiando en la implementación automática !Unpin de un campo Vec<T> propiedad e implementando sus propios accesos que asumieron que la fijación era transitiva para algunos !Unpin T . Si bien es cierto que PinMut<Vec<T>> realidad no le brinda una forma segura de obtener un PinMut<T> , el código inseguro aún podría explotar PinMut::deref para obtener punteros sin procesar y hacer suposiciones sobre los punteros son estables. Así que supongo que esta es otra situación en la que es compatible con versiones anteriores solo si el código inseguro no depende de que !Unpin sea ​​transitivo hasta Vec (o lo que sea). Sin embargo, ese tipo de fuerte dependencia del razonamiento negativo me parece sospechoso de todos modos; si quieres asegurarte de que estás !Unpin donde T no lo es, siempre puedes agregar un PhantomData<T> (supongo que este argumento también se aplica a punteros sin formato). Una declaración general como "es UB asumir que los tipos en la biblioteca estándar son! Desanclar en código inseguro, independientemente de sus parámetros de tipo, a menos que el tipo se excluya explícitamente o su documentación declare que se puede confiar en esto" probablemente sería suficiente.

Sin embargo, el basado en macros es interesante; si eso es realmente seguro, es bastante ingenioso. No pensé que pudiera funcionar al principio porque un pánico durante una de las caídas en el alcance podría provocar que las otras fugas, pero aparentemente eso se ha solucionado con MIR.

Sería un error en MIR si el pánico durante la caída llevara a omitir la caída de las variables locales restantes. Eso debería cambiar de "caída normal" a "caída de desenrollado". Otro pánico abortará el programa.

@RalfJung

Sería un error en MIR si el pánico durante la caída llevara a omitir la caída de las variables locales restantes. Eso debería cambiar de "caída normal" a "caída de desenrollado". Otro pánico abortará el programa.

¿Se descartan otros campos de la estructura considerados variables locales en este contexto? Eso realmente no me queda claro a partir de la documentación que enfrenta el usuario (en realidad, esto es cierto para toda la garantía de la que está hablando; solo descubrí que en realidad se consideraba un error que debía solucionarse desde el rastreador de problemas).

@comex

Lo que pasa con Pinned (lo que estás proponiendo) es que creo que si quisiéramos implementarlo (una vez que Rust tuviera todas las características en su lugar) no tendríamos que hacer mucho más que esto para hacerlo compatible con versiones anteriores del código existente:

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

(Creo que también se propuso tener una implementación deref de Pin a Pinned ). Es instructivo mirar los lugares donde parece que esto no funcionaría. Por ejemplo:

impl Drop for Pinned<TwoGenerators>

no funciona (al menos, no directamente) con PinMut . Incluso si asumimos que esto reemplaza el Drop por TwoGenerators sí mismo cuando se construye el tipo Pinned (no estoy seguro de cómo funcionaría esto), Rust aún no lo haría Sepa llamar a la versión Pinned del constructor para cualquier campo que se proyecte, ya que los campos solo se mantendrían por valor. Si la versión anclada de un destructor siempre se usó si existiera, esto es efectivamente idéntico a PinDrop , solo con una sintaxis extraña, y no veo cómo es mejor.

Sin embargo, uno se siente tentado a considerar la posibilidad de elegir el destructor analizando recursivamente si un valor tiene su raíz en Pinned . Observe que si alguna vez queremos permitir objetos de rasgo por valor, no necesariamente podemos confiar en poder decidir si usar la caída Pinned<T> o la caída T en tiempo de compilación; Supongo que está pensando que podría haber una entrada de vtable separada para la versión Pinned en tales casos. Esta idea me intriga un poco. Definitivamente requiere un soporte sustancial del compilador (mucho más que el PinDrop ), pero podría ser mejor en general de alguna manera.

Sin embargo, al releer el hilo, recuerdo que hay otros problemas: la implementación deref para tipos anclados realmente no funciona bien para Pinned<T> (sospecho que esto se debe a que la implementación deref on PinMut es lógicamente incorrecto de alguna manera, por lo que sigue causando problemas, pero es realmente difícil justificar su pérdida dada su conveniencia, a menos que cree un montón de tipos incondicionalmente Unpin todos modos). Específicamente, encuentro el ejemplo de RefCell bastante preocupante, ya que en presencia de Pinned::deref significa que ni siquiera podríamos imponer la fijación dinámicamente con el tipo existente (no sé si la especialización fuera suficiente). Esto sugiere además que si mantenemos la implementación deref , tendremos que terminar duplicando la superficie API casi tanto con Pinned como lo hacemos con Pin ; y si no lo guardamos, Pinned<T> vuelve increíblemente difícil de usar. Tampoco parece que Box<Pinned<T>> funcionaría a menos que agreguemos ?Move separado de ?DynSized (como se señala en el hilo).

Todo esto puede ser una buena idea, sin embargo, en el contexto de Rust actual, todo parece poco atractivo para mí, especialmente cuando consideras que básicamente ningún método en Rust actual tendría ?Move límites (es decir, la falta de un deref realmente dolería, a menos que el tipo sea Unpin en cuyo caso Pinned no ofrece ventajas). Sospecho que está modelando lo que realmente está sucediendo más de cerca al hacer que la fijación sea una propiedad propietaria, lo que a su vez hace que sea más difícil salirse con la suya con decisiones ad hoc como PinMut::deref y crea una interfaz mucho más agradable (subjetivamente de todos modos) , pero agrega una gran cantidad de maquinaria de lenguaje para hacerlo y no parece que piense que ninguno de ellos es particularmente útil (en comparación con los tipos inamovibles nativos que está proponiendo). Tampoco sé si lo que obtenemos que no pudimos obtener completamente compatible con versiones anteriores (principalmente, llamar a una implementación de caída diferente según el estado del pin) es realmente tan útil, incluso si se puede hacer (tal vez podría guardar un rama en el destructor a veces si sabía que el tipo estaba fijado?). Entonces, no estoy seguro de si vale la pena cambiar la propuesta PinMut en este momento. Pero tal vez me esté perdiendo algún caso de uso concreto realmente convincente.

Lo que pasa con Pinned (lo que estás proponiendo) es que creo que si quisiéramos implementarlo (una vez que Rust tuviera todas las características en su lugar) no tendríamos que hacer mucho más que esto para hacerlo compatible con versiones anteriores del código existente:

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

En primer lugar, Pinned sí mismo requeriría un soporte de compilador mínimo o nulo, si eso es lo que quiere decir con "características". Si te refieres al diseño de bibliotecas, como DynSized y rasgos relacionados, entonces eso es válido, pero ...

Lo que sugirió no sería realmente compatible con versiones anteriores, ya que, por ejemplo, podría intentar implementar el mismo rasgo para Pin<'a, T> y &'a T , lo que de repente se volvería conflictivo.

De manera más general, hay una diferencia significativa en los diseños de API. Con Pin , los rasgos que se espera estén implícitos en tipos inamovibles deben tener sus métodos en PinMut<Self> , las funciones genéricas que quieran tomar referencias a tipos inamovibles deben verse como fn foo<T>(p: PinMut<T>) , etc. Por otro lado, el diseño Pinned evita la necesidad de nuevos rasgos en la mayoría de los casos, ya que puede implicar rasgos por Pinned<MyStruct> . Así:

  1. Los tipos inamovibles serían incompatibles con los rasgos existentes cuyos métodos toman &self o &mut self : por ejemplo, los generadores no podrían implicar Iterator . Por lo tanto, necesitaríamos un montón de rasgos nuevos que sean equivalentes a los existentes, pero en su lugar tomaríamos PinMut<Self> . Si cambiamos de rumbo y hacemos de PinMut<T> un alias para &mut Pinned<T> , podríamos volver atrás y desaprobar todos esos rasgos duplicados, pero eso sería bastante tonto. Es mejor no necesitar los duplicados en primer lugar.

  2. Por otro lado, los rasgos de nuevo diseño o específicos del generador probablemente tomarían PinMut<Self> como la única opción, a costa de agregar ruido para los tipos que desean implementar los rasgos pero no son inamovibles y no necesitan para ser fijado. (Específicamente, las personas que llaman tendrían que llamar a PinMut::new para pasar de &mut self a PinMut<Self> , suponiendo que Self: Unpin .) Incluso si Pin<T> convirtió en alias por &mut Pinned<T> , no habría forma de deshacerse de ese ruido. Y los futuros tipos de inmuebles nativos que estoy imaginando estarían en la misma situación que los tipos móviles, necesitando innecesariamente estar envueltos en Pinned cuando siempre se consideran anclados.

Responderé al resto de tu publicación en una segunda publicación.

Respecto a Drop

Estoy un poco confundido por lo que dices acerca de Drop , pero en la medida en que intentes hacerlo compatible con PinMut , no voy a pensar en eso. porque no creo que sea un buen enfoque.

Creo que el mejor enfoque es que si tiene una estructura como TwoGenerators , tiene dos opciones:

  1. No hay Drop implícitos manuales para TwoGenerators o Pinned<TwoGenerators> ;
  2. Implica Drop por Pinned<TwoGenerators> ; mientras tanto, la misma macro que le da acceso generará un Drop impl para TwoGenerators sí mismo que simplemente se lanza a sí mismo a &mut Pinned<TwoGenerators> y lo elimina. (Esto es seguro: el invariante requerido para convertir &mut T a &mut Pinned<T> es que no moverá T después de que finalice el préstamo, y en el caso de Drop , tiene el último préstamo que se creará por ese valor).

La única razón para tener dos opciones es que, como se mencionó anteriormente, es posible que no desee que su estructura implique Drop porque las estructuras que no implican que el verificador de préstamos las trate de manera más flexible.

No veo por qué querrías tener un destructor separado real para el estado anclado o no anclado, por lo que no hay necesidad de jugar trucos con vtables para diferenciarlos.

Respecto a RefCell

No creo que Pinned::deref deba existir. Los accesos de campo seguros generados por macros deberían ser suficientes; No veo cómo eso es "increíblemente difícil de usar". Es un poco menos agradable que poder usar la sintaxis de campo nativa, pero algún día eso será arreglado por estructuras inamovibles nativas. De todos modos, si es difícil de usar, el mismo problema se aplica a Pin .

Esto evita el problema con RefCell .

especialmente cuando consideras que básicamente ningún método en Rust actual tendría ?Move límites (lo que significa que la falta de un deref realmente perjudicaría [..])

Por el contrario, todo lo que tiene un límite de ?Sized es implícitamente ?Move .

Esto tiene sentido porque, en general, es imposible que el código con un ?Sized asuma movilidad. La única excepción es el código inseguro que llama a size_of_val y luego memcpy's esa cantidad de bytes, por lo que necesitamos el truco donde size_of_val entraría en pánico por tipos inamovibles (y quedaría obsoleto a favor de una nueva función con un límite adecuado).

Estoy un poco confundido por lo que estás diciendo sobre Drop, pero en la medida en que intentes hacerlo compatible con PinMut, no voy a pensar en eso porque no creo que sea un buen enfoque. .

Estaba diciendo que si algo no es compatible con versiones anteriores de PinMut , debería haber una buena razón para ello. Sin embargo, lo que está proponiendo es funcionalmente idéntico a PinDrop en todos los aspectos, excepto que desea implementarlo en Pinned<T> (que no funciona en Rust actual). Personalmente, creo que la especialización Drop establece un precedente realmente dudoso y es casi seguro que no es deseable por razones que no tienen nada que ver con la fijación, por lo que no considero esto como una ventaja intrínseca. En cualquier caso, creo que PinDrop se puede separar principalmente del resto de su propuesta.

De todos modos, si es difícil de usar, el mismo problema se aplica a Pin.

Claro, y si estuviéramos dispuestos a deshacernos de PinMut::deref , también se compondría muy bien con tipos como RefCell ; la diferencia es que aún podemos improvisar una solución con PinMut mientras se admite deref , que parece no funcionar con Pinned . Si tuviéramos que deshacernos de la implementación deref , creo que estaría mucho más de acuerdo en que Pinned proporciona una ventaja significativa.

Pero realmente no estoy seguro de estar de acuerdo en que no tener deref es solo un pequeño problema en la práctica: por ejemplo, significa que en su estado actual no puede hacer nada con &Pinned<Vec<T>> donde T: !Unpin , y lo mismo se aplicaría básicamente a todos los tipos de bibliotecas existentes. Este es un problema que surge de la forma en que funciona Unpin , no del tipo particular de referencia que tiene. El ecosistema tendría que decidir colectivamente hacer cosas como impl Deref for Pinned<Vec<T>> { type Target = Pinned<[T]>; } o algo, lo cual creo que estoy de acuerdo sería preferible a un impl PinDeref<Vec<T>> si se puede hacer que funcione, pero eso está en un mundo sin deref . En el mundo con deref , casi todas las bibliotecas pueden salirse con la suya sin ningún tipo de acceso relacionado con pines, y aún tener un soporte medio decente para los tipos !Unpin .

Por el contrario, todo lo que tiene un límite de tamaño es implícitamente Move.

Ah, sí, ese es un buen punto. Desafortunadamente, un montón de código de Rust no funciona con tipos con un límite de !Sized , porque no es el predeterminado, pero al menos parte de él sí. No creo que sea una ventaja tan convincente porque la mayoría de las cosas que puedo hacer con valores sin tamaño son llamar a los métodos & o &mut en ellos (por ejemplo, para cortes u objetos de rasgo), ninguno de los lo cual podría hacer bajo su propuesta (excepto para Unpin tipos) ya que no quiere Pinned::deref . ¿Quizás los casos comunes podrían resolverse haciendo que las implementaciones de #[derive] también generen instancias distintas para Pinned<T> o algo así?

Drop

Personalmente, creo que la especialización de Drop sienta un precedente realmente dudoso y es casi seguro que no es deseable por razones que no tienen nada que ver con la fijación, por lo que no considero esto como una ventaja intrínseca. En cualquier caso, creo que PinDrop se puede separar principalmente del resto de su propuesta.

Estoy de acuerdo en que esto es separable, pero no creo que sea dudoso. Al menos ... lo que dijiste es correcto, es una forma de especialización. No se está especializando literalmente en alguna implícita general principal de Drop , pero el pegamento de caída generado por el compilador está haciendo el equivalente a la especialización llamando a Drop solo si está implementado. Una implementación de 'userland' se vería así (ignorando el hecho de que no puede llamar manualmente 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() }
}

Entonces, me imagino que la razón por la que actualmente no puede escribir Drop impls 'especializados' es la misma razón por la que la especialización en sí no es sólida actualmente: incoherencia entre trans (que borra los parámetros de por vida) y typeck (que no lo hace). En otras palabras, si pudiéramos escribir, digamos, impl Drop for Foo<'static> , en realidad se llamaría para cualquier Foo<'a> , no solo Foo<'static> , porque codegen asume que los dos tipos son idénticos.

La buena noticia es que, como probablemente sepa, ha habido intentos de encontrar una manera de limitar las implicaciones especializadas para que no puedan crear ese tipo de incoherencia. Y se espera que la especialización eventualmente se envíe con algún límite de ese tipo. Una vez que eso sucede, no veo ninguna razón por la que no podamos aplicar las mismas reglas a Drop impls, y para que el lenguaje sea lo más coherente posible, deberíamos hacerlo.

Ahora, no queremos bloquear la especialización. Sin embargo, afirmo que permitir impl Drop for Pinned<MyStruct> - o más en general, permitir impl<params> Drop for Pinned<MyStruct<params>> en las mismas condiciones que el compilador permite actualmente impl<params> Drop for MyStruct<params> - está garantizado como un subconjunto de lo que la especialización Permitir, por lo que si hoy presentamos un caso especial, eventualmente desaparecerá en una regla más general.

Pero nuevamente, esto es separable; si a la gente no le gusta esto, podríamos tener un rasgo separado en su lugar.

Unpin

Pero realmente no estoy seguro de estar de acuerdo en que no tener deref es solo un pequeño problema en la práctica: por ejemplo, significa que en su estado actual no puede hacer nada con &Pinned<Vec<T>> donde T: !Unpin , y lo mismo se aplicaría básicamente a todos los tipos de bibliotecas existentes. Este es un problema que surge de la forma en que funciona Unpin , no del tipo particular de referencia que tiene.

Er ... está bien, déjame corregir mi declaración. Pinned::deref debería existir, pero debería estar limitado a Unpin , aunque lo voy a llamar Move .

La única razón por la que Deref impl para PinMut causa problemas con RefCell es que (a diferencia de DerefMut impl) no está limitado a Unpin . Y la razón para no tener el límite es el deseo de permitir que los usuarios obtengan &MyImmovableType , permitiendo que los tipos inamovibles impliquen rasgos con métodos que toman &self , para pasar a funciones genéricas que toman &T , etc. Esto es fundamentalmente imposible para &mut self , pero principalmente funciona con &self porque no puedes salir de él con mem::swap o mem::replace - es decir, excepto cuando usa RefCell . Pero, según el razonamiento, la compatibilidad con las referencias existentes es lo suficientemente valiosa como para ser compatible, incluso si la limitación a las referencias inmutables se siente arbitraria, incluso si causa errores.

Con Pinned , podemos admitir referencias tanto inmutables como mutables: simplemente implica sus rasgos en Pinned<MyStruct> lugar de directamente en MyStruct . La desventaja es que no es compatible con rasgos o funciones que toman &T pero por separado tienen un límite de Self: Sized ; pero son relativamente raros y, a menudo, no intencionales.

Curiosamente, Pinned sí mismo realmente no requiere que Unpin exista en absoluto. Después de todo, ¿por qué alguien realmente crearía un &Pinned<Vec<T>> ? Con PinMut , varios rasgos tomarían PinMut<Self> , por lo que incluso las implicaciones de esos rasgos para tipos móviles tendrían que recibir un PinMut . Con Pinned , como dije, los rasgos seguirían tomando &self o &mut self , y los implicaría por Pinned<MyStruct> . Si desea implicar el mismo rasgo por Vec<T> , Pinned no necesita entrar en escena.

Sin embargo, una fuente potencial sería la macro de acceso al campo. Si usted tiene

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

entonces el diseño más simple siempre generaría accesos usando 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>> { … }
}

… Y depende de usted lidiar con los Pinned si no lo desea. De hecho, creo que esto está bien, porque Unpin / Move debería existir, ver más abajo. Pero si no existiera, una alternativa sería tener una forma de optar por participar por campo para recibir una referencia directa en lugar de una Pinned . Es decir, tendrías

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

No sería correcto tener accesos Pinned y no Pinned , pero cualquiera de los dos por sí solo debería estar bien.

… Pero sí, necesitamos tener un rasgo Move , pero no por Pinned . Por ejemplo, sería parte del límite para la nueva versión de size_of_val (corrección: no lo sería, pero se esperaría que el código inseguro lo verificara antes de intentar memcpy tipos arbitrarios basados ​​en el resultado de size_of_val ); en el futuro, con valores r sin tamaño, será el límite (relajado de Sized ) para ptr::read y mem::swap y mem::replace ; si alguna vez obtenemos &move , sería el límite para permitirle, bueno, salir de uno; y algo similar se aplica a la elisión de copia garantizada.

Entonces, mientras tengamos el rasgo, no hay razón para no tener Pinned::deref (y deref_mut ) con un límite de T: Move .

[editar: como Pythonesque me recordó, esto tiene una semántica diferente de Unpin , así que no importa].

(Y luego tipos como Vec y Box querrán implicar manualmente Move para que se aplique independientemente de si el tipo de elemento es Move ).

Er ... está bien, déjame corregir mi declaración. Pinned :: deref debería existir, pero debería estar limitado a Unpin, aunque lo voy a llamar Move.

Está bien, espera. ¿Es esto ?Move o Move ? Lo primero significaría que los tipos !Unpin ni siquiera se pueden construir en muchos casos; lo último me hace preguntarme cómo, exactamente, nos referimos a tipos como Pinned<T> (ya que ?DynSized no es realmente el destino correcto para ellos). Ciertamente espero que no sean lo mismo; de lo contrario, tratar Move como Unpin una vez más hace exactamente lo que estamos tratando de evitar y hace que el tipo sea inamovible en el momento en que se construye.

La desventaja es que no es compatible con rasgos o funciones que toman & T pero por separado tienen un límite de Self: Sized; pero son relativamente raros y, a menudo, no intencionales.

Hay una desventaja práctica mucho más significativa, que es que pocos de esos rasgos o funciones realmente funcionan con & Pinnedhoy. Se podría hacer que funcionen con él, pero requeriría una gran cantidad de implementaciones de rasgos adicionales y (como dije) probablemente una revisión significativa de las implementaciones existentes de #[derive] . Este también es un costo que también tendría que pagarse por las cosas nuevas; tendría que implementar todo por &Pinned<Self> también si quisiera que funcione en los tipos !Unpin . Esta es una situación (mucho) mejor para los rasgos que toman &mut self que con PinMut , pero peor para &self , que es (sospecho) mucho más común. Por eso digo que creo que esta es la solución más correcta (en el sentido de que si no tuviéramos muchas bibliotecas Rust existentes, la versión Pinned sería mejor) pero posiblemente no la más utilizable.

Con Pinned, como he dicho, los rasgos seguirían tomando & self o & mut self, y los implicaría para Pinned

Reimplementar cada rasgo en la superficie API de Vec , solo con Pinned esta vez, no me suena tan bien (especialmente porque algunos de los rasgos ni siquiera funcionan con él). Estoy bastante seguro de implementar selectivamente Deref caso por caso (dejando que &Pinned<Vec<T>> vaya a &[Pinned<T>] , por ejemplo), o simplemente dejar que la totalidad de Vec be Unpin (y no permitir la proyección de pin), es mucho más sensato. En cualquier caso, ambos son más trabajo que no hacer absolutamente nada, y tendría que replicarse en un montón de tipos y rasgos existentes para que las cosas inamovibles funcionen con ellos.

Podría estar convencido de lo contrario: en realidad, me gusta más la solución Pinned más lo pienso, pero no sé dónde están en realidad todas estas nuevas implementaciones de rasgos en Pinned<T> va a venir Me parece más probable que la gente simplemente no se moleste en implementarlos.

Curiosamente, Pinned en sí no requiere que Unpin exista en absoluto. Después de todo, ¿por qué alguien crearía un & Pinned>?

Hay muy buenas razones para querer hacer esto (como: su tipo fijado tiene un Vec en él). Las colecciones intrusivas se encontrarán con este tipo de escenario con mucha frecuencia. Creo que cualquier propuesta basada en la idea de que la gente nunca va a querer referencias de Pinned a contenedores existentes, o que tiene que optar por hacer que Unpin funcione, probablemente no funcione bien. No poder optar básicamente por el ecosistema regular de Rust agregando un límite de Unpin sería increíblemente disruptivo (de hecho, casi todos los casos de uso que tengo para tipos inamovibles se volverían significativamente más difíciles).

Con PinMut, varios rasgos tomarían PinMut, por lo que incluso las implicaciones de esos rasgos para tipos móviles deberían recibir un PinMut.

¡Por supuesto! La gran ventaja de la versión Pinned es que no necesitaría rasgos distintos para referencias fijadas mutables. Sin embargo, es peor o neutral que PinMut con deref para casi todos los demás escenarios.

No sería correcto tener accesos fijados y no fijados, pero cualquiera de ellos por sí solo debería estar bien.

Los accesos manuales que requieren un código inseguro para implementar me parecen una mala idea; No veo cómo una propuesta de este tipo permitiría que la generación de accesos sea segura (¿cómo se evita que alguien proporcione accesos no fijados sin hacer que escriban unsafe para afirmar que no lo harán?). Sin embargo, como puede observar, usar Move (asumiendo que realmente significa Unpin ) funcionará bien.

Entonces, mientras tengamos el rasgo, no hay razón para no tener Pinned :: deref (y deref_mut) con un límite T: Move.

Por supuesto. Estoy hablando específicamente de los tipos !Unpin aquí. Unpin tipos PinMut , por lo que no son tan relevantes desde mi perspectiva. Sin embargo, los límites de Unpin (o Move ) en los genéricos no son agradables e idealmente debería poder evitarlos siempre que sea posible. Nuevamente, como dije antes: Unpin y lo que esté implícito en !Sized no son

… Pero sí, necesitamos tener un rasgo Move, pero no para Pinned. Por ejemplo, sería parte del límite de la nueva versión de size_of_val; en el futuro con rvalues ​​sin tamaño, será el límite (relajado de Sized) para ptr :: read y mem :: swap y mem :: replace; si alguna vez conseguimos y nos movemos, sería el límite para dejarte, bueno, salir de uno; y algo similar se aplica a la elisión de copia garantizada.

Creo que esto es una vez más la combinación de !Unpin (que dice que un tipo puede tener un invariante de fijación no predeterminado) y !DynSized -como !Move ; en realidad, no pueden ser iguales sin causar el comportamiento de congelación no deseado.

Vaya, tienes toda la razón. Unpin no puede ser lo mismo que Move .

Entonces creo que he vuelto a creer que Unpin y, por lo tanto, Pinned::deref no deberían existir en absoluto, y en su lugar deberíamos evitar cualquier situación (como con la macro generadora de accesos) en la que obtenga un tipo como &Pinned<MovableType> . Pero tal vez haya un argumento de que debería existir como un rasgo separado.

Reimplementar cada rasgo en la superficie API de Vec , solo con Pinned esta vez, no me suena tan bien (especialmente porque algunos de los rasgos ni siquiera funcionan con él). Estoy bastante seguro de implementar selectivamente Deref caso por caso (dejando que &Pinned<Vec<T>> vaya a &[Pinned<T>] , por ejemplo), o simplemente dejar que la totalidad de Vec be Unpin (y no permitir la proyección de pin), es mucho más sensato.

Sí, definitivamente no quise proponer reimplementar la totalidad de la superficie API de Vec ni nada por el estilo.

Estoy de acuerdo en que sería una buena idea no permitir la "proyección de pin" en Vec , ya que &Pinned<Vec<T>> parece un invariante extraño; debería poder mover el Vec sin invalidando el pin del contenido.

Como alternativa, ¿qué tal si se permite la transmutación de Vec<T> a Vec<Pinned<T>> , que tendría la mayor parte de la API Vec pero omitiría los métodos que pueden provocar la reasignación? Es decir, cambie la definición de Vec del actual struct Vec<T> a struct Vec<T: ?Sized + ActuallySized> , por un nombre menos tonto, donde básicamente Sized convierte en un alias de ActuallySized + Move ; luego agregue un Sized vinculado a los métodos que pueden causar la reasignación, y un método para realizar la transmutación.

También sería necesario cambiar el límite en el tipo de elemento del tipo de segmento incorporado de Sized a ActuallySized . Pensar en esto me recuerda la incomodidad de cambiar Sized para que signifique algo diferente a Sized , pero por otro lado, sigue siendo cierto que la gran mayoría de los límites de Sized en el código existente inherentemente requieren Move . Necesitar saber size_of::<T>() para indexar en un segmento es una pequeña excepción ...

¡Por supuesto! La gran ventaja de la versión Pinned es que no necesitaría rasgos distintos para referencias fijadas mutables. Sin embargo, es peor o neutral que PinMut con deref para casi todos los demás escenarios.

También tiene la ventaja de evitar conflictos con RefCell , ciertamente a costa de entrar en conflicto con otras cosas (límites de Sized ).

Los accesos manuales que requieren un código inseguro para implementar me parecen una mala idea; No veo cómo una propuesta de este tipo permitiría que la generación de accesos sea segura (¿cómo se puede evitar que alguien proporcione accesos no fijados sin hacer que escriban de forma insegura para afirmar que no lo harán?).

Debido a que los accesos están implícitos en Pinned<MyStruct> , no en MyStruct directamente. Si tiene &mut MyStruct , siempre puede acceder manualmente a un campo para obtener &mut MyField , pero aún se encuentra en un estado móvil. Si tiene &mut Pinned<MyStruct> , no puede obtener &mut MyStruct (asumiendo que MyStruct es !Unpin o Unpin no existe), así que tienes que usar un descriptor de acceso para acceder a los campos. El descriptor de acceso toma &mut Pinned<MyStruct> (es decir, toma &mut self y está implícito en Pinned<MyStruct> ) y le da &mut Pinned<MyField> o &mut MyField , dependiendo de la opción que elija. Pero solo puede tener un tipo de acceso u otro, ya que el invariante crítico es que no debe poder obtener un &mut Pinned<MyField> , escribir en él, liberar el préstamo y luego obtener un &mut MyField (y muévelo).

Hay muy buenas razones para querer hacer esto (como: su tipo fijado tiene un Vec en él). Las colecciones intrusivas se encontrarán con este tipo de escenario con mucha frecuencia. Creo que cualquier propuesta basada en la idea de que la gente nunca va a querer referencias de Pinned a contenedores existentes, o que tiene que optar por hacer que Unpin funcione, probablemente no funcione bien. No poder optar básicamente por el ecosistema regular de Rust agregando un límite de Unpin sería increíblemente disruptivo (de hecho, casi todos los casos de uso que tengo para tipos inamovibles se volverían significativamente más difíciles).

No entiendo completamente lo que quiere decir aquí, pero puede deberse a que estaba reaccionando a mi propio error wrt Unpin versus Move :)

Ahora que estoy corregido ... Si Unpin existe, entonces Vec debería implicarlo. Pero suponiendo que no exista, ¿cuáles son exactamente los escenarios a los que se refiere?

Para una estructura que tiene Vec como uno de sus campos, expliqué anteriormente cómo obtendría una referencia no fijada al campo (a costa de no poder obtener una referencia fijada a él, que está bien).

Supongo que esto sería problemático si desea una estructura genérica con un campo que podría contener un Vec , o podría contener un tipo inamovible, según el parámetro de tipo. Sin embargo, podría haber una forma diferente de resolver esto sin necesidad de un rasgo Unpin que todo tiene que pensar en si implementar.

@comex

Como alternativa, ¿qué tal permitir la transmutación de Veca Vec>, ¿cuál tendría la mayor parte de la API Vec pero omitiría los métodos que pueden causar la reasignación?

Porque...

Es decir, cambie la definición de Vec de la estructura actual Vecpara estructurar Vec, para un nombre menos tonto, donde básicamente Sized se convierte en un alias de ActuallySized + Move; luego agregue un límite de tamaño a los métodos que pueden causar la reasignación y un método para realizar la transmutación.

... eso suena muy, muy complicado y en realidad no hace lo que quieres (que es obtener un Vec<T> normal de &mut Pinned<Vec<T>> o lo que sea). Es una especie de fresco que le permite anclar un vector después de los hechos, por lo que obtener un análogo agradable a Box<Pinned<T>> , pero eso es una preocupación ortogonal; es solo otra ilustración del hecho de que el hecho de que la fijación sea una propiedad del tipo propio probablemente sea correcto. Creo que todo lo relacionado con Unpin tiene casi ninguna relación con la cuestión de cómo se construye el tipo de referencia.

Ahora que estoy corregido ... Si Unpin existe, Vec debería implicarlo. Pero suponiendo que no exista, ¿cuáles son exactamente los escenarios a los que se refiere?

Solo responderé a esto porque creo que ilustrará mi punto: ni siquiera puedo mutar un campo i32 a través de un PinMut sin Unpin . En algún momento, si desea hacer algo con su estructura, a menudo tiene que mover algo dentro de ella (a menos que sea completamente inmutable). Necesitar que las personas implementen explícitamente los accesos de campo en Pinned<MyType> parece realmente molesto, especialmente si el campo en cuestión siempre es seguro para mover. Este enfoque también parece que sería realmente confuso usarlo con un tipo Pinned incorporado, ya que las proyecciones legales de alguna manera variarían según el campo de una manera que no depende del tipo de campo, algo que ya se rechazó en Rust. cuando se eliminaron los campos mut (y en mi opinión, si vamos a agregar una anotación de este tipo, unsafe es una mejor opción, ya que los campos inseguros son un gran problema en la práctica). Dado que los tipos fijos incorporados son casi la única forma de hacer que las enumeraciones fijadas sean agradables de usar, me importa que puedan ser algo coherentes con el resto del lenguaje.

Pero mas importante...

Supongo que esto sería problemático si desea una estructura genérica con un campo que podría contener un Vec, o podría contener un tipo inamovible, según el parámetro de tipo

Ese es más o menos el caso de uso excelente para Unpin , y (para mí) el hecho de que realmente parece funcionar es bastante fantástico y valida todo el modelo de fijación (hasta el punto de que creo que incluso si estuviéramos comenzando desde antes de Rust 1.0 probablemente querríamos mantener Unpin como está). Los parámetros genéricos que viven en línea en la estructura también son prácticamente el único momento en el que debe limitarse a Unpin si los planes actuales (para hacer que casi todos los tipos de referencia seguros implementen Unpin incondicionalmente) atravesar.

Pero lo que realmente no obtengo es: ¿por qué quieres eliminar Unpin ? Eliminarlo no le compra esencialmente nada; todas las cosas buenas que obtienes de Pinned sobre PinMut (o viceversa) prácticamente no tienen relación con su presencia. Agregarlo le brinda una fácil compatibilidad con el resto del ecosistema Rust. FWIW, Unpin y la implementación incondicional de deref en Pin tampoco están relacionados entre sí, excepto que sin Unpin todos los tipos sienten lo mismo dolor que hacen los tipos !Unpin (lo que significa que probablemente haría más útil la implementación incondicional deref ). No puedo evitar sentir que me falta algo.

¿Se descartan otros campos de la estructura considerados variables locales en este contexto? Eso no me queda claro en ninguna documentación de cara al usuario.

Bastante justo, abrí https://github.com/rust-lang/rust/issues/50765 para rastrear esto.


@pythonesque

Específicamente, encuentro que el ejemplo de RefCell es bastante preocupante, ya que en presencia de Pinned :: deref significa que ni siquiera podríamos imponer el anclaje dinámicamente con el tipo existente (no sé si la especialización sería suficiente). Esto sugiere además que si mantenemos la implementación de deref, tendremos que terminar duplicando la superficie de la API casi tanto con Pinned como con Pin; y si no lo guardamos, fijadose vuelve increíblemente difícil de usar.

La solución para RefCell es proporcionar métodos adicionales borrow_pin y borrow_pin_mut (tomando Pin<RefCell<T>> ), y realizar un seguimiento del estado anclado del interior de RefCell en tiempo de ejecución. Esto debería funcionar tanto para PinMut como para Pinned . Entonces, ¿su argumento aquí es que Pinned no ayuda? Tampoco debería empeorar las cosas por RefCell .

Pero luego escribes

la diferencia es que todavía podemos improvisar una solución con PinMut mientras se admite deref, que parece no funcionar con Pinned

y no sé a qué te refieres, ¿por qué no debería funcionar con Pinned ?

@comex

No creo que Pinned :: deref deba existir. Los accesos de campo seguros generados por macros deberían ser suficientes; No veo cómo eso es "increíblemente difícil de usar".

No veo la conexión entre deref y los descriptores de acceso de campo. Pero tampoco veo cómo el deref vuelve más problemático con Pinned<T> , así que supongo que primero esperaré una respuesta a mi pregunta anterior.

Al igual que @pythonesque , creo que rastrear el estado

Si vamos a optar deliberadamente por el enfoque que creemos que es menos "fundamentalmente correcto", al menos deberíamos dejarnos mucho tiempo para experimentar antes de estabilizarlo, de modo que podamos estar tan seguros como sea razonablemente posible de que no estamos va a terminar arrepintiéndome.

@pythonesque , wow, ¡muchas gracias por el extenso resumen! Me alegra saber que hay un RFC en proceso. :)

Es seguro llamar a una función que toma una referencia mutable a un valor fijado siempre que esa función no use algo como mem::swap o mem::replace . Por eso, se siente más natural que estas funciones usen el Unpin obligado que tener todos los derechos mutables de un Pin a Unpin inseguros.

Si una función se actualizara más tarde para usar swap , ya no sería seguro llamarla en una referencia mutable a un valor fijado. Cuando swap y replace tienen este límite, la función actualizada tenía que funcionar también hace que sea mucho más obvio que este no es un cambio compatible con versiones anteriores.

Así que algunos pensamientos que tuve:

  1. Básicamente, Drop proporciona el mismo privilegio que Unpin : puede obtener un &mut a algo que anteriormente estaba en un PinMut . Y Drop es seguro, lo que significa que Unpin debería ser seguro (esto es mem::forget y el apocalipsis de fugas de nuevo).
  2. Eso es bueno, porque significa que cosas como las API actuales basadas en futuros, que no manejan generadores de desanclaje, son 100% seguras de implementar incluso si toman self: PinMut<Self> (no unsafe impl Unpin ).
  3. ¿Es buena la API si Unpin es seguro? La respuesta es sí: siempre que los generadores no implementen Unpin y no sea seguro anclar el proyecto a un tipo !Unpin , todo está seguro.
  4. ¡Pero esto significa que las proyecciones de clavijas no son seguras! No es ideal.
  5. Las proyecciones de pines son seguras si Self no implementa Unpin o Drop [editar: ¿verdadero o falso?] ¿Podemos automatizar esa verificación?

Tengo algunas ideas para una alternativa más compatible con el lenguaje a esta API de biblioteca, que implican eliminar Unpin completo y, en su lugar, cambiar la polaridad, un rasgo de Pin que opta por obtener estas garantías, en lugar de optar por no participar . Pero esto requeriría un soporte de lenguaje significativo, mientras que la implementación actual está completamente basada en bibliotecas. Haré otra publicación después de haberlo pensado más.


otra nota porque sigo olvidando:

La seguridad de la proyección de clavijas depende solo del tipo Self, no del tipo de campo, porque el tipo de campo debe garantizar la seguridad de su API pública, que es incondicional. Por lo tanto, no es una verificación recursiva: si Self nunca mueve nada de su Pin , proyectar un pin a un campo de cualquier tipo es seguro.

@withoutboats FWIW esto coincide exactamente con las conclusiones a las que llegamos @cramertj y yo en nuestra ronda anterior de discusiones. Y creo que podemos automatizar la verificación de exclusión mutua, inicialmente a través de algunos atributos específicos emitidos por el derivado.

@sin barcos

Básicamente, Drop proporciona el mismo privilegio que Unpin: puede obtener un & mut para algo que estaba anteriormente en un PinMut. Y Drop es seguro, lo que significa que Unpin debería ser seguro (esto es mem :: olvide y lea el apocalipsis de nuevo).

No veo la conexión con el apocalipsis de fugas, pero estoy de acuerdo en lo contrario. La única razón por la que estoy (¿estaba?) Un poco indeciso es que mientras sea solo Drop , esto se sintió más como un caso de esquina para mí por el que no mucha gente tiene que preocuparse. Sin embargo, no estoy seguro de si eso es una ventaja o no. Y de todos modos, un Unpin seguro no solo aumenta la consistencia aquí y "resuelve" el problema Drop al no convertirlo en un caso especial (en su lugar, podemos pensar en cada impl Drop como viene con un impl Unpin implícito); por lo que dices, también hace que sea más fácil de usar en el lado Future de las cosas. Entonces, parece ser una victoria general.

@pythonesque a menos que me falte algo, el seguro Unpin tampoco causa ningún problema nuevo para las colecciones intrusivas, ¿verdad? Si funcionaron a pesar de la seguridad Drop , aún deberían funcionar.


@sin barcos

Las proyecciones de pines son seguras si Self no implementa Desanclar o Soltar [editar: ¿verdadero o falso?] ¿Podemos automatizar esa verificación?

Inicialmente también mencionaste el tipo de campo aquí y estaba a punto de preguntar por qué crees que es relevante. Ahora te veo editar la publicación. :) Estoy de acuerdo en que se trata únicamente del tipo Self . A continuación, repito prácticamente su argumento en mis términos.

Básicamente, la pregunta es: ¿Cómo elige Self su invariante de fijación? Por defecto, asumimos (¡incluso si hay código inseguro!) Que el invariante de fijación es exactamente el mismo que el invariante de propiedad, es decir, el invariante es independiente de la ubicación y este tipo no realiza la fijación. Siempre que no pueda hacer nada con un PinMut<T> más que convertirlo en un &mut T , esa es una suposición segura.

Para habilitar las proyecciones de campo, el invariante de fijación debería ser "todos mis campos están fijados en el invariante de su tipo respectivo". Ese invariante justifica fácilmente las proyecciones de fijación, independientemente de los tipos de campo (es decir, pueden elegir cualquier invariante de fijación que deseen). Por supuesto, este invariante es incompatible con convertir PinMut<T> en &mut T , por lo que es mejor asegurarnos de que esos tipos no sean Drop o Unpin .

No veo la conexión con el apocalipsis de fugas, pero estoy de acuerdo en lo contrario.

solo una analogía - Desanclar es soltar como mem :: olvidar es para ciclos de Rc. mem :: olvide se marcó originalmente como inseguro, pero no había ninguna justificación para ello. (Y el mismo argumento de que los ciclos Rc son un caso límite se hizo en contra de marcar mem :: olvide seguro).

Copiando y pegando (espiritualmente) de Discord, realmente me gustaría ver evidencia de que no solo hemos cambiado el problema: hacer que Unpin sea seguro de implementar, al hacer que los accesos de pines estructurales no sean seguros de implementar (esto también sería cierto con cualquier rasgo inseguro que se introdujo, ¿verdad? Aún tendría que escribir código inseguro). Esto me molesta porque la gran mayoría de las veces son completamente seguros, básicamente, siempre que no haya una implícita Unpin explícita para el tipo, al igual que siempre estamos seguros si no hay una implícita Drop para el tipo. Con el plan actual, estamos requiriendo algo mucho más fuerte - debería haber un implícito! Desanclar explícito para el tipo - que será cierto para muchos menos tipos (eso es casi todo lo que podemos hacer en una biblioteca).

Desafortunadamente, no tengo idea de cómo o si el compilador puede verificar si hay un impl Unpin manual para un tipo en particular o no, a diferencia de "tiene algún impl", y no estoy seguro si tiene malas interacciones con la especialización. Si tenemos alguna forma definida de realizar esa verificación, de modo que el creador de un tipo no tenga que escribir ningún código inseguro para obtener un anclaje estructural, estaría mucho más feliz con impl Unpin siendo seguro, creo ... ¿es eso algo que parece factible?

Tengo una pregunta simple que estoy tratando de entender ahora. En genéricos, ¿desanclar será un límite implícito como tamaño a menos que su API para todos los parámetros genéricos?

que debe ser correcto para vecpara seguir estando a salvo.

se desanclará será un límite implícito como tamaño

No.

que debe ser correcto para que vec continúe siendo seguro.

¿Por qué piensas eso?

Lo molesto que @MajorBreakfast golpeó hoy: PinMut::get_mut le da un &mut con la vida útil del PinMut , pero no hay una forma segura de hacer lo mismo, ya que DerefMut le da un préstamo con la duración de la referencia mutable a PinMut . ¿Quizás deberíamos hacer que get_mut el seguro y agregar get_mut_unchecked ?

¿Quieres decir así?

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

Sí, deberíamos tener eso.

@RalfJung Exactamente.

El método de la caja de futuros en el que me gustaría usar esto se ve así:

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

Tuve que usar un bloque inseguro a pesar de que F tiene un límite de Unpin y es completamente seguro. Actualmente no existe una forma segura de producir una referencia que conserve la vida útil de PinMut .

Sí, eso es solo una omisión en la API.

¿Quieres preparar un PR o debería?

Yo puedo hacerlo. ¿Queremos llamarlo get_mut y get_mut_unchecked ?

Agregaré un map seguro y cambiaré el nombre del actual a map_unchecked también. Principalmente por consistencia. Entonces todas las funciones inseguras de PinMut terminan en _unchecked y tienen un equivalente seguro

¿Queremos llamarlo get_mut y get_mut_unchecked?

Suena razonable.

Agregaré una caja fuerte map

Cuidado ahí. ¿Cómo quieres que se vea? La razón por la que el mapa no es seguro es que no sabemos cómo hacer uno seguro.

Cuidado ahí. ¿Cómo quieres que se vea?

Tienes razón. Necesita requerir un límite de Unpin en el valor de retorno.

Acabo de tener una idea: ¿Qué pasa con PinArc ? Podríamos usar algo así en el BiLock impl (https://github.com/rust-lang-nursery/futures-rs/pull/1044) en la caja de futuros.

@MajorBreakfast PinArc (y PinRc , etc.) depende de Pin (la versión no Mut ). Estaría dispuesto a agregarlo, pero no estaba seguro de si había un consenso sobre qué garantías ofrecería exactamente.

Ya no estoy tan seguro de si agregar PinArc agrega algo ^^ ' Arc ya no permite "salir del contenido prestado"

@MajorBreakfast Puede mudarse de Arc usando Arc::try_unwrap . Sería un cambio incompatible con versiones anteriores eliminar eso o introducir un límite de T: Unpin .

¡Hola!
He estado pensando un poco en cómo hacer que los accesores de pines funcionen de forma segura. Hasta donde tengo entendido, el problema con los accesos de pines ocurre _sólo_ cuando tienes la combinación T: !Unpin + Drop . Con ese fin, vi una discusión tratando de evitar la posibilidad de que exista un tipo T , por ejemplo, haciendo que los rasgos !Unpin y Drop mutuamente excluyentes de varias maneras, pero hay no era una forma clara de hacer esto sin romper la compatibilidad con versiones anteriores. En cuanto a este problema más de cerca, podemos obtener descriptores de acceso de PIN seguro sin impedir un tipo de ser !Unpin y Drop , es sólo que un tipo así no podía ser puesto en un Pin<T> . Evitar que eso suceda es algo que creo que es mucho más factible, y más en el espíritu de cómo funciona el rasgo Unpin todos modos.

Actualmente, tengo una "solución" para evitar que un tipo ingrese de manera segura un Pin<T> que sea tanto !Unpin como Drop . Requiere características que aún no tenemos en rust, lo cual es un problema, pero espero que sea un comienzo. De todos modos aquí está el código ...

/// 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 {}

Tanto los tipos Unpin como los tipos !Unpin pero también !Drop , no tienen problemas (que yo sepa) con los accesos de pines. Esto no rompe ningún código existente que use Drop , solo limita lo que se puede poner en una estructura Pin . En el improbable * caso de que alguien necesite que un tipo sea tanto !Unpin como Drop (y que pueda colocarse dentro de un Pin ), podría unsafe impl Pinnable por su tipo.

* No tengo experiencia con Pin , pero espero que la mayoría de los casos en los que alguien necesita impl Drop para un tipo que es !Unpin no sean causados ​​por lo que debe descartarse es inherentemente !Unpin , pero más bien causado por una estructura que tiene campos Drop campos !Unpin . En este caso, los campos Drop podrían separarse en su propia estructura que no contenga los campos !Unpin . Puedo entrar en más detalles sobre eso si es necesario, porque siento que la explicación fue dudosa, pero no se pretendía que este fuera el cuerpo principal del comentario, así que lo dejaré breve por ahora.

Por lo que tengo entendido, el problema con los accesos de pines ocurre solo cuando tienes la combinación T:! Desanclar + Soltar.

Es más un "o" que un "y".

Los accesos de pines implican una interpretación "estructural" de la fijación: cuando se fija T , también lo están todos sus campos. impl Unpin y Drop , por otro lado, son seguros porque asumen que al tipo no le importa en absoluto la fijación - T esté fijada o no haga ninguna diferencia; en particular, los campos de T no están anclados en ningún caso.

Sin embargo, lo entendiste bien más tarde cuando dijiste que los tipos !Unpin + !Drop son aquellos para los que los accesos son seguros para agregar. (El tipo aún debe tener cuidado de no hacer nada malo en su código inseguro , pero Unpin ad Drop son las dos formas seguras de romper los accesos de fijación).

Mirando este problema más de cerca, podemos obtener accesos de Pin seguros sin evitar que un tipo sea! Desanclar y soltar, es solo que tal tipo no se podría poner en un Pin.

Yo no sigo. ¿Por qué crees que es suficiente? Y si es así, ¿por qué estaríamos interesados ​​en los accesos fijados si un tipo no se puede fijar ...?

Es más un "o" que un "y".

Hablemos de esto por ahora, porque toda mi idea se basó en lo opuesto a esto, así que si he entendido mal esto, el resto de la idea no ayuda.

Según tengo entendido (actual), los accesores de pines con un tipo que es Unpin son completamente sólidos, independientemente de que se caigan, ya que Pin<T> es efectivamente &mut T todos modos. Además, los accesos de pines en un tipo que es !Drop son completamente sólidos, incluso para un tipo !Unpin , ya que drop es la única función que podría obtener &mut self en el tipo !Unpin una vez que ingresa un Pin , por lo que nada podría mover las cosas.

Si he cometido un error al respecto, hágamelo saber. Pero si no es así, entonces Unpin + Drop tipos o !Unpin + !Drop tipos estarían completamente bien con los accesos de pines, y los únicos tipos de problemas serían aquellos que son !Unpin + Drop .

@ashfordneil

Además, los accesos de pines en un tipo que es! Drop son completamente sólidos, incluso para un tipo! Unpin, ya que drop es la única función que podría obtener & mut self en el tipo! Unpin una vez que ingresa a un Pin, por lo que nada podría hacerlo mover las cosas.

Si escribo struct Foo(InnerNotUnpin); impl Unpin for Foo {} entonces no es correcto pasar de PinMut<Foo> a PinMut<InnerNotUnpin> . Para que la proyección sea sólida, debe (a) prohibir la caída de impls y (b) asegurarse de que la estructura sea Unpin solo cuando el campo sea Unpin .

(a) debería estar prohibiendo la caída de implicaciones en cosas que son !Unpin ¿verdad?
(b) debe manejarse por el hecho de que implementar Unpin no es seguro; seguro que es posible dispararse en el pie al afirmar de manera insegura que un tipo se puede desanclar cuando en realidad no puede, pero es tan posible etiquetar algo como Sync cuando no debería ser y terminar con un comportamiento inadecuado de esa manera

(a) ¡Debería prohibir soltar implicaciones en cosas que son!

Sí, pero eso es imposible por razones de compatibilidad con versiones anteriores.

Y eso llega a mi punto: ¡prohibir la caída implica en cosas que son! Desanclar es imposible por razones de compatibilidad con versiones anteriores. Pero no es necesario que evitemos que se produzcan cambios indirectos en las cosas que están! El marcador! Unpin en un tipo no tiene significado y no hace promesas hasta que ese tipo está dentro de un Pin, por lo que el implícito de caída en un tipo! Unpin es completamente correcto _ siempre que ese tipo no esté dentro de un Pin_. Lo que propongo es que cambiemos Pin de Pin<T> a Pin<T: Pinnable> donde el rasgo Pinnable actúa como un guardián para evitar tipos que son Drop + !Unpin (o más generalmente, los tipos para los que los accesos de pines son problemáticos) se coloquen dentro de Pin .

En ese punto, estás haciendo que el rasgo Unpin completamente inútil, ¿no es así? Además, todavía no puedes expresar los límites necesarios con Rust actual. Si pudiéramos decir " Unpin y Drop son incompatibles", no habría ningún problema. Parece que necesita algo similar con Pinnable . No veo cómo esto ayuda.

El rasgo Unpin, por sí solo, ya es "inútil". El rasgo _sólo_ significa algo una vez que el tipo ha sido fijado, el tipo se puede mover antes de entrar en el Pin.

@RalfJung y yo acabamos de tener una reunión para hablar sobre estas API y acordamos que estamos listos para estabilizarlas sin cambios importantes en la API.

Resolución de preguntas pendientes

Mi creencia sobre las API de PIN es que:

  1. Los conceptos básicos de la API de PinMut y Unpin constituyen la mejor solución posible que no implica soporte de lenguaje incorporado.
  2. Estas API no excluyen el soporte futuro de idiomas integrados si decidimos que es necesario.

Sin embargo, hay algunas preguntas pendientes en torno a sus limitaciones y compensaciones por las que quiero dejar constancia de nuestra decisión.

El problema de Drop

Un problema que no descubrimos hasta después de fusionar el RFC fue el problema del rasgo Drop . El método Drop::drop tiene una API que se toma a sí mismo &mut self . Esto es esencialmente "desanclar" Self , violando las garantías que se supone que deben proporcionar los tipos anclados.

Afortunadamente, esto no afecta la solidez de los tipos principales de "generador inamovible" que estamos tratando de proyectar con el pin: no implementan un destructor, por lo que su implementación !Unpin realmente lo significa .

Sin embargo, lo que esto significa es que para los tipos que usted mismo define, es necesariamente seguro "desanclarlos", todo lo que tiene que hacer es implementar Drop . Lo que esto significa es que Unpin siendo un unsafe trait no nos estaba comprando ninguna seguridad adicional: implementar Drop es tan bueno como implementar Unpin en lo que respecta a la solidez preocupado.

Por esta razón, hemos hecho del rasgo Unpin un rasgo seguro de implementar.

Proyección de pin

La principal limitación con la API actual es que no es posible determinar automáticamente que es seguro realizar una proyección de pin cuando el campo no implementa Unpin . Una proyección de alfiler es:

Dado un tipo T con un campo a con el tipo U , puedo "proyectar" de forma segura desde PinMut<'a, T> a un PinMut<'a, U> accediendo el campo a .

O, en código:

struct Foo {
    bar: Bar,
}

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

En este momento, a menos que el tipo de campo implemente Unpin , no tenemos que rustc _probar_ que esto es seguro. Es decir, implementar este tipo de método de proyección requiere actualmente un código inseguro. Afortunadamente, puede demostrar que es seguro. Si el tipo de campo no puede implementar Unpin , esto solo es seguro si todos estos se cumplen:

  1. El tipo Self no implementa Unpin , o solo implementa condicionalmente Unpin cuando lo hace el tipo de campo.
  2. El tipo Self no implementa Drop , o el destructor del tipo Self nunca mueve nada fuera de ese campo.

Con las extensiones de lenguaje, podríamos hacer que el compilador verifique que algunas de estas condiciones se cumplen (por ejemplo, que Self no implementa Drop y solo implementa de forma condicional Unpin ).

Debido al problema de Drop , un rasgo estable, este problema existía antes de cualquier decisión que pudiéramos tomar sobre este tema, y ​​no hay forma sin cambios de idioma para que la proyección de pines sea automáticamente segura. Algún día, podemos hacer esos cambios, pero no bloquearemos la estabilización de estas API en eso.

Especificación de la garantía "pin"

Al final del proceso de RFC, @cramertj se dio cuenta de que con una ligera extensión más allá de lo que necesitamos para los generadores inamovibles, la fijación podría usarse para encapsular de manera segura las garantías de las listas intrusivas. Ralf escribió una descripción de esta extensión aquí .

Básicamente, la garantía de fijar es la siguiente:

Si tiene un PinMut<T> , y T no implementa Unpin , T no se moverá y el respaldo de la memoria T no se invalidará hasta que el destructor se ejecute por T .

Esto no es exactamente lo mismo que "libertad de fugas": T aún puede tener fugas, por ejemplo, si se queda atascado detrás de un ciclo Rc, pero incluso si se filtra, eso significa que la memoria nunca se invalidará (hasta que el programa termina), porque el destructor nunca se ejecutará.

Esta garantía excluye ciertas API para la fijación de pilas, por lo que al principio no estábamos seguros de extender la fijación para incluir esto. Sin embargo, en última instancia deberíamos apostar por esta garantía por varias razones:

  1. Se han descubierto otras API de fijación de pilas, incluso más ergonómicas, como esta macro , que eliminan las desventajas.
  2. ¡La ventaja es bastante grande! Este tipo de listas intrusivas podría tener un gran impacto en bibliotecas como josephine que integran Rust con lenguajes de recolección de basura.
  3. Ralf nunca estuvo seguro de que algunas de estas API fueran realmente sólidas para empezar (especialmente las que se basan en el "préstamo permanente").

Reorganización de API

Sin embargo, sí quiero hacer un cambio propuesto final a la API, que es mover las cosas. Al revisar las API que existen, Ralf y yo nos dimos cuenta de que no había un buen lugar para describir las garantías de alto nivel y las invariantes relacionadas con Drop y así sucesivamente. Por tanto, propongo

  1. Creamos un nuevo módulo std::pin . Los documentos del módulo proporcionarán una descripción general de alto nivel de la fijación, las garantías que proporciona y los usuarios invariantes de sus partes inseguras que se espera que mantengan.
  2. Movemos PinMut y PinBox de std::mem y std::boxed al módulo pin . Dejamos Unpin en el módulo std::marker , con los otros rasgos del marcador.

También necesitaremos escribir esa documentación más extensa antes de terminar de estabilizar estos tipos.

Adición de implementaciones Unpin

Ralf y yo también discutimos agregar más implementaciones de Unpin a la biblioteca estándar. Al implementar Unpin , siempre hay una compensación: si Unpin se implementa incondicionalmente con respecto al tipo de campo, no puede anclar el proyecto a ese campo. Por esa razón, no hemos sido muy agresivos con la implementación de Unpin hasta ahora.

Sin embargo, creemos que por regla general:

  1. La proyección de clavijas a través de un puntero nunca debe considerarse segura (consulte el recuadro de notas a continuación).
  2. La proyección de clavija a través de mutabilidad interior nunca debe tratarse como segura.

En general, los tipos que hacen una de estas cosas hacen algo (como reasignar la tienda de respaldo como en Vec ) que hace que la proyección de pines sea insostenible. Por esa razón, creemos que sería apropiado agregar una implementación incondicional de Unpin a todos los tipos en std que contienen un parámetro genérico ya sea detrás de un puntero o dentro de una UnsafeCell; esto incluye todo std::collections por ejemplo.

No he investigado demasiado de cerca, pero creo que sería suficiente para la mayoría de estos tipos si solo agregamos estas implicaciones:

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

Sin embargo, hay un problema: técnicamente, creemos que el pin que se proyecta a través de Box podría hacerse técnicamente seguro: es decir, pasar de PinMut<Box<T>> a PinMut<T> se puede hacer de forma segura. Esto se debe a que Box es un puntero de propiedad total que nunca se reasigna.

Es posible que queramos proporcionar un agujero para que Unpin solo se implemente condicionalmente para Box<T> cuando T: Unpin . Sin embargo, mi opinión personal es que debería ser posible pensar en la fijación como un bloque de memoria que se ha fijado en su lugar, por lo que toda proyección a través de la indirecta del puntero debería ser insegura.

Podríamos retrasar esta decisión sin bloquear la estabilización y agregar estas implicaciones con el tiempo.

Conclusión

Realmente me encantaría escuchar los pensamientos de otras personas que han invertido mucho en las API de pin hasta ahora, y si este plan de estabilización suena razonable o no. Haré un resumen más corto con una propuesta de FCP en una publicación posterior, para el resto del equipo de idiomas.

@rfcbot fcp fusionar

Propongo estabilizar las API de pin existentes después de una pequeña reorganización. Puedes leer un resumen más largo en mi publicación anterior . El TL; DR en mi opinión es que la API actual es:

  • Sonido
  • Tan bueno como se pone sin cambios de idioma
  • Adelante compatible con cambios de idioma que lo mejorarían

Hemos resuelto todas las preguntas principales con respecto a las garantías precisas y las invariantes de la fijación en los últimos meses, las decisiones en las que estamos actualmente (que creo que son las decisiones en las que deberíamos estabilizarnos) están documentadas en la publicación más larga.

Propongo algunos cambios antes de que estabilicemos las cosas:

  1. Reorganice PinMut y PinBox bajo un nuevo módulo std::pin , que contendrá documentación de alto nivel con respecto a las invariantes y garantías de fijación en general.
  2. Agregue implementaciones incondicionales de Unpin para los tipos en std que contienen parámetros genéricos, ya sea detrás de una indirección de puntero o dentro de una UnsafeCell; la justificación de por qué estas implementaciones son apropiadas se encuentra en el resumen más largo.

El miembro del equipo @withoutboats ha propuesto fusionar esto. El siguiente paso lo revisan el resto de equipos etiquetados:

  • [] @Kimundi
  • [] @SimonSapin
  • [] @alexcrichton
  • [] @dtolnay
  • [] @sfackler
  • [x] @withoutboats

No hay preocupaciones actualmente enumeradas.

Una vez que la mayoría de los revisores aprueben (y ninguno objete), entrará en su período de comentarios final. Si detecta un problema importante que no se ha planteado en ningún momento de este proceso, ¡hable!

Consulte este documento para obtener información sobre los comandos que pueden darme los miembros del equipo etiquetados.

@rfcbot fcp cancelar

Estoy etiquetando a lang también debido a las decisiones fundamentales invariantes que hemos tenido que tomar, así que cancelo y reinicio el FCP.

propuesta de @withoutboats cancelada.

@rfcbot fcp merge Vea el mensaje de combinación anterior y el resumen extenso, ¡lo siento!

El miembro del equipo @withoutboats ha propuesto fusionar esto. El siguiente paso lo revisan el resto de equipos etiquetados:

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

Preocupaciones:

Una vez que la mayoría de los revisores aprueben (y ninguno objete), entrará en su período de comentarios final. Si detecta un problema importante que no se ha planteado en ningún momento de este proceso, ¡hable!

Consulte este documento para obtener información sobre los comandos que pueden darme los miembros del equipo etiquetados.

Estoy feliz de ver cierta confianza en que esto es correcto. Sin embargo, es difícil manejar todo lo que ha cambiado desde el RFC original; ¿Podría actualizarse el texto RFC para reflejar el diseño que se propone para la estabilización? (En general, este es un problema que tienen muchos RFC, donde cambian tanto durante el período de implementación que tratar de comprenderlo leyendo el RFC es inútil. Deberíamos encontrar una manera de hacerlo mejor allí).

Las RFC no son una especificación. La lectura del RFC no sustituye a la documentación real.

@bstrie, el cambio significativo del RFC ( Unpin es seguro) está cubierto en mi publicación de resumen. A largo plazo, las personas deben comprender la API de fijación leyendo los documentos en std::pin (un bloqueador de la estabilización), no desenterrando el RFC original.

Si la intención es que la documentación bloquee la estabilización, pero que la documentación aún no se ha escrito, ¿no es este FCP prematuro? Esperar que los comentaristas potenciales reconstruyan la documentación tentativa ellos mismos juntando fuentes dispares es, en mi opinión, una barrera de entrada bastante más alta de lo que uno quisiera.

@bstrie es nuestro procedimiento estándar proponer FCP antes de la documentación. Escribí un comentario de resumen muy extenso sobre el estado de la situación que debería proporcionar suficiente información para cualquiera que no haya seguido el tema del seguimiento, así como un comentario más corto para las personas que no quieren tanto contexto.

Para desarrollar un poco más, FCP es la pregunta "¿queremos estabilizar esto?" Si esa respuesta es "no", entonces se ha desperdiciado un montón de trabajo en documentos. Completar FCP no significa que la función se estabilice instantáneamente; significa que se ha tomado la decisión de estabilizarlo, por lo que ahora es el momento de hacer el trabajo necesario para hacerlo; que incluye trabajo de compilación y documentación.

El 2 de agosto de 2018, a las 12:56 p.m., boats [email protected] escribió:

@bstrie es nuestro procedimiento estándar proponer FCP antes de la documentación. Escribí un comentario de resumen muy extenso sobre el estado de la situación que debería proporcionar suficiente información para cualquiera que no haya seguido el problema del seguimiento

-
Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub o silencia el hilo.

@sin barcos

La proyección de clavijas a través de un puntero nunca debe considerarse segura (consulte el recuadro de notas a continuación).
La proyección de clavija a través de mutabilidad interior nunca debe tratarse como segura.

¿Puedes aclarar un poco este punto? ¿Se refiere específicamente a los tipos de la biblioteca estándar ? Similar a Box , hay tipos como Mutex que podrían proyectar la fijación ya que son los únicos propietarios de sus contenidos (aunque estén fuera de banda). Al menos fuera de la biblioteca estándar, supongo que querríamos tener la capacidad de tener primitivas de concurrencia que proyecten PinRef<InPlaceMux<T>> a PinMut<T> en una llamada a .lock() , que parece como un caso de "proyección por mutabilidad interior".

@cramertj Proyectar en un Mutex parece peligroso; uno podría usar la conversión segura a &Mutex y luego Mutex::lock para obtener un &mut y luego mover las cosas.

EDITAR: Oh, te refieres a un Mutex siempre fijado. Como PinMutex . Sí, eso funcionaría. La mutabilidad interior está bien si nunca reparte &mut . Pero parece poco probable que tengamos tales estructuras de datos en libstd.

Pero sí, para una precisión total, la declaración de @withoutboats debe enmendarse para decir que "la proyección de pines a través de mutabilidad interior es segura solo si el tipo siempre pines, es decir, nunca reparte &mut ".

@RalfJung Exactamente, sí.

Pero parece poco probable que tengamos tales estructuras de datos en libstd.

Correcto, quería asegurarme de que los requisitos establecidos sobre no proyectar a través de punteros o mutabilidad interior estuvieran específicamente relacionados con libstd, no con reglas generales.

@cramertj definitivamente no es una regla

Estoy decepcionado por la decisión de estabilizar esto. No me preocupa demasiado lo que suceda a corto plazo, ya que sin el soporte del compilador, cualquier diseño potencial se verá un poco pirateado. Sin embargo, a largo plazo, si Rust gana tipos de inmuebles nativos, no se sentirán verdaderamente de primera clase a menos que se puedan usar con métodos de rasgos que acepten &self y &mut self . Como consecuencia, la fijación debe ser una propiedad del tipo de referencia, no del tipo de referencia. Eso todavía puede suceder, por supuesto, en un diseño futuro para tipos de inmuebles nativos. Pero resultaría en que Pin<T> volviera redundante, no solo, digamos, un alias obsoleto para algunos &pin T nativos, sino que sería completamente innecesario. A su vez, rasgos como Future (y potencialmente muchos otros) que aceptan Pin<Self> necesitarán ser renovados o desaprobados, lo que requeriría un período de transición complicado.

Desordenado, no imposible. Pero a riesgo de ser demasiado pesimista, me preocupa que el deseo de evitar tal transición sesgue las decisiones futuras hacia la adopción de &pin como base para los tipos de inmuebles nativos, que, en mi opinión, dejarán ellos permanentemente de segunda clase. Y sigo pensando que entre los diseños a corto plazo, el enfoque &Pinned<T> habría sido factible, evitando estas preocupaciones de compatibilidad hacia adelante y teniendo otros beneficios también (por ejemplo, evitando por completo el problema de la mutabilidad interior).

Oh bien. Tal como están las cosas, todavía estoy deseando usar Pin , pero hubiera preferido que se quedara en crates.io en lugar de ingresar a la biblioteca estándar.

Estoy de acuerdo en que es demasiado pronto para estabilizar esto. Actualmente es algo incómodo en algunas situaciones, por ejemplo, al acceder a campos de tipo !Unpin dentro de un método llamado PinMut<Self> . Creo que los cambios de @withoutboats mejorarían la situación, pero no estoy seguro. ¿No valdría la pena esperar hasta que esos cambios se apliquen y se prueben al menos por un tiempo?

@Thomasdezeeuw ¿Qué cambios crees que mejorarían la situación? Los cambios que he propuesto no parecen relacionarse con ese inconveniente.

No hay forma de escribir una solución genérica, es decir, agregar un método a std, pero esta variación de la macro de futuros es segura:

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 Eso es seguro porque solo proporciona acceso a Unpin datos, ¿verdad?

@RalfJung exactamente! la línea de la cláusula where lo hace seguro

@withoutboats es una buena macro, pero solo funciona para los tipos Unpin .

Pero, ¿no sería posible agregar un método / macro que tomaría PinMut<Self> y devolvería PinMut<Self.field> , preferiblemente en código seguro? Podría estar equivocado, pero mi opinión es que si la persona que llama garantiza que Self está anclado, entonces también lo está Self.field , ¿verdad? Eso también debería funcionar para tipos que no implementan Unpin .

@Thomasdezeeuw Este problema se analiza en mi resumen en la sección "Proyección de alfiler"; no es seguro proyectar en los campos !Unpin menos que también garantice que Self no hace ciertas otras cosas, para las cuales no podemos generar un cheque. Solo podemos garantizar automáticamente la capacidad de proyectar en campos que implementan Unpin (porque eso siempre es seguro, y podemos verificarlo con una cláusula where ).

@withoutboats He leído el resumen, pero tal vez lo T es !Unpin entonces PinMut<T> no implementará DerefMut for T , por lo que mover el valor no sería posible sin un código inseguro. Todavía tienes el problema con el rasgo Drop , pero eso es más grande que solo tener acceso al campo de un tipo. Entonces no veo qué hace que el mapeo de PinMut<T> a PinMut<T.field> inseguro, incluso si T.field es !Unpin .

@Thomasdezeeuw En futuros-rs tenemos situaciones en las que tenemos un tipo dentro de un PinMut , pero uno de sus campos no se considera anclado

La desventaja de la macro de @withoutboats es que solo puede dar acceso a un campo a la vez porque contiene una referencia mutable a self . Los accesos a los campos de estructuras no tienen esta limitación.

Creo que decidir estabilizar la API de fijación es la decisión correcta. Sin embargo, sugeriría implementar primero el módulo core::pin .

Todavía tiene el problema con el rasgo Drop, pero eso es más grande que simplemente obtener acceso al campo de un tipo.

No, no lo es. Drop está bien siempre y cuando no realice el mapeo de campos . Es por eso que el mapeo de campo no es seguro.

Todavía tiene el problema con el rasgo Drop, pero eso es más grande que simplemente obtener acceso al campo de un tipo. Entonces no veo qué hace que el mapeo de PinMuta PinMutinseguro, incluso si T.field lo es!

Para ser claros, tampoco podemos generar automáticamente una prueba de que T: !Unpin . Pero incluso si no lo es, aquí hay un ejemplo de cómo se puede usar 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();
    }
}

Como dice @RalfJung , este es exactamente el problema con Drop : si no hace ninguna proyección de pin en los campos !Unpin , todo lo que haga en Drop está bien.

La desventaja de la macro de @withoutboats es que solo puede dar acceso a un campo a la vez porque contiene una referencia mutable a sí mismo. Los accesos a los campos de estructuras no tienen esta limitación.

Puede escribir el método que da acceso a múltiples campos, pero debe hacerlo para cada combinación que le interese.

Por curiosidad, ¿hay otros casos de uso concretos además de los futuros, o al menos algo más que motiva el deseo de estabilizarlos ahora (a diferencia de más adelante con más experiencia)?

Parece que no hay más discusión aquí con respecto a los puntos planteados por @comex. Me dieron mucha curiosidad, porque no recuerdo la idea de hacer de pin una propiedad del tipo en lugar de la referencia. ¿Esto ya se ha discutido en otra parte? No es fácil seguir todo desde "el exterior", hago lo mejor que puedo ;-)

A su vez, rasgos como Future (y potencialmente muchos otros) que aceptan Pinserá necesario renovarlo o desaprobarlo, lo que requeriría un período de transición complicado.

Hmmm. ¿Podría alguna magia relacionada con la edición permitirnos envolver y desenvolver silenciosamente estos Pin s, y hacer que los cambios sean totalmente compatibles con versiones anteriores? Algo asqueroso, pero no demasiado, y mantendría el lenguaje y las API limpios durante dicha transición.

Tal vez la idea debería desarrollarse más para responder a esta pregunta (recuerdo que esto se mencionó antes en el hilo, pero perdí la noción de dónde).

@yasammez Pude retomar la discusión sobre la fijación orientada a valores por aquí

Solo para confirmar, ¿el camino hacia la estabilización no incluye un plan para proyecciones de campo seguras? Me parece que esto introducirá una gran cantidad de código inseguro en todas las bibliotecas futuras. En mi experiencia, las implicaciones futuras manuales no son infrecuentes y sé que todas las mías requieren sondeo de campos.

No creo que importe que gran parte de este código inseguro se pruebe trivialmente como seguro. La mera existencia de un código inseguro diluye la capacidad de auditar código inseguro más real .

@tikue a corto plazo, puede:

  1. use una macro como la macro unsafe_pinned que desarrolló la biblioteca de futuros, limitándose a una única insegura para cada encuesta interna, que debe verificar con las restricciones descritas en mi largo resumen (tampoco puede usar una macro y simplemente escriba el descriptor de acceso a mano, también es solo un unsafe ).
  2. requiera que sus campos implementen Unpin , lo que hace que tales proyecciones sean trivialmente seguras y no requieran código inseguro, a costa de la asignación dinámica de futuros de !Unpin .

@withoutboats Entiendo las opciones pero las encuentro difíciles de paladar. Exigir Unpin restringirá la compatibilidad con una gran cantidad de futuros, como cualquier cosa que se tome prestada, por ejemplo, cualquier fn asincrónico que use canales. Y la contaminación peligrosa es una preocupación real.

Hablando en términos prácticos, he intentado migrar una biblioteca a futuros 0.3 y me ha resultado complicado por estas razones.

@tikue Esto es compatible con el futuro y le proporciona el cheque de forma automatizada. Aún no lo hemos diseñado o implementado por completo, pero nada aquí impide que suceda.

Sin embargo, realmente no entiendo esta preocupación:

Y la contaminación peligrosa es una preocupación real.

Creo que el "problema de lo inseguro" se deja con demasiada frecuencia en este nivel: solo una prohibición general de unsafe .

Para la mayoría de los futuros anidados, debería ser muy fácil determinar si puede usar con seguridad la macro unsafe_pinned! . La lista de verificación suele ser así:

  1. No implementé manualmente Unpin para mi futuro, sino que solo confío en el implícito del rasgo automático.
  2. No implementé manualmente Drop para mi futuro, porque no tengo un destructor personalizado.
  3. El campo en el que quiero proyectar es privado.

En ese caso: ¡estás bien! unsafe_pinned es seguro de usar.

Si se implementaron manualmente Unpin o Drop , usted tiene que pensar realmente en ello, pero incluso en ese caso no es necesariamente un problema tan difícil:

  1. Si implementé Unpin , está restringido en el futuro que abstraigo sobre ser Unpin .
  2. Si implementé Drop , nunca muevo el campo futuro durante mi destructor.

@tikue como referencia, he escrito un resumen de la propuesta básica para una solución basada en atributos . Esto requiere compatibilidad con el compilador, pero no extensiones significativas del lenguaje: solo un nuevo rasgo y un nuevo atributo incorporado.

También hay una solución más "intrusiva" en la que agregamos los tipos de referencia &pin adecuados, que tendrían la misma forma en términos de restricciones, pero tendrían más impacto en el lenguaje en su conjunto.

¿Qué pasa con 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);
  } }
}

Incluso si no queremos agregar un método de este tipo, deberíamos tener una respuesta a la pregunta de si esto es seguro, por PinMut , y también por PinBox que podría tener algo similar.

Creo que ciertamente es seguro para PinBox , porque me estoy ocupando de llamar al destructor "en su lugar" y luego esto es solo reutilizar la memoria. Sin embargo, estoy un poco preocupado con PinMut acerca de que el valor se elimine antes, es decir, antes de que termine su vida útil. No me parece claro que esto siempre esté bien, pero tampoco puedo encontrar un contraejemplo. @cramertj @withoutboats ¿ Alguna idea?

( Agregaría esto a

@rfcbot preocupación reemplazar

@RalfJung Podría estar equivocado aquí, pero, ¿no podría ese código caer dos veces si T entra en pánico?

@RalfJung Ya tenemos PinMut::set .

@cramertj D'oh. Sí, está bien, podría haber sido un poco más minucioso al verificar esto.

Perdón por el ruido, mi inquietud está resuelta.

@rfcbot resolver reemplazar

Este es el código real que estoy escribiendo en este momento para futuros 0.1 => 0.3 compatibilidad con tokio_timer::Deadline . Necesita hacer un PinMut<Future01CompatExt<Delay>> , donde Delay es un campo 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)
        };

Además del uso no trivial de inseguro, también me tomó alrededor de 5 iteraciones en este código encontrar una versión que compilara. Escribir y pensar en lo que está sucediendo aquí es bastante poco ergonómico.

Creo que hay mucho espacio entre una prohibición general de inseguridad y este código. Por ejemplo, map_unchecked es demasiado restrictivo para ayudar con este código porque requiere devolver una referencia, mientras que este código requiere devolver Future01CompatExt<&mut Delay> .

Editar: Otra fuente de falta de ergonomía es que no puedes pedir prestado de manera mutante en los guardias de partido, por lo que PinMutno puedo trabajar con un código como este:

`` óxido
coincidir con self.poll_listener (cx)? {
// solía ser si self.open_connections> 0
Encuesta :: Listo (Ninguno) si self.open_connections ()> 0 => {
^^^^ prestado mutantemente en patrón protector
return Poll :: Pendiente;
}
Encuesta :: Listo (Ninguno) => {
return Poll :: Ready (Ninguno);
}
}
`` ``

Su comentario sobre map_unchecked está en el punto, tal vez haya una firma más genérica que también pueda permitir devolver temporales alrededor de punteros.

¿Existen pautas sobre cuándo es seguro obtener una referencia &mut de un campo de PinMut<Type> ? Creo que siempre que Type nunca asuma que el campo está anclado, ¿está bien? ¿Tiene que ser privado?

Sí, el campo implementa Unpin o nunca construyes un PinMut del campo.

@rfcbot varianza

Estoy escribiendo una cantidad decente de código basado en PinMut y una cosa que ha sucedido mucho es que tengo una operación que no se basa en la fijación, pero se demora PInMut en lugar de &mut como una forma de decir "Prometo que no saldré de esta memoria". Sin embargo, esto obliga a todos los usuarios a tener un valor fijo, lo cual es innecesario. Lo he estado pensando y no pude encontrar una buena API, pero sería maravilloso si hubiera una manera de separar "Necesito que mi argumento esté fijado" de "Puedo trabajar con un argumento".

Creo que "varianza" es el término equivocado allí; lo que desea son constructores de tipos asociados para abstraerlos sobre diferentes tipos de referencia: D

@RalfJung Sí, eso es cierto. Sin embargo, me conformaría con un tipo como podría provenir de un PinMut o de un & mut, pero solo se puede usar como un PinMut, aunque sé que esto no se escala particularmente bien. Creo que eso es más alcanzable que los arbitrary_self_types genéricos: smile: Quizás sea mejor cerrar con "estamos bastante seguros de que estamos preparados para el futuro para funciones de firmas como esta":

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

@rfcbot preocupación api-refactor

Un poco de estructura de inspiración y anoche descubrí cómo podríamos refactorizar esta API para que solo haya un tipo Pin , que envuelve un puntero, en lugar de tener que crear una versión anclada de cada puntero. Esta no es una remodelación fundamental de la API de ninguna manera, pero se siente mejor extraer el componente "fija la memoria" en una pieza componible.

Cuando estaba trabajando en la pila de red para nt-rs , me dijeron que las referencias fijadas ayudarían con el "baile de propiedad" que actualmente estoy resolviendo con fold como se ve aquí (tx.send mueve tx a un futuro Send<<type of tx>> , lo que dificulta el bucle de datos entrantes para enviar paquetes). ¿De qué manera ayudaría fijar con alfileres ese tipo de cosas?

@Redrield send() toma &mut self en futuros 0.3.

@withoutboats ¡Estoy ansioso por probar esta nueva API en futures-rs!

Dado que la nueva API usa diferentes identificadores, creo que debería ser posible tener ambas API disponibles simultáneamente. Creo que sería mejor tener un período de transición corto durante el cual ambas API estén disponibles, lo que nos permite experimentar, preparar un PR y portar la API de futuros en libcore al nuevo estilo.

@MajorBreakfast parece que potencialmente podríamos alias type PinMut<T> = Pin<&mut T>; y ofrecer varios de los mismos métodos, lo que reduciría la magnitud e inmediatez de la rotura.

@sin barcos

Entonces, con la nueva API:

  1. Pin<&T> y Pin<&mut T> tienen el invariante de "fijación estándar", donde el valor detrás de ellos:
    1.a. ya no es un &T / &mut T válido, a menos que Unpin
    1.b. no se utilizará como Pin de otra dirección de memoria.
    1.c. se eliminará antes de invalidar la memoria.
  2. Pin<Smaht<T>> no tiene ninguna garantía "especial", excepto que devuelve Pin<&mut T> y Pin<&T> válidos cuando se le solicita (que es solo la garantía de seguridad estándar, dado que las API son seguras ).
    2.a. ¿ Queremos una garantía de DerefPure , donde se requiere Pin<Smaht<T>> para devolver el mismo valor a menos que esté mutado? ¿Alguien quiere eso?

Corrección sobre el invariante.

El invariante para Pin<Smaht<T>> es:

  1. Llamar a Deref::deref(&self.inner) dará un Pin<&T::Target> válido (tenga en cuenta que &self.inner no necesita ser un &Smaht<T> seguro).
  2. Si Smaht<T>: DerefMut , llamar a DerefMut::deref_mut(&mut self.inner) dará un Pin<&mut T::Target> válido (tenga en cuenta que &mut self.inner no necesita ser un &mut Smaht<T> seguro).
  3. Llamar al destructor de self.inner para destruirlo está bien siempre que sea seguro para la destrucción.
  4. self.inner no tiene que ser un Smaht<T> seguro; no tiene que admitir otras funciones.

Hubo algunos comentarios publicados en la publicación de reddit sobre la propuesta de @withoutboats :

  • (múltiple) Own es un nombre extraño, vea a continuación las posibles formas de resolver esto.

  • ryani pregunta por qué las implementaciones que restringen Deref<Target = T> tienen ese T genérico adicional? ¿No puedes usar P::Target ?

  • jnicklas pregunta cuál es el propósito del rasgo Own , ¿qué hay de agregar pinned métodos inherentes por convención? Esto significa que los usuarios de la API no necesitan importar el rasgo, sin embargo, perderá la capacidad de ser genéricos en lugar de constructores pinnables. ¿Es esto un gran problema? La motivación del rasgo parece estar insuficientemente motivada: _ [ellos] tienen la misma forma después de todo '.

  • RustMeUp (yo mismo) pregunta, en el rasgo Own , ¿cuál es el propósito del método own ? ¿Por qué el rasgo no puede simplemente dejar pinned para que lo implementen los tipos que desean participar? Esto permite que el rasgo sea seguro y permite que el rasgo se llame Pinned que se siente menos incómodo que Own . La razón del propio método no parece estar suficientemente motivada.

Agregaré otra pregunta:

¿Por qué ni Pin<P> ni Pin<P>::new_unchecked restringidos a 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 }
    }
}

o

#[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 }
    }
}

ambos evitarían Pin<P> donde P: !Deref instancias que son inútiles ya que no hay nada que puedas con él a menos que se agreguen métodos adicionales. Yo creo que...

Aquí hay un resumen con el mío y los comentarios de ryani tratados.

Ryani pregunta por qué las implementaciones que restringen a Deref¿Tienes esa T genérica adicional? ¿No puedes usar P :: Target?

No hay ninguna diferencia entre estas dos implicaciones aparte de cómo están escritas.

¿Por qué ni Pin

sí mismo ni Pin

:: new_unchecked restringido a P: Deref?

No tengo opinión; históricamente, la biblioteca estándar no se ha limitado a estructuras, pero no estoy seguro de si esa política está bien motivada o es un accidente histórico.


Planear implementar esto después de que # 53227 sea aterrizado, pero no implementará el rasgo Own porque es controvertido Por ahora, solo un pinned constructor inherente en Box , Rc y Arc .

Siguiendo un poco lo que escribió @ arielb1 , veo Pin aquí actuando en realidad sobre "constructores de tipo puntero", cosas como Box o &'a mut que tienen "kind" * -> * . Por supuesto, en realidad no tenemos sintaxis para eso, pero esa es una forma en la que puedo darle sentido a esto en términos de invariantes. Todavía tenemos 4 (seguridad (invariantes por tipo como en la publicación de mi blog : propiedad, compartida, propiedad fijada, compartida fijada.

Creo que una formalización adecuada requiere esta vista, porque la idea es que Pin<Ptr><T> toma los invariantes de T , los transforma (usando los invariantes fijados en todas partes) y luego aplica el Ptr constructor a ese tipo resultante. (Esto requiere cambiar la forma en que definimos Owned , pero eso es algo que planeé a largo plazo de todos modos). En términos de formalismo, uno realmente preferiría escribir Ptr<Pin<T>> , pero lo hemos intentado eso y no funciona muy bien con Rust.

La promesa, entonces, que hace un constructor de tipo puntero cuando "opta por" Pin es que aplicar Deref / DerefMut será seguro: que cuando la entrada sea realmente Ptr aplicado a la variante de propiedad fijada / compartida del tipo, la salida satisfará la variante de propiedad fijada / compartida de ese tipo.

Estos son realmente los únicos dos "métodos inseguros" aquí (en el sentido de que deben tener cuidado con las invariantes):

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 }
}

Todo lo demás que sea seguro de usar se puede implementar además de esto con un código seguro, aprovechando que Pin<&T> -> &T es una conversión segura, y para Unpin tipos lo mismo vale para Pin<&mut T> -> &mut T .

Preferiría que pudiéramos cambiar las implementaciones Deref y DerefMut por Pin a algo como

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))
    }
}

Eso llevaría a casa el punto de que estos dos no hacen nada nuevo , solo componen as_ref / as_mut con una conversión segura de Pin<&[mut] T> a &[mut] T - - lo que claramente deja as_ref / as_mut como lo único de lo que deben preocuparse otros constructores de tipo puntero.


El actual PinMut tiene un método borrow que toma &mut self para volver a pedir prestado más fácilmente (dado que esto requiere self , obtenemos un préstamo automático). Este método hace exactamente lo mismo que Pin::as_mut . Supongo que también querríamos algo como esto aquí.

Acabo de notar que las rebanadas tienen get_unchecked_mut , pero el pin tiene get_mut_unchecked . ¿Parece que deberíamos ceñirnos a algo consistente?

¿Alguien podría agregar esto a las preocupaciones de rfcbot?

@rfcbot preocupación get_mut_unchecked_mut_mut

Me acabo de dar cuenta de que nos olvidamos de una situación en la que rustc copiará cosas; probablemente no sea gran cosa, por ahora. Estoy hablando de estructuras empaquetadas. Si una estructura empaquetada tiene un campo que debe soltarse, rustc emitirá un código para copiar los datos de ese campo en algún lugar alineado y luego llamar a drop en eso. Eso es para asegurarse de que el &mut pasado a drop esté realmente alineado.

Para nosotros, significa que una estructura repr(packed) no debe ser wrt "estructural" o "recursiva". anclaje: sus campos no se pueden considerar anclados incluso si la estructura en sí lo está. En particular, no es seguro utilizar la macro pin-accessor-macro en tal estructura. Eso debería agregarse a su documentación.

Eso parece una pistola gigante que debería estar pegada sobre todos los documentos de fijación.

@alercah No creo que sea mucho más un arma de fuego que la capacidad existente para implicar Unpin o Drop ; ciertamente, ambos se ven mucho más comúnmente junto con valores fijados que #[repr(packed)] .

Eso es justo. Mi preocupación es que alguien pueda pensar que una proyección es segura para un tipo que no escribió y no darse cuenta de que packed hace que no sea seguro hacerlo, porque decididamente no es obvio. Es cierto que quienquiera que esté haciendo la proyección es responsable de estar al tanto de esto, pero creo que debe documentarse adicionalmente en cualquier lugar donde pueda ocurrir dicha proyección.

Hm, ¿esto también significaría que todas las estructuras empaquetadas podrían ser Unpin independientemente de los tipos de campo, ya que la fijación no es recursiva?

@alercah No estoy muy familiarizado con cómo se implementan los tipos empaquetados, pero creo que es seguro anclar a un tipo empaquetado, pero no anclar a través de un tipo empaquetado. Entonces, el único caso en el que no controla el tipo empaquetado es si proyecta a un campo público del tipo de otra persona, lo que podría ser igualmente inseguro debido a Drop o cualquier otra cosa. En general, parece desaconsejado anclar el proyecto a los campos de otra persona a menos que proporcionen una proyección de pin que indique que es seguro.

Hm, ¿esto también significaría que todas las estructuras empaquetadas podrían ser Desanclar independientemente de los tipos de campo, ya que la fijación no es recursiva?

Un caso de uso que podría imaginar es una lista vinculada intrusiva, en la que solo te importa la dirección de toda la estructura, pero tiene un montón de datos mal alineados que quieres combinar sin preocuparte por la dirección de esos datos.

Tuve que aprender la API Pin debido a mi trabajo con Futures, y tengo una pregunta.

  • Box implementa incondicionalmente Unpin .

  • Box implementa incondicionalmente DerefMut .

  • El método Pin::get_mut siempre funciona en &mut Box<T> (porque Box implementa incondicionalmente Unpin ).

En conjunto, eso permite mover un valor fijado con un código completamente seguro .

¿Es esto intencionado? ¿Cuál es la razón fundamental por la que esto es seguro?

Parece que tienes Pin<&mut Box<...>> , que fija Box , no las cosas dentro de Box . Siempre es seguro mover un Box , porque Box nunca almacena referencias a su ubicación en la pila.

Lo que probablemente quieras es Pin<Box<...>> . Enlace de juegos .

EDITAR: ya no es preciso

@Pauan El hecho de que un Box esté fijado no significa que la cosa _dentro_ esté fijada. Cualquier código que dependa de esto sería incorrecto.

Lo que está buscando es probablemente PinBox , que no permite el comportamiento que mencionó y le permite obtener un PinMut<Foo> .

@tikue Todavía es posible salir de un Pin<Box<...>> , que creo que es lo que estaban tratando de evitar.

@tmandry Corrígeme si me equivoco, pero Pin<Box<...>> fija la cosa dentro de Box , no la caja en sí. En el ejemplo original de @Pauan , tenían un Pin<&mut Box<...>> , que solo fijaba el Box . Vea el enlace de mi patio de recreo que muestra cómo Pin<Box<...>> evita obtener una referencia mutable a la cosa en el cuadro.

Tenga en cuenta que PinBox se eliminó recientemente, y Pin<Box<T>> ahora tiene la misma semántica que PinBox .

@tmandry PinBox<T> ha sido eliminado y reemplazado por Pin<Box<T>> en Nightly (el enlace de documento que proporcionó es para Estable). Aquí está el enlace nocturno correcto.

Oh, las reglas deben haber cambiado desde la última vez que las usé. Perdón por la confusion.

@tmandry Sí, los cambios fueron muy recientes. Dado que las cosas todavía están cambiando, es difícil mantenerse al día con todos los cambios.

El comentario de @tikue es correcto. Debe recordar que los pines solo fijan un nivel de direccionamiento indirecto hacia abajo.

@tikue @tmandry @withoutboats ¡ Gracias por las respuestas! Fue muy útil.

Entonces, ¿cuáles son los estados de las dos preocupaciones ahora? ( api-refactor & get_mut_unchecked_mut_mut ) Como alguien que está esperando ansiosamente la función de serie async / await, me pregunto a qué versión de rustc se dirigirán las API de Pin ¿Existe una estimación?

@ crlf0710 ver la propuesta de estabilización .

@withoutboats ¿ Parece hecho? ¿Cerramos?

Ok, me disculpo si este no es el lugar para publicar esto, pero he estado pensando en el problema Drop + !Unpin y he tenido la siguiente idea:

  1. Idealmente, si Drop::drop fuera fn(self: Pin<&mut Self>) no habría ningún problema. Llamemos a tal Drop PinDrop . No podemos simplemente reemplazar Drop por PinDrop debido a problemas de retrocompatibilidad.
  1. dado que el único problema con Drop::drop(&mut self) es para el caso Drop + !Unpin , podríamos derivar un impl predeterminado de PinDrop para Drop + Unpin (desde entonces Pin<&mut T> : DerefMut<Target = T> ) y hacer que PinDrop sea ​​el rasgo utilizado automágicamente por rustc (gracias a Pin::new_unchecked(&mut self) , ya que la caída es el único caso de fijación de pilas cuando lo pensamos).

Aquí hay una PoC esquemática de la idea: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=9aae40afe732babeafef9dab3d7525a8

De todos modos, en mi humilde opinión, esto debería permanecer en beta y no volverse estable todavía, incluso si tiene que ser una excepción. Si hay un momento en Drop comportamiento de Unpin sin romper la compatibilidad, ese momento es ahora.

@danielhenrymantilla No veo cómo eso resuelve el problema de compatibilidad con las impls de caída genéricas existentes, como impl<T> Drop for Vec<T> .

Tienes razón, requiere otra cosa:
un T: Unpin implícito vinculado a todos los genéricos, con una exclusión voluntaria utilizando ?Unpin de la misma manera que Sized

Eso debería convertirlo en

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

un T: Unpin implícito vinculado a todos los genéricos, con una exclusión voluntaria utilizando ?Unpin de la misma manera que Sized

Esto tiene un gran efecto en el diseño general de la API y se discutió ampliamente como parte de las propuestas ?Move . Por ejemplo, significaría que muchas, muchas bibliotecas existentes necesitarían actualizarse para trabajar con el anclaje. La conclusión fue que optar por una solución exclusiva de biblioteca como la que tenemos ahora es mejor porque no requiere nada de esto.

Sí, un costo enorme a corto plazo, ya que todas las bibliotecas existentes necesitarían actualizarse para ser compatibles con !Unpin , pero a la larga terminaríamos con un Drop "más seguro". No parecía tan mal al principio, ya que al menos no estamos rompiendo nada.

Pero es una preocupación justa (no sabía que se había planteado anteriormente; gracias por señalarlo, @RalfJung ), y supongo que los inconvenientes prácticos a corto plazo drop(Pin<&mut Self>) .

¿Ha habido alguna discusión sobre Hash implementación de Pin tipos de hash de direcciones?

Pin probablemente debería tener una implementación de Hash que simplemente delegue al puntero contenido, no hay precedencia para el hash basado en direcciones (y no veo ninguna razón por la que fijar un valor deba cambiar cómo obtiene hash).

¿Fue útil esta página
0 / 5 - 0 calificaciones