Rust: Uniones no etiquetadas (problema de seguimiento para RFC 1444)

Creado en 8 abr. 2016  ·  210Comentarios  ·  Fuente: rust-lang/rust

Problema de seguimiento para rust-lang / rfcs # 1444.

Preguntas sin resolver:

  • [x] ¿La asignación directa a un campo de unión provoca una caída del contenido anterior?
  • [x] Al mudarse de un campo de un sindicato, ¿se consideran invalidados los demás? ( 1 , 2 , 3 , 4 )
  • [] ¿Bajo qué condiciones se puede implementar Copy para un sindicato? Por ejemplo, ¿qué pasa si algunas variantes no son de tipo copia? ¿Todas las variantes?
  • [] ¿Qué interacción hay entre uniones y optimizaciones de diseño de enumeración? (https://github.com/rust-lang/rust/issues/36394)

Problemas abiertos de gran importancia:

B-RFC-approved B-unstable C-tracking-issue F-untagged_unions T-lang disposition-merge finished-final-comment-period

Comentario más útil

@nrc
Bueno, el subconjunto es bastante obvio - "uniones FFI", o "uniones C", o "uniones pre-C ++ 11" - incluso si no es sintáctico. Mi objetivo inicial era estabilizar este subconjunto lo antes posible (este ciclo, idealmente) para que pudiera usarse en bibliotecas como winapi .
No hay nada especialmente dudoso sobre el subconjunto restante y su implementación, simplemente no es urgente y es necesario esperar una cantidad de tiempo poco clara hasta que se complete el proceso de RFC "Unions 1.2". Mis expectativas serían estabilizar las partes restantes en 1, 2 o 3 ciclos después de la estabilización del subconjunto inicial.

Todos 210 comentarios

Puede que me lo haya perdido en la discusión sobre el RFC, pero ¿estoy en lo correcto al pensar que los destructores de variantes de unión nunca se ejecutan? ¿Se ejecutaría el destructor para Box::new(1) en este ejemplo?

union Foo {
    f: i32,
    g: Box<i32>,
}

let mut f = Foo { g: Box::new(1) };
f.g = Box::new(2);

@sfackler Mi entendimiento actual es que f.g = Box::new(2) _will_ ejecutará el destructor pero f = Foo { g: Box::new(2) } _no_. Es decir, asignar a un Box<i32> lvalue provocará una caída como siempre, pero asignar a un Foo lvalue no lo hará.

Entonces, ¿una asignación a una variante es como una afirmación de que el campo era previamente "válido"?

@sfackler Para los Drop , sí, eso es lo que entiendo. Si no eran válidos anteriormente, debe usar el formulario de constructor Foo o ptr::write . Sin embargo, a partir de un grep rápido, no parece que el RFC sea explícito sobre este detalle. Lo veo como una instancia de la regla general de que escribir en un Drop lvalue causa una llamada al destructor.

¿Debería ser una pelusa una unión & mut con variantes Drop?

El viernes 8 de abril de 2016, Scott Olson [email protected] escribió:

@sfackler https://github.com/sfackler Para tipos de gota, sí, esa es mi
comprensión. Si no eran válidos anteriormente, debe usar el Foo
forma de constructor o ptr :: write. De un grep rápido, no parece
Sin embargo, el RFC es explícito sobre este detalle.

-
Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -207634431

El 8 de abril de 2016 a las 3:36:22 p.m. PDT, Scott Olson [email protected] escribió:

@sfackler Para los Drop , sí, eso es lo que entiendo. Si ellos
no eran válidos anteriormente, debe usar el formulario de constructor Foo o
ptr::write . De un grep rápido, no parece que el RFC sea
explícito sobre este detalle, sin embargo.

Debería haber cubierto ese caso explícitamente. Creo que ambos comportamientos son defendibles, pero creo que sería mucho menos sorprendente no abandonar implícitamente un campo. El RFC ya recomienda un lint para campos de unión con tipos que implementan Drop. No creo que la asignación a un campo implique que el campo fuera válido anteriormente.

Sí, ese enfoque también me parece un poco menos peligroso.

No eliminar al asignar a un campo de unión haría que f.g = Box::new(2) actuara de manera diferente a let p = &mut f.g; *p = Box::new(2) , porque no puede hacer que el último caso _no_ se elimine. Creo que mi enfoque es menos sorprendente.

Tampoco es un problema nuevo; unsafe ya tienen que lidiar con otras situaciones en las que foo = bar es UB si foo no está inicializado y Drop .

Personalmente, no planeo usar tipos Drop con sindicatos en absoluto. De modo que me referiré completamente a las personas que han trabajado con código inseguro análogo en la semántica para hacerlo.

Tampoco tengo la intención de usar tipos Drop en uniones, por lo que de cualquier manera no me importa siempre que sea consistente.

No pretendo utilizar referencias mutables a sindicatos, y probablemente
sólo los "extrañamente etiquetados" con Into

El viernes 8 de abril de 2016, Peter Atashian [email protected] escribió:

Tampoco tengo la intención de usar tipos Drop en uniones, por lo que de cualquier manera no
me importa siempre que sea coherente.

-
Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -207653168

Parece que este es un buen tema para plantear como una pregunta sin resolver. Todavía no estoy seguro de qué enfoque prefiero.

@nikomatsakis Por mucho que me @tsion parece casi inevitable. Creo que esto podría ser solo un error asociado con el código que deshabilita intencionalmente el lint para colocar un tipo con Drop en una unión. (Y debería haber una breve explicación en el texto explicativo de esa pelusa).

Y me gustaría reiterar que los programadores de unsafe ya deben saber en general que a = b significa drop_in_place(&mut a); ptr::write(&mut a, b) para escribir código seguro. No eliminar los campos de unión sería _una más_ excepción para aprender, no una menos.

(NB: la caída no ocurre cuando a es _staticamente_ conocido como no inicializado, como let a; a = b; .)

Pero apoyo tener una advertencia predeterminada contra las variantes Drop en las uniones que la gente tiene para #[allow(..)] ya que este es un detalle bastante no obvio.

@tsion esto no es cierto para a = b y tal vez solo a veces sea cierto para a.x = b pero ciertamente es cierto para *a = b . Esta incertidumbre es lo que me hizo dudar al respecto. Por ejemplo, esto compila:

fn main() {
  let mut x: (i32, i32);
  x.0 = 2;
  x.1 = 3;
}

(aunque intentar imprimir x luego falla, pero lo considero un error)

@nikomatsakis Ese ejemplo es nuevo para mí. Supongo que lo habría considerado un error que compila ese ejemplo, dada mi experiencia previa.

Pero no estoy seguro de ver la relevancia de ese ejemplo. ¿Por qué lo que dije no es cierto para a = b y solo a veces para a.x = b ?

Digamos, si x.0 tuviera un tipo con un destructor, seguramente ese destructor se llama:

fn main() {
    let mut x: (Box<i32>, i32);
    x.0 = Box::new(2); // x.0 statically know to be uninit, destructor not called
    x.0 = Box::new(3); // x.0 destructor is called before writing new value
}

¿Quizás solo pelusa contra ese tipo de escritura?

Mi punto es solo que = no _siempre_ ejecuta el destructor; eso
utiliza algunos conocimientos sobre si se sabe que el objetivo
inicializado.

El martes 12 de abril de 2016 a las 04:10:39 PM -0700, Scott Olson escribió:

@nikomatsakis Ese ejemplo es nuevo para mí. Supongo que lo habría considerado un error que compila ese ejemplo, dada mi experiencia previa.

Pero no estoy seguro de ver la relevancia de ese ejemplo. ¿Por qué lo que dije no es cierto para a = b y solo a veces para 'ax = b'?

Digamos, si x.0 tuviera un tipo con un destructor, seguramente ese destructor se llama:

fn main() {
    let mut x: (Box<i32>, i32);
    x.0 = Box::new(2); // x.0 statically know to be uninit, destructor not called
    x.0 = Box::new(3); // x.0 destructor is called
}

@nikomatsakis

Ejecuta el destructor si se establece la bandera de caída.

Pero creo que ese tipo de escritura es confusa de todos modos, así que ¿por qué no prohibirlo? Siempre puedes hacer *(&mut u.var) = val .

Mi punto es solo que = no _siempre_ ejecuta el destructor; utiliza algunos conocimientos sobre si se sabe que el destino está inicializado.

@nikomatsakis Ya mencioné eso:

(NB: la caída no ocurre cuando se sabe estáticamente que a ya está sin inicializar, como let a; a = b ;.)

Pero no tuve en cuenta la verificación dinámica de los indicadores de caída, por lo que definitivamente es más complicado de lo que pensaba.

@tsion

Los indicadores de caída son solo semidinámicos: después de que la caída a cero desaparece, son parte del codegen. Digo que prohibimos ese tipo de escritura porque genera más confusión que bien.

¿Deberían permitirse los tipos Drop en las uniones? Si entiendo las cosas correctamente, la razón principal para tener uniones en Rust es interactuar con el código C que tiene uniones, y C ni siquiera tiene destructores. Para todos los demás propósitos, parece que es mejor usar un enum en el código de Rust.

Existe un caso de uso válido para usar una unión para implementar un tipo NoDrop que inhibe la caída.

Además de invocar dicho código manualmente a través de drop_in_place o similar.

Para mí, dejar caer un valor de campo mientras escribo es definitivamente incorrecto porque el tipo de opción anterior no está definido.

¿Sería posible prohibir los montadores de campo pero exigir el reemplazo total de la unión? En este caso, si la unión implementa la caída de unión completa, se solicitará el valor reemplazado como se esperaba.

No creo que tenga sentido prohibir a los montadores de campo; la mayoría de los usos de las uniones no deberían tener problemas para usarlos, y los campos sin una implementación de Drop probablemente seguirán siendo el caso común. Los sindicatos con campos que implementen Drop producirán una advertencia de forma predeterminada, lo que hará que sea aún menos probable que se produzca un caso accidental.

En aras de la discusión, tengo la intención de exponer referencias mutables a campos en uniones _y_ poner tipos arbitrarios (posiblemente Drop ) en ellos. Básicamente, me gustaría usar uniones para escribir enumeraciones personalizadas eficientes en el espacio. Por ejemplo,

union SlotInner<V> {
    next_empty: usize, /* index of next empty slot */
    value: V,
}

struct Slot<V> {
    inner: SlotInner<V>,
    version: u64 /* even version -> is_empty */
}

@nikomatsakis Me gustaría proponer una respuesta concreta a la pregunta que actualmente figura como no resuelta aquí.

Para evitar una semántica innecesariamente compleja, la asignación a un campo de unión debería actuar como la asignación a un campo de estructura, lo que significa eliminar el contenido antiguo. Es bastante fácil evitar esto si lo sabe, asignando a todo el sindicato. Esto sigue siendo un comportamiento ligeramente sorprendente, pero tener un campo de unión que implemente Drop producirá una advertencia, y el texto de esa advertencia puede mencionarlo explícitamente como una advertencia.

¿Tendría sentido proporcionar una solicitud de extracción RFC que modifique RFC1444 para documentar este comportamiento?

@joshtriplett Como @nikomatsakis está de vacaciones, responderé: Creo que es una buena forma de presentar una RFC de enmienda para resolver preguntas como esta. A menudo aceleramos tales RFC PR cuando sea apropiado.

@aturon Gracias. Presenté el nuevo RFC PR https://github.com/rust-lang/rfcs/issues/1663 con estas aclaraciones. A RFC1444, para resolver este problema.

( @aturon , puede marcar esa pregunta sin resolver ahora).

Tengo una implementación preliminar en https://github.com/petrochenkov/rust/tree/union.

Estado: Implementado (errores de módulo), PR enviado (https://github.com/rust-lang/rust/pull/36016).

@petrochenkov ¡Impresionante! Se ve muy bien hasta ahora.

No estoy muy seguro de cómo tratar las uniones con campos que no sean Copy en el verificador de movimientos.
Supongamos que u es un valor inicializado de union U { a: A, b: B } y ahora salimos de uno de los campos:

1) A: !Copy, B: !Copy, move_out_of(u.a)
Esto es simple, u.b también se pone en estado no inicializado.
Verificación de cordura: union U { a: T, b: T } debería comportarse exactamente como struct S { a: T } + alias de campo.

2) A: Copy, B: !Copy, move_out_of(u.a)
Supuestamente u.b aún debería inicializarse, porque move_out_of(u.a) es simplemente un memcpy y no cambia u.b de ninguna manera.

2) A: !Copy, B: Copy, move_out_of(u.a)
Este es el caso más extraño; supuestamente u.b también debería ponerse en estado no inicializado a pesar de ser Copy . Copy valores let a: u8; ), pero cambiar su estado de inicializado a no inicializado es algo nuevo, AFAIK.

@ retep998
Sé que esto es completamente irrelevante para las necesidades de FFI :)
La buena noticia es que no es un bloqueador, voy a implementar cualquier comportamiento que sea más simple y enviar PR este fin de semana.

@petrochenkov mi instinto es que los sindicatos son un "cubo de bits", esencialmente. Usted es responsable de rastrear si los datos se inicializan o no y cuál es su tipo verdadero. Esto es muy similar al referente de un puntero sin formato.

Es por eso que no podemos eliminar los datos por usted y también por qué cualquier acceso a los campos no es seguro (incluso si, por ejemplo, solo hay una variante).

Según estas reglas, esperaría que los sindicatos implementen Copy si se implementa una copia para ellos. Sin embargo, a diferencia de las estructuras / enumeraciones, no habría comprobaciones de cordura internas: siempre puede implementar una copia para un tipo de unión si lo desea.

Permítanme dar algunos ejemplos para aclarar:

union Foo { ... } // contents don't matter

Esta unión es afín, porque Copy no se ha implementado.

union Bar { x: Rc<String> }
impl Copy for Bar { }
impl Clone for Bar { fn clone(&self) -> Self { *self } }

Este tipo de unión Bar es copia, porque Copy se ha implementado.

Tenga en cuenta que si Bar fuera una estructura, sería un error implementar Copy debido al tipo de campo x .

Sin embargo, supongo que no estoy respondiendo a tu pregunta ahora que la releo. =)

Bien, me doy cuenta de que no respondí tu pregunta en absoluto. Déjame intentarlo de nuevo. Siguiendo el principio del "cubo de bits", _todavía_ esperaría que podamos dejar un sindicato a voluntad. Pero, por supuesto, otra opción sería tratarlo como tratamos a un *mut T , y exigirle que use ptr::read para mudarse.

EDITAR: No estoy del todo seguro de por qué prohibiríamos tales movimientos. ¿Podría haber tenido que ver con la caída en movimiento, o quizás simplemente porque es fácil cometer un error y parece mejor hacer que los "movimientos" sean más explícitos? Me cuesta recordar la historia aquí.

@nikomatsakis

mi instinto es que los sindicatos son un "pequeño cubo", esencialmente.

Ja, yo, por el contrario, me gustaría dar tantas garantías sobre el contenido del sindicato como podamos para una construcción tan peligrosa.

La interpretación es que union es una enumeración para la que no conocemos el discriminante, es decir, podemos garantizar que en cualquier momento al menos una de las variantes de union tiene un valor válido (a menos que esté involucrado un código inseguro).

Todas las reglas de pedir prestado / mover en la implementación actual respaldan esta garantía, simultáneamente esta es la interpretación más conservadora, que nos permite ir por el camino "seguro" (por ejemplo, permitiendo el acceso seguro a uniones con campos con el mismo tipo, esto puede ser útil ) o en el futuro, cuando se acumule más experiencia con las uniones Rust.

En realidad, me gustaría hacerlo aún más conservador como se describe en https://github.com/rust-lang/rust/pull/36016#issuecomment -242810887

@petrochenkov

La interpretación es que union es una enumeración para la que no conocemos el discriminante, es decir, podemos garantizar que en cualquier momento al menos una de las variantes de union tiene un valor válido (a menos que esté involucrado un código inseguro).

Tenga en cuenta que el código inseguro siempre está involucrado, cuando se trabaja con un sindicato, ya que cada acceso a un campo es inseguro.

La forma en que lo veo es, creo, similar. Básicamente, una unión es como una enumeración, pero puede estar en más de una variante simultáneamente. El compilador no conoce el conjunto de variantes válidas en ningún momento, aunque a veces podemos darnos cuenta de que el conjunto está vacío (es decir, la enumeración no está inicializada).

Entonces veo cualquier uso de some_union.field básicamente como una afirmación implícita (e insegura) de que el conjunto de variantes válidas actualmente incluye field . Esto parece compatible con la forma en que funciona la integración de prestatario-verificador; si toma prestado el campo x y luego intenta usar y , obtendrá un error porque básicamente está diciendo que los datos son simultáneamente x y y (y es prestado). (Por el contrario, con una enumeración regular, no es posible habitar más de una variante a la vez, y puede ver esto en cómo se desarrollan las reglas de préstamo ).

De todos modos, el punto es que, cuando nos "movemos" de un campo de una unión, la pregunta que tenemos entre manos, supongo, es si podemos deducir que esto implica que interpretar el valor como las otras variantes ya no es válido. Sin embargo, creo que no sería tan difícil discutir de cualquier manera. Considero que esta es una zona gris.

El peligro de ser conservadores es que bien podríamos descartar un código inseguro que de otro modo tendría sentido y sería válido. Pero estoy de acuerdo con comenzar más apretado y decidir si aflojar más tarde.

Debemos discutir el asunto de las condiciones que se necesitan para implementar Copy en un sindicato; además, debemos asegurarnos de tener una lista completa de estas áreas grises enumeradas anteriormente para asegurarnos de abordar y documentar antes de la estabilización.

Básicamente, una unión es como una enumeración, pero puede estar en más de una variante simultáneamente.

Un argumento en contra de la interpretación de "más de una variante" es cómo se comportan las uniones en expresiones constantes; para estas uniones siempre conocemos la variante activa única y tampoco podemos acceder a las variantes inactivas porque la transmutación en tiempo de compilación es generalmente mala (a menos que estemos intentando para convertir el compilador en una especie de emulador de destino parcial).
Mi interpretación es que, en tiempo de ejecución, las variantes inactivas siguen inactivas pero se puede acceder a ellas si su diseño es compatible con la variante activa de la unión (definición más restrictiva) o más bien con el historial de asignación de fragmentos de la unión (más vago, pero más útil).

debemos asegurarnos de tener una lista completa de estas áreas grises

¡Voy a enmendar el RFC sindical en un futuro no tan remoto! La interpretación de "enumeración" tiene consecuencias bastante divertidas.

transmutar en tiempo de compilación es generalmente malo (a menos que estemos tratando de convertir el compilador en algún tipo de emulador de destino parcial)

@petrochenkov Este es uno de los objetivos de mi proyecto Miri . Miri ya puede hacer transmutaciones y varias travesuras de punteros crudos. Sería una pequeña cantidad de trabajo hacer que Miri manejara las uniones (nada nuevo en el lado del manejo de memoria sin procesar).

Y @eddyb está presionando para reemplazar la evaluación constante de rustc con una versión de Miri.

@petrochenkov

Un argumento en contra de la interpretación de "más de una variante" es cómo se comportan las uniones en expresiones constantes ...

Cómo apoyar mejor el uso de uniones en constantes es una pregunta interesante, pero no veo ningún problema en restringir las expresiones constantes a un subconjunto de comportamiento en tiempo de ejecución (esto es lo que siempre hacemos, de todos modos). Es decir, el hecho de que no podamos admitir por completo alguna transmutación en particular en tiempo de compilación no significa que sea ilegal en tiempo de ejecución.

Mi interpretación es que, en tiempo de ejecución, las variantes inactivas siguen inactivas, pero se puede acceder a ellas si su diseño es compatible con la variante activa de la unión.

Hmm, estoy tratando de pensar en qué se diferencia esto de decir que la unión pertenece a todas esas variantes simultáneamente. Realmente no veo ninguna diferencia todavía. :)

Siento que esta interpretación tiene interacciones extrañas con los movimientos en general. Por ejemplo, si los datos son "realmente" una X y los interpretas como una Y, pero Y es afín, ¿sigue siendo una X?

Independientemente, creo que está bien que hacer que un movimiento de cualquier campo consuma toda la unión pueda considerarse coherente con cualquiera de estas interpretaciones. Por ejemplo, en el enfoque del "conjunto de variantes", la idea es simplemente que mover el valor desinicializa todas las variantes existentes (y, por supuesto, la variante que utilizó debe ser una del conjunto válido). En su versión, parecería "transmutar" en esa variante (y consumir el original).

¡Voy a enmendar el RFC sindical en un futuro no tan remoto! La interpretación de "enumeración" tiene consecuencias bastante divertidas.

¡Qué confianza! Lo vas a intentar;)

¿Le importaría arrojar algunos detalles más sobre los cambios concretos que tiene en mente?

¿Le importaría arrojar algunos detalles más sobre los cambios concretos que tiene en mente?

Descripción más detallada de la implementación (es decir, mejor documentación), algunas extensiones pequeñas (como uniones vacías y .. en patrones de unión), dos alternativas principales (contradictorias) de la evolución de la unión: un "espacio de borrador" más inseguro y menos restrictivo interpretación y una interpretación más segura y restrictiva de "enumeración con discriminante desconocido", y sus consecuencias para el verificador de movimiento / inicialización, Copy impls, unsafe ty de acceso al campo, etc.

También sería útil definir cuándo acceder a un campo de unión inactivo es UB, por ejemplo

union U { a: u8, b: () }
let u = U { b: () };
let a = u.a; // most probably an UB, equivalent to reading from `mem::uninitialized()`

pero esta es un área infinitamente complicada.

Suena probable, la semántica de campo cruzado es básicamente un lanzamiento de puntero, ¿verdad?
_ (_ () como * u8)

El jueves 1 de septiembre de 2016, Vadim Petrochenkov [email protected]
escribió:

También sería útil definir al acceder a un campo de unión inactivo
es UB, p. ej.

unión U {a: u8, b: ()}
sea ​​u = U {b: ()};
sea ​​a = ua; // muy probablemente un UB, equivalente a leer de mem::uninitialized()

pero esta es un área infinitamente complicada.

-
Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -244154751,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABxXhi68qRITTFW5iJn6omZQQBQgzweNks5qlw4qgaJpZM4IDXsj
.

¿No es siempre inseguro el acceso al campo?

El jueves 1 de septiembre de 2016, Vadim Petrochenkov [email protected]
escribió:

¿Le importaría arrojar algunos detalles más sobre los cambios concretos que tiene en mente?

Descripción más detallada de la implementación (es decir, mejor
documentación), algunas pequeñas extensiones (como uniones vacías y .. en unión
patrones), dos alternativas principales (contradictorias) de evolución sindical: más
Interpretación insegura y menos restrictiva del "espacio cero" y más segura
y una interpretación más restrictiva de "enumeración con discriminante desconocido", y
sus consecuencias para el corrector de movimiento / inicialización, copia implícita, inseguridad
de acceso al campo, etc.

-
Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -244151164,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABxXhuHStN8AFhR3KYDU27U29MiMpN5Bks5qlws9gaJpZM4IDXsj
.

¿No es siempre inseguro el acceso al campo?

A veces se puede hacer seguro, p. Ej.

  • la asignación a campos de unión trivialmente destructibles es segura.
  • cualquier acceso a campos de union U { f1: T, f2: T, ..., fN: T } (es decir, todos los campos tienen el mismo tipo) es seguro en la interpretación de "enumeración con discriminante desconocido".

Parece mejor no aplicar condiciones especiales a esto, desde el punto de vista del usuario. Llámalo inseguro, siempre.

Actualmente probando el soporte para sindicatos en el último rustc de git. Todo lo que he probado funciona perfectamente.

Me encontré con un caso interesante en el corrector de campo muerto. Prueba el siguiente código:

#![feature(untagged_unions)]

union U {
    i: i32,
    f: f32,
}

fn main() {
    println!("{}", std::mem::size_of::<U>());
    let u = U { f: 1.0 };
    println!("{:#x}", unsafe { u.i });
}

Obtendrá este error:

warning: struct field is never used: `f`, #[warn(dead_code)] on by default

Parece que el verificador dead_code no notó la inicialización.

(Ya presenté PR # 36252 sobre el uso de "campo de estructura", cambiándolo a solo "campo").

Actualmente, las uniones no pueden contener campos de tamaño dinámico, pero el RFC no especifica este comportamiento de ninguna manera:

#![feature(untagged_unions)]

union Foo<T: ?Sized> {
  value: T,
}

Salida:

error[E0277]: the trait bound `T: std::marker::Sized` is not satisfied
 --> <anon>:4:5
  |
4 |     value: T,
  |     ^^^^^^^^ trait `T: std::marker::Sized` not satisfied
  |
  = help: consider adding a `where T: std::marker::Sized` bound
  = note: only the last field of a struct or enum variant may have a dynamically sized type

La palabra clave contextual no funciona fuera del contexto raíz del módulo / caja:

fn main() {
    // all work
    struct Peach {}
    enum Pineapple {}
    trait Mango {}
    impl Mango for () {}
    type Strawberry = ();
    fn woah() {}
    mod even_modules {
        union WithUnions {}
    }
    use std;

    // does not work
    union Banana {}
}

Parece una verruga de consistencia bastante desagradable.

@nagisa
¿Estás usando alguna versión anterior de rustc por accidente?
Acabo de comprobar su ejemplo en el parque y funciona (errores de módulo "unión vacía").
También hay una comprobación de prueba de ejecución y aprobación para esta situación específica: https://github.com/rust-lang/rust/blob/master/src/test/run-pass/union/union-backcomp.rs.

@petrochenkov ah, usé play.rlo, pero parece que podría haber vuelto a stable o algo así. Entonces no me hagas caso.

Creo que los sindicatos eventualmente necesitarán apoyar _campos seguros_, los gemelos malvados de los campos inseguros de esta propuesta .
cc https://github.com/rust-lang/rfcs/issues/381#issuecomment -246703410

Creo que tendría sentido tener una forma de declarar "uniones seguras", basándose en varios criterios.

Por ejemplo, una unión que contenga exclusivamente campos Copy non-Drop, todos con el mismo tamaño, parece segura; No importa cómo acceda a los campos, es posible que obtenga datos inesperados, pero no puede encontrar un problema de seguridad de la memoria o un comportamiento indefinido.

@joshtriplett También debería asegurarse de que no haya "huecos" en los tipos de los que podría leer datos no inicializados. Como me gusta decir: "Los datos no inicializados son datos aleatorios impredecibles o su clave privada SSH, lo que sea peor".

¿No es &T copiar y no soltar? Ponlo en una unión "segura" con usize , y tendrás un generador de referencia falso. Entonces, las reglas tendrán que ser un poco más estrictas que eso.

Por ejemplo, una unión que contenga exclusivamente campos Copy non-Drop, todos con el mismo tamaño, parece segura; No importa cómo acceda a los campos, es posible que obtenga datos inesperados, pero no puede encontrar un problema de seguridad de la memoria o un comportamiento indefinido.

Me doy cuenta de que esto es solo un ejemplo improvisado, no una propuesta seria, pero aquí hay algunos ejemplos para ilustrar lo complicado que es esto:

  • u8 y bool tienen el mismo tamaño, pero la mayoría de los valores de u8 no son válidos para bool e ignorar esto desencadena UB
  • &T y &U tienen el mismo tamaño y son Copy + !Drop para todos los T y U (siempre que ambos o ninguno sea Sized )
  • transmutar entre uN / iN y fN actualmente solo es posible en código inseguro. Creo que tales transmutaciones son siempre seguras, pero esto amplía el lenguaje seguro, por lo que puede ser controvertido.
  • Violar la privacidad (por ejemplo, jugar a juegos de palabras entre struct Foo(Bar); y Bar ) es un gran no no, ya que la privacidad podría usarse para mantener invariantes relevantes para la seguridad.

@Amanieu Cuando estaba escribiendo eso, tenía la intención de incluir una nota sobre no tener ningún relleno interno, y de alguna manera me olvidé de hacerlo. Gracias por captar eso.

@cuviper Estaba tratando de definir "datos antiguos sin formato", como en algo que no contiene punteros. Tienes razón, la definición debería excluir referencias. Probablemente sea más fácil incluir en la lista blanca un conjunto de tipos permitidos y combinaciones de esos tipos.

@rkruppe

u8 y bool tienen el mismo tamaño, pero la mayoría de los valores de u8 no son válidos para bool e ignorar esto desencadena UB

Buen punto; el mismo problema se aplica a las enumeraciones.

& T y & U tienen el mismo tamaño y son Copy +! Drop para todos los T y U (siempre que ambos o ninguno sean de tamaño)

Lo había olvidado.

la transmutación entre uN / iN y fN actualmente solo es posible en código inseguro. Creo que tales transmutaciones son siempre seguras, pero esto amplía el lenguaje seguro, por lo que puede ser controvertido.

Estuvo de acuerdo en ambos puntos; esto parece aceptable para permitir.

Violar la privacidad (por ejemplo, juegos de palabras entre struct Foo (Bar) y Bar) también es un gran no, ya que la privacidad podría usarse para mantener invariantes relevantes para la seguridad.

Si no conoce las partes internas del tipo, no puede saber si las partes internas cumplen con los requisitos (por ejemplo, sin relleno interno). Por lo tanto, podría excluir esto solicitando que todos los componentes sean datos antiguos simples, de forma recursiva, y que tenga suficiente visibilidad para verificarlo.

la transmutación entre uN / iN y fN actualmente solo es posible en código inseguro. Creo que tales transmutaciones son siempre seguras, pero esto amplía el lenguaje seguro, por lo que puede ser controvertido.

Los números de coma flotante tienen señalización NaN, que es una representación de trampa que da como resultado UB.

@ retep998 ¿Rust se ejecuta en alguna plataforma que no admita la desactivación de trampas de punto flotante? (Eso no cambia el problema de la UB, pero en teoría podríamos hacer que ese problema desaparezca).

@petrochenkov

La interpretación es que union es una enumeración para la que no conocemos el discriminante, es decir, podemos garantizar que en cualquier momento al menos una de las variantes de union tiene un valor válido

Creo que he llegado a esta interpretación, bueno, no exactamente esto. Sigo pensando en ello como que hay un conjunto de variantes legales que se determina en el punto donde se almacena, como siempre lo hice. Creo que almacenar un valor en una unión es un poco como ponerlo en un "estado cuántico": ahora podría transmutarse potencialmente en una de las muchas interpretaciones legales. Pero estoy de acuerdo en que cuando se sale de una de estas variantes, la ha "forzado" a una de ellas y ha consumido el valor. Por lo tanto, no debería poder usar la enumeración nuevamente (si ese tipo no es Copy ). Entonces 👍, básicamente.

Pregunta sobre #[repr(C)] : como @pnkfelix me señaló recientemente, la especificación actual establece que si una unión no es #[repr(C)] , es ilegal almacenar con el campo x y leer con el campo y . Es de suponer que esto se debe a que no estamos obligados a iniciar todos los campos con el mismo desplazamiento.

Puedo ver alguna utilidad en esto: por ejemplo, un desinfectante podría implementar uniones almacenándolas como una enumeración normal (¿o incluso una estructura ...?) Y verificando que usa la misma variante que ingresó.

_Pero_ parece una especie de arma de fuego, y también una de esas garantías de repr que nunca podríamos cambiar _en realidad_ en la práctica, porque demasiada gente dependerá de él en la naturaleza.

Pensamientos

@nikomatsakis

La interpretación es que union es una enumeración para la que no conocemos el discriminante, es decir, podemos garantizar que en cualquier momento al menos una de las variantes de union tiene un valor válido

La peor parte son los fragmentos de variantes / campos, que son directamente accesibles para los sindicatos.
Considere este código:

union U {
    a: (u8, bool),
    b: (bool, u8),
}
fn main() {
    unsafe {
        let mut u = U { a: (2, false) };
        u.b.1 = 2; // turns union's memory into (2, 2)
    }
}

Todos los campos son Copy , no hay propiedad involucrada y el verificador de movimiento está contento, pero la asignación parcial al campo inactivo b convierte la unión en un estado con 0 variantes válidas. Todavía no he pensado cómo lidiar con eso. ¿Hacer esas asignaciones UB? ¿Cambiar la interpretación? ¿Algo más?

@petrochenkov

¿Hacer esas asignaciones UB?

Esta sería mi suposición, sí. Cuando asignó a , la variante b no estaba en el conjunto de variantes válidas y, por lo tanto, el uso posterior de u.b.1 (ya sea para leer o asignar) no es válido.

Pregunta sobre # [repr (C)]: como @pnkfelix me señaló recientemente, la especificación actual establece que si una unión no es # [repr (C)], es ilegal almacenar con el campo xy leer con el campo y . Es de suponer que esto se debe a que no estamos obligados a iniciar todos los campos con el mismo desplazamiento.

Creo que la redacción adecuada aquí es que 1) La lectura de campos que no son "compatibles con el diseño" (esto es vago) con campos / fragmentos de campo escritos anteriormente es UB 2) Para #[repr(C)] uniones, los usuarios saben qué diseños son (de ABI docs) para que puedan discernir entre UB y no UB 3) Para #[repr(Rust)] union, los diseños no están especificados, por lo que los usuarios no pueden decir qué es UB y qué no, pero WE (rustc / libstd + su pruebas) tienen este conocimiento sagrado, por lo que podemos separar el trigo de la paja y usar #[repr(Rust)] de manera diferente a UB.

4) Después de que se decidan las preguntas de tamaño / zancada y reordenamiento de campo, esperaría que los diseños de estructura y unión se establezcan en piedra y se especifiquen, para que los usuarios también conozcan los diseños y puedan usar #[repr(Rust)] uniones tan libremente como #[repr(C)] y el problema desaparecerá.

@nikomatsakis En la discusión del RFC sindical, la gente mencionó querer tener código nativo de Rust que use uniones para construir estructuras de datos compactas.

¿Hay algo que impida que las personas usen #[repr(C)] ? Si no es así, entonces no veo la necesidad de proporcionar ningún tipo de garantía para #[repr(Rust)] , simplemente déjelo como "aquí hay dragones". Probablemente sería mejor tener una pelusa que se advierte de forma predeterminada para las uniones que no son #[repr(C)] .

@ retep998 Me parece razonable que repr(Rust) no garantice ningún diseño o superposición en particular. Solo sugeriría que repr(Rust) no debería en la práctica romper las suposiciones de las personas sobre el uso de memoria de una unión ("no más grande que el miembro más grande").

¿Rust se ejecuta en alguna plataforma que no admita la desactivación de trampas de punto flotante?

Esa no es realmente una pregunta válida para hacer. En primer lugar, el propio optimizador puede confiar en la UB de las representaciones de trampas y reescribir el programa de formas inesperadas. Además, Rust tampoco admite la alteración del entorno FP.

Pero parece una especie de arma de fuego, y también una de esas garantías de repr que nunca podríamos cambiar en la práctica, porque demasiadas personas dependerán de él en la naturaleza.

Pensamientos

Agregar un lint o algo similar que inspeccione el flujo del programa y arroje una queja al usuario si la lectura se realiza desde un campo cuando la enumeración fue probadamente escrita en algún otro campo ayudaría con esto¹. La pelusa basada en MIR haría un pequeño trabajo con eso. Si un CFG no permite sacar conclusiones sobre la legalidad de la carga de campo de unión y el usuario se equivoca, el comportamiento indefinido es el mejor que podemos especificar sin haber especificado la propia repr de Rust IMO.

¹: Especialmente efectivo si la gente comienza a usar unión como transmutación de un pobre por alguna razón.

en la práctica, no debería romper las suposiciones de la gente sobre el uso de la memoria de un sindicato ("no más grande que el miembro más grande").

Estoy en desacuerdo. Puede tener mucho sentido extender repr(Rust) cosas al tamaño de una palabra de máquina en algunas arquitecturas, por ejemplo.

Un tema que quizás desee considerar antes de la estabilización es https://github.com/rust-lang/rust/issues/37479. Parece que con la versión más reciente de LLDB, las uniones de depuración pueden no funcionar :(

@alexcrichton ¿

Por lo que puedo decir, sí. Los bots de Linux parecen estar ejecutando la prueba sin problemas.

Entonces eso significa que Rust proporciona toda la información de depuración correcta, y LLDB solo tiene un error aquí. No creo que un error en uno de varios depuradores, que no esté presente en otro, deba bloquear la estabilización de esto. LLDB solo necesita ser arreglado.

Sería genial ver si podemos incluir esta función en FCP para el ciclo 1.17 (esa es la versión beta del 16 de marzo). ¿Alguien puede dar un resumen de las preguntas pendientes y la situación actual de la función para que podamos ver si podemos llegar a un consenso y resolverlo todo?

@sin barcos
Mis planes son

  • Espere el próximo lanzamiento (3 de febrero).
  • Proponer la estabilización de los sindicatos con campos Copy . Esto cubrirá todas las necesidades de FFI: las bibliotecas de FFI podrán utilizar uniones en el establo. Las uniones "POD" se usan durante décadas en C / C ++ y se entienden bien (aliasing basado en el tipo de módulo, pero Rust no lo tiene), tampoco hay bloqueadores conocidos.
  • Escriba "Unions 1.2" RFC hasta el 3 de febrero. Describirá la implementación actual de los sindicatos y describirá las direcciones futuras. El futuro de los sindicatos con campos que no sean Copy se decidirá en el proceso de discusión de este RFC.

Tenga en cuenta que exponer algo como ManuallyDrop o NoDrop de la biblioteca estándar no requiere uniones estabilizadoras.

ACTUALIZACIÓN DE ESTADO (4 de febrero): Estoy escribiendo el RFC, pero tengo un bloqueo de escritura después de cada oración, como de costumbre, por lo que existe la posibilidad de que lo termine el próximo fin de semana (11 al 12 de febrero) y no este fin de semana. (4-5 de febrero).
ACTUALIZACIÓN DE ESTADO (11 de febrero): El texto está listo en un 95%, lo enviaré mañana.

@petrochenkov que parece un curso de acción muy razonable.

@petrochenkov Eso me suena razonable. También revisé su propuesta de sindicatos 1.2 y proporcioné algunos comentarios; en general, me parece bien.

@joshtriplett Estaba pensando que, mientras en la reunión de @ rust-lang / lang hablamos sobre mantener actualizadas las listas de verificación, me gustaría ver que, para cada uno de estos puntos, tomemos una decisión afirmativa. (es decir, idealmente con @rfcbot). Esto probablemente sugeriría un problema distinto (o incluso una enmienda RFC). Podríamos hacer esto con el tiempo, pero hasta entonces no siento que hayamos "resuelto" definitivamente las respuestas a las preguntas abiertas. En ese sentido, extraer y resumir la conversación relevante en una RFC de enmienda o incluso simplemente en un tema al que podemos vincular desde aquí parece un paso excelente para ayudar a garantizar que todos estén en la misma página, y algo que cualquier persona interesada puede hacer. , por supuesto, no solo miembros de @ rust-lang / lang o pastores.

Por lo tanto, envié el RFC "Unions 1.2" - https://github.com/rust-lang/rfcs/pull/1897.

Ahora me gustaría proponer la estabilización de un subconjunto conservador de unión: todos los campos de la unión deben ser Copy , el número de campos debe ser distinto de cero y la unión no debe implementar Drop .
(Sin embargo, no estoy seguro de que el último requisito sea viable, porque se puede eludir fácilmente envolviendo la unión en una estructura e implementando Drop para esa estructura).
Estas uniones cubren todas las necesidades de las bibliotecas de FFI, que se supone que son las principales consumidoras de esta función de lenguaje.

El texto de "Unions 1.2" RFC en realidad no dice nada nuevo sobre las uniones al estilo FFI, excepto que confirma explícitamente que se permite el juego de palabras.
EDITAR : "Unions 1.2" RFC también hará que las asignaciones a campos Copy trivialmente destructibles sean seguros (consulte https://github.com/rust-lang/rust/issues/32836#issuecomment-281296416, https : //github.com/rust-lang/rust/issues/32836#issuecomment-281748451), esto también afecta a las uniones de estilo FFI.

Este texto también proporciona la documentación necesaria para la estabilización.
La sección "Descripción general" se puede copiar y pegar en el libro y "Diseño detallado" en la referencia.

ping @nikomatsakis

¿Es realmente necesario agregar algo como esto como parte del lenguaje? Me tomó unos 20 minutos poner en marcha una implementación de un sindicato usando un poco de unsafe y ptr::write() .

use std::mem;
use std::ptr;


/// A union of `f64`, `bool`, and `i32`.
#[derive(Default, Clone, PartialEq, Debug)]
struct Union {
    data: [u8; 8],
}

impl Union {
    pub unsafe fn get<T>(&self) -> &T {
        &*(&self.data as *const _ as *const T)
    }

    pub unsafe fn set<T>(&mut self, value: T) {
        // "transmute" our pointer to self.data into a &mut T so we can 
        // use ptr::write()
        let data_ptr: &mut T = &mut *(&mut self.data as *mut _ as *mut T);
        ptr::write(data_ptr, value);
    }
}


fn main() {
    let mut u = Union::default();
    println!("data: {0:?} ({0:#p})", &u.data);
    {
        let as_i32: &i32 = unsafe { u.get() };
        println!("as i32: {0:?} ({0:#p})", as_i32);
    }

    unsafe {
        u.set::<f64>(3.14);
    }

    println!("As an f64: {:?}", unsafe { u.get::<f64>() });
}

Siento que no sería difícil para alguien escribir una macro que pueda generar algo así, excepto asegurarse de que la matriz interna sea del tamaño del tipo más grande. Luego, en lugar de mi get::<T>() completamente genérico (y terriblemente inseguro), podrían agregar un rasgo destinado a limitar los tipos que puede obtener y configurar. Incluso puede agregar métodos getter y setter específicos si desea campos con nombre.

Estoy pensando que podrían escribir algo como esto:

union! { Foo(u64, Vec<u8>, String) };

Mi punto es que esto es algo que podría hacer de manera bastante factible como parte de una biblioteca en lugar de agregar sintaxis y complejidad adicionales a un lenguaje que ya es bastante complejo. Además, con las macros proc ya es bastante posible, incluso si aún no se ha estabilizado por completo.

@ Michael-F-Bryan Sin embargo, todavía no tenemos size_of constantes.

@ Michael-F-Bryan No solo es suficiente tener una matriz [u8] , también es necesario que la alineación sea correcta. De hecho, ya uso macros para manejar uniones, pero debido a la falta de size_of y align_of constantes, tengo que asignar manualmente el espacio correcto, además de que no hay concatenación de identidades utilizables en macros declarativas. tiene que especificar manualmente los nombres tanto para los captadores como para los definidores. Incluso simplemente inicializar una unión es difícil en este momento porque primero tengo que inicializarla con algún valor predeterminado y luego establecer el valor en la variante que quiero (o agregar otro conjunto de métodos para construir la unión que es aún más verbosidad en la definición del sindicato). En general, es mucho más laborioso, propenso a errores y más feo que el apoyo nativo a los sindicatos. Tal vez debería leer el RFC y la discusión que lo acompañó para que pueda comprender por qué esta función es tan importante.

Y lo mismo para la alineación.

Me imagino que la concatenación de identidades no debería ser demasiado difícil ahora que syn existe. Le permite realizar operaciones en el AST pasado, por lo que puede tomar dos Idents , extraer su representación de cadena ( Ident implementa AsRef<str> ), luego crear un nuevo Ident que es la concatenación de los dos usando Ident::From<String>() .

El RFC menciona mucho acerca de cómo las implementaciones de macros existentes son engorrosas de usar, sin embargo, con la reciente creación de cajas como syn y quote , ahora es mucho más fácil hacer macros proc. Siento que eso contribuiría en gran medida a mejorar la ergonomía y hacer que las cosas sean menos propensas a errores.

Por ejemplo, podría tener un MyUnion::default() que solo ponga en cero el búfer interno de la unión, luego un fn MyUnion::new<T>(value:T) -> MyUnion , donde T tiene un límite de rasgo que garantiza que solo puede inicializar con los tipos correctos .

En términos de alineación y tamaño, ¿puede usar el módulo mem de la biblioteca estándar (es decir, std :: mem :: align_of () y amigos)? Supongo que todo lo que propongo dependerá de poder usarlos en el momento de la macro expansión para determinar el tamaño y la alineación necesarios. El 99,9% de las veces que se usan uniones, se hace con tipos primitivos de todos modos, así que creo que podría escribir una función auxiliar que toma el nombre de un tipo y devuelve su alineación o tamaño (posiblemente preguntando al compilador, aunque eso es más de un detalle de implementación).

Lo admito, la coincidencia de patrones incorporada sería muy agradable, pero la mayoría de las veces, las uniones que usa en FFI se envuelven en una fina capa de abstracción de todos modos. Por lo tanto, es posible que pueda salirse con la suya con un par de declaraciones if / else o usando una función auxiliar.

En términos de alineación y tamaño, ¿puede usar el módulo mem de la biblioteca estándar (es decir, std :: mem :: align_of () y amigos)?

Eso no va a funcionar en ningún contexto de compilación cruzada.

@ Michael-F-Bryan Todas estas discusiones y muchas más se tuvieron en la historia de https://github.com/rust-lang/rfcs/pull/1444 . Para resumir las respuestas a sus preocupaciones específicas, además de las ya mencionadas: tendría que volver a implementar las reglas de relleno y alineación de cada plataforma / compilador de destino, y usar una sintaxis incómoda en todo su código FFI (que

También:

El 99,9% de las veces que se utilizan uniones se hace con tipos primitivos de todos modos

No es cierto del todo. El código C usa ampliamente un patrón de "estructura de uniones de estructuras", donde la mayoría de los campos de unión consisten en diferentes tipos de estructuras.

@rfcbot FCP fusión por @petrochenkov 's comentario https://github.com/rust-lang/rust/issues/32836#issuecomment -279256434

No tengo nada que agregar, solo activando el bot

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

  • [x] @aturon
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @withoutboats

No hay preocupaciones actualmente enumeradas.

Una vez que estos revisores lleguen a un consenso, 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.

PSA: Voy a actualizar el RFC "Unions 1.2" con un cambio más que afecta a los sindicatos al estilo FFI: moveré las asignaciones seguras a campos de sindicatos trivialmente destructibles de "Direcciones futuras" al RFC propiamente dicho.

union.trivially_destructible_field = 10; // safe

Por qué:

  • Las asignaciones a campos sindicales trivialmente destructibles son incondicionalmente seguras, independientemente de la interpretación de los sindicatos.
  • Eliminará aproximadamente la mitad de los bloques unsafe relacionados con la unión.
  • Será más difícil hacerlo más adelante debido a la gran cantidad potencial de unused_unsafe advertencias / errores en el código estable.

@petrochenkov ¿Te refieres a "campos sindicales trivialmente destructibles" o "sindicatos con campos totalmente destructibles trivialmente"?

¿Está proponiendo que todo el comportamiento inseguro ocurra en la lectura, donde elige una interpretación? Por ejemplo, ¿tener una unión que contiene una enumeración y otros campos, donde el valor de unión contiene un discriminante no válido?

A un alto nivel que parece plausible. Sí permite algunas cosas que consideraría inseguras, pero Rust en general no lo hace, como evitar destructores o perder memoria. En un nivel bajo, dudaría en considerar ese sonido.

Me siento bien por estabilizar ese subconjunto. Aún no conozco mi opinión sobre este RFC de Unions 1.2 porque no he tenido tiempo de leerlo. No estoy seguro de lo que pienso acerca de permitir el acceso seguro a los campos en algunos casos. Siento que nuestros esfuerzos por hacer una noción "mínima" de lo que es inseguro (simplemente desreferenciar punteros) fue un error, en retrospectiva, y deberíamos haber declarado una franja más amplia de cosas inseguras (por ejemplo, muchos yesos), ya que interactuar de formas complejas con LLVM. Siento que este también puede ser el caso aquí. Dicho de otra manera, preferiría retirar las reglas sobre unsafe junto con un mayor progreso en las pautas de códigos inseguros.

@joshtriplett
"Campos trivialmente destructibles", modifiqué la redacción.

¿Está proponiendo que todo el comportamiento inseguro ocurra en la lectura, donde elige una interpretación?

Si. La escritura por sí sola no puede causar nada peligroso sin una lectura posterior.

EDITAR:

Siento que nuestros esfuerzos por hacer una noción "mínima" de lo que es inseguro (simplemente desreferenciar punteros) fue un error, en retrospectiva

Oh.
Las escrituras seguras están completamente en línea con el enfoque actual de la inseguridad, pero si va a cambiarlo, probablemente debería esperar.

No me siento bien por estabilizar este subconjunto. Por lo general, cuando estabilizamos un subconjunto, es un subconjunto sintáctico o al menos un subconjunto bastante obvio. Este subconjunto me parece un poco complejo. Si hay tanto indeciso sobre la característica que no estamos listos para estabilizar la implementación actual, entonces prefiero dejar todo inestable por un tiempo más.

@nrc
Bueno, el subconjunto es bastante obvio - "uniones FFI", o "uniones C", o "uniones pre-C ++ 11" - incluso si no es sintáctico. Mi objetivo inicial era estabilizar este subconjunto lo antes posible (este ciclo, idealmente) para que pudiera usarse en bibliotecas como winapi .
No hay nada especialmente dudoso sobre el subconjunto restante y su implementación, simplemente no es urgente y es necesario esperar una cantidad de tiempo poco clara hasta que se complete el proceso de RFC "Unions 1.2". Mis expectativas serían estabilizar las partes restantes en 1, 2 o 3 ciclos después de la estabilización del subconjunto inicial.

Creo que tengo un argumento fundamental para las asignaciones de campo seguras.
Asignación de campo insegura

unsafe {
    u.trivially_destructible_field = value;
}

es equivalente a asignación de unión completa segura

u = U { trivially_destructible_field: value };

excepto que la versión segura es, paradójicamente, menos seguro, ya que se sobreponen a u 's bytes fuera de trivially_destructible_field con undefs, mientras que la asignación del campo tienen garantía sobre lo que les deja intacta.

@petrochenkov El extremo de eso es size_of_val(&value) == 0 , ¿verdad?

La equivalencia entre los dos fragmentos solo es verdadera si el campo en cuestión es
"trivialmente destructible", ¿no?

En ese sentido, hacer asignaciones como estas de forma segura, pero solo en algunos casos
me parece extremadamente inconsistente.

El 22 de febrero de 2017 a las 14:50, "Vadim Petrochenkov" [email protected]
escribió:

Creo que tengo un argumento fundamental para las asignaciones de campo seguras.
Asignación de campo insegura

inseguro {
u.trivially_destructible_field = valor;
}

es equivalente a asignación de unión completa segura

u = U {trivially_destructible_field: value};

excepto que la versión segura es paradójicamente menos segura porque
sobrescribe los bytes de u fuera de trivially_destructible_field con undefs,
mientras que la asignación de campo tiene garantía de dejarlos intactos.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment-281660298 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AApc0lUOXLU5xNTfM5PEfEz9nutMZhXUks5rfC8UgaJpZM4IDXsj
.

@eddyb

El extremo de eso es size_of_val (& value) == 0, ¿verdad?

Sí.

@nagisa
No entiendo por qué una regla simple adicional que elimina una gran parte de los falsos positivos es extremadamente inconsistente. "sólo algunos casos" cubren a todos los sindicatos de FFI en particular.
Creo que "extremadamente inconsistente" es una gran sobreestimación. No tan grande como "sólo se pueden asignar variables mut ? ¡Una inconsistencia horrible!", Pero aún así va en esta dirección.

@petrochenkov , considere este caso:

// Somebody Somewhere in some crate (v 1.0.0)
struct Peach; // trivially destructible
union Banana { pub actually: Peach }

// Somebody Else in their dependent crate
extern some crate;
fn somefn(banana: &mut Banana) {
    banana.actually = Peach;
}

Ahora, dado que agregar implementaciones de rasgos generalmente no es un cambio rotundo, Mr. Somebody Somewhere se da cuenta de que puede ser una buena idea agregar la siguiente implementación

impl Drop for Peach { fn drop(&mut self) { println!("Moi Peach!") }

y lanzar una versión 1.1.0 (semver compatible con 1.0.0 AFAIK) de la caja.

De repente, la caja del Sr.Alguien más ya no se compila:

fn somefn(banana: &mut Banana) {
    banana.actually = Peach; // ERROR: Something something… unsafe assingment… somewhat somewhat trivially indestructible… 
}

Y, por lo tanto, a veces permitir asignaciones seguras a campos de unión no es tan trivial como solo mut locales que pueden mutar.


Mientras escribía este ejemplo, no estaba seguro de qué postura debería tomar con esto, honestamente. Por un lado, me gustaría preservar la propiedad de que agregar implementaciones generalmente no es un cambio rotundo (ignorando casos potenciales de XID). Por otro lado, cambiar cualquier campo de unión de trivialmente destructible a no trivialmente destructible es obviamente un cambio semver incompatible que es extremadamente fácil de pasar por alto y la regla propuesta haría que estas incompatibilidades sean más visibles (siempre que la asignación no esté en un bloque inseguro ya).

@nagisa
Este es un buen argumento, no pensé en la compatibilidad.

Sin embargo, el problema parece solucionable. Para evitar problemas de compatibilidad, haga lo mismo que hace la coherencia: evite el razonamiento negativo. Es decir, reemplace "trivialmente destructible" == "ningún componente implementa Drop " con la aproximación positiva más cercana - "implementa Copy ".
Copy no puede deshacerse de la implementación de Copy compatible con versiones anteriores, y los tipos Copy todavía representan la mayoría de los tipos "trivialmente destructibles", especialmente en el contexto de las uniones de FFI.

La implementación de Drop ya no es compatible con versiones anteriores y esto no tiene nada que ver con la función de unión:

// Somebody Somewhere in some crate (v 1.0.0)
struct Apple; // trivially destructible
struct Pineapple { pub actually: Apple }

// Somebody Else in their dependent crate
extern some crate;
fn pineapple_to_apple(pineapple: Pineapple) -> Apple {
    pineapple.actually
}
// some crate v 1.1.0
impl Drop for Pineapple { fn drop(&mut self) { println!("Moi Pineapple!") }
fn pineapple_to_apple(pineapple: Pineapple) -> Apple {
    pineapple.actually // ERROR: can't move out of Pineapple
}

Lo que a su vez suena como implementar Drop drops Copia implícita. Y copiar
se puede confiar en.

El miércoles 22 de febrero de 2017 a las 10:11 a.m., jethrogb [email protected] escribió:

La implementación de Drop ya no es compatible con versiones anteriores y esto
nada que ver con la función de unión:

// Alguien en algún lugar de alguna caja (v 1.0.0)
estructura Apple; // trivialmente destructible
struct Pineapple {pub en realidad: Apple}

// Alguien más en su caja dependiente
extern alguna caja;
fn pineapple_to_apple (piña: piña) -> manzana {
piña en realidad
}

// alguna caja v 1.1.0
impl Drop for Pineapple {fn drop (& mut self) {println! ("Moi Pineapple!")}

fn pineapple_to_apple (piña: piña) -> manzana {
banana.actually // ERROR: no puedo salir de Pineapple
}

-
Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment-281752949 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABxXhgbFgRNzYOsU4c6Gu1KFfwdjDHn3ks5rfHpYgaJpZM4IDXsj
.

@jethrogb
Quería mencionar este problema, pero no lo hice porque tiene algunas condiciones previas bastante especiales: la estructura que implementa Drop debe tener un campo público y ese campo no debe ser Copy . El caso de la unión afecta a todas las estructuras incondicionalmente.

@petrochenkov Considero que tal vez un argumento de que crear un sindicato debería ser inseguro :)

@petrochenkov ¿cuál es la antes de que se estabilice?

@pnkfelix
Lo que asumí es simplemente dejar de requerir #[feature(untagged_unions)] para este subconjunto, sin nuevas funciones u otra burocracia.
Se supone que las uniones al estilo FFI son las que se utilizan con más frecuencia, por lo que una nueva característica significaría rotura garantizada justo antes de la estabilización, lo que, supongo, sería molesto.

Solo me gustaría señalar que debido a que los atributos de alineación y empaquetado aún no se han implementado (sin importar que se hayan estabilizado), realmente no tengo una gran necesidad de que esto se estabilice todavía.

@ retep998
¿Embalaje? Si te refieres a #[repr(packed)] entonces es compatible con las uniones en este momento (a diferencia de los atributos align(>1) ).

@petrochenkov #[repr(packed(N))] . En winapi se necesita bastante embalaje diferente al 1. No es que necesite que estas cosas se apoyen específicamente en los sindicatos, simplemente no quiero saltar a una nueva versión principal para aumentar mi requisito mínimo de Rust a menos que pueda obtener todas esas cosas al mismo tiempo.

Para aclarar un poco la situación aquí:

La propuesta actual de FCP es solo para uniones puras Copy . Ese es el caso, que yo sepa, básicamente no hay preguntas pendientes más que "¿Deberíamos estabilizarnos?" La discusión desde la moción al FCP ha sido sobre el nuevo RFC de @petrochenkov .

@nrc y @nikomatsakis , de la discusión en IRC, sospecho que ambos están listos para marcar sus casillas, pero eso se lo dejo a ustedes ;-)

: bell: Esto ahora está entrando en su período de comentarios final , según la revisión anterior . :campana:

@petrochenkov
Me parece que esto se beneficiaría de una idea que planteé recientemente. (https://internals.rust-lang.org/t/automatic-marker-trait-for-unconditionally-valid-repr-c-types/5054)

Si bien no se propuso teniendo en cuenta los sindicatos, el rasgo Plain como se explicó (sujeto a eliminación de bicicletas) permitiría que cualquier sindicato compuesto por solo tipos Plain se use sin ninguna inseguridad. Codifica la propiedad de que _cualquier_ patrón de bits en la memoria es igualmente válido, por lo que puede resolver los problemas de inicialización ordenando que la memoria se ponga a cero, y garantiza que las lecturas no puedan invocar UB.

En el contexto de FFI, ser Plain como se define también es un requisito de facto para que dicho código sea sólido en muchos casos, mientras que los casos en los que los tipos que no son Plain son útiles son raros y difíciles de configurar sin peligro.

Dejando a un lado la existencia fáctica de un rasgo con nombre, sería prudente dividir este rasgo en dos. Con union no calificados haciendo cumplir los requisitos y permitiendo el uso sin inseguridad, y unsafe union con requisitos relajados para el contenido y más dolor de cabeza para los usuarios. Eso permitiría estabilizar a cualquiera sin obstaculizar el camino para agregar el otro en el futuro.

@ le-jzr
Esto parece suficientemente ortogonal a los sindicatos.
Calculo que aceptar Plain en Rust en un futuro cercano ya que no es muy probable + hacer más seguros los accesos a los campos sindicales es más o menos compatible con versiones anteriores (no completamente debido a las pelusas), por lo que no retrasaría las uniones debidas lo.

@petrochenkov No estoy sugiriendo retrasar los sindicatos a la espera de un cierre de mi propuesta, sino más bien considerar la _existencia_ de tales posibles restricciones por derecho propio. Hacer más seguros los accesos a los campos sindicales en el futuro puede enfrentar obstáculos porque crea más inconsistencia en el idioma. En particular, hacer que la seguridad de un acceso de campo varíe de un campo a otro parece feo.

De ahí mi sugerencia de hacer la declaración unsafe union , de modo que la versión incondicionalmente segura se pueda introducir más adelante sin agregar nuevas palabras clave. Vale la pena señalar que la versión completamente segura de usar es suficiente para la mayoría de los casos de uso.

Editar: Traté de aclarar lo que quiero decir.

Otro lugar donde esta posibilidad puede informar el diseño actual y futuro es la inicialización. Para una unión incondicionalmente segura, es necesario poner a cero todo el espacio de memoria reservado para ella. Incluso con la versión actual, garantizar esto reduciría los posibles escenarios de UB y facilitaría el uso de los sindicatos.

El período de comentarios final ahora está completo.

Ahora que el FCP para fusionar está completo, ¿cuál es el siguiente paso aquí? Sería bueno estabilizar esto en 1,19.

Sería increíble si alguien pudiera agregar más detalles a ese mensaje de @rfcbot ( código fuente aquí ). Podría facilitar que alguien que no esté familiarizado con el proceso salte y haga avanzar las cosas.

El camino está despejado para llegar a 1,19. ¿Alguien enganchado por eso? cc @joshtriplett

¿Se garantiza que la optimización del diseño de enumeración NonZero aplicará a través de un union ? Por ejemplo, Option<ManuallyDrop<&u32>> no debe representar None como un puntero nulo. Some(ManuallyDrop::new(uninitialized::<[Vec<Foo>; 10]>())).is_some() no debería leer la memoria no inicializada.

https://crates.io/crates/nodrop (usado en https://crates.io/crates/arrayvec) tiene trucos para lidiar con esto.

@SimonSapin
Actualmente, esto está marcado como una pregunta sin resolver en el RFC .
En la implementación actual, este programa

#![feature(untagged_unions)]

struct S {
    _a: &'static u8
}
union U {
    _a: &'static u8
}

fn main() {
    use std::mem::size_of;
    println!("struct {}", size_of::<S>());
    println!("optional struct {}", size_of::<Option<S>>());
    println!("union {}", size_of::<U>());
    println!("optional union {}", size_of::<Option<U>>());
}

huellas dactilares

struct 8
optional struct 8
union 8
optional union 16

, es decir, no se realiza la optimización.
cc https://github.com/rust-lang/rust/issues/36394

Es poco probable que sea 1,19.

@brson

Es poco probable que sea 1,19.

El PR de estabilización se fusiona.

Con las uniones sin etiquetar que ahora se envían en 1.19 (en parte de https://github.com/rust-lang/rust/pull/42068), ¿queda algo pendiente sobre este problema o deberíamos cerrar?

@jonathandturner
¡Todavía hay un mundo entero de sindicatos con campos que no son Copy !
El progreso está mayormente bloqueado en la RFC de aclaración / documentación (https://github.com/rust-lang/rfcs/pull/1897).

¿Ha habido algún progreso en los sindicatos con campos que no son Copy desde agosto? El RFC de Unions 1.2 parece estancado (¿supongo que debido al período implícito?)

Permitir tipos ?Sized en uniones, aunque solo sea para uniones de un solo tipo, facilitaría la implementación de https://github.com/rust-lang/rust/issues/47034 :

`` óxido
union ManuallyDrop{
valor: T
}

@mikeyhew Realmente solo necesita requerir que como máximo un tipo pueda ser sin tamaño.

Estoy viendo un código de Rust usando union s, y no tengo idea de si invoca un comportamiento indefinido o no.

La referencia [artículos :: sindicatos] solo menciona:

También se puede acceder a los campos inactivos (utilizando la misma sintaxis) si su diseño es suficientemente compatible con el valor actual mantenido por la unión. La lectura de campos incompatibles da como resultado un comportamiento indefinido.

Pero no puedo encontrar una definición de "diseño compatible" ni en [items :: uniones] ni en [type_system :: type_layout] .

Mirando a través de los RFC, tampoco he podido encontrar una definición de "diseño compatible", solo ejemplos de lo que debería y no debería funcionar en el RFC 1897: Unions 1.2 (no combinado).

El RFC1444: uniones solo parece permitir transmutar una unión a sus variantes siempre que no invoque un comportamiento indefinido, pero no puedo encontrar en ningún lugar del RFC cuando ese es / no es el caso.

¿Son las reglas _precisas_ las que me dicen si un fragmento de código que usa uniones tiene un comportamiento definido escrito en algún lugar (y cuál es el comportamiento definido)?

@gnzlbg Para una primera aproximación: no puede acceder al relleno, no puede acceder a una enumeración que contiene un discriminante no válido, no puede acceder a un bool que contiene un valor que no sea verdadero o falso, no puede acceder a un valor de punto flotante no válido o de señalización , y algunas otras cosas como esas.

Si señala un código específico que involucra sindicatos, podríamos mirarlo y decirle si está haciendo algo indefinido.

En una primera aproximación: no puede acceder al relleno, no puede acceder a una enumeración que contiene un discriminante no válido, no puede acceder a un bool que contiene un valor que no sea verdadero o falso, no puede acceder a un valor de punto flotante de señalización o no válido, y algunas otras cosas como esas.

En realidad, el consenso más reciente es que leer flotantes arbitrarios está bien (# 46012).

Agregaría un requisito más: las variantes de unión de origen y destino son #[repr(C)] y también lo son todos sus campos (y de forma recursiva) si son estructuras.

@Amanieu Estoy corregido, gracias.

¿Entonces supongo que las reglas no están escritas en ninguna parte?

Estoy viendo cómo usar stdsimd con su nueva interfaz. A menos que estabilicemos algunos extras con él, será necesario usar uniones para hacer juegos de palabras con algunos de los tipos de simd, como este:

https://github.com/rust-lang-nursery/stdsimd/blob/03cb92ddce074a5170ed5e5c5c20e5fa4e4846c3/coresimd/src/x86/test.rs#L17

AFAIK escribir un campo de unión y leer otro es muy parecido a usar transmute_copy , con las mismas restricciones. Que esas restricciones todavía sean un poco nebulosas no es específico de un sindicato.

Para el caso, la función que vinculó podría usar transmute::<__m128d, [f64; 2]> . Aunque la versión de unión es discutiblemente más agradable, al menos una vez que se elimina la transmutación que está allí actualmente: podría ser solo A { a }.b[idx] .

@rkruppe He llenado un problema clippy para agregar esa pelusa: https://github.com/rust-lang-nursery/rust-clippy/issues/2361

la función que vinculó podría usar transmutar :: <__ m128d i = "8">

Supongo que las reglas que estoy buscando son cuándo la transmutación invoca un comportamiento indefinido (así que iré a buscarlas).

Creo que me habría ayudado si la referencia del lenguaje sobre las uniones hubiera especificado las reglas para las uniones en términos de transmutar (incluso si las reglas para transmutar aún no son 100% claras) en lugar de simplemente mencionar "compatibilidad de diseño" y dejarlo a eso. El salto de "compatibilidad de diseño" a "si transmutar no invoca un comportamiento indefinido, entonces los tipos son compatibles con el diseño y se puede acceder a ellos a través de juegos de palabras" no me resultó obvio.

Para ser claros, transmutar [_copy] no es "más primitivo" que las uniones. De hecho, transmute_copy es literalmente solo un puntero as casts más ptr::read . transmute adicionalmente necesita mem::uninitialized (obsoleto) o MaybeUninitialized (una unión) o algo así, y se implementa como intrínseco para la eficiencia, pero también se reduce a un tipo- juego de palabras memcpy. La razón principal por la que establecí la conexión para transmutar es porque es más antiguo e históricamente demasiado enfatizado y, por lo tanto, actualmente tenemos más escritos y conocimiento del folclore que se enfoca en transmutar específicamente. El concepto subyacente real, que dicta lo que es válido y lo que no (y lo que describiría una especificación), es cómo se almacenan los valores en la memoria como bytes y qué secuencias de bytes son UB para leer como qué tipos.

Corrección: transmutar no necesita en realidad un almacenamiento no inicializado (a través de intrínsecos, uniones o de otro modo). Dejando a un lado la eficiencia, puede hacer algo como esto (no probado, puede contener errores tipográficos vergonzosos):

fn transmute<T, U>(x: T) -> U {
    assert!(size_of::<T>() == size_of::<U>());
    let mut bytes = [0u8; size_of::<U>()];
    ptr::write(bytes.as_mut_ptr() as *mut T, x);
    mem::forget(x);
    ptr::read(bytes.as_ptr() as *const U)
}

La única parte "mágica" de transmutar es que puede restringir los parámetros de tipo para que sean del mismo tamaño en el momento de la compilación .

La referencia y Unions 1.2 RFC son intencionalmente vagas sobre este asunto porque las reglas para la transmutación en general no están establecidas.
La intención era "para repr(C) uniones ver especificaciones ABI de terceros, para repr(Rust) uniones la compatibilidad de diseño no está especificada en su mayoría (a menos que lo esté)".

¿Es demasiado tarde para revisar la semántica de verificación de caída de uniones con campos de caída?

El problema original es que agregar ManuallyDrop hizo que Josephine se volviera inestable, porque (bastante maliciosamente) se basó en valores prestados de forma permanente y no se reclamó su tienda de respaldo sin ejecutar primero su destructor.

Un ejemplo simplificado se encuentra en https://play.rust-lang.org/?gist=607e2dfbd51f4062b9dc93d149815695&version=nightly. La idea es que hay un tipo Pin<'a, T> , con un método pin(&'a self) -> &'a T cuya seguridad depende del invariante "después de llamar a pin.pin() , si la memoria que respalda el pin se recupera alguna vez, el destructor del pin debe haberse ejecutado ".

Rust mantuvo este invariante hasta que se agregó #[allow(unions_with_drop_fields)] y ManuallyDrop https://doc.rust-lang.org/src/core/mem.rs.html#949 lo utilizó

El invariante se restauraría si el comprobador de caída considerara que las uniones con campos de caída tienen una implicación de caída. Este es un cambio rotundo, pero dudo que cualquier código en la naturaleza se base en la semántica actual.

Conversación IRC: https://botbot.me/mozilla/rust-lang/2018-02-01/?msg=96386869&page=3

Número de Josephine: https://github.com/asajeffrey/josephine/issues/52

cc: @nox @eddyb @pnkfelix

El problema original es que agregar ManuallyDrop hizo que Josephine se volviera inestable, porque (de manera bastante traviesa) se basó en valores prestados de forma permanente para que no se recuperara su tienda de respaldo sin ejecutar primero su destructor.

No se garantiza que los destructores funcionen. El óxido no garantiza eso. Lo intenta, pero, por ejemplo, std::mem::forget se convirtió en una función segura.

El invariante se restauraría si el comprobador de caída considerara que las uniones con campos de caída tienen una implicación de caída. Este es un cambio rotundo, pero dudo que cualquier código en la naturaleza se base en la semántica actual.

Los sindicatos son inseguros en gran parte porque no se puede saber qué campo del sindicato es válido. El sindicato no puede tener un Drop impl automático; Si quisiera una implícita de este tipo, necesitaría escribirla manualmente, teniendo en cuenta los medios que tenga para saber si el campo de unión con una implícita Drop es válido.

Una aclaración aquí: no creo que jamás debemos permitir que las uniones con Drop campos por defecto sin al menos una pelusa advertir por defecto, si no una pelusa de error por defecto. unions_with_drop_fields no debería desaparecer como parte del proceso de estabilización.

EDITAR: oops, no quise presionar "cerrar y comentar".

@joshtriplett sí, Rust no garantiza que los destructores se ejecutarán, pero sucedió (antes de 1.19) mantener el invariante de que los valores prestados de forma permanente solo tendrían su memoria reclamada si el destructor se ejecutaba. Esto es incluso cierto en presencia de mem::forget , ya que no puede llamarlo por un valor prestado de forma permanente.

Esto es en lo que Joephine confiaba de manera bastante traviesa, pero ya no es cierto debido a cómo el verificador de gotas trata unions_with_drop_fields .

Estaría bien si allow(unions_with_drop_fields) se considerara una anotación insegura, esto no sería un cambio drástico, AFAICT, solo requeriría deny(unsafe_code) para verificar allow(unions_with_drop_fields) .

@asajeffrey Todavía estoy tratando de entender la cosa Pin ... así que, si sigo el ejemplo correctamente, la razón por la que esto "funciona" es que fn pin(&'a Pin<'a, T>) -> &'a T obliga al préstamo a durar como siempre que la duración 'a anotada en el tipo, y esa duración sea además invariante.

¡Esa es una observación interesante! No estaba al tanto de este truco. Mi intuición es que esto funciona "por accidente", es decir, seguro que Rust no proporciona una forma de evitar que el destructor se ejecute, pero eso no hace que esto sea parte del "contrato". En particular, https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html no enumera las fugas.

En mi opinión, no importa si funciona por accidente o a propósito. No había forma de evitar que Drop ejecutara con este truco antes de ManuallyDrop existente (que requiere la implementación de un código inseguro), y ahora ya no podemos confiar en eso.

La adición de ManuallyDrop básicamente mató ese comportamiento ordenado de Rust y decir que no se debería haber confiado en él en primer lugar me suena a un razonamiento circular. Si ManuallyDrop no permitiera llamar a Pin::pin , ¿habría alguna otra forma de hacer que llamar a Pin::pin no sea correcto? No lo creo.

No creo que podamos comprometernos a preservar todas las garantías que rustc proporciona accidentalmente en este momento. No tenemos idea de cuáles pueden ser estas garantías, por lo que estaríamos estabilizando a un cerdo de un tirón (bueno, espero que este modismo tenga sentido ... es lo que el diccionario me dice que coincide con mi idioma nativo, que literalmente se traduciría como el gato en la bolsa ";) - lo que quiero decir es que no tendríamos ni idea de lo que estaríamos estabilizando).

Además, esta es una espada de dos filos: cada garantía adicional que decidamos brindar es algo de lo que el código inseguro debe ocuparse. Por lo tanto, descubrir una nueva garantía puede romper el código inseguro existente (sentado en silencio en algún lugar de crates.io sin darse cuenta) como habilitar un nuevo código inseguro (este último fue el caso aquí).

Por ejemplo, es muy concebible que las vidas léxicas permitan un código inseguro que se rompa con las vidas no léxicas. Actualmente, todas las vidas están bien anidadas, ¿tal vez haya una forma de que el código inseguro explote esto? Solo con vidas no léxicas puede haber vidas que se superponen, pero ninguna está incluida en la otra. ¿Esto hace que NLL sea un cambio radical? ¡Espero que no!

Si ManuallyDrop no permitiera llamar a Pin :: pin, ¿habría alguna otra forma de hacer que llamar a Pin :: pin no sea correcto? No lo creo.

Con el código unsafe , habría. Entonces, al declarar este sonido de truco Pin , estás declarando un código inseguro unsound que sería correcto si decidimos que ManuallyDrop está bien.

Lo que describimos es una forma muy ergonómica de integrar Rust con GC. Lo que estoy tratando de decir es que me suena mal decirnos que esto fue solo un accidente, que funcionó y que debemos olvidarnos de eso, cuando no puedo encontrar ningún caso de uso para no restringir las uniones con Drop campos como lo describe @asajeffrey aquí, y cuando esta es realmente la única verruga que rompe a Josephine.

Estaré feliz de olvidarlo si alguien puede demostrar que no era correcto incluso sin ManuallyDrop .

Lo que estoy tratando de decir es que me suena mal decirnos que esto fue solo un accidente que funcionó

No veo ninguna indicación de que este truco haya sido "diseñado" alguna vez, así que creo que es bastante justo llamarlo un accidente.

y que lo olvidemos

Debería haber dejado más claro que esta parte es solo mi instinto personal. Creo que también podría ser un punto de acción razonable declarar esto como un "feliz accidente" y convertirlo en una garantía, si estamos razonablemente seguros de que todos los demás códigos unsafe respetan esta garantía, y que proporcionar esta garantía es más importante que el caso de uso ManuallyDrop . Esta es una compensación, similar a la tribulación de fugas, en la que no podemos comer nuestro pastel y tenerlo también (no podemos tener tanto Rc con su API actual como drop - subprocesos de alcance basados; no podemos tener tanto ManuallyDrop como Pin ) por lo que tenemos que tomar una decisión de cualquier manera.

Dicho esto, me resulta difícil expresar la garantía real proporcionada aquí de una manera precisa, lo que hace que personalmente me incline más hacia el lado de " ManuallyDrop está bien".

si estamos razonablemente seguros de que todos los demás códigos unsafe respetan esta garantía, y que proporcionar esta garantía es más importante que el caso de uso ManuallyDrop . Esta es una compensación, similar a la tribulación de fugas, en la que no podemos comer nuestro pastel y tenerlo también (no podemos tener tanto Rc con su API actual como drop - subprocesos de alcance basados; no podemos tener tanto ManuallyDrop como Pin ) por lo que tenemos que tomar una decisión de cualquier manera.

Muy bien, estoy totalmente de acuerdo con eso. Tenga en cuenta que si consideramos lo que @asajeffrey describió como un comportamiento indefinido al final, eso puede traer de vuelta una API de hilo con alcance basado en drop .

Por lo que tengo entendido, la propuesta de Alan no es eliminar ManuallyDrop , solo para hacer que dropck asuma que él (y otras uniones con campos Drop ) tiene un destructor. (Que los destructores no hacen nada, pero su mera existencia afecta a los programas que dropck acepta o rechaza).

Estaré feliz de olvidarlo si alguien puede demostrar que no era correcto incluso sin ManuallyDrop.

No estoy seguro de si esto califica, pero aquí está mi primer intento: una implementación tonta de algo como ManuallyDrop que funciona antes de union Rust.

pub mod manually_drop {
    use std::mem;
    use std::ptr;
    use std::marker::PhantomData;

    pub struct ManuallyDrop<T> {
        data: [u8; 32],
        phantom: PhantomData<T>,
    }

    impl<T> ManuallyDrop<T> {
        pub fn new(x: T) -> ManuallyDrop<T> {
            assert!(mem::size_of::<T>() <= 32);
            let mut data = [0u8; 32];
            unsafe {
                ptr::copy(&x as *const _ as *const u8, &mut data[0] as *mut _, mem::size_of::<T>());
            }
            mem::forget(x);
            ManuallyDrop { data, phantom: PhantomData }
        }

        pub fn deref(&self) -> &T {
            unsafe {
                &*(&self.data as *const _ as *const T)
            }
        }
    }
}

(Sí, probablemente tenga que trabajar un poco más para lograr la alineación correcta, pero eso también podría hacerse sacrificando algunos bytes).
Patio de recreo que muestra esto rompe Pin : https://play.rust-lang.org/?gist=fe1d841cedb13d45add032b4aae6321e&version=nightly

Esto es lo que quise decir con espada de dos filos arriba: por lo que puedo ver, mi ManuallyDrop respeta todas las reglas que hemos publicado. Entonces, tenemos dos piezas de código inseguro incompatible: ManuallyDrop y Pin . ¿Quién tiene "razón"? Yo diría que Pin basa en garantías que nunca hemos hecho y, por lo tanto, está "mal" aquí, pero esto es una decisión de juicio, no una prueba.

Eso es interesante. En algunas versiones de nuestro material de fijación, Pin::pin toma un &'this mut Pin<'this, T> , pero no sería irrazonable que su ManuallyDrop tenga un DerefMut impl, correcto ?

Aquí hay un campo de juego que muestra que el de @RalfJung (como era de esperar) todavía rompe Pin con un método &mut -taking pin .

https://play.rust-lang.org/?gist=5057570b54952e245fa463f8d7719663&version=nightly

No sería irrazonable que su ManuallyDrop tuviera una implicación DerefMut, ¿verdad?

Sí, acabo de agregar la API que necesitaba para este ejemplo. El deref_mut obvio debería funcionar bien.

Por lo que tengo entendido, la propuesta de Alan no es eliminar ManuallyDrop, solo para hacer que dropck asuma que él (y otras uniones con campos Drop) tiene un destructor. (Que los destructores no hacen nada, pero su mera existencia afecta a los programas que dropck acepta o rechaza).

Ah, me había perdido eso; Lo siento por eso. Sin embargo, agregar lo siguiente a mi ejemplo lo mantiene funcionando:

    unsafe impl<#[may_dangle] T> Drop for ManuallyDrop<T> {
        fn drop(&mut self) {}
    }

Solo si elimino el #[may_dangle] Rust lo rechaza. Así que, como mínimo, tendríamos que idear alguna regla que infrinja el código anterior: simplemente decir "existe un código que queremos que sea sólido y con el que esto es incompatible" es una mala decisión porque lo convierte en es prácticamente imposible mirar un código y comprobar si es correcto.


Creo que lo que más me incomoda acerca de esta "garantía accidental" es que no veo una sola buena razón para que esto funcione. La forma en que se conectan las cosas en Rust hace que esto se mantenga unido, pero se ha agregado dropck no para evitar fugas, sino para evitar referencias erróneas a datos muertos (un problema común en los destructores). El razonamiento para que Pin funcione no se basa en "aquí hay algún mecanismo en el compilador de Rust, o alguna garantía del sistema de tipos, que claramente dice que los datos tomados en préstamo permanente no se pueden filtrar", sino que se basa en "Lo hemos intentado mucho y no hemos podido filtrar los datos prestados de forma permanente, así que creemos que está bien". Confiar en esto para la solidez me pone bastante nervioso. EDITAR: El hecho de que dropck esté involucrado me pone aún más nervioso porque esta parte del compilador tiene un historial de desagradables errores de solidez. La razón por la que esto funciona parece ser que los préstamos permanentes están en desacuerdo con los seguros drop . Esto realmente parece ser "un razonamiento basado en un análisis de caso exhaustivo de lo que se puede hacer con datos tomados en préstamo permanente".

Ahora, para ser justos, se podrían decir cosas similares sobre la mutabilidad interior: sucede que permitir modificaciones a través de referencias compartidas realmente funciona de manera segura en algunos casos, si elegimos la API correcta. Sin embargo, por lo que este trabajo requiere realmente un apoyo explícito en el compilador ( UnsafeCell ), ya que choca con optimizaciones, y no hay código no seguro que sería el sonido sin mutabilidad interior, pero no es sólida, con la mutabilidad de interiores. Otra diferencia es que la mutabilidad interior fue un objetivo de diseño desde el principio (o desde muy temprano, esto es mucho antes de mi tiempo en la comunidad de Rust), lo cual no es el caso de "el préstamo permanente no se filtra". Y finalmente, para la mutabilidad interior, creo que hay una historia bastante buena sobre "compartir hace que la mutación sea peligrosa , pero no imposible , y la API de referencias compartidas solo dice que no se obtiene mutabilidad en general, pero no excluye permitir más operaciones para tipos ", lo que da como resultado una imagen global coherente. Por supuesto, he pasado mucho tiempo pensando en referencias compartidas, por lo que tal vez haya una imagen igualmente coherente para el tema en cuestión que no conozco.

Las zonas horarias son divertidas, ¡me acabo de levantar! Parece que hay dos problemas aquí (invariantes en general y dropck en particular), así que los pondré en comentarios separados ...

@RalfJung : sí, este es un problema sobre los invariantes que mantiene el inseguro Rust. Para cualquier versión de Rust + std, hay más de una opción de I invariante que se mantiene utilizando el razonamiento de garantía de confianza. Y, de hecho, puede haber dos bibliotecas L1 y L2 , que eligieron I1 y I2 incompatibles, de modo que Rust + L1 es seguro y Rust + L2 es seguro, pero Rust + L1 + L2 no es seguro.

En este caso, L1 es ManuallyDrop y L2 es Josephine , y está bastante claro que ManuallyDrop va a ganar ya que ahora en std , que tiene restricciones de compatibilidad con versiones anteriores mucho más fuertes que Josephine.

Curiosamente, las pautas en https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html están escritas como "Es responsabilidad del programador al escribir código inseguro que no es posible dejar código seguro exhibir estos comportamientos: ... "es decir, es una propiedad contextual (para todos los contextos seguros C, C [P] no puede salir mal) y por lo tanto depende de la versión (ya que la v1.20 de Rust + std tiene más contextos que la v1.18). En particular, diría que el anclaje satisfacía esta restricción para Rust antes de la 1.20, ya que no había un contexto seguro. C st C [Fijar] sale mal.

Sin embargo, esto es solo un abogado de cuartel, creo que todos están de acuerdo en que hay un problema con esta definición contextual, de ahí todas las discusiones sobre pautas de códigos inseguros.

Al menos, creo que la fijación ha mostrado un ejemplo interesante de invariantes accidentales que van mal.

Lo que hicieron las uniones sin etiquetar (y por lo tanto ManuallyDrop ) fue en la interacción con el verificador de caída, en particular ManualDrop actúa como si su defn fuera:

unsafe impl<#[may_dangle] T> Drop for ManuallyDrop<T> { ... }

y luego puede tener una conversación sobre si esto está permitido o no :) De hecho, esta conversación está sucediendo en el hilo may_dangle comienza en https://github.com/rust-lang/rust/issues/ 34761 # issuecomment -362375924

@RalfJung su código muestra un caso de esquina interesante, donde el tipo de tiempo de ejecución para data es T , pero su tipo de tiempo de compilación es [u8; N] . ¿Qué tipo cuenta en lo que respecta a may_dangle ?

Curiosamente, las pautas en https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html están escritas como "Es responsabilidad del programador al escribir código inseguro que no es posible dejar código seguro exhibir estos comportamientos: ... "es decir, es una propiedad contextual

Ah, interesante. Estoy de acuerdo en que esto claramente no es suficiente, esto haría que los hilos de alcance originales suenen. Para ser significativo, esto tiene que (al menos) especificar el conjunto de código inseguro al que se le permite llamar al código seguro.

Personalmente, creo que una mejor manera de especificar esto es dar las invariantes que se van a mantener. Pero estoy claramente sesgado aquí, porque la metodología que utilizo para probar cosas sobre Rust requiere tal invariante. ;)

Estoy un poco sorprendido de que la página no contenga algún tipo de descargo de responsabilidad por ser preliminar; todavía no estamos realmente seguros de cuál será exactamente el límite, como muestra esta discusión. Requerimos código inseguro para al menos hacer lo que dice ese documento, pero probablemente tengamos que requerir más.

Por ejemplo, los límites de un comportamiento indefinido y lo que puede hacer código no seguro no son los mismos. Consulte https://github.com/nikomatsakis/rust-memory-model/issues/44 para ver una discusión reciente sobre ese tema: Duplicar un &mut T por mem::size_of::<T>() == 0 no conduce a ningún comportamiento indefinido directamente y, sin embargo, se considera claramente ilegal que lo haga un código inseguro. La razón es que otros códigos inseguros pueden depender de que se respete su disciplina de propiedad, y duplicar cosas viola esa disciplina.

Al menos, creo que la fijación ha mostrado un ejemplo interesante de invariantes accidentales que van mal.

Oh, eso ciertamente. Y me pregunto qué podemos hacer para evitar esto en el futuro. Tal vez ponga una gran advertencia en https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html diciendo "solo porque un invariante se mantenga en rustc + libstd, no significa que el código inseguro confiar en él; en cambio, aquí hay algunos invariantes en los que puede confiar "?

@RalfJung sí, no creo que nadie esté enamorado de la definición contextual de "corrección", principalmente porque es frágil con el poder de observación de los contextos. Sería mucho más feliz con una definición semántica en términos de invariantes.

Lo único que pediría es si podemos darnos un margen de maniobra y definir dos invariantes para el razonamiento de garantía de confianza (el código puede depender de R y debería garantizar G, donde G implica R). De esa manera, hay espacio para fortalecer R y debilitar G. ¡Si solo tenemos una invariante (es decir, R = G), estamos atascados y nunca podremos cambiarlos!

La verificación constante actualmente no tiene campos de unión de casos especiales: (cc @solson @ oli-obk)

union Transmute<T, U> { from: T, to: U }

const SILLY: () = unsafe {
    (Transmute::<usize, Box<String>> { from: 1 }.to, ()).1
};

fn main() {
    SILLY
}

El código anterior produce el error de evaluación miri "llamando a fn no constante std::ptr::drop_in_place::<(std::boxed::Box<std::string::String>, ())> - shim(Some((std::boxed::Box<std::string::String>, ()))) ".

Cambiarlo para forzar el tipo de .to a ser observado por el verificador de const:

const fn id<T>(x: T) -> T { x }

const SILLY: () = unsafe {
    (id(Transmute::<usize, Box<String>> { from: 1 }.to), ()).1
};

da como resultado "los destructores no se pueden evaluar en tiempo de compilación".

El código de implementación relevante está aquí (específicamente la llamada restrict ):
https://github.com/rust-lang/rust/blob/5e4603f99066eaf2c1cf19ac3afbac9057b1e177/src/librustc_mir/transform/qualify_consts.rs#L557

Un mejor análisis de # 41073 había revelado que la semántica para cuando se ejecutan los destructores al asignar a subcampos de uniones no está suficientemente preparada para la estabilización. Consulte ese problema para obtener más detalles.

¿Es realista descartar por completo los tipos Drop en uniones e implementar ManuallyDrop separado (como un elemento de idioma)? Por lo que puedo decir, ManuallyDrop parece ser la mayor motivación para Drop en los sindicatos, pero ese es un caso muy especial.

En ausencia de un rasgo positivo de "no dejar caer", podríamos decir que una unión está bien formada si cada campo es Copy o de la forma ManuallyDrop<T> . Eso evitaría por completo todas las complicaciones relacionadas con la eliminación al asignar campos de unión (donde parece que todas las soluciones posibles estarán llenas de pistolas sorprendentes), y el ManuallyDrop es un marcador claro para los programadores de que tienen que manejar Drop ellos mismos aquí. (La verificación podría ser más inteligente, por ejemplo, podría atravesar tipos de productos y tipos nominales que se declaran en la misma caja. Por supuesto, tener una forma positiva de decir "este tipo nunca implementará Drop " sería mejor.)


La lista de verificación en la primera publicación no menciona uniones sin tamaño, ni el RFC --- pero aún tenemos una implementación , restringida a uniones de variante única. Esto está estrechamente relacionado con la interacción con las optimizaciones de diseño porque presupone (una vez que las DST de puntero delgado entran en la imagen) que una unión de una sola variante tiene que ser "válida" en algún sentido (puede descartarse, pero no puede ser cualquier patrón de bits extraño tampoco).

Esto entra en conflicto con la forma en que las uniones a veces se usan en C, que es un "punto de extensión" (IIRC @joshtriplett fue el que mencionó esto en todas las manos): un archivo de encabezado puede declarar 3 variantes para una unión, pero esto se considera compatible hacia adelante con la adición de más variantes más adelante (siempre que eso no aumente el tamaño de la unión). El usuario de la biblioteca se compromete a no tocar los datos de unión si la etiqueta (en otro lugar) indica que no conoce la variante actual. Fundamentalmente, si solo conoce una única variante, ¡eso no significa que solo haya una única variante!

La verificación podría ser más inteligente, por ejemplo, podría atravesar los tipos de productos y los tipos nominales que se declaran en la misma caja.

Este predicado ya existe, pero es conservador en los genéricos debido a que no hay ningún rasgo al que unirse.
Puede acceder a él a través de std::mem::needs_drop (que usa un intrínseco que rustc implementa).

@eddyb needs_drop tendrá en cuenta la compatibilidad con versiones posteriores, o estudiará con Drop ? El objetivo aquí es tener una verificación que nunca se separe de los cambios compatibles con semver, donde, por ejemplo, agregar un impl Drop a una estructura sin parámetros de tipo o de por vida y solo los campos privados es semver compatible.

@RalfJung

Esto entra en conflicto con la forma en que las uniones a veces se usan en C, que es un "punto de extensión" (IIRC @joshtriplett fue el que mencionó esto en todas las manos): un archivo de encabezado puede declarar 3 variantes para una unión, pero esto se considera compatible hacia adelante con la adición de más variantes más adelante (siempre que eso no aumente el tamaño de la unión). El usuario de la biblioteca se compromete a no tocar los datos de unión si la etiqueta (en otro lugar) indica que no conoce la variante actual. Fundamentalmente, si solo conoce una única variante, ¡eso no significa que solo haya una única variante!

Ese es un caso muy específico.
Solo afecta a las uniones de estilo C (por lo que no hay destructores y todo es Copy , exactamente el subconjunto disponible en estable) generado a partir de encabezados C.
Podemos agregar fácilmente un campo _dummy: () o _future: () a tales uniones y seguir beneficiándonos de un modelo "enum" más seguro por defecto. Un sindicato de FFI como "punto de extensión" es algo que debe estar bien documentado de todos modos.

El 17 de abril de 2018 a las 10:08:54 a.m. PDT, Vadim Petrochenkov [email protected] escribió:

Podemos agregar fácilmente un campo _dummy: () o _future: () a dichas uniones
y siga beneficiándose de nuestro modelo de "enumeración" más seguro de forma predeterminada.

He visto a gente hablar de tratar a los sindicatos como enumeraciones para las que simplemente no conocemos al discriminante, pero que yo sepa, no conozco ningún modelo o tratamiento real de ellos como tales. En la discusión original, incluso los sindicatos que no eran FFI querían el modelo de "múltiples variantes válidas a la vez", incluidos los casos de uso motivadores para querer uniones que no son FFI.

Agregar una variante () a un sindicato no debería cambiar nada, y los sindicatos no deberían estar obligados a hacerlo para obtener la semántica que esperan. Los sindicatos deberían seguir siendo una bolsa de bits, y Rust no tiene idea de lo que podrían contener en un momento dado hasta que un código inseguro acceda a ellos.

Unión FFI
ser un "punto de extensión" es algo que debe estar bien documentado
de todas formas.

Sin duda, deberíamos documentar la semántica con la mayor precisión posible.

@RalfJung No, se comporta como lo hacen auto trait s, exponiendo todos los detalles internos.

Actualmente hay algo de discusión en torno a los "campos activos" y la caída de sindicatos en https://github.com/rust-lang/rust/issues/41073#issuecomment -380291471

Los sindicatos deberían seguir siendo una bolsa de bits, y Rust no tiene idea de lo que podrían contener en un momento dado hasta que un código inseguro acceda a ellos.

Así es exactamente como esperaría que funcionen los sindicatos. Son una característica avanzada para exprimir el rendimiento adicional e interactuar con el código C, donde no existen los destructores.

Para mí, si desea eliminar el contenido de una unión, debería ~ tener que emitir / transmutar (tal vez no se pueda transmutar porque podría ser más grande con algunos bits sin usar al final para otra variante) al tipo desea soltar ~ tomar punteros a los campos que necesitan soltar y usar std::ptr::drop_in_place , o usar la sintaxis del campo para extraer el valor.

Si no supiera nada sobre los sindicatos, así es como esperaría que funcionen:

Ejemplo: representación de mem::uninitialized como una unión

pub union MaybeValid<T> {
    valid: T,
    invalid: ()
}

impl<T> MaybeValid<T> {
    #[inline] // this should optimize to a no-op
    pub fn from_valid(valid: T) -> MaybeValid<T> {
        MaybeValid { valid }
    }

    pub fn invalid() -> MaybeValid<T> {
        MaybeValid { invalid: () }
    }

   pub fn zeroed() -> MaybeValid<T> {
        // do whatever is necessary here...
        unimplemented!()
    }
}

fn example() {
    let valid_data = MaybeValid::from_valid(1_u8);
    // Destructor of a union always does nothing, but that's OK since our 
    // data type owns nothing.
    drop(valid_data);
    let invalid_data = MaybeValid::invalid();
    // Destructor of a union again does nothing, which means it needs to know 
    // nothing about its surroundings, and can't accidentally try to free unused memory.
    drop(invalid_data);
    let valid_data = MaybeValid::from_valid(String::from("test string"));
    // Now if we dropped `valid_data` we would leak memory, since the string 
    // would never get freed. This is already possible in safe rust using e.g. `Rc`. 
    // `union` is a similarly advanced feature to `Rc` and so new users are 
    // protected by the order in which concepts are introduced to them. This is 
    // still "safe" even though it leaks because it cannot trigger UB.
    //drop(valid_data)
    // Since we know that our union is of a particular form, we can safely 
    // move the value out, in order to run the destructor. I would expect this 
    // to fail if the drop method had run, even though the drop method does 
    // nothing, because that's the way stuff works in rust - once it's dropped
    // you can't use it.
    let _string_to_drop = unsafe { valid_data.valid };
    // No memory leak and all unsafety is encapsulated.
}

Voy a publicar esto y luego editarlo para no perder mi trabajo.
EDITAR @SimonSapin forma de soltar campos.

si desea eliminar el contenido de una unión, debe emitir / transmutar (tal vez no se pueda transmutar porque podría ser más grande con algunos bits sin usar al final para otra variante) al tipo que desea eliminar o use la sintaxis del campo para extraer el valor

(Si es solo para dejarlo caer, no es necesario extraer el valor en el sentido de moverlo, puede llevar un puntero a uno de los campos y usar std::ptr::drop_in_place ).

Relacionado: para las constantes, actualmente estoy argumentando que al menos un campo de una unión dentro de una constante debe ser correcto: https://github.com/rust-lang/rust/pull/51361 (si tiene un campo ZST que es siempre cierto)

Voy a publicar esto y luego editarlo para no perder mi trabajo.

Tenga en cuenta que las modificaciones no se reflejan en las notificaciones por correo electrónico. Si va a realizar cambios significativos en su comentario, considere hacer un nuevo comentario en su lugar o además.

@derekdreery (y todos los demás) Me interesarían sus comentarios para https://internals.rust-lang.org/t/pre-rfc-unions-drop-types-and-manuallydrop/8025

Relacionado: para las constantes, actualmente estoy argumentando que al menos un campo de una unión dentro de una constante debe ser correcto: # 51361

He visto la implementación pero no he visto el argumento. ;)

Bueno ... el argumento de "no verificar en absoluto parecía extraño".

Estaré feliz de implementar cualquier esquema que se nos ocurra en el verificador de const, pero mi intuición siempre fue que una variante de una unión debe ser completamente correcta.

De lo contrario, las uniones son solo una manera bonita de especificar un tipo con un tamaño y alineación específicos y cierta conveniencia generada por el compilador para transmutar entre un conjunto fijo de tipos.

Creo que los sindicatos son "bolsas de bits no interpretados" con alguna forma conveniente de acceder a ellos. No veo nada extraño en no revisarlos.

AFAIK, en realidad, hay algunos casos de uso que @joshtriplett mencionó en el Hands de Berlín donde la primera mitad de la unión coincide con un campo y la segunda mitad coincide con otro campo.

Creo que los sindicatos son "bolsas de bits no interpretados" con alguna forma conveniente de acceder a ellos. No veo nada extraño en no revisarlos.

Siempre pensé que esta interpretación va un poco en contra del espíritu del idioma.
En otros lugares utilizamos el análisis estático para evitar pistolas, verifique que no se acceda a valores no inicializados o prestados, pero para las uniones que el análisis se deshabilita repentinamente, por favor dispare.

Veo eso exactamente como el propósito de union . Quiero decir que también tenemos punteros sin procesar donde todos los análisis están deshabilitados. Las uniones proporcionan un control total sobre el diseño de los datos, al igual que los punteros sin procesar proporcionan un control total sobre el acceso a la memoria. Ambos van a costa de la seguridad.

Además, esto hace que union simple . Creo que ser simple es importante, e incluso más importante cuando se trata de un código inseguro (que siempre será el caso de los sindicatos). Solo deberíamos aceptar una complejidad adicional aquí si proporciona beneficios tangibles.

No creemos que tengamos que pagar ese costo por los sindicatos, ya que el modelo de bolsa de bits no brinda nuevas oportunidades en comparación con el modelo enum-with-unknown-variant.

La propiedad en cuestión aquí es al menos una carga para el código inseguro de mantener como una salvaguardia. No hay un análisis estático que pueda evitar todos los errores que podrían romper esta propiedad, ya que queremos usar uniones para juegos de palabras de tipo inseguros leer desde la unión, ya que leer ya requiere saber (a través de canales que el compilador no entiende) que los bits son válidos para la variante que estás leyendo. De hecho, solo podemos advertir a los usuarios sobre una unión que no es válida para ninguna de sus variantes cuando se ejecuta bajo miri, no en la gran mayoría de los casos en los que ocurre en tiempo de ejecución.

1 Por ejemplo, asumiendo que las tuplas son repr (C) por simplicidad, union Foo { a: (bool, u8), b: (u8, bool) } permite construir algo que no es válido solo por asignaciones de campo.

@rkruppe

union Foo {a: (bool, u8), b: (u8, bool)}

Oye, ese es mi ejemplo :)
Y es válido bajo el modelo RFC 1897 (al menos uno de los fragmentos de "hoja" bool -1, u8 -1, u8 -2, bool -2 es válido después de cualquier asignación parcial).

el código que maneja los sindicatos debe tener mucho cuidado con la forma en que escribe al sindicato o arriesgarse a UB instantáneo

Ese es el punto del modelo de RFC 1897, la verificación estática garantiza que ninguna operación segura (como asignación o asignación parcial) pueda convertir la unión en un estado no válido, por lo que no necesita tener mucho cuidado todo el tiempo y no obtener UB instantáneo .
Solo las operaciones inseguras no relacionadas con la unión, como las escrituras a través de punteros salvajes, pueden invalidar una unión.

Por otro lado, sin la comprobación de movimientos, la unión se puede poner en estado inválido muy fácilmente.

let u: Union;
let x = u.field; // UB

Ese es el punto del modelo de RFC 1897, la verificación estática garantiza que ninguna operación segura (como asignación o asignación parcial) pueda convertir la unión en un estado no válido, por lo que no es necesario tener mucho cuidado todo el tiempo y no obtener UB instantáneo .
Solo las operaciones inseguras no relacionadas con la unión, como las escrituras a través de punteros salvajes, pueden invalidar una unión.

Puede reconocer automáticamente que algunos tipos de escrituras no violan las invariantes adicionales impuestas a las uniones, pero aún así, los escritores deben respetar las invariantes adicionales. Dado que la lectura sigue siendo insegura y requiere asegurarse manualmente de que los bits serán válidos para la variante que se lee, esto en realidad no ayuda a los lectores, solo dificulta la vida de los escritores. Ni "bolsa de bits" ni "enumeración con variante desconocida" ayudan a resolver el difícil problema de las uniones: cómo asegurarse de que realmente almacene el tipo de datos que desea leer.

¿Cómo afectaría la revisión de tipos más elegante a Dropping? Si crea una unión y luego se la pasa a C, que toma posesión, ¿tratará rust de liberar los datos, quizás causando una doble liberación? ¿O siempre implementaría Drop usted mismo?

editar sería genial si las uniones fueran como "enumeraciones donde la variante se verifica estáticamente en el tiempo de compilación", si he entendido la sugerencia

editar 2 ¿ Podrían las uniones comenzar como una bolsa de bits y luego permitir un acceso seguro mientras son compatibles con versiones anteriores?

Y es válido según el modelo de RFC 1897 (al menos uno de los fragmentos de "hoja" bool-1, u8-1, u8-2, bool-2 es válido después de cualquier asignación parcial).

Si decidimos que queremos que esto sea válido, creo que @ oli-obk debería actualizar las comprobaciones de miri para reflejar eso, con https://github.com/rust-lang/rust/pull/51361 combinado, sería rechazado por miri.

@petrochenkov La parte que no entiendo es lo que esto nos compra. Obtenemos una complejidad adicional, en términos de implementación (análisis estático) y uso (el usuario aún debe conocer las reglas exactas). Esta complejidad adicional se suma al hecho de que cuando se utilizan los sindicatos, ya estamos en un contexto inseguro así que las cosas son naturalmente más compleja. Creo que deberíamos tener una motivación clara de por qué vale la pena esta complejidad adicional. No considero que "viola un poco el espíritu del idioma" como una motivación clara.

Lo único en lo que puedo pensar es en optimizaciones de diseño. En un modelo de "bolsa de bits", un sindicato nunca tiene un nicho. Sin embargo, creo que es mejor abordar dándole al programador más control manual sobre el nicho, lo que también sería útil en otros casos .

Creo que me falta algo fundamental aquí. Estoy de acuerdo con @rkruppe en que
el gran problema con los sindicatos es asegurarse de que el sindicato actualmente almacena
los datos que el programa quiere leer.

Pero AFAIK, este problema no se puede resolver "localmente" mediante análisis estático. Nosotros
al menos necesitaría un análisis completo del programa, e incluso entonces
un problema difícil de resolver.

Entonces ... ¿hay una solución para este problema sobre la mesa? O, ¿qué
las soluciones exactas que se proponen realmente nos compran? Digamos que obtengo un sindicato de C
sin analizar todo el programa de Rust y C, ¿qué puede
¿Los análisis estáticos realmente garantizan a los lectores?

@gnzlbg Creo que la única garantía que obtendríamos es lo que @petrochenkov escribió arriba

La verificación estática garantiza que ninguna operación segura (como asignación o asignación parcial) pueda convertir la unión en un estado no válido

Por otro lado, sin la comprobación de movimientos, la unión se puede poner en estado inválido muy fácilmente.

Tu propuesta tampoco protege contra malas lecturas, no creo que eso sea posible.

Además, me imaginé un seguimiento "inicializado" muy básico a lo largo de las líneas de "escribir en cualquier campo inicializa la unión". De todos modos, necesitaríamos algo cuando se permita impl Drop for MyUnion . Para bien o para mal, tenemos que decidir cuándo y dónde insertar llamadas de desconexión automática para un sindicato. Esas reglas deben ser tan simples como sea posible porque este es un código adicional que estamos insertando en un código inseguro sutil existente. Para los sindicatos que implementan Drop , también imaginé una restricción similar a struct que no permite escribir en un campo a menos que la estructura de datos ya esté inicializada.

@derekchiang

¿Podrían los sindicatos comenzar como una bolsa de bits y luego permitir un acceso seguro mientras son compatibles con versiones anteriores?
No. Una vez que decimos que es una bolsa de bits, podría haber un código inseguro asumiendo que está permitido.

Creo que hay valor en la comprobación mínima de movimientos para ver si se inicializa una unión. El RFC original especificó explícitamente que inicializar o asignar a cualquier campo de unión hace que se inicialice toda la unión. Más allá de eso, sin embargo, rustc no debería intentar inferir nada sobre el valor en una unión que el usuario no especifique explícitamente; una unión puede contener cualquier valor, incluido un valor que no es válido para ninguno de sus campos.

Un caso de uso para eso, por ejemplo: considere una unión etiquetada de estilo C que sea explícitamente extensible con más etiquetas en el futuro. La lectura de código C y Rust que unión no debe asumir que conoce todos los tipos de campo posibles.

@RalfJung

Quizás debería empezar por la otra dirección.

¿Debería funcionar este código 1) para sindicatos 2) para no sindicados?

let x: T;
let y = x.field;

Para mí, la respuesta es obvia "no" en ambos casos, porque se trata de toda una clase de errores que Rust puede y quiere evitar, independientemente de la "unión" de T .

Esto significa que el verificador de movimientos debe tener algún tipo de esquema según el cual implemente ese soporte. Dado que el verificador de movimientos (y el verificador de préstamos) generalmente funcionan por campo, el esquema más simple para las uniones sería "las mismas reglas que para las estructuras + (des) inicialización / préstamo de un campo también (des) inicializa / toma prestados sus campos hermanos ".
Esta sencilla regla cubre todas las comprobaciones estáticas.

Entonces, el modelo enum es simplemente una consecuencia de la verificación estática descrita anteriormente + una condición más.
Si 1) la verificación de inicialización está habilitada y 2) el código inseguro no escribe bytes inválidos arbitrarios en el área que pertenece a la unión, entonces uno de los campos "hoja" de las uniones es automáticamente válido. Esto es una garantía dinámica que no se puede verificar (al menos para las uniones con> 1 campos y fuera de la constante-evaluador), pero está dirigido a personas que leen el código en primer lugar.

Este caso de @joshtriplett , por ejemplo

Un caso de uso para eso, por ejemplo: considere una unión etiquetada de estilo C que sea explícitamente extensible con más etiquetas en el futuro. La lectura de código C y Rust que unión no debe asumir que conoce todos los tipos de campo posibles.

sería mucho más claro para las personas que leen código si el sindicato tuviera explícitamente un campo adicional para "posibles extensiones futuras".

Por supuesto, podemos mantener la comprobación de inicialización estática básica, pero rechazar la segunda condición y permitir la escritura de datos arbitrarios posiblemente inválidos en la unión a través de algún medio inseguro de "terceros" sin que sea una UB instantánea. Entonces ya no tendríamos esa garantía dinámica dirigida a personas, solo creo que sería una pérdida neta.

@petrochenkov

¿Debería funcionar este código 1) para sindicatos 2) para no sindicados?

let x: T;
let y = x.field;

Para mí, la respuesta es obvia "no" en ambos casos, porque se trata de toda una clase de errores que Rust puede y quiere evitar, independientemente de la "unión" de T .

De acuerdo, este nivel de verificación de valores no inicializados parece razonable y bastante factible.

Esto significa que el verificador de movimientos debe tener algún tipo de esquema según el cual implemente ese soporte. Dado que el verificador de movimientos (y el verificador de préstamos) generalmente funcionan por campo, el esquema más simple para las uniones sería "las mismas reglas que para las estructuras + (des) inicialización / préstamo de un campo también (des) inicializa / toma prestados sus campos hermanos ".
Esta sencilla regla cubre todas las comprobaciones estáticas.

De acuerdo hasta ahora, asumiendo que entiendo las reglas para estructuras.

Entonces, el modelo enum es simplemente una consecuencia de la verificación estática descrita anteriormente + una condición más.
Si 1) la verificación de inicialización está habilitada y 2) el código inseguro no escribe bytes inválidos arbitrarios en el área que pertenece a la unión, entonces uno de los campos "hoja" de las uniones es automáticamente válido. Esto es una garantía dinámica que no se puede verificar (al menos para las uniones con> 1 campos y fuera de la constante-evaluador), pero está dirigido a personas que leen el código en primer lugar.

Esa condición adicional no es válida para los sindicatos.

Este caso de @joshtriplett , por ejemplo

Un caso de uso para eso, por ejemplo: considere una unión etiquetada de estilo C que sea explícitamente extensible con más etiquetas en el futuro. La lectura de código C y Rust que unión no debe asumir que conoce todos los tipos de campo posibles.

sería mucho más claro para las personas que leen código si el sindicato tuviera explícitamente un campo adicional para "posibles extensiones futuras".

No es así como funcionan los sindicatos C, ni cómo se especificó que funcionaran los sindicatos de Rust. (Y me preguntaría si sería más claro, o simplemente si coincide con un conjunto diferente de expectativas). Cambiar esto haría que las uniones Rust ya no se ajusten a algunos de los propósitos para los que fueron diseñadas y propuestas.

Por supuesto, podemos mantener la comprobación de inicialización estática básica, pero rechazar la segunda condición y permitir la escritura de datos arbitrarios posiblemente inválidos en la unión a través de algún medio inseguro de "terceros" sin que sea una UB instantánea. Entonces ya no tendríamos esa garantía dinámica dirigida a personas, solo creo que sería una pérdida neta.

Esos 'medios inseguros de "terceros" incluyen "obtener una unión de FFI", que es un caso de uso completamente válido.

He aquí un ejemplo concreto:

union Event {
    event_id: u32,
    event1: Event1,
    event2: Event2,
    event3: Event3,
}

struct Event1 {
    event_id: u32, // always EVENT1
    // ... more fields ...
}
// ... more event structs ...

match u.event_id {
    EVENT1 => { /* ... */ }
    EVENT2 => { /* ... */ }
    EVENT3 => { /* ... */ }
    _ => { /* unknown event */ }
}

Ese es un código completamente válido que la gente puede escribir y escribirá utilizando sindicatos.

@petrochenkov

¿Debería funcionar este código 1) para sindicatos 2) para no sindicados?
Para mí, la respuesta es obviamente "no" en ambos casos, porque esta es toda una clase de errores que Rust puede y quiere prevenir, independientemente de la "unión" de T.

Bien por mí.

el esquema más simple para uniones sería "las mismas reglas que para estructuras + (des) inicialización / préstamo de un campo también (des) inicializa / toma prestados sus campos hermanos".

Woah. Las reglas de estructura tienen sentido porque todas se basan en el hecho de que los diferentes campos son inconexos . No puede simplemente invalidar esa suposición básica y seguir usando las mismas reglas. El hecho de que necesite un anexo a las reglas lo demuestra. Nunca esperaría que las uniones se verificaran de manera similar a las estructuras. En todo caso, uno podría esperar que se verifiquen de manera similar a las enumeraciones, pero, por supuesto, eso no puede funcionar, porque solo se puede acceder a las enumeraciones a través de coincidencias.

Si 1) la verificación de inicialización está habilitada y 2) el código inseguro no escribe bytes inválidos arbitrarios en el área que pertenece a la unión, entonces uno de los campos "hoja" de las uniones es automáticamente válido. Esto es una garantía dinámica que no se puede verificar (al menos para las uniones con> 1 campos y fuera de la constante-evaluador), pero está dirigido a personas que leen el código en primer lugar.

Creo que es extremadamente deseable que los supuestos básicos de validez sean verificables dinámicamente (información de tipo dada). Luego, podemos verificarlos durante CTFE en miri, incluso podemos verificarlos durante ejecuciones de miri "completas" (por ejemplo, de un conjunto de pruebas), eventualmente podemos tener algún tipo de desinfectante o tal vez un modo donde Rust emite debug_assert! en lugares críticos para comprobar las invariantes de validez.
Creo que la experiencia con las reglas incontrolables de C proporciona una amplia evidencia de que son problemáticas. Por lo general, el primer paso para comprender y aclarar realmente cuáles son las reglas es encontrar una forma dinámicamente comprobable de expresarlas. Incluso para los modelos de memoria de concurrencia, las variantes "dinámicamente comprobables" (semántica operativa que explica todo en términos de la ejecución paso a paso de una máquina virtual) están apareciendo y parecen ser la única forma de resolver problemas abiertos de larga data de lo axiomático. modelos que se utilizaron anteriormente ("problema de la nada" es una palabra clave aquí).

Difícilmente puedo exagerar lo importante que creo que es tener reglas que se puedan verificar dinámicamente. Creo que deberíamos apuntar a tener 0 casos no comprobables de UB. (Aún no hemos llegado allí, pero es el objetivo que deberíamos tener). Esa es la única forma responsable de tener UB en su idioma, todo lo demás es un caso de compiladores / autores de idiomas que les facilitan la vida a expensas de todos los que tiene que vivir con las consecuencias. (Actualmente estoy trabajando en reglas verificables dinámicamente para aliasing y accesos de puntero sin procesar).
Incluso si ese fuera el único problema, en lo que a mí respecta, "no verificable dinámicamente" es motivo suficiente para no utilizar este enfoque.

Dicho esto, no veo ninguna razón fundamental por la que esto no deba ser comprobable: para cada byte en la unión, revise todas las variantes para ver qué valores están permitidos para ese byte en esta variante, y tome la unión (heh;)) de todos de esos conjuntos. Una secuencia de bytes es válida para una unión si cada byte es válido según esta definición.
Sin embargo, esto es bastante difícil de implementar realmente una verificación, con mucho, el invariante de validez de tipo básico más complejo que tendríamos en Rust. Esa es una consecuencia directa del hecho de que esta regla de validez es algo complicada de describir, razón por la cual no me gusta.

Por supuesto, podemos mantener la comprobación de inicialización estática básica, pero rechazar la segunda condición y permitir la escritura de datos arbitrarios posiblemente inválidos en la unión a través de algún medio inseguro de "terceros" sin que sea una UB instantánea. Entonces ya no tendríamos esa garantía dinámica dirigida a personas, solo creo que sería una pérdida neta.

¿Qué nos compra esa garantía? ¿Dónde ayuda realmente? En este momento, todo lo que veo es que todos tienen que trabajar duro y tener cuidado de mantenerlo. No veo el beneficio que nosotros, la gente, obtengamos de eso.

@joshtriplett

considere una unión etiquetada de estilo C que sea explícitamente extensible con más etiquetas en el futuro. La lectura de código C y Rust que unión no debe asumir que conoce todos los tipos de campo posibles.

El modelo propuesto por @petrochenkov permite esos casos de uso, agregando un campo __non_exhaustive: () a la unión. Sin embargo, no creo que sea necesario. Posiblemente, los generadores de enlace podrían agregar ese campo.

@RalfJung

Esto es dinámico no verificable (al menos para uniones con> 1 campos y fuera de const-evaluator) garantía

Creo que es extremadamente deseable que los supuestos básicos de validez sean verificables dinámicamente

Una aclaración: quise decir que no se puede marcar en "por defecto" / "en modo de lanzamiento", por supuesto que se puede marcar en "modo lento" con algo de instrumentación adicional, pero ya escribiste sobre esto mejor que yo.

@RalfJung

El modelo propuesto por @petrochenkov permite esos casos de uso, agregando un campo __non_exhaustive: () a la unión.

Sí, entendí que esa era la propuesta.

Sin embargo, no creo que sea necesario. Posiblemente, los generadores de enlace podrían agregar ese campo.

Podrían, pero tendrían que agregarlo sistemáticamente a cada sindicato.

Todavía tengo que ver un argumento de por qué tiene sentido romper los casos de uso primarios de uniones a favor de algún caso de uso no especificado que depende de limitar los patrones de bits que pueden contener.

@joshtriplett

casos de uso principal de uniones

No es obvio para mí en absoluto por qué este es el caso de uso principal.
Puede ser cierto para repr(C) uniones si asume que todos los usos de uniones para uniones etiquetadas / "Rust enum emulation" en FFI asumen extensibilidad (lo cual no es cierto), pero por lo que he visto, los usos de repr(Rust) uniones (control de caída, control de inicialización, transmutaciones) no espere que aparezcan "variantes inesperadas" en ellas.

@petrochenkov No dije "romper el caso de uso principal", dije "romper los casos de uso primarios". FFI es uno de los principales casos de uso de sindicatos.

y toma la unión (je;)) de todos esos conjuntos

Ciertamente, hay una obviedad atractiva en una afirmación de que "los valores posibles de una unión son la unión de los valores posibles de todas sus variantes posibles" ...

Cierto. Sin embargo, esa no es la propuesta; todos estamos de acuerdo en que lo siguiente debería ser legal:

union F {
  x: (u8, bool),
  y: (bool, u8),
}
fn foo() -> F {
  let mut f = F { x: (5, false) };
  unsafe { f.y.1 = 17; }
  f
}

En realidad, creo que es un error que esto incluso requiera unsafe .

Entonces, la unión debe tomarse por bytes, al menos.
Además, no creo que la "obviedad atractiva" por sí sola sea una razón suficientemente buena. Cualquier invariante que decidamos es una carga significativa para los autores de código inseguro, deberíamos tener ventajas concretas que obtenemos a su vez.

@RalfJung

En realidad, creo que es un error que esto incluso requiere inseguro.

No sé acerca de la nueva implementación del verificador de inseguridad basada en MIR, pero en la antigua basada en HIR era ciertamente una limitación / simplificación del verificador: solo se analizaron las expresiones de la forma expr1.field = expr2 para el campo "posible" asignación "exclusión voluntaria por inseguridad, todo lo demás se trató de manera conservadora como" acceso de campo "genérico que no es seguro para los sindicatos.

Respondiendo al comentario en https://github.com/rust-lang/rust/issues/52786#issuecomment -408645420:

Así que la idea es que el compilador aún no sepa nada sobre el Wrap<T> y no pueda, por ejemplo, hacer optimizaciones de diseño. Ok, esta posición se entiende.
Esto significa que internamente, dentro del módulo Wrap , la implementación del módulo Wrap<T> puede, por ejemplo, escribir temporalmente "valores inesperados" en él, si no los filtra a los usuarios, y el compilador estará bien con ellos.

Sin embargo, no estoy seguro de cómo se relaciona exactamente la parte del Wrap de

En primer lugar, independientemente de que los campos sean privados o públicos, los valores inesperados no se pueden escribir directamente a través de esos campos. Necesita algo como un puntero sin formato, o código en el otro lado de FFI para hacerlo, y se puede hacer sin ningún acceso al campo, simplemente con un puntero a toda la unión. Por lo tanto, debemos abordar esto desde otra dirección que no sea el acceso a un campo restringido.

Como interpreto que comenta, el enfoque es decir que un campo privado (en unión o una estructura, no importa) implica un invariante arbitrario desconocido para el usuario, por lo que cualquier operación que cambie ese campo (directamente o mediante punteros salvajes, no importa importa) dan como resultado UB porque potencialmente pueden romper ese invariante no especificado.

Esto significa que si una unión tiene un solo campo privado, entonces su implementador (pero no el compilador) puede asumir que ningún tercero escribirá un valor inesperado en esa unión.
Esa es una "cláusula de documentación de unión predeterminada" para el usuario en cierto sentido:
- (Predeterminado) Si una unión tiene un campo privado, no puede escribir basura en él.
- De lo contrario, puede escribir basura en una unión a menos que sus documentos lo prohíban explícitamente.

Si algún sindicato quiere prohibir valores inesperados mientras sigue proporcionando pub acceso a sus campos esperados (por ejemplo, cuando esos campos no tienen sus propios invariantes), aún puede hacerlo a través de la documentación, por eso el "a menos que" en la segunda cláusula es necesaria.

@RalfJung
¿Describe esto su posición con precisión?

¿Cómo se tratan escenarios como este?

mod m {
    union MyPrivateUnion { /* private fields */ }
    extern {
        fn my_private_ffi_function() -> MyPrivateUnion; // Can return garbage (?)
    }
}

Como interpreto que comenta, el enfoque es decir que un campo privado (en unión o una estructura, no importa) implica un invariante arbitrario desconocido para el usuario, por lo que cualquier operación que cambie ese campo (directamente o mediante punteros salvajes, no importa importa) dan como resultado UB porque potencialmente pueden romper ese invariante no especificado.

No, eso no es lo que quise decir.

Hay múltiples invariantes. No sé cuántos necesitaremos, pero habrá al menos dos (y no tengo buenos nombres para ellos):

  • El "invariante a nivel de diseño" (o "invariante sintáctico") de un tipo está completamente definido por la forma sintáctica del tipo. Estas son cosas como " &mut T no es NULL y está alineado", " bool es 0 o 1 ", " ! no puede existe". En este nivel, *mut T es lo mismo que usize ; ambos permiten cualquier valor (o tal vez cualquier valor inicializado , pero esa distinción queda para otra discusión). Eventualmente, vamos a tener un documento que detalla estos invariantes para todos los tipos, por recursividad estructural: El invariante de nivel de diseño de una estructura es que todos sus campos tienen su invariante mantenido, etc. La visibilidad no juega un papel aquí.
Violating the layout-level invariant is instantaneous UB. This is a statement we can make because we have defined this invariant in very simple terms, and we make it part of the definition of the language itself. We can then exploit this UB (and we already do), e.g. to perform enum layout optimizations.
  • El "invariante de nivel de tipo personalizado" (o "invariante semántico") de un tipo lo elige quien lo implemente. El compilador no puede conocer este invariante ya que no tenemos un lenguaje para expresarlo, y lo mismo ocurre con la definición del lenguaje. ¡No podemos violar este UB invariante, ya que ni siquiera podemos decir qué es ese invariante! El hecho de que sea posible tener invariantes personalizados es una característica de cualquier sistema de tipos útil: la abstracción. Escribí más sobre esto en una publicación de blog anterior .

    La conexión entre el invariante semántico personalizado y UB es que declaramos que el código inseguro puede depender de que sus invariantes semánticos sean preservados por código externo . Eso hace que sea incorrecto seguir adelante con cualquier elemento al azar en un campo de tamaño de Vec . Tenga en cuenta que he dicho incorrecta (a veces uso el término erróneo) - pero no un comportamiento indefinido! Otro ejemplo para demostrar esta diferencia (en realidad, el mismo ejemplo) es la discusión sobre las reglas de alias para &mut ZST . Crear un &mut ZST colgando bien alineado y no nulo nunca es un UB inmediato, pero sigue siendo incorrecto / incorrecto porque uno puede escribir código inseguro que depende de que esto no suceda.

Sería bueno alinear estos dos conceptos, pero no creo que sea práctico. En primer lugar, para algunos tipos (punteros de función, rasgos dinámicos), la definición del invariante semántico personalizado utiliza en realidad la definición de UB en el lenguaje. Esta definición sería circular si quisiéramos decir que es UB violar alguna vez la invariante semántica personalizada. En segundo lugar, preferiría si la definición de nuestro lenguaje, y si un determinado rastro de ejecución exhibe UB, fuera una propiedad decidible. Los invariantes semánticos y personalizados con frecuencia no son decidibles.


Sin embargo, no estoy seguro de cómo se relaciona exactamente la parte del contrato Wraps sobre la ausencia de valores inesperados con la privacidad del campo.

Básicamente, cuando un tipo elige su invariante personalizado, tiene que asegurarse de que cualquier cosa que pueda hacer el código seguro preserva el invariante . Después de todo, la promesa es que el solo uso de la API segura de este tipo nunca puede conducir a UB. Esto se aplica tanto a estructuras como a uniones. Una de las cosas que puede hacer el código seguro es acceder a los campos públicos, que es de donde proviene esta conexión.

Por ejemplo, un campo público de una estructura no puede tener un invariante personalizado que sea diferente del invariante personalizado del tipo de campo : después de todo, cualquier usuario seguro podría escribir datos arbitrarios en ese campo o leer el campo y esperar "bueno" datos. Una estructura en la que todos los campos son públicos se puede construir de forma segura, imponiendo más restricciones al campo.

Una unión con un ámbito público ... bueno eso es algo interesante. De todos modos, leer los campos de unión no es seguro, por lo que nada cambia allí. Escribir campos de unión es seguro, por lo que una unión con un campo público debe poder manejar datos arbitrarios que satisfagan el invariante personalizado de ese tipo de campo que se coloca en el campo. Dudo que esto sea de mucha utilidad ...

Entonces, para recapitular, cuando elige un invariante personalizado, es su responsabilidad asegurarse de que el código inseguro no violar su invariante cuando ese código hace algo que el código seguro no podría hacer.


Esto significa que internamente, dentro del módulo de Wrap, la implementación de WrapEl módulo puede, por ejemplo, escribir temporalmente "valores inesperados" en él, si no los filtra a los usuarios, y el compilador estará de acuerdo con ellos.

Correcto. (La seguridad contra el pánico es una preocupación aquí, pero probablemente usted lo sepa). Esto es como, en Vec , puedo hacerlo con seguridad

let sz = self.size;
self.size = 1337;
self.size = sz;

y no hay UB.


mod m {
    union MyPrivateUnion { /* private fields */ }
    extern {
        fn my_private_ffi_function() -> MyPrivateUnion; // Can return garbage (?)
    }
}

En términos del invariante de diseño sintáctico, my_private_ffi_function puede hacer cualquier cosa (suponiendo que la función llame a ABI y la firma coincida). En términos del invariante semántico personalizado, eso no es visible en el código; quien escribió este módulo tenía un invariante en mente, debe documentarlo junto a su definición de unión y luego asegurarse de que la función FFI devuelva un valor que satisfaga el invariante .

Finalmente escribí esa publicación de blog sobre si se debe inicializar &mut T y cuándo, y los dos tipos de invariantes que mencioné anteriormente.

¿Queda algo por seguir aquí que no esté cubierto por https://github.com/rust-lang/rust/issues/55149 , o deberíamos cerrar?

E0658 todavía apunta aquí:

error [E0658]: las uniones con campos que no son Copy son inestables (consulte el problema # 32836)

Esto actualmente juega terriblemente con los atomics, ya que no implementan Copy . ¿Alguien sabe una solución?

Cuando se implemente https://github.com/rust-lang/rust/issues/55149 , podrá usar ManuallyDrop<AtomicFoo> en una unión. Hasta entonces, la única solución es usar Nightly (o no usar union y buscar alguna alternativa).

Con eso implementado, ni siquiera debería necesitar ManuallyDrop ; después de todo, rustc sabe que Atomic* no implementa Drop .

Asignarme a mí mismo para cambiar el problema de seguimiento al nuevo.

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