Rust: Problema de seguimiento para RFC 1892, "Desaprobar sin inicializar en favor de un nuevo tipo MaybeUninit"

Creado en 19 ago. 2018  ·  382Comentarios  ·  Fuente: rust-lang/rust

NUEVO NÚMERO DE SEGUIMIENTO = https://github.com/rust-lang/rust/issues/63566

Este es un problema de seguimiento para el RFC "Deprecate uninitialized en favor de un nuevo tipo MaybeUninit " (rust-lang / rfcs # 1892).

Pasos:

  • [x] Implementar el RFC (cc @ rust-lang / libs)
  • [x] Ajustar la documentación (en https://github.com/rust-lang/rust/pull/60445)
  • [x] Estabilización de relaciones públicas (en https://github.com/rust-lang/rust/pull/60445)

Preguntas sin resolver:

  • ¿Deberíamos tener un configurador seguro que devuelva un &mut T ?
  • ¿Deberíamos cambiar el nombre de MaybeUninit ?
  • ¿Deberíamos cambiar el nombre de into_inner ?
  • ¿Debería MaybeUninit<T> ser Copy por T: Copy ?
  • ¿Deberíamos permitir llamar a get_ref y get_mut (pero no leer de las referencias devueltas) antes de que se inicialicen los datos? (AKA: "¿Las referencias a datos no inicializados son insta-UB, o solo UB cuando se leen?") ¿Deberíamos renombrarlo similar a into_inner ?
  • ¿Podemos hacer que into_inner (o como se llame) entre en pánico cuando T está deshabitado, como lo hace mem::uninitialized actualmente? (hecho)
  • Parece que no queremos desaprobar mem::zeroed .
B-RFC-approved C-tracking-issue E-mentor T-lang T-libs

Comentario más útil

mem::zeroed() es útil para ciertos casos de FFI donde se espera que ponga a cero un valor con memset(&x, 0, sizeof(x)) antes de llamar a una función C. Creo que esta es una razón suficiente para mantenerlo sin prejuicios.

Todos 382 comentarios

cc @RalfJung

[] Implementar el RFC

Puedo ayudar a implementar el RFC.

Genial, puedo ayudar a revisar :)

Me gustaría una aclaración sobre esta parte del RFC:

Hacer que las llamadas sin inicializar en un tipo vacío desencadenen un pánico en tiempo de ejecución que también imprime el mensaje de desaprobación.

¿ Solo mem::uninitialized::<!>() entrar en pánico? ¿O debería esto también cubrir estructuras (¿y tal vez enumeraciones?) Que contienen el tipo vacío (por ejemplo, (!, u8) )?

AFAIK, solo hacemos la generación de código realmente dañino por ! . La mayoría de los otros usos de mem::uninitialized son igualmente incorrectos, pero el compilador no los aprovecha.

Así que lo haría por ! solamente, pero también por mem::zeroed . (Parece que olvidé enmendar esa parte cuando agregué zeroed al RFC).

Podríamos comenzar haciendo esto:
https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/intrinsic.rs#L184 -L198

compruebe si fn_ty.ret.layout.abi es Abi::Uninhabited y al menos emite una trampa, por ejemplo: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/ operand.rs # L400 -L403

Una vez que haya visto la trampa (es decir, intrinsics::abort ) en acción, podrá ver si hay alguna forma agradable de provocar el pánico. Será complicado debido a que se desenrolla, necesitaremos un caso especial aquí: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L445 - L447

Para entrar en pánico, necesitarías algo como esto: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L360 -L407
(puede ignorar el brazo EvalErrorKind::BoundsCheck )

@eddyb Gracias por los consejos.


Ahora estoy arreglando (varias) advertencias de depreciación y me siento (muy) tentado a ejecutar sed -i s/mem::uninitialized()/mem::MaybeUninit::uninitialized().into_inner()/g pero supongo que no entendería ... ¿O está bien si sé que el valor es un concreto (Copiar) tipo? por ejemplo, let x: [u8; 1024] = mem::uninitialized(); .

Eso exactamente perdería el punto, sí. ^^

Al menos por ahora, me gustaría considerar mem::MaybeUninit::uninitialized().into_inner() UB para todos los tipos no sindicalizados. Note que Copy ciertamente no es suficiente; tanto bool como &'static i32 son Copy y su fragmento está destinado a ser insta-UB para ellos. Es posible que deseemos una excepción para "tipos donde todos los patrones de bits están bien" (tipos enteros, esencialmente), pero me opondría a hacer tal excepción porque undef no es un patrón de bits normal. Es por eso que RFC dice que debe inicializar completamente antes de llamar a into_inner .

También dice que por get_mut , pero la discusión de RFC planteó el deseo de algunas personas de relajar la restricción aquí. Esa es una opción con la que podría vivir. Pero no por into_inner .

Me temo que todos estos usos de uninitialized tendrán que revisarse más detenidamente y, de hecho, esta fue una de las intenciones del RFC. Nos gustaría que el ecosistema más amplio fuera más cuidadoso aquí, si todo el mundo usa into_inner inmediatamente, entonces el RFC no tiene valor.

Nos gustaría que el ecosistema más amplio fuera más cuidadoso aquí, si todo el mundo usa into_inner inmediatamente, entonces el RFC no tiene valor.

Esto me da una idea ... ¿quizás deberíamos lint (grupo: "corrección") para este tipo de código? cc @ oli-obk

Ahora estoy arreglando (varias) advertencias de obsolescencia

Solo deberíamos enviar Nightly con esas advertencias una vez que el reemplazo recomendado esté disponible al menos en Stable. Vea una discusión similar en https://github.com/rust-lang/rust/pull/52994#issuecomment -411413493

@RalfJung

Es posible que deseemos una excepción para "tipos en los que todos los patrones de bits están bien" (tipos enteros, esencialmente)

Has participado en discusiones sobre esto antes, pero publicaré aquí para que circule más ampliamente: esto ya es algo para lo que tenemos muchos casos de uso existentes en Fuchsia, y tenemos un rasgo para esto ( FromBytes ) y una macro de derivación para estos tipos. También hubo un Pre-RFC interno para agregarlos a la biblioteca estándar (cc @gnzlbg @joshlf).

Me opondría a hacer tal excepción porque undef no es un patrón de bits normal.

Sí, este es un aspecto en el que mem::zeroed() es significativamente diferente de mem::uninitialized() .

@cramertj

Has participado en una discusión sobre esto antes, pero publicaré aquí para circular más ampliamente: esto ya es algo para lo que tenemos muchos casos de uso existentes en Fuchsia, y tenemos un rasgo para esto (FromBytes) y una macro derivada para estos tipos. También hubo un Pre-RFC interno para agregarlos a la biblioteca estándar (cc @gnzlbg @joshlf).

Esas discusiones fueron sobre formas de permitir memcpy seguras en todos los tipos, pero creo que eso es bastante ortogonal a si la memoria que se está copiando está inicializada o no: si coloca memoria no inicializada, obtendrá memoria no inicializada.

El consenso también fue que sería incorrecto que cualquier enfoque discutido permitiera leer bytes de relleno, que son una forma de memoria no inicializada, en Rust seguro. Es decir, si coloca la memoria inicializada, no puede sacar la memoria no inicializada.

IIRC, nadie sugirió ni discutió ningún enfoque en el que se pudiera poner memoria no inicializada y sacar memoria inicializada, así que no sigo lo que esas discusiones tienen que ver con este. Para mí son completamente ortogonales.

Para enfatizar un poco más el punto, LLVM define los datos no inicializados como Poison, que es distinto de "algún patrón de bits arbitrario pero válido". La ramificación basada en un valor de Veneno o su uso para calcular una dirección que luego se desreferencia es UB. Entonces, desafortunadamente, los "tipos en los que todos los patrones de bits están bien" todavía no son seguros de construir porque usarlos sin inicializarlos por separado será UB.

Bien, lo siento, debería haber aclarado lo que quise decir. Estaba tratando de decir que "tipos donde todos los patrones de bits están bien" ya es algo que estamos interesados ​​en definir por otras razones. Como @RalfJung dijo arriba,

Me opondría a hacer tal excepción porque undef no es un patrón de bits normal.

Gracias a Dios hay gente que sabe leer, porque aparentemente yo no puedo ...

Bien, lo que quise decir es: definitivamente tenemos tipos donde todos los patrones de bits inicializados están bien: todos los tipos i* y u* , punteros sin procesar, creo que f* también y luego tuplas / estructuras que solo constan de esos tipos.

Lo que es una pregunta abierta es en qué circunstancias se permite que no se

El consenso también fue que sería incorrecto que cualquier enfoque discutido permitiera leer bytes de relleno, que son una forma de memoria no inicializada, en Rust seguro. Es decir, si coloca la memoria inicializada, no puede sacar la memoria no inicializada.

Leer bytes de relleno como MaybeUninit<u8> debería estar bien.

El consenso también fue que sería incorrecto que cualquier enfoque discutido permitiera leer bytes de relleno, que son una forma de memoria no inicializada, en Rust seguro. Es decir, si coloca la memoria inicializada, no puede sacar la memoria no inicializada.

Leer bytes de relleno como MaybeUninitdebería estar bien.

La discusión en pocas palabras fue sobre proporcionar un rasgo, Compatible<T> , con un método seguro fn safe_transmute(self) -> T que "reinterpreta" / "memcpys" los bits de self en un T . La garantía de este método es que si self se inicializa correctamente, también lo será el T resultante. Se propuso que el compilador completara las implementaciones transitivas automáticamente, por ejemplo, si hay un impl Compatible<V> for U y un impl Compatible<W> for V entonces hay un impl Compatible<W> for U (ya sea porque se proporcionó manualmente, o el compilador lo genera automáticamente; cómo se podría implementar esto fue completamente manual)

Se propuso que debería ser unsafe para implementar el rasgo: si lo implementas para un T que tiene bytes de relleno donde Self tiene campos, entonces todo está bien al menos hasta que intente usar T y el comportamiento de su programa termine dependiendo del contenido de la memoria no inicializada.

No tengo idea de qué tiene que ver todo esto con MaybeUninit<u8> , ¿tal vez podrías explicarlo con más detalle?

Lo único que puedo imaginar es que podríamos agregar un impl general: unsafe impl<T> Compatible<[MaybeUninit<u8>; size_of::<T>()]> for T { ... } ya que transmutar cualquier tipo en un [MaybeUninit<u8>; N] de su tamaño es seguro para todos los tipos. No sé qué tan útil sería tal implícita, dado que MaybeUninit es una unión, y quien usa el [MaybeUninit<u8>; N] no tiene idea de si un elemento particular de la matriz está inicializado o no .

@gnzlbg en ese entonces estabas hablando de FromBits<T> for [u8] . Ahí es donde digo que tenemos que usar [MaybeUninit<u8>] lugar.

Discutí esta propuesta con

@joshlf ¿de qué propuesta hablas?

@RalfJung

@gnzlbg en ese entonces estabas hablando de FromBitspara [u8]. Ahí es donde digo que tenemos que usar [MaybeUninit] en su lugar.

Entendido, totalmente de acuerdo aquí. Había olvidado por completo que también queríamos hacer eso 😆

@joshlf ¿de qué propuesta hablas?

Una propuesta de FromBits / IntoBits . TLDR: T: FromBits<U> significa que cualquier patrón de bits que sea válido U corresponde a un T válido. U: IntoBits<T> significa lo mismo. El compilador infiere automáticamente ambos para todos los pares de tipos dadas ciertas reglas, y esto desbloquea muchas bondades divertidas que actualmente requieren unsafe . Hay un borrador de este RFC aquí que escribí hace un tiempo, pero tengo la intención de cambiar gran parte de él, así que no tome ese texto como algo más que una guía aproximada.

@joshlf Creo que ese par de rasgos se

  • ¿Se repite por debajo de las referencias? Creo cada vez más firmemente que no debería, ya que vemos más ejemplos. Por lo tanto, es probable que debamos adaptar los documentos MaybeUninit::get_mut consecuencia (en realidad, no es UB usar eso antes de completar la inicialización, pero es UB eliminar la
  • ¿Tiene que inicializarse un u8 (y otros tipos de enteros, punto flotante, puntero sin formato), es decir, es MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Creo que sí, pero sobre todo basado en el presentimiento de que queremos mantener los lugares donde permitimos poison / undef al mínimo. Sin embargo, se me podría persuadir de lo contrario si hay muchos usos de este patrón (y espero usar miri para ayudar a determinar esto).

¿Se repite por debajo de las referencias?

@RalfJung, ¿ puede mostrar un ejemplo de lo que quiere decir con "recurrir a las referencias debajo"?

¿Se debe inicializar u8 (y otros tipos de enteros, punto flotante, puntero sin formato), es decir, es MaybeUinit:: uninitialized (). into_inner () insta-UB?

¿Qué pasa si no es UB instantánea? ¿Qué puedo hacer con ese valor? ¿Puedo igualarlo? Si es así, ¿el comportamiento del programa es determinista?

Siento que si no puedo igualar el valor sin introducir UB, entonces hemos reinventado mem::uninitialized . Si puedo coincidir sobre el valor y la misma rama se toma siempre en todas las arquitecturas, opt-niveles, etc. tenemos re-inventado mem::zeroed (y son una especie de hacer el uso de la MaybeUninit escriba un poco discutible). Si el comportamiento del programa no es determinista y cambia con los niveles de optimización, a través de arquitecturas, dependiendo de factores externos (como si el sistema operativo le dio al proceso páginas con cero), etc., entonces siento que estaríamos introduciendo una enorme pistola en el idioma.

¿Tiene que inicializarse un u8 (y otros tipos de enteros, punto flotante, puntero sin formato), es decir, es MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Creo que sí, pero sobre todo basado en el presentimiento de que queremos mantener los lugares donde permitimos poison / undef al mínimo. Sin embargo, se me podría persuadir de lo contrario si hay muchos usos de este patrón (y espero usar miri para ayudar a determinar esto).

FWIW, dos de los beneficios de que esto no sea UB son que a) se alinea con lo que hace LLVM y, b) permite más flexibilidad con optimizaciones. También parece más coherente con su propuesta reciente para definir la seguridad en el momento del uso, no en el momento de la construcción.

¿Qué pasa si no es UB instantánea? ¿Qué puedo hacer con ese valor? ¿Puedo igualarlo? Si es así, ¿el comportamiento del programa es determinista?

Siento que si no puedo igualar el valor sin introducir UB, entonces hemos reinventado mem::uninitialized . Si puedo igualar el valor y siempre se toma la misma rama en todas las arquitecturas, niveles de opción, etc., hemos reinventado mem::zeroed (y estamos haciendo el uso de MaybeUninit escriba un poco discutible). Si el comportamiento del programa no es determinista y cambia con los niveles de optimización, a través de arquitecturas, dependiendo de factores externos (como si el sistema operativo le dio al proceso páginas con cero), etc., entonces siento que estaríamos introduciendo una enorme pistola en el idioma.

¿Por qué querrías poder hacer coincidir algo que no esté inicializado? Definirlo como UB para bifurcar o indexar en función de valores no inicializados le brinda a LLVM más espacio para optimizar, por lo que no creo que atar más sus manos sea una buena idea, especialmente si no hay un caso de uso convincente.

¿Por qué querrías poder hacer coincidir algo que no esté inicializado?

No dije que quisiera, dije que si esto no se puede hacer, no entiendo la diferencia entre MaybeUinit<u8>::uninitialized().into_inner() y solo mem::uninitialized() .

@RalfJung, ¿ puede mostrar un ejemplo de lo que quiere decir con "recurrir a las referencias debajo"?

Básicamente, la pregunta es si permitimos lo siguiente:

let mut b = MaybeUninit::<bool>::uninitialized();
let bref = b.get_mut(); // insta-UB?

Si decidimos que una referencia es válida solo si apunta a algo válido (eso es lo que quiero decir con "recurrir a las referencias por debajo de"), este código es UB.

¿Qué pasa si no es UB instantánea? ¿Qué puedo hacer con ese valor? ¿Puedo igualarlo? Si es así, ¿el comportamiento del programa es determinista?

No puede inspeccionar un u8 no inicializado de ninguna manera. match puede hacer muchas cosas, tanto vincular nombres como probar la igualdad; el primero está bien pero el segundo no. Pero se puede escribir de nuevo a la memoria.

Básicamente, esto es lo que actualmente implementa miri.

Siento que si no puedo igualar el valor sin introducir UB, entonces hemos reinventado mem :: uninitialized.

¿Por qué eso? El mayor problema con mem::uninitialized estaba en torno a los tipos que tienen restricciones sobre cuáles son sus valores válidos. Podríamos decidir que u8 no tiene tales restricciones, por lo que mem::uninitialized() estaba bien para u8 . Era casi imposible usarlo correctamente en código genérico, por lo que es mejor deshacerse de él por completo.
De cualquier manera, todavía no está bien pasar un u8 no inicializado a un código seguro, pero podría estar bien usarlo con cuidado en un código no seguro.

Tampoco puede "hacer coincidir" en un &mut apunte a datos no válidos. IOW, creo que el ejemplo de bool que di arriba está bien, pero el siguiente ciertamente no lo es:

let mut b = MaybeUninit::<bool>::uninitialized();
let bref = b.get_mut();
match bref {
  &b => // insta-UB! We have a bad bool in scope.
}

Esto está usando match para hacer una desreferencia de puntero normal.

FWIW, dos de los beneficios de que esto no sea UB son que a) se alinea con lo que hace LLVM y, b) permite más flexibilidad con optimizaciones. También parece más coherente con su propuesta reciente para definir la seguridad en el momento del uso, no en el momento de la construcción.

¿Qué optimizaciones permitiría esto?
Tenga en cuenta que LLVM realiza optimizaciones en código esencialmente sin tipo, por lo que nada de esto es una preocupación allí. Aquí solo estamos hablando de optimizaciones MIR.

Básicamente, vengo desde la perspectiva de que deberíamos permitir lo menos posible hasta que tengamos un uso claro. Siempre podemos permitir más cosas más adelante, pero no al revés. Dicho esto, recientemente han surgido algunos buenos usos de los segmentos de bytes que pueden envejecer cualquier dato, lo que podría ser un argumento suficiente para hacer esto al menos por u* y i* .

Si decidimos que una referencia es válida solo si apunta a algo válido (eso es lo que quiero decir con "recurrir a las referencias por debajo de"), este código es UB.

Te tengo.

El mayor problema con mem :: uninitialized fue alrededor de tipos que tienen restricciones sobre cuáles son sus valores válidos.

mem::uninitialized también tiene el problema que señaló anteriormente: que crear una referencia a un valor no inicializado puede ser un comportamiento indefinido (o no). Entonces, ¿es la siguiente UB?

let mut b = MaybeUninit::<u8>::uninitialized().into_inner();
let bref = &mut b; // Insta UB ?

Pensé que una de las razones para introducir MaybeUninit era evitar este problema al tener siempre inicializada la unión (por ejemplo, a la unidad), lo que le permite tomar una referencia a ella y mutar su contenido, por ejemplo, estableciendo el campo activo al u8 y darle un valor a través de ptr::write sin introducir UB.

Por eso estoy un poco confundido. No veo cómo into_inner es mejor que:

let mut b: u8 = uninitialized();
let bref = &mut b; // Insta UB ? 

Ambos me parecen bombas de tiempo de comportamiento indefinido.

¿Qué optimizaciones permitiría esto?
Tenga en cuenta que LLVM realiza optimizaciones en código esencialmente sin tipo, por lo que nada de esto es una preocupación allí. Aquí solo estamos hablando de optimizaciones MIR.

Si decimos que la memoria indefinida tiene algún valor y, por lo tanto, se le permite ramificarse de acuerdo con la semántica de Rust, entonces no podemos reducirla a la versión de indefinido de LLVM, porque no sería correcto.

Básicamente, vengo desde la perspectiva de que deberíamos permitir lo menos posible hasta que tengamos un uso claro. Siempre podemos permitir más cosas más adelante, pero no al revés.

Eso es justo.

Dicho esto, recientemente han surgido algunos buenos usos de los segmentos de bytes que pueden envejecer cualquier dato, lo que podría ser un argumento suficiente para hacer esto al menos por u* y i* .

¿Alguno de estos casos de uso incluye tener segmentos de bytes que contienen valores no inicializados?

Un lugar en el que un &mut [u8] no inicializado pero no venenoso podría ser valioso es para Read::read ; nos gustaría poder evitar la necesidad de poner a cero el búfer solo porque algunos Read extraños

Un lugar en el que un &mut [u8] no inicializado pero no venenoso podría ser valioso es para Read::read ; nos gustaría poder evitar la necesidad de poner a cero el búfer solo porque algunos Read extraños

Ya veo, entonces la idea es que MaybeUninit representaría un tipo que está inicializado, pero con contenido indefinido, mientras que otros tipos de datos no inicializados (por ejemplo, campos de relleno) todavía estarían completamente sin inicializar en el sentido de envenenamiento LLVM.

No creo que sea necesario aplicar a MaybeUninit en general. En teoría, podría haber alguna API para "congelar" el contenido de indefinido a definido pero arbitrario.

Si decimos que la memoria indefinida tiene algún valor y, por lo tanto, se le permite ramificarse de acuerdo con la semántica de Rust, entonces no podemos reducirla a la versión de indefinido de LLVM, porque no sería correcto.

Esa nunca fue la propuesta. Es y seguirá siendo UB para bifurcar el poison .

La pregunta es si es UB simplemente "tener" un poison en un u8 local.

¿Alguno de estos casos de uso incluye tener segmentos de bytes que contienen valores no inicializados?

Los cortes son como referencias, por lo que &mut [u8] de datos no inicializados está bien siempre y cuando solo estén escritos (asumiendo que esa es la solución que tomamos para la validez de referencia).

@sfackler

Un lugar en el que un & mut [u8] no inicializado-pero-no-venenoso podría ser valioso es para Read :: read; nos gustaría poder evitar la necesidad de poner a cero el búfer solo porque algún impl de lectura extraño podría leerlo en lugar de simplemente escribir en él.

Bueno, sin &out solo podrá hacer eso si conoce el impl. La pregunta no es si el código seguro tiene que manejar poison en u8 (no es así, ¡ese no es un uso correcto del código seguro!), La pregunta es si el código inseguro puede manejarlo con cuidado. de esta manera. (Vea la publicación de blog que quería escribir hoy sobre la distinción entre invariantes de seguridad e invariantes de validez ...)

Tal vez llego tarde, pero sugiero cambiar la firma del método set() para devolver &mut T . De esta manera, sería seguro escribir código completamente seguro trabajando con MaybeUninit (al menos en algunas situaciones).

fn init(dest: &mut MaybeUninit<u8>) -> &mut u8 {
    dest.set(produce_value())
}

Esto es prácticamente una garantía estática de que init() inicializará el valor o divergerá. (Si intentara devolver algo más, el tiempo de vida sería incorrecto y &'static mut u8 es imposible en código seguro). Tal vez podría usarse como parte de la API de placer en el futuro.

@Kixunil Ha sido así antes, y estoy de acuerdo en que es bueno. Encuentro el mismo set confuso para una función que devuelve algo.

@Kixunil

Esto es prácticamente una garantía estática de que init() inicializará el valor o divergerá. (Si intentara devolver algo más, la duración sería incorrecta y &'static mut u8 es imposible en código seguro).

No exactamente; puede obtener uno con Box::leak .

En una base de código que escribí recientemente, se me ocurrió un esquema similar; es un poco más complicado, pero proporciona una verdadera garantía estática de que se inicializó la referencia proporcionada. En vez de

fn init(dest: &mut MaybeUninit<u8>) -> &mut u8

yo tengo

fn init<'a>(dest: Uninitialized<'a, u8>) -> DidInit<'a, u8>

El truco es que Uninitialized y DidInit son invariantes en sus parámetros de vida, por lo que no hay forma de reutilizar un DidInit con un parámetro de vida diferente, incluso por ejemplo, 'static .

DidInit impls Deref y DerefMut , por lo que el código seguro puede usarlo como referencia, como en su ejemplo. Pero la garantía de que en realidad fue la referencia pasada original la que se inicializó, no alguna otra referencia aleatoria, es útil para el código inseguro . Significa que puede definir inicializadores estructuralmente:

struct Foo {
    a: i32,
    b: u8,
}

fn init_foo<'a>(dest: Uninitialized<'a, Foo>,
                init_a: impl for<'x> FnOnce(Uninitialized<'x, i32>) -> DidInit<'x, i32>,
                init_b: impl for<'x> FnOnce(Uninitialized<'x, u8>) -> DidInit<'x, u8>)
                -> &'a mut DidInit<'a, Foo> {
    let ptr: *mut Foo = dest.ptr;
    unsafe {
        init_a(Uninitialized::new(&mut (*ptr).a));
        init_b(Uninitialized::new(&mut (*ptr).b));
        dest.did_init()
    }
}

Esta función inicializa un puntero a la estructura Foo inicializando cada uno de sus campos por turno, utilizando las devoluciones de llamada de inicialización proporcionadas por el usuario. Requiere que las devoluciones de llamada devuelvan DidInit s, pero no se preocupan por sus valores; el hecho de que existan es suficiente. Una vez que se han inicializado todos los campos, sabe que el Foo completo es válido, por lo que llama a did_init() en el Uninitialized<'a, Foo> , que es un método inseguro que simplemente lo envía al correspondiente DidInit tipo, que init_foo luego devuelve.

También tengo una macro que automatiza el proceso de escritura de tales funciones, y la versión real es un poco más cuidadosa con los destructores y los pánicos (aunque necesita mejoras).

De todos modos, me pregunto si algo como esto podría implementarse en la biblioteca estándar.

Enlace de juegos

(Nota: DidInit<'a, T> es en realidad un alias de tipo para &'a mut _DidInitMarker<'a, T> , para evitar problemas de por vida con DerefMut .)

Por cierto, mientras que el enfoque vinculado anteriormente ignora los destructores, un enfoque ligeramente diferente sería hacer que DidInit<‘a, T> responsable de ejecutar el destructor de T . En este caso, tendría que ser una estructura, no un alias; y solo podría entregar referencias a T que vivan mientras el DidInit sí, no para todo ’a (ya que de lo contrario podría continuar accediendo a él después de la destrucción).

+1 por incluir un método para dar el comportamiento que había solicitado anteriormente en set , pero estoy de acuerdo con que esté disponible a través de otro nombre.

¿Alguna buena idea de cuál podría ser ese nombre? set_and_as_mut ? ^^

set_and_borrow_mut ?

insert / insert_mut ? El tipo Entry tiene un método or_insert algo similar (pero OccupiedEntry también tiene insert que devuelve el valor anterior, por lo que no es similar en absoluto).

¿Existe una razón realmente convincente para tener dos métodos separados? Parece lo suficientemente simple como para ignorar el valor de retorno, e imagino que la función se marcaría como #[inline] por lo que no esperaría ningún costo real de tiempo de ejecución.

¿Existe una razón realmente convincente para tener dos métodos separados? Parece lo suficientemente simple como para ignorar el valor de retorno

Supongo que la única razón es que ver set devolver algo es bastante sorprendente.

Tal vez me esté perdiendo algo, pero ¿qué podría salvarnos de tener un valor no válido? Quiero decir si nosotros

let mut foo: MaybeUninit<T> = MaybeUninit {
    uninit: (),
};
let mut foo_ref = &mut foo as *mut MaybeUninit<T>;

unsafe {
    some_native_function(&mut (*foo_ref).value, val);
}

¿Qué pasa si some_native_function no es operativo y en realidad no inicia el valor? ¿Sigue siendo UB? ¿Cómo se podría manejar?

@Pzixel, todo esto está cubierto por la documentación de la API por MaybeUninit .

Si some_native_function es un NOP, no pasa nada; si luego usa foo_ref.value (o más bien foo_ref.as_mut() ya que solo puede usar la API pública), eso es UB porque la función solo se puede llamar una vez que todo está inicializado.

MaybeUninit no evita tener valores inválidos; si pudiera, sería seguro, pero eso no es posible. Sin embargo, hace que trabajar con valores inválidos sea menos complicado porque ahora la información de que el valor podría ser inválido está codificada en el tipo, tanto para el compilador como para el programador.

Quería documentar una conversación de IRC que tuve con @sfackler sobre un problema hipotético que podría surgir en el futuro.

La pregunta principal es si mem::zeroed es una representación en memoria válida para la propuesta de implementación actual para MaybeUninit<NonZeroU8> . En mi opinión, en el estado "uninit" el valor es solo relleno, que el compilador puede usar para cualquier propósito, y en el estado "valor", todos los valores posibles excepto mem::zeroed son válidos (debido a NonZero ).

Un futuro sistema de diseño de tipos con empaquetamiento discriminante de enumeración más avanzado (del que tenemos ahora) podría almacenar un discriminante en el relleno del estado "uninit" / memoria cero en el estado "valor". En ese sistema hipotético, el tamaño de Option<MaybeUninit<NonZeroU8>> es 1, mientras que actualmente es 2. Además, en ese sistema hipotético, Some(MaybeUninit::uninitialized()) sería indistinguible de None . Creo que probablemente podamos solucionar este problema cambiando la implementación de MaybeUninit (pero no su API pública) una vez que nos mudemos a dicho sistema.

No veo ninguna diferencia entre NonZeroU8 y &'static i32 a este respecto. Ambos son tipos donde "0" no es válido . Entonces, para ambos, MaybeUninit<T>::zeroed().into_inner() es insta-UB.

Si Option<Union> puede realizar optimizaciones de diseño depende de la validez de una unión. Esto aún no está decidido para todos los casos, pero existe un acuerdo general de que para las uniones que tienen una variante del tipo () , cualquier patrón de bits es válido y, por lo tanto, no es posible realizar optimizaciones de diseño. Esto cubre MaybeUninit . Entonces Option<MaybeUninit<NonZeroU8>> nunca tendrá talla 1.

Existe un acuerdo general de que para las uniones que tienen una variante de tipo (), cualquier patrón de bits es válido y, por lo tanto, no es posible realizar optimizaciones de diseño.

¿Es este un caso especial para "uniones que tienen una variante de tipo ()"? ¿La estabilización de esta característica estabiliza implícitamente esa parte del Rust ABI? ¿Qué tal un union contenga struct UnitType; o struct NewType(()); ? ¿Qué pasa con struct Padded (abajo)? ¿Qué tal un union contiene struct Padded ?

#[repr(C, align(4))]
struct Padded {
    a: NonZeroU8,
    b: (),
    c: NonZeroU16
}

Mi redacción fue tremendamente específica porque es literalmente lo único en lo que estoy bastante seguro de que tenemos un acuerdo general. :) Creo que querríamos que esto dependiera solo del tamaño (es decir, todos los ZST lo obtendrían), pero en realidad creo que esta variante ni siquiera debería ser necesaria y las uniones nunca obtendrán optimizaciones de diseño por defecto (pero eventualmente los usuarios pueden optar por utilizar atributos). Pero esa es solo mi opinión.

Tendremos una discusión adecuada para medir el consenso actual y tal vez llegar a un acuerdo sobre más cosas en una de las próximas discusiones en el repositorio de UCG , y puede unirse allí cuando suceda.

¿La estabilización de esta característica estabiliza implícitamente esa parte del Rust ABI?

Estamos hablando de invariantes de validez aquí, no de diseño de datos (que supongo que se refiere cuando abre el ABI). Así que nada de esto estabilizaría ningún ABI. Estos están relacionados pero son distintos y, de hecho, actualmente hay una discusión en curso sobre el ABI de los sindicatos .

Estos están relacionados pero son distintos y, de hecho, actualmente hay una discusión en curso sobre el ABI de los sindicatos.

AFAICT que la discusión es sobre la representación de la memoria de los sindicatos solamente, y no incluye cómo los uniones pasan a través de los límites de funciones y otras cosas que podrían ser relevantes para una ABI. No creo que el objetivo del repositorio de UCG sea crear un ABI para Rust.

Bueno, el objetivo es definir suficientes cosas para la interoperabilidad con C. Cosas como "Rust bool y C bool son compatibles con ABI".

Pero de hecho, por repr(Rust) , creo que no hay planes para definir una llamada de función ABI, pero lo ideal sería que sea una declaración explícita en cualquier forma que adopte el documento resultante, no solo una omisión.

Tengo curiosidad por saber si hay algún argumento en contra de la optimización del diseño Option<Foo> donde Foo se define así:

union Foo {
   bar: NonZeroUsize,
   baz: &'static str,
}

@Kixunil, ¿ podrías plantear eso en https://github.com/rust-rfcs/unsafe-code-guidelines/issues/13? Su pregunta realmente no está relacionada con MaybeUninit .

Quiero saber qué sección contendrá variables estáticas sin inicialización.
En "C" puedo escribir uint8_t a[100]; en un archivo de alto nivel, y sé que se colocará un símbolo en la sección .bss . Si escribo uint8_t a[100] = {}; del símbolo será puesto a la sección .data (que será copiada desde Flash a la memoria RAM antes de principal).

Es un pequeño ejemplo en Rust que usó MaybeUninit:

struct A {
    data: MaybeUninit<[u8; 100]>,
    len: usize,
}

impl A {
    pub const fn new() -> Self {
        Self {
            data: MaybeUninit::uninitialized(),
            len: 0,
        }
    }
}

static mut a: MaybeUninit<[u8; 100]> = MaybeUninit::uninitialized();
static mut b: A = A::new();

¿Qué sección será contiene símbolos A y B?

PD: Sé sobre la manipulación de símbolos, pero no es un asunto para esta pregunta.

@ qwerty19106 En su ejemplo, tanto a como b se colocarán en .bss . LLVM trata los valores undef , como MaybeUninit::uninitialized() , como ceros al seleccionar en qué sección entrará una variable.

Si A::new() inicializara len a 1, entonces b habría terminado en .data . Si un static contiene un valor distinto de cero, la variable irá en .data . El relleno se trata como valores cero.

Esto es lo que hace LLVM. Rust no hace ~ garantías ~ promesas (*) sobre en qué sección del enlazador entrará una variable static . Simplemente hereda el comportamiento de LLVM.

(*) A menos que use #[link_section]

Dato curioso: en algún momento LLVM consideró undef como un valor distinto de cero, por lo que la variable a en su ejemplo terminó en .data . Ver # 41315.

Gracias @japaric por tu respuesta. Fue muy útil para mí.

Tengo la nueva idea.
Se puede usar la sección .init_array para inicializar variables mut estáticas antes de llamar a main .

Esta es una prueba de concepto:

#[macro_export]
macro_rules! static_singleton {
    ($name_var: ident, $ty:ty, $name_init_fn: ident, $name_init_var: ident, $init_block: block) => {
        static mut $name_var: MaybeUninit<$ty> = unsafe {MaybeUninit::uninitialized()};

        extern "C" fn $name_init_fn() {
            unsafe {
                $init_block
            }
        }

        #[link_section = ".init_array"]
        #[used]
        static $name_init_var: [extern "C" fn(); 1] = [$name_init_fn];
    };
}

El código de prueba :

static_singleton!(A, u8, a_init_fn, A_INIT_VAR, {
    let ptr = A.get_mut();
    *ptr = 5;
});

fn main() {
    println!("A inited to {}", unsafe {&A.get_ref()});
}

Resultado : A intitulado a 5

Ejemplo completo : parque infantil

Pregunta sin resolver :
No pude usar concat_idents para generar a_init_fn y A_INIT_VAR . Parece que el # 1628 aún no está listo para usarse.

Esta prueba no es muy útil. Pero puede ser útil en embebido para inicializar estructuras complicadas (se colocará en .bss , por lo que permite economizar FLASH ).

¿Por qué rustc no usa la sección .init_array ? Es una sección estandarizada del formato ELF ( enlace ).

@ qwerty19106 Porque la vida antes de main () se considera un error y se

Ok, es un buen diseño de lang.

Pero en # [no_std] no tenemos una buena alternativa ahora (tal vez estaba buscando mal).

Podemos usar spin :: Once , pero es muy caro ( Ordering :: SeqCst en cada referencia get).

Me gustaría tener una verificación en tiempo de compilación en el archivo embedded .

es muy caro ( Ordering::SeqCst en cada referencia obtenida).

Eso no me suena bien. ¿No se supone que todas las abstracciones "únicas" se relajan en el acceso y se sincronizan en la inicialización? ¿O estoy pensando en otra cosa?
cc @Amanieu @alexcrichton

@ qwerty19106 :

Cuando dice "incrustado", ¿se refiere a bare-metal? Vale la pena señalar que .init_array no es, de hecho, parte del formato ELF en sí mismo ¹ - Ni siquiera es parte del System V ABI ² que lo extiende; solo .init es. No encuentra .init_array hasta que llega al borrador de la actualización de System V ABI , de la cual la ABI de Linux hereda.

Como resultado, si está ejecutando en bare metal, es posible que .init_array ni siquiera funcione de manera confiable para su caso de uso; después de todo, se implementa en non-bare-metal por código en el cargador dinámico y / o libc. A menos que su cargador de arranque asuma la responsabilidad de ejecutar el código referenciado en .init_array , no haría nada en absoluto.

1: Consulte la página 28, figura 1-13 "Secciones especiales"
2: Consulte la página 63, figura 4-13 "Secciones especiales (continuación)"

@eddyb Necesita una carga Acquire como mínimo al leer Once . Esta es una carga normal en x86 y una carga + valla en ARM.

La implementación actual usa load(SeqCst) , pero en la práctica esto genera el mismo asm que load(Acquire) en todas las arquitecturas.

(¿Le importaría mover estas discusiones a otra parte? Ya no tienen nada que ver con MaybeUninit vs mem :: uninitialized. Ambos se comportan de la misma manera que LLVM - generar undef. Lo que sucede con ese undef más adelante no está en el tema aquí. )

13 de septiembre de 2018 00:59:20 MESZ schrieb Amanieu [email protected] :

@eddyb Necesita una carga de Acquire como mínimo al leer el
Once . Esta es una carga normal en x86 y una carga + valla en ARM.

La implementación actual usa load(SeqCst) , pero en la práctica esto
genera el mismo conjunto que load(Acquire) en todas las arquitecturas.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente o véalo en GitHub:
https://github.com/rust-lang/rust/issues/53491#issuecomment -420825802

MaybeUninit ha aterrizado en master y estará en la próxima noche. :)

https://github.com/rust-lang/rust/issues/54470 propone usar Box<[MaybeUninit<T>]> en RawVec<T> . Para habilitar esta y posiblemente otras combinaciones interesantes con cajas y rebanadas con menos transmutaciones, ¿tal vez podríamos agregar más API a la biblioteca estándar?

En particular, para asignar sin inicializar (creo que Box::new(MaybeUninit::uninitialized()) todavía copiaría size_of::<T>() bytes de relleno?):

impl<T> Box<MaybeUninit<T>> {
    pub fn new_uninit() -> Self {…}
    pub unsafe fn assert_init(s: Self) -> Box<T> { transmute(s) }
}

impl<T> Box<[MaybeUninit<T>]> {
    pub fn new_uninit_slice(len: usize) -> Self {…}
    pub unsafe fn assert_init(s: Self) -> Box<[T]> { transmute(s) }
}

En core::slice / std::slice , se puede usar después de tomar una sub-porción:

pub unsafe fn assert_init<T>(s: &[MaybeUninit<T>]) -> &[T] { transmute(s) }
pub unsafe fn assert_init_mut<T>(s: &mut [MaybeUninit<T>]) -> &mut [T] { transmute(s) }

Creo que Box :: new (MaybeUninit :: uninitialized ()) todavía copiaría size_of ::() bytes de relleno

No debería, y hay una prueba de codegen destinada a probar eso.

Los bytes de relleno no tienen que copiarse ya que su representación de bits no importa (cualquier cosa que observe la representación de bits es UB de todos modos).

Ok, entonces tal vez Box::new_uninit es innecesario? Sin embargo, la versión de corte es diferente, ya que Box::new requiere T: Sized .

Me gustaría abogar por que MaybeUninit::zeroed sea ​​un const fn . Hay algunos usos relacionados con FFI que le daría (por ejemplo, una estática que debe inicializarse a cero) y creo que a otros les puede resultar útil. Me complacería ofrecer mi tiempo como voluntario para const-ifir la función zeroed .

@mjbshaw necesitará usar #[rustc_const_unstable(feature = "const_maybe_uninit_zeroed")] para eso, ya que zeroed hace cosas que no pasan la verificación min_const_fn (https://github.com/rust-lang/ rust / issues / 53555) lo que significa que la constness de MaybeUninit::zeroed será inestable incluso si la función es estable.

¿Podría la implementación / estabilización de esto dividirse en un par de pasos, para que el tipo MaybeUninit esté disponible para el ecosistema más amplio antes? Los pasos pueden ser:

1) agregar MaybeUninit
2) convertir todos los usos de mem :: uninitialized / zeroed y desaprobar

@scottjmaddox

añadir MaybeUninit

https://doc.rust-lang.org/nightly/core/mem/union.MaybeUninit.html :)

¡Agradable! Entonces, ¿el plan es estabilizar MaybeUninit lo antes posible?

El siguiente paso es averiguar por qué https://github.com/rust-lang/rust/pull/54668 sufre una regresión tan grave (en algunos puntos de referencia). Sin embargo, no tendré mucho tiempo para ver eso esta semana, estaría feliz si alguien más pudiera echar un vistazo. :RE

Además, no creo que debamos apresurarnos. Conseguimos mal la última API para manejar datos no inicializados, no nos apresuremos y volvamos a equivocarnos. ;)

Dicho esto, también preferiría no agregar retrasos innecesarios, para que finalmente podamos desaprobar el antiguo arma de fuego. :)

Ah, y se me acaba de ocurrir algo más ... con https://github.com/rust-lang/rust/pull/54667 aterrizado, las API antiguas en realidad protegen contra algunas de las peores pistolas. Me pregunto si podríamos conseguir algo de eso por MaybeUninit también. No está bloqueando la estabilización, pero podríamos tratar de encontrar una manera de hacer que MaybeUninit::into_inner pánico cuando se llame a un tipo deshabitado. En las compilaciones de depuración, también podría imaginar que *x entrará en pánico cuando x: &[mut] T con T deshabitado.

Actualización de estado: para avanzar con https://github.com/rust-lang/rust/pull/54668, es probable que necesitemos que alguien modifique el cálculo del diseño para las uniones. @eddyb está dispuesto a ser mentor, pero necesitamos a alguien que haga la implementación. :)

Creo que un método que se mueva fuera del contenedor, reemplazándolo con un valor no inicializado, sería útil:

pub unsafe fn take(&mut self) -> T

¿Debo enviar esto?

@shepmaster Esto se siente muy similar al método into_inner . ¿Quizás podamos intentar evitar la duplicación aquí?

También "reemplazar con" es probablemente la imagen incorrecta aquí, esto no debería cambiar el contenido de self en absoluto. La propiedad justa se transfiere, por lo que ahora está efectivamente en el mismo estado que cuando se construyó sin inicializar.

cambiar el contenido de self en absoluto

Claro, entonces la implementación sería básicamente ptr::read , pero desde un punto de vista de uso, recomendaría enmarcarlo como reemplazo de un valor válido con un valor no inicializado.

evitar la duplicación

No tengo ninguna objeción fuerte, ya que espero que la implementación de uno llame al otro. Simplemente no sé cuál sería el estado final.

Siento que into_inner es un nombre de función demasiado inocente. Las personas, probablemente sin leer los documentos con demasiada atención, todavía hacen MaybeUninit::uninitialized().into_inner() . ¿Podemos cambiar el nombre a algo como was_initialized_unchecked o que indique que solo debe llamar a esto después de que se hayan inicializado los datos?

Creo que probablemente se aplica lo mismo a take .

Si bien es un poco incómodo, ¿algo como unchecked_into_initialized podría funcionar?

¿O deberían eliminarse esos métodos por completo y los documentos dan ejemplos con x.as_ptr().read() ?

@SimonSapin into_inner consume self cual es bueno.

Pero para take de @shepmaster , hacer as_mut_ptr().read() haría lo mismo ... aunque, por supuesto, ¿por qué te molestarías con un puntero mutable?

¿Qué tal take_unchecked y into_inner_unchecked ?

Ese sería un plan de respaldo, supongo, pero preferiría que pudiera indicar que debe haberse inicializado.

Poner tanto el énfasis en que tiene que ser inicializado como una descripción de lo que hace (desenvolver / into_inner / etc.) En un nombre se vuelve bastante difícil de manejar, entonces, ¿qué tal si simplemente hacemos lo primero con assert_initialized y dejan el segundo? implícito en la firma? Posible unchecked_assert_initialized para evitar implicar una verificación de tiempo de ejecución como assert!() .

Posible unchecked_assert_initialized para evitar implicar una verificación en tiempo de ejecución como assert! ().

Ya diferenciamos entre suposiciones y afirmaciones a través de intrinsics::assume(foo) vs assert!(foo) , así que tal vez assume_initialized

assume es una API inestable, un ejemplo estable de asumir vs afirmar es unreachable_unchecked vs unreachable y get_unchecked vs get . Entonces creo que unchecked es el término correcto.

Yo diría que foo_unchecked solo tiene sentido cuando hay un foo , de lo contrario, la naturaleza pura de la función es unsafe me indica que algo "diferente" está sucediendo. en.

Este cobertizo para bicicletas es claramente del color incorrecto

Con esta API específica, ya hemos visto y seguiremos viendo que los programadores asumen que la inseguridad se debe a que "los datos no inicializados son basura, por lo que puede causar UB si los maneja sin cuidado", en lugar de lo previsto "es UB llamar a esto en datos no inicializados, punto ". No sé con certeza si un bly️ posiblemente redundante como unchecked ayudará con eso, pero prefiero errar por el lado de ser más desconcertante (= es más probable que las personas pregunten o lean los documentos muy cuidadosamente).

@RalfJung

Siento que into_inner es un nombre de función demasiado inocente. Las personas, probablemente sin leer los documentos con demasiada atención, todavía hacen MaybeUninit::uninitialized().into_inner() . ¿Podemos cambiar el nombre a algo como was_initialized_unchecked o que indique que solo debe llamar a esto después de que se hayan inicializado los datos?

I _ _ realmente gusta esta idea; Creo firmemente que dice lo correcto tanto sobre la semántica como que esto es potencialmente peligroso.

@rkruppe

Poner tanto el énfasis en que tiene que ser inicializado como una descripción de lo que hace (desenvolver / into_inner / etc.) En un nombre se vuelve bastante difícil de manejar, entonces, ¿qué tal si hacemos lo primero con assert_initialized y se deja el segundo? implícito en la firma? Posible unchecked_assert_initialized para evitar implicar una verificación en tiempo de ejecución como assert!() .

No tengo reparos en los nombres largos y difíciles de manejar para cosas peligrosas. Si hace que más gente lo piense dos veces, incluso was_initialized_into_inner_unchecked está completamente bien para mí. Hacer que sea poco ergonómico (dentro de lo razonable) escribir código inseguro es una característica, no un error;)

Recuerde que es probable que la gran mayoría de la gente utilice un IDE con alguna forma de autocompletar, por lo que un nombre largo es un obstáculo menor.

No me importa particularmente la ergonomía del uso de esta función, pero creo que más allá de cierto punto, los nombres tienden a leerse más que a leerlos, y este nombre realmente debería leerse para comprender lo que está sucediendo. Además, espero que esta función se discuta / explique casi tan a menudo como se usa realmente (ya que es relativamente un nicho y muy sutil), y aunque escribir identificadores largos en el código fuente puede estar bien (por ejemplo, gracias a IDE), escribirlos desde la memoria en un sistema de chat es ... menos agradable (estoy medio bromeando sobre este punto, pero sólo la mitad).

@shepmaster Seguro; También utilizo un IDE con finalización automática; pero creo que un nombre más largo con unchecked dentro, incluido el interior de un bloque unsafe , me daría una pausa adicional.

@rkruppe

escribirlos de memoria en un sistema de chat es ... menos agradable (estoy medio bromeando sobre este punto, pero sólo la mitad).

Haría ese intercambio. Si un nombre es un poco especial, incluso puede hacerlo más memorable. ;)

Cualquiera de (o nombres similares que incluyan las mismas connotaciones semánticas):

  • was_initialized_unchecked
  • was_initialized_into_inner_unchecked
  • is_initialized_unchecked
  • is_initialized_into_inner_unchecked
  • was_init_unchecked
  • was_init_into_inner_unchecked
  • is_init_unchecked
  • is_init_into_inner_unchecked
  • assume_initialized_unchecked
  • assume_init_unchecked

están bien para mí.

¿Qué pasa con initialized_into_inner ? O initialized_into_inner_unchecked , si crees que unchecked es realmente necesario, aunque suelo estar de acuerdo con unchecked solo es necesario para distinguir de alguna otra variante _checked_ del mismo funcionalidad, donde no se realizan comprobaciones en tiempo de ejecución.

Al implementar manualmente un generador de autoprestado, terminé usando ptr::drop_in_place(maybe_uninit.as_mut_ptr()) varias veces, parece que esto funcionaría bien como un método inherente unsafe fn drop_in_place(&mut self) en MaybeUninit .

Existe un precedente con ManuallyDrop::drop .

Yo diría que foo_unchecked solo tiene sentido cuando hay un foo correspondiente, de lo contrario, la naturaleza pura de la función que no es segura me indica que algo "diferente" está sucediendo.

No creo que no tener una versión segura sea una buena razón para eliminar la señal de advertencia de la versión insegura.

eliminar la señal de advertencia de la versión insegura

Siendo un poco hiperbólico, ¿cuándo una función unsafe no debe tener _unchecked pegada al final entonces? ¿Cuál es el punto de tener dos advertencias que digan lo mismo?

Esa es una pregunta justa. :) Pero creo que la respuesta es "casi nunca", y de hecho lamento que tengamos offset como función insegura en punteros que de ninguna manera expresa que no sea segura. No tiene que ser literalmente unchecked , pero en mi opinión debería haber algo . Cuando estoy en un bloque inseguro de todos modos y escribo accidentalmente .offset lugar de .wrapping_offset , hice una promesa al compilador que no tenía la intención de hacer.

como función insegura en punteros que de ninguna manera expresa que sea insegura

Esto resume mi desconcierto en esta etapa.

@shepmaster para que no crea que sea realista que alguien edite el código dentro de un bloque unsafe (tal vez uno grande, tal vez dentro de un unsafe fn grande que implícitamente tiene un unsafe block), y no tenga en cuenta que la llamada que están agregando es unsafe ?

alguien editará el código dentro de un bloque unsafe [...] y no se dará cuenta de que la llamada que está agregando es unsafe

Lo siento, no tenía la intención de descartar esta posibilidad y parece real. Mi opinión es que agregar calificadores al nombre de una función para indicar que una función no es segura porque el calificador unsafe no nos ayuda parece indicar una falla más profunda.

Tal vez sea una falla que no podemos solucionar de una manera compatible con versiones anteriores y agregar palabras a los nombres es la única solución posible, pero espero que ese no sea el caso.

tal vez uno grande, tal vez dentro de un unsafe fn grande que implícitamente tiene un bloque unsafe

Me han preguntado por qué Rust permite el sombreado de variables porque el sombreado es claramente una mala idea cuando se tiene una función de cientos de líneas. Personalmente, desdeño mucho estos casos porque creo que dicho código se acepta generalmente como una forma incorrecta para empezar.

Ahora bien, si algún aspecto de Rust obliga a los bloques inseguros a ser más grandes de lo que "necesitan", tal vez eso también indique un problema más fundamental.


Aparte, me pregunto si IDE + RLS pueden identificar alguna función marcada como insegura y resaltarlas especialmente. Mi editor ya destaca la palabra clave unsafe , por ejemplo.

Ahora bien, si algún aspecto de Rust obliga a los bloques inseguros a ser más grandes de lo que "necesitan", tal vez eso también indique un problema más fundamental.

Bueno, hay https://github.com/rust-lang/rfcs/pull/2585;)

Aparte, me pregunto si IDE + RLS pueden identificar alguna función marcada como insegura y resaltarlas especialmente.

¡Eso seria genial! Sin embargo, no todo el mundo lee el código en los IDE, es decir, las revisiones no suelen realizarse en los IDE.

Ahora bien, si algún aspecto de Rust obliga a los bloques inseguros a ser más grandes de lo que "necesitan", tal vez eso también indique un problema más fundamental.

Creo que los métodos inseguros en cadenas son uno de los ejemplos más importantes: dividir un método en el medio en un enlace let puede ser bastante poco ergonómico, pero a menos que lo haga, toda la cadena está cubierta.

No del todo "fuerza", pero definitivamente "motiva".

l hay rust-lang / rfcs # 2585 ;)

Sí, pero no lo mencioné porque tampoco ayudaría en su caso. La gente siempre puede agregar un bloque unsafe alrededor de todo el cuerpo (como se menciona en los comentarios) y luego volverá al mismo problema: llamadas de función inseguras "colarse".

Sin embargo, no todo el mundo lee el código en IDE

Sí, por eso lo dejo de lado. Supongo que debería haberlo dicho más claramente.


Supongo que mi problema es que, efectivamente , estás defendiendo esto:

unsafe fn unsafe_real_name_of_function() { ... }
          ^~~~~~ for humans
^~~~~~           for the compiler

Esto le permite ver claramente todas las funciones inseguras al leer el código. La repetición me molesta mucho e indica que algo no es óptimo.

Esto le permite ver claramente todas las funciones inseguras al leer el código. La repetición me molesta mucho e indica que algo no es óptimo.

Entiendo. También podría ver esta repetición como una implementación del principio de los 4 ojos, donde el compilador proporciona dos ojos. ;)

@shepmaster Creo que esto se está invariantes de este método, es decir, cuándo el código unsafe realidad no es UB , con el nombre más simple.

Estoy de acuerdo en que "sin marcar" no es la mejor opción, pero tiene un precedente como "invariantes que violan fácilmente".

Esto me hace desear que tuviéramos una convención de nomenclatura similar a initialized_or_ub .

Creo que esto se está desviando un poco

Estaba a punto de decirlo yo mismo. He dicho mi artículo (y aparentemente nadie está de acuerdo conmigo), así que lo dejaré reposar; ustedes eligen lo que quieran.

teníamos una convención de nomenclatura en la línea de initialized_or_ub

¿Te refieres a maybe_uninit(ialized) ? ¿Algo que podría aplicarse ampliamente a un conjunto de métodos relacionados de alguna manera? 😇

No, me refiero a unwrap_or_else - poniendo lo que sucede en el "caso infeliz" en el nombre del método.

@eddyb Oye, eso no es tan malo ... .initialized_or_unsound ¿tal vez?

En general, agregar información de tipo a los nombres de los identificadores se considera un anti-patrón (por ejemplo, foo_i32 , bar_mutex , baz_iterator ) porque para eso están los tipos.

Sin embargo, cuando se trata de funciones, aunque unsafe es parte del tipo fn , agregar _unchecked , _unsafe , _you_better_know_what_you_are_doing parece ser bastante común.

Me pregunto, ¿por qué es este el caso?

Además, para su información, hay un problema (https://github.com/rust-analyzer/rust-analyzer/issues/190) en rust-analyzer para exponer si las funciones son unsafe o no. Los editores y los IDE deben poder enfatizar las operaciones que requieren unsafe dentro de los bloques unsafe , que incluyen no solo las funciones de llamada unsafe (independientemente de si tienen un sufijo con un identificador como, por ejemplo, _unchecked o no), pero también desreferenciar punteros sin formato, etc.

Podría decirse que rust-analyzer no puede hacer esto todavía (EDITAR: intellij-Rust tipo de lata: https://github.com/intellij-rust/intellij-rust/issues/3013#issuecomment-440442306), pero si el La intención es dejar en claro que llamar a esto dentro de un bloque unsafe requiere unsafe , el resaltado de sintaxis es una posible alternativa a agregar el sufijo con cualquier cosa. Quiero decir, si realmente quieres esto ahora mismo, probablemente puedas agregar el nombre de esta función como una "palabra clave" a tu resaltador de sintaxis en un par de minutos y terminarlo.

@gnzlbg

En general, agregar información de tipo a los nombres de los identificadores se considera un anti-patrón (por ejemplo, foo_i32 , bar_mutex , baz_iterator ) porque para eso están los tipos.

Claro, la notación húngara se considera en general un anti-patrón. Yo estaría de acuerdo. Sin embargo, en general, la seguridad no se considera en estas discusiones y creo que, dado el peligro que presenta la UB, hay buenas razones para hacer una excepción aquí.

Sin embargo, cuando se trata de funciones, aunque unsafe es parte del tipo fn , agregar _unchecked , _unsafe , _you_better_know_what_you_are_doing parece ser bastante común.

Me pregunto, ¿por qué es este el caso?

En pocas palabras: inseguridad. Cuando se trata de inseguridad, la redundancia es, en mi opinión, su amiga. Esto se aplica tanto al código como al hardware crítico para la seguridad y demás.

Además, para su información, hay un problema ( analizador de óxido / analizador de óxido # 190 ) en rust-analyzer para exponer si las funciones son unsafe o no. Los editores y los IDE deben poder enfatizar las operaciones que requieren unsafe dentro de los bloques unsafe , que incluyen no solo las funciones de llamada unsafe (independientemente de si tienen un sufijo con un identificador como, por ejemplo, _unchecked o no), pero también desreferenciar punteros sin formato, etc.

Podría decirse que rust-analyzer no puede hacer esto todavía, pero si la intención es dejar en claro que llamar a esto dentro de un bloque unsafe requiere unsafe , el resaltado de sintaxis es una posible alternativa al sufijo esto con cualquier cosa.

Todo esto es bastante impresionante. Sin embargo, como señaló @RalfJung : "Sin embargo,

Si la compensación es entre ser feo y ser propenso al uso incorrecto (y por lo tanto poco sólido) de unsafe , creo que siempre deberíamos preferir lo primero. Se puede decir mucho para que un programador _tiene_ que hacer una pausa y pensar: "Espera, ¿lo estoy haciendo bien?"

Por ejemplo, si desea utilizar una operación criptográfica insegura en Mundane, debe:

  • Importarlo desde el módulo insecure
  • Escriba allow(deprecated) o simplemente viva con las advertencias del compilador emitidas cada vez que use esa operación
  • Escriba un código que se parezca a let mut hash = InsecureSha1::default(); hash.insecure_write(bytes); ...

Todo está documentado aquí con más detalle. No creo que debamos ser _ tan_ agresivos en todas las circunstancias, pero creo que la filosofía correcta es que, si la operación es lo suficientemente peligrosa como para preocuparse, entonces no debería haber ninguna forma de que un programador pierda la gravedad. de lo que están haciendo.

Sugerencia completamente seria

Dado que estamos un 95% preocupados por la gente que hace mal uso de este tipo, y solo un 5% preocupado por los nombres largos, comencemos por cambiar el nombre del tipo a MaybeUninitialized . Los 7 caracteres extra merecen la pena.

Sugerencias en su mayoría serias

  1. Cambie el nombre a MaybeUninitializedOrUndefinedBehavior para que realmente llegue a los usuarios finales.

  2. Opte por este tipo para no tener métodos y todo puede ser una función asociada, reforzando el punto en cada llamada de función, como se desee:

    MaybeUninitializedOrUndefinedBehavior::into_inner(value)
    

Sugerencia tonta

MaybeUninitializedOrUndefinedBehaviorReadTheDocsAllOfThemYesThisMeansYou

Bueno ... sinceramente, tener un nombre largo como MaybeUninitializedOrUndefinedBehavior en el tipo me parece fuera de lugar. Es la operación .into_inner() que necesita el buen nombre porque esa es la parte potencialmente problemática que necesita atención adicional. Sin embargo, no tener métodos podría ser una buena idea. MaybeUninit::initialized_or_undefined(foo) parece bastante claro.

En mi opinión, no deberíamos hacer todo lo posible para hacer que las operaciones inseguras no sean ergonómicas como esta. Necesitamos nombres ergonómicos y formas de escribir código inseguro correcto. Si lo abarrotamos con un montón de nombres excesivamente largos y utilidades y conversiones poco claras, desalentará a los usuarios a escribir código inseguro correcto y hará que el código inseguro sea más difícil de leer y validar.

Recuerde que es probable que la gran mayoría de la gente utilice un IDE con alguna forma de autocompletar, por lo que un nombre largo es un obstáculo menor.

Hasta que RLS sea más funcional, al menos este no es mi caso.

Creo que la mayoría de nosotros estamos de acuerdo en que

  • Los nombres más descriptivos son buenos

  • Los nombres menos ergonómicos son malos

y la pregunta es de qué manera resolver las cosas cuando están en tensión.

Aun así, creo que into_inner en particular es un mal nombre para este método (para usar un término elegante, no está en la frontera de Pareto). La convención general es que tenemos un into_inner cuando un Foo<T> contiene exactamente un T , y quieres sacarlo. Pero esto no es cierto para MaybeUninit<T> . Contiene cero o uno T s.

Entonces, una opción menos mala, al menos, sería llamarlo unwrap , o quizás unwrap_unchecked .

También creo que from_initialized o from_initialized_unchecked suenan bien, aunque "from" suele aparecer en los nombres de los métodos estáticos.

¿Quizás unwrap_initialized_unchecked estaría bien?

Llámelo take_initialized y conviértalo en &mut self lugar de self . El nombre deja en claro que espera que se inicialice el valor interno. En el contexto de MaybeUninit , unsafe y el hecho de que no devuelve un Option / Result también deja claro que esta operación no está marcada.

Hacer que tome un &mut self parece un arma de fuego que hace que sea más fácil perder de vista si se ha movido semánticamente fuera del MaybeUninit .

Nombre alternativo: dado que realmente está moviendo la propiedad al igual que los métodos llamados into implican, tal vez into_initialized_unchecked

Hacer que tome un & mut self parece un arma de fuego que hace que sea más fácil perder de vista si se ha mudado semánticamente de la MaybeUninit.

Sin embargo, es un método que se ha solicitado , y siempre que haga un seguimiento de que esto no suceda dos veces, está bien.

Y no parece que valga la pena tener tanto la variante prestada como la consumidora.

Me gusta take_initialized , o la variante más explícita take_initialized_unchecked .

Comencemos por cambiar el nombre del tipo a MaybeUninitialized

¿Alguien quiere preparar un PR?

para preparar un PR?

Puedo usar mis increíbles habilidades sed desde que lo sugerí ;-)

Creo que sería una mejora llamar al método into_inner algo que enfatice que asume que está inicializado, pero creo que la adición de unchecked es superflua e inútil. Tenemos una forma de informar a los usuarios que las funciones inseguras no lo son: generamos un error de compilador si no las envuelven en un bloque inseguro.

EDITAR: take_initialized parece bueno

¿Qué pasa con assume_initialized ? Esta:

  • Se conecta al modelo de 'obligación de prueba'
  • Visceralmente se conecta con 'las suposiciones son riesgosas'
  • Solo necesita dos palabras
  • Describe el significado semántico de la operación.
  • Lee con bastante naturalidad
  • Al igual que el LLVM assume intrínseco, es UB si se asume incorrectamente

¿Alguien quiere preparar un PR?

No importa. El equipo de bibliotecas decidió que no valía la pena.

¿Hay alguna razón por la que MaybeUninit<T> no sea Copy cuando T: Copy ?

@tommit debido a que MaybeUninit<T> basa en ManuallyDrop<T> , los programadores debemos garantizar que el valor interno se elimina cuando nuestras estructuras están fuera de alcance. Si implementa Copy , creo que puede ser más difícil para los recién llegados de Rust recordar eliminar el valor interno T cada vez, ya sea de la estructura misma o de sus copias. De esta manera, puede producir pérdidas de memoria menos visibles de las que no esperamos.

@ luojia65 No estoy seguro de que esa línea de razonamiento se aplique cuando T sí es Copy , independientemente de lo que hagan ManuallyDrop y MaybeUninit .

No creo que haya ninguna razón. Nadie pensó en agregar #[derive(Copy)] ;)

Una observación de tal vez un aspecto algo sutil de esto:
Creo que aunque MaybeUninit<T> debería ser Copy cuando T: Copy , MaybeUninit<T> no debería ser Clone cuando T: Clone y T no es Copy .

Oh, sí, definitivamente no podemos simplemente llamar clone .

Sigo olvidando que Copy: Clone ...

Eso está bien, podemos implementar Clone for MaybeUninit<T> where T: Copy basándonos en devolver *self .

Hice todo lo posible para actualizar la descripción del problema con todas las preguntas que han surgido aquí. ¡Avísame si me perdí algo!

La documentación de ManuallyDrop::drop dice

Esta función ejecuta el destructor del valor contenido y, por lo tanto, el valor envuelto ahora representa datos no inicializados. Depende del usuario de este método asegurarse de que los datos no inicializados no se utilicen realmente.

¿Alguna sugerencia sobre cómo mejorar esa redacción para que no pueda confundirse con el tipo de "falta de inicialización" que maneja MaybeUninit ?

En mis términos, un ManuallyDrop<T> eliminado ya no es un T seguro , pero es un T válido ... al menos en lo que respecta a las optimizaciones de diseño.

"rancio" / "inválido", ¿quizás? Está inicializado .

FWIW Creo que la redacción es clara (al menos para mí) asegurándose de que un
el objeto no se deja caer dos veces es un problema de "seguridad". Si tuviéramos un pequeño documento
en la UCG, que define "seguridad", probablemente se debería hacer un hipervínculo. Tú podrías
añadir que T debe ser una definición "válida" y un hipervínculo "válido", pero como
todavía no tengo estas definiciones escritas en ninguna parte ... No lo sé. yo
no crea que deberíamos parafrasearlos en todos los documentos.

¿Podemos estabilizar MaybeUninit antes de desaprobar sin inicializar?

@RalfJung Yo diría que el lugar está "movido". FWIW deberíamos usar el mismo tipo de terminología en std::ptr::read , pero tampoco está muy claro allí.

@bluss , nunca deberíamos desaprobar nada de uso generalizado sin un "mejor
solución ”/“ ruta de migración ”para los usuarios actuales.

Las advertencias de obsolescencia deben ser: "X está en desuso, use Y en su lugar". Si nosotros
no tienen una Y y X se usa ampliamente ... entonces deberíamos considerar mantenernos
la advertencia de desaprobación hasta que tengamos una Y.

De lo contrario, estaríamos enviando un mensaje realmente extraño.

@cramertj "inválido" no es una buena opción, ya que todavía (¡debe!) satisfacer la invariante de validez.

Si tuviéramos un pequeño documento en la UCG que defina “seguridad” probablemente deberíamos hacer un hipervínculo. Podría agregar que T debe ser una definición "válida" y un hipervínculo "válido", pero como aún no tenemos estas definiciones escritas en ninguna parte ...

Definitivamente deberíamos hacer eso una vez que tengamos algo: D

@RalfJung No creo que "invariante de validez" esté en el léxico de la mayoría (¿casi ninguno?) De los usuarios de Rust. Creo que referirse coloquialmente a "datos inválidos" es aceptable (el ManuallyDrop<T> ya no se puede usar como a T ). Decir que tiene que mantener ciertos invariantes de representación que el compilador usa para las optimizaciones no hace que los datos sean menos inválidos.

No creo que la "validez invariante" esté en el léxico de la mayoría (¿casi ninguno?) De los usuarios de Rust.

Bastante justo, el término no es oficial (todavía). Pero deberíamos elegir un término oficial para esto eventualmente, y luego deberíamos evitar tales enfrentamientos. Podemos decir que los datos "válidos" son lo que yo llamo "seguros" en mi publicación, pero luego necesitamos otra palabra para lo que yo llamo "válido".

@shepmaster escribió hace un tiempo

Creo que un método que se mueva fuera del contenedor, reemplazándolo con un valor no inicializado, sería útil:

pub unsafe fn take(&mut self) -> T

Creo que mi mayor preocupación con esto es que con dicha función, es tremendamente fácil copiar accidentalmente datos que no se copian. En caso de que lo necesite, ¿es realmente tan malo hacer maybe_uninit.as_ptr().read() ?

Creo que pude haber sugerido en algún lugar que algo como take reemplace algo como into_inner . Ya no creo que eso sea una buena idea: la mayoría de las veces, la restricción adicional de que into_inner consume self es realmente útil.

@RalfJung Al final, todos los métodos de MaybeUninit no son seguros y son simplemente envoltorios de conveniencia alrededor de as_ptr . Sin embargo, espero que take sea ​​una de las operaciones más comunes, ya que MaybeUninit es efectivamente solo un Option donde la etiqueta se administra externamente. Esto es útil en muchos casos, por ejemplo, una matriz donde no se inicializan todos los elementos (por ejemplo, una tabla hash).

En https://github.com/rust-lang/rust/pull/57045 , propongo agregar dos nuevas operaciones a MaybeUninit :

    /// Get a pointer to the first contained values.
    pub fn first_ptr(this: &[MaybeUninit<T>]) -> *const T {
        this as *const [MaybeUninit<T>] as *const T
    }

    /// Get a mutable pointer to the first contained values.
    pub fn first_mut_ptr(this: &mut [MaybeUninit<T>]) -> *mut T {
        this as *mut [MaybeUninit<T>] as *mut T
    }

Vea ese PR para motivación y discusión.

Al eliminar zeroed , parece que solo se reemplaza por MaybeUninit::zeroed().into_inner() que se convierte en una forma equivalente de escribir lo mismo. No hay ningún cambio práctico. Con valores uninit, en cambio, tenemos el cambio práctico de que todos los datos no inicializados se guarden en valores del tipo MaybeUninit o unión equivalente.

Por esta razón, consideraría mantener std::mem::zeroed tal como está, ya que es una función ampliamente utilizada en FFI. La obsolescencia haría que emitiera advertencias fuertes, lo que equivale casi a eliminarlo, y al menos muy molesto; esto también puede llevar a un número creciente de #[allow(deprecated)] que puede ocultar otros problemas más importantes.

Este ejercicio de aclarar el modelo y las pautas de Rust para el código marcado con unsafe es muy útil, pero evitemos cambios como para zeroed donde solo está reformulando el mismo efecto práctico usando una nueva forma de decirlo .

@bluss Tengo entendido (lo que podría estar equivocado) que std::mem:zeroed es tan peligroso como std::mem::uninitialized , y es muy probable que resulte en UB. Quizás se esté utilizando para inicializar matrices de bytes, que se inicializarían mejor con vec![0; N] o [0; N] , en cuyo caso tal vez se podría agregar una regla rustfix para automatizar el cambio. Sin embargo, aparte de inicializar matrices de bytes o enteros, tengo entendido que existe una buena posibilidad de que el uso de std::mem::zeroed pueda conducir a UB.

@scottjmaddox Es muy fácil invocar UB con std::mem:zeroed , pero a diferencia de std::mem::uninitialized , hay algunos tipos para los que std::mem:zeroed es perfectamente válido (por ejemplo, tipos nativos, muchos relacionados con FFI struct s, etc.). Como muchas funciones unsafe , zeroed() no debe usarse a la ligera, pero no es tan problemático como uninitialized() . Por mi parte, sería triste tener que usar MaybeUninit::zeroed().into_inner() lugar de std::mem:zeroed() ya que no hay ninguna diferencia entre los dos en términos de seguridad y la versión MaybeUninit es más difícil de manejar y un poco menos legible (y cuando debo usar código inseguro, valoro mucho la legibilidad).

@mjbshaw

a diferencia de std :: mem :: uninitialized, hay algunos tipos para los que std :: mem: zeroed es perfectamente válido (por ejemplo, tipos nativos,

Hay algunos tipos para los que mem::uninitialized es perfectamente seguro ( por ejemplo, unit ), mientras que hay algunos tipos "nativos" (por ejemplo, bool , &T , etc. .) para el cual mem::zeroed invoca un comportamiento indefinido.


Parece haber una idea errónea aquí de que MaybeUninit alguna manera se trata de memoria no inicializada (y puedo ver por qué: "Uninitialized" está en su nombre).

El peligro que estamos tratando de prevenir es el causado por la creación de un valor _invalido_, ya sea que el valor _invalid_ contenga todos ceros, o bits no inicializados, o algo más (por ejemplo, un bool de un patrón de bits que no es true o false ), realmente no importa - mem::zeroed y mem::uninitialized pueden usarse para crear un valor _invalido_, y por lo tanto son prácticamente igualmente peligroso desde mi punto de vista.

OTOH MaybeUninit::zeroed() y MaybeUninit::uninitialized() son métodos _safe_ porque devuelven un union . MaybeUninit::into_inner es unsafe , y llamarlo solo es _seguro_ si se cumple la condición previa de que los bits actuales en MaybeUninit<T> representan un valor _valido_ de T . Si el patrón de bits es _invalid_, el comportamiento no está definido. Si el patrón de bits no es válido porque contiene todos ceros, bits no inicializados u otra cosa, realmente no importa.

@RalfJung Estoy empezando a tener la sensación de que el nombre MaybeUninit podría ser un poco engañoso. Tal vez deberíamos cambiarle el nombre a MaybeInvalid o algo así para transmitir mejor el problema que resuelve y los peligros que evita. EDITAR: siguiendo las sugerencias de @Centril que publiqué sobre el tema de la bici.


EDITAR: FWIW, creo que tener una forma ergonómica (por ejemplo, sin usar directamente MaybeUninit ) para crear memoria cero de forma segura sería útil, pero mem::zeroed no es así. Podríamos agregar un rasgo Zeroed similar a Default que solo se implementa para tipos para los que el patrón de bits de todos los ceros es válido o algo así, como una forma de lograr un efecto similar a lo que mem::zeroed hace ahora, pero sin sus inconvenientes.

En general, creo que no deberíamos desaprobar la funcionalidad hasta que exista una ruta de migración para los usuarios actuales a una mejor solución. MaybeUninit es una mejor solución que mem::zeroed en mi opinión, aunque puede que no sea perfecto (es más seguro, pero no es tan ergonómico), por lo que estaría bien con desaprobar mem::zeroed tan pronto como llegue MaybeUninit , incluso si para cuando eso suceda no tengamos un reemplazo más ergonómico en su lugar.

Tal vez deberíamos cambiarle el nombre a MaybeInvalid o algo así para transmitir mejor el problema que resuelve y los peligros que evita.

Bikeshed en https://github.com/rust-lang/rust/pull/56138.

@gnzlbg

hay algunos tipos "nativos" (por ejemplo, bool

Siempre que bool sea ​​seguro para FFI (lo que generalmente se considera que es, a pesar de que RFC 954 se rechaza y luego se acepta de manera no oficial y oficial), debería ser seguro usar mem::zeroed para ello.

, &T , etc.) para los cuales mem::zeroed invoca un comportamiento indefinido.

Sí, pero estos tipos que tienen UB por mem::zeroed también tienen UB por MaybeUninit::zeroed().into_inner() (tuve cuidado de incluir intencionalmente .into_inner() en mi comentario original). MaybeUninit no agrega nada si el usuario simplemente llama inmediatamente a .into_inner() (que es precisamente lo que yo y muchos otros haríamos si mem::zeroed estuviera en desuso, porque solo estoy usando mem::zeroed para tipos que son seguros para cero).

Siempre que bool sea seguro para FFI (lo que generalmente se considera que es, a pesar de que RFC 954 se rechaza y luego se acepta de manera no oficial-oficial), debería ser seguro usar mem :: zeroed para él.

No quería entrar en los detalles de esto, pero bool es seguro para FFI en el sentido de que se define como igual a _Bool C. Sin embargo, los valores true y false de C _Bool no están definidos en el estándar C (aunque podrían estarlo algún día, tal vez en C20), así que si mem::zeroed crea un bool válido o no está técnicamente definido por la implementación.

Sí, pero estos tipos que tienen UB para mem :: zeroed también tienen UB para MaybeUninit :: zeroed (). Into_inner () (tuve cuidado de incluir intencionalmente .into_inner () en mi comentario original). MaybeUninit no agrega nada si el usuario llama inmediatamente a .into_inner () (que es precisamente lo que yo y muchos otros haríamos si mem :: zeroed estuviera en desuso, porque solo estoy usando mem :: zeroed para tipos que son seguros para cero) .

Realmente no entiendo qué punto estás tratando de hacer aquí. MaybeUninit agrega la opción de llamar o no llamar into_inner , que mem::zeroed no tiene, y eso tiene valor ya que esa es la operación que puede introducir un comportamiento indefinido ( construir la unión como no inicializada o cero es seguro).

¿Por qué alguien traduciría ciegamente mem::zeroed a MayeUninit + into_inner ? Esa no es la forma adecuada de "arreglar" la advertencia de baja de mem::zeroed , y silenciar la advertencia de baja tiene el mismo efecto y un costo mucho menor.

La forma adecuada de pasar de mem::zeroed a MaybeUninit es evaluar si es seguro llamar a into_inner , en cuyo caso uno puede hacerlo y escribir un comentario explicando por qué es seguro, o simplemente sigue trabajando con MaybeUninit como union hasta que llamar a into_inner sea ​​seguro (es posible que tengas que cambiar una gran cantidad de código hasta que ese sea el caso, romper API cambia para devolver MaybeUninit lugar de T s, etc.).

No quería entrar en los detalles de esto, pero bool es seguro para FFI en el sentido de que se define como igual a _Bool C. Sin embargo, los valores true y false de C's _Bool are not defined in the C standard (although they might be some day, maybe in C20), so whether mem :: cero creates a valid bool` o no están técnicamente definidos por la implementación .

Disculpas por continuar con la tangente, pero C11 requiere que all-bits-set-to-zero represente el valor 0 para los tipos de enteros (ver sección 6.2.6.2 "Tipos de enteros", párrafo 5) (que incluye _Bool ) . Además, los valores de true y false se definen explícitamente (consulte la sección 7.18 "Tipo y valores booleanos <stdbool.h> ").

Realmente no entiendo qué punto estás tratando de hacer aquí. MaybeUninit agrega la opción de llamar o no llamar into_inner , que mem::zeroed no tiene, y eso tiene valor, ya que esa es la operación que puede introducir un comportamiento indefinido ( construir la unión como no inicializada o cero es seguro).

Hay valor en MaybeUninit y MaybeUninit::zeroed . Ambos estamos de acuerdo en eso. No estoy argumentando que se elimine MaybeUninit::zeroed . Mi punto es que también hay valor en std::mem::zeroed .

Hay algunos tipos para los que mem :: uninitialized es perfectamente seguro (por ejemplo, unit), mientras que hay algunos tipos "nativos" (por ejemplo, bool, & T, etc.) para los que mem :: zeroed invoca un comportamiento indefinido.

Esto es una pista falsa. El hecho de que tanto zeroed como uninitialized sean válidos para algún subconjunto de tipos no los hace comparables en el uso real. Necesita mirar el tamaño de esos subconjuntos. El número de tipos para los cuales mem::uninitialized es válido es muy, muy pequeño (de hecho, ¿son solo tipos de tamaño cero?), Y nadie escribiría código que lo haga (por ejemplo, para ZSTs que simplemente usarías el constructor de tipos). Por otro lado, hay muchos tipos para los que mem::zeroed es válido. mem::zeroed es válido para al menos los siguientes tipos (espero haberlo hecho bien):

  • todos los tipos de enteros (incluidos bool , como se mencionó anteriormente)
  • todos los tipos de punteros sin formato
  • Option<T> donde T activa la optimización del diseño de enumeración. T incluye:

    • NonZeroXXX (todos los tipos de enteros)

    • NonNull<U>

    • &U

    • &mut U

    • fn -punteros

    • cualquier matriz de cualquier tipo en esta lista

    • any struct donde cualquier campo es un tipo en esta lista.

  • Cualquier matriz, struct o union consta solo de los tipos de esta lista.

Sí, tanto uninitialized como zeroed tratan con valores potencialmente inválidos. Sin embargo, los programadores utilizan estas primitivas de formas muy diferentes.

El patrón común para mem::uninitialized es:

let val = MaybeUninit::uninitialized();
initialize_value(val.as_mut_ptr()); // or val.set
val.into_inner()

Si no escribe su uso de valores no inicializados de esta manera, lo más probable es que esté cometiendo un gran error.

El uso más común de mem::zeroed hoy es para los tipos descritos anteriormente, y esto es perfectamente válido. Estoy completamente de acuerdo con @bluss en que no veo ninguna ganancia en la prevención de pistolas al reemplazar mem::zeroed() todas partes por MaybeUninit::zeroed().into_inner() .

En resumen, el uso común de uninitialized es para tipos para los que pueden tener valores no válidos. El uso común de zeroed es para tipos que son válidos si se ponen a cero.

Un rasgo de Zeroed o similar (por ejemplo, Pod , pero tenga en cuenta que T: Zeroed no implica T: Pod ) como se ha sugerido, parece una buena idea agregar el futuro, pero no despreciemos fn zeroed<T>() -> T hasta que tengamos un fn zeroed2<T: Zeroed>() -> T estable.

@mjbshaw

Disculpas por continuar la tangente, pero C11 requiere que

¡En efecto! ¡Es solo bool C ++ lo que deja los valores válidos sin especificar! Gracias por corregirme, enviaré un PR a la UCG con esta garantía.

@jethrogb

Necesita mirar el tamaño de esos subconjuntos. El número de tipos para los que mem::uninitialized es válido es muy, muy pequeño (de hecho, ¿son solo tipos de tamaño cero?), Y nadie escribiría código que lo haga (p. Ej. el constructor de tipos).

Ni siquiera es correcto para todos los ZST si tiene en cuenta la privacidad con la que es posible tener los ZST como una especie de "prueba de trabajo" o "símbolo de recurso" o simplemente "testigo de prueba" en general. Un ejemplo trivial :

mod refl {
    use core::marker::PhantomData;
    use core::mem;

    /// Having an object of type `Id<A, B>` is a proof witness that `A` and `B`
    /// are nominally equal type according to Rust's type system.
    pub struct Id<A, B> {
        witness: PhantomData<(
            // Make sure `A` is Id is invariant wrt. `A`.
            fn(A) -> A,
            // Make sure `B` is Id is invariant wrt. `B`.
            fn(B) -> B,
        )>
    }

    impl<A> Id<A, A> {
        /// The type `A` is always equal to itself.
        /// `REFL` provides a proof of this trivial fact.
        pub const REFL: Self = Id { witness: PhantomData };
    }

    impl<A, B> Id<A, B> {
        /// Casts a value of type `A` to `B`.
        ///
        /// This is safe because the `Id` type is always guaranteed to
        /// only be inhabited by `Id<A, B>` types by construction.
        pub fn cast(self, value: A) -> B {
            unsafe {
                // Transmute the value;
                // This is safe since we know by construction that
                // A == B (including lifetime invariance) always holds.
                let cast_value = mem::transmute_copy(&value);

                // Forget the value;
                // otherwise the destructor of A would be run.
                mem::forget(value);

                cast_value
            }
        }
    }
}

fn main() {
    use core::mem::uninitialized;

    // `Id<?A, ?B>` is a ZST; let's make one out of thin air:
    let prf: refl::Id<u8, String> = unsafe { uninitialized() };

    // Segfault:
    let _ = prf.cast(42u8);
}

@Centril, esto es una especie de tangente, pero no estoy seguro de si su código es en realidad un ejemplo de un tipo para el que llamar a uninitialized crea un valor no válido. Está utilizando un código inseguro para violar las invariantes internas que se supone que debe mantener Id . Hay muchas formas de hacer esto, por ejemplo transmute(()) , o punteros sin formato de conversión de tipos.

@jethrogb Mi único punto es que a) tenga más cuidado con la redacción, b) la privacidad no parece suficientemente razonada en las discusiones sobre qué valores válidos son. Me parece que "violar las invariantes internas" y "valor inválido" son lo mismo; aquí hay una condición secundaria "si A != B entonces Id<A, B> está deshabitado".

Me parece que "violar las invariantes internas" y "valor inválido" son lo mismo; aquí hay una condición secundaria "si A != B entonces Id<A, B> está deshabitado".

Las invariantes "impuestas por el código de la biblioteca" son diferentes de las invariantes "impuestas por el compilador" de varias maneras, consulte la publicación de blog de . En esa terminología, su ejemplo Id tiene un invariante de seguridad y mem::zeroed u otras formas de sintetizar genéricamente un Id<A, B> no pueden ser seguras , pero no es UB inmediato para solo construya un Id incorrecto con mem::zeroed o mem::uninitialized porque Id no tiene invariante de validez . Si bien los autores de códigos inseguros ciertamente deben tener en cuenta ambos tipos de invariantes, hay algunas razones por las que esta discusión se centra principalmente en la validez:

  • Los invariantes de seguridad son definidos por el usuario, rara vez formalizados y pueden ser arbitrariamente complicados, por lo que hay pocas esperanzas de razonar genéricamente sobre ellos o el compilador / lenguaje que ayude a mantener un invariante de seguridad en particular.
  • Ocasionalmente puede ser necesario romper el invariante de seguridad (internamente dentro de una biblioteca de sonidos), por lo que incluso si pudiéramos descartar mecánicamente mem::zeroed::<T>() función del invariante de seguridad de T , es posible que no queramos hacerlo.
  • De manera relacionada, las consecuencias de los invariantes de validez rota son de alguna manera peores que un invariante de seguridad roto (menos posibilidades de depurarlo porque el infierno se desata inmediatamente y, a menudo, el comportamiento real resultante de la UB es menos comprensible porque todo el compilador y optimizador factores en él, mientras que el invariante de seguridad solo es explotado directamente por el código en el mismo módulo / caja).

Después de leer el comentario de @jethrogb , acepto que mem::zeroed no debería quedar obsoleto con la introducción de MaybeUninit .

@jethrogb Pequeña liendre:

cualquier matriz de cualquier tipo en esta lista
cualquier estructura donde cualquier campo sea un tipo en esta lista.

No estoy seguro de si se trata de un error tipográfico simple o una diferencia semántica, pero creo que debe superar estas dos viñetas; no creo que sea necesariamente el caso de que None de, por ejemplo, Option<[&u8; 2]> tiene ceros bit a bit como una representación válida (por ejemplo, podría usar [0, 24601] como la representación del caso None ; solo uno de los valores internos debe tomar una representación de nicho - cc @ eddyb para comprobarme en esto). Dudo que hagamos esto hoy, pero no parece del todo imposible que algo como esto pueda aparecer en el futuro.

@jethrogb

El uso más común de mem :: zeroed hoy en día es para los tipos descritos anteriormente, y esto es perfectamente válido.

¿Existe una fuente para esto?

Por otro lado, hay muchos tipos para los que mem :: zeroed es válido.

También hay una infinidad de casos en los que se puede utilizar incorrectamente.

Entiendo que para aquellos que usan mem::zeroed gran medida y correctamente, retrasar la desaprobación hasta que haya una solución más ergonómica disponible es una alternativa muy atractiva.

Prefiero la compensación de reducir o eliminar la cantidad de usos incorrectos de mem::zeroed incluso si eso conlleva un costo ergonómico temporal. Una desaprobación advierte a los usuarios que lo que están haciendo potencialmente invoca un comportamiento indefinido (particularmente los nuevos usuarios que lo usan por primera vez), y tenemos una solución sólida sobre qué hacer en su lugar, lo que hace que la advertencia sea procesable.

Utilizo MaybeUninit menudo y es menos ergonómico de usar que mem::zeroed y mem::uninitialized , pero no me ha resultado tan poco ergonómico. Si MaybeUninit es tan doloroso como afirman algunos comentarios en esta discusión, entonces una biblioteca y / o RFC para una alternativa segura mem::zeroed aparecerá en poco tiempo (nada bloquea a nadie aquí AFAICT).

Alternativamente, los usuarios pueden ignorar la advertencia y seguir usando mem::zeroed , eso depende de ellos, nunca podemos eliminar mem::zeroed de libcore todos modos.

Pero las personas que usan mem::zeroed gran medida deberían inspeccionar activamente si todos sus usos son correctos de todos modos. Particularmente los que usan mem::zeroed gran medida, los que lo usan en código genérico, los que lo usan como una alternativa "menos aterradora" a mem::uninitialized , etc. Retrasar la desaprobación solo retrasa advertir a los usuarios de lo que están haciendo puede ser un comportamiento indefinido.

@bluss

Al eliminar zeroed, parece que solo se reemplaza por MaybeUninit :: zeroed (). Into_inner () que se convierte en una forma equivalente de escribir lo mismo. No hay ningún cambio práctico. Con valores uninit, en cambio, tenemos el cambio práctico de que todos los datos no inicializados se guarden almacenados en valores del tipo MaybeUninit o unión equivalente.

Esto es cierto cuando hablamos de números enteros, pero una vez que miramos, por ejemplo, los tipos de referencia, mem::zeroed() convierte en un problema.

Sin embargo, estoy de acuerdo en que es mucho más probable que la gente se dé cuenta de que mem::zeroed::<&T>() es un problema que la gente que se dé cuenta de que mem::uninitialized::<bool>() es un problema. Entonces, tal vez tenga sentido mantener mem::zeroed() .

Sin embargo, observe que aún podemos decidir que mem::uninitialized::<u32>() está bien - si permitimos bits no inicializados en tipos enteros, mem::uninitialized() vuelve válido para casi todos los "tipos POD". No creo que debamos permitir esto, pero aún tenemos que tener esta discusión.

El número de tipos para los que mem :: uninitialized es válido es muy pequeño (de hecho, ¿son solo tipos de tamaño cero?), Y nadie escribiría código que lo haga (por ejemplo, para ZSTs, solo usarías el tipo constructor).

FWIW, algunos códigos de iteradores de sectores tienen que crear un ZST en código genérico sin poder escribir un constructor de tipos. Utiliza mem::zeroed() / MaybeUninit::zeroed().into_inner() para eso.

mem::zeroed() es útil para ciertos casos de FFI donde se espera que ponga a cero un valor con memset(&x, 0, sizeof(x)) antes de llamar a una función C. Creo que esta es una razón suficiente para mantenerlo sin prejuicios.

@Amanieu Eso parece innecesario. La construcción de Rust que coincide con memset es write_bytes .

mem :: zeroed () es útil para ciertos casos de FFI

Además, la última vez que verifiqué, mem::zeroed fue la forma idiomática de inicializar estructuras libc con campos privados o dependientes de la plataforma.

@RalfJung El código completo en cuestión suele ser Type x; memset(&x, 0, sizeof(x)); y la primera parte no tiene un gran equivalente de Rust. Usar MaybeUninit para este patrón es mucho ruido de línea (y mucho peor codegen sin optimizaciones) cuando la memoria nunca es realmente inválida después de memset .

Tengo una pregunta sobre el diseño de MaybeUninit : ¿Hay alguna forma de escribir en un solo campo de T contenido dentro de un MaybeUninit<T> modo que con el tiempo pueda escribir todos los campos y terminan con un tipo válido / inicializado?

Supongamos que tenemos una estructura como la siguiente:

// Let us suppose that Foo can in principle be any struct containing arbitrary types
struct Foo {bar: bool, baz: String}

¿Generar una referencia de & mut Foo y luego escribir en ella activa UB?

main () {
    let uninit_foo = MaybeUninitilized::<Foo>::uninitialized();
    unsafe { *uninit_foo.get_mut().bar = true; }
    unsafe { *uninit_foo.get_mut().baz = "hello world".to_owned(); }
}

¿El uso de un puntero sin formato en lugar de una referencia evita este problema?

main () {
    let uninit_foo = MaybeUninitilized::<Foo>::uninitialized();
    unsafe { *uninit_foo.as_mut_pointer().bar = true; }
    unsafe { *uninit_foo.as_mut_pointer().baz = "hello world".to_owned(); }
}

¿O hay alguna otra forma de implementar este patrón sin activar UB? Intuitivamente, me parece que mientras no esté leyendo memoria no inicializada / inválida, entonces todo debería estar bien, pero varios de los comentarios en este hilo me llevan a dudar de eso.

Mi caso de uso para esta funcionalidad sería un patrón de constructor en el lugar para tipos donde algunos de los campos deben ser especificados por el usuario (y no tienen un valor predeterminado sensato), pero algunos de los campos tienen un valor predeterminado valor.

¿Hay alguna forma de escribir en un solo campo de la T contenido dentro de MaybeUninit?de modo que, con el tiempo, pueda escribir en todos los campos y terminar con un tipo válido / inicializado?

Si. Utilizar

ptr::write(&mut *(uninit.as_mut_ptr()).bar, val1);
ptr::write(&mut *(uninit.as_mut_ptr()).baz, val2);
...

No debe usar get_mut() para esto, es por eso que los documentos para get_mut dicen que el valor debe inicializarse antes de llamar a este método. Podríamos relajar esa regla en el futuro, que se está discutiendo en https://github.com/rust-rfcs/unsafe-code-guidelines/.

@RalfJung ¿No se *(uninit.as_mut_ptr()).bar = val1; eliminar el valor anterior en bar , que podría no estar inicializado? Creo que es necesario hacer

ptr::write(&mut (*uninit.as_mut_ptr()).bar, val1);

@scottjmaddox ah, cierto. Me olvidé de Drop . Actualizaré la publicación.

¿De qué manera esta variante de escritura en campos no inicializados exhibe un comportamiento menos indefinido que get_mut() ? En el punto del código donde se evalúa el primer argumento de ptr::write , el código ha creado un &mut _ en el campo interno que debería ser tan indefinido como la referencia a toda la estructura que de otro modo sería creado. ¿No debería permitirse al compilador asumir que esto ya está en estado inicializado?

¿No necesitaría eso un nuevo método de proyección de puntero que no requiera intermedios &mut _ expuestos?


Ejemplo un poco interesante:

pub struct A { inner: bool }

pub fn init(mut uninit: MaybeUninit<A>) -> A {
    unsafe {
        let mut previous: [u8; std::mem::size_of::<bool>()] = [0];

        {
            // Doesn't the temorary reference assert inner was in valid state before?
            let inner_ptr: *mut _ = &mut (*uninit.as_mut_ptr()).inner;
            ptr::copy(inner_ptr as *const [u8; 1], (&mut previous) as *mut _, 1);

            // With the assert below, couldn't the compiler drop this?
            std::ptr::write(inner_ptr, true);
        }

        // Assert Inner wasn't false before, so it must have been true already!
        assert!(previous[0] != 0);

        // initialized all fields, good to proceed.
        uninit.into_inner()
    }
}

Pero si el compilador puede suponer que &mut _ es una representación válida, puede simplemente desecharla en ptr::write ? Si superamos la aserción, el contenido no era 0 pero el único otro bool válido es true/1 . Por lo tanto, podría suponer que esto es una operación no operativa si superamos la afirmación. Dado que no se accede al valor antes, después de reordenar podríamos terminar con esto? No parece que llvm explote esto en este momento, pero no estoy muy seguro de que esto esté garantizado.


Si, en cambio, creamos nuestro propio MaybeUninit dentro de la función, obtenemos una realidad ligeramente diferente. En el patio de recreo , en cambio, descubrimos que asume que la aserción nunca puede activarse, presumiblemente ya que asume que str::ptr::write es la única escritura en inner lo que debe haber sucedido antes de leer de previous ? De todos modos, esto parece un poco sospechoso. Para apoyar esta teoría, observe lo que sucede cuando cambia la escritura del puntero a false lugar.


Me doy cuenta de que este problema de seguimiento puede no ser el mejor lugar para esta pregunta.

@RalfJung @scottjmaddox Gracias por sus respuestas. Estos matices son exactamente la razón por la que pregunté.
@HeroicKatora Sí, me preguntaba sobre eso.

¿Quizás el encantamiento correcto es este?

struct Foo {bar: bool, baz: String}

fn main () {
    let mut uninit_foo = MaybeUninit::<Foo>::uninitialized();
    unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).bar) as *mut bool, true); }
    unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).baz) as *mut String, "".to_string()); }
}

( patio de recreo )

Leí un comentario en Reddit (que desafortunadamente ya no puedo encontrar) que sugirió que lanzar inmediatamente una referencia a un puntero ( &mut foo as *mut T ) en realidad se compila para simplemente crear un puntero. Sin embargo, el bit *uninit_foo.as_mut_ptr() me preocupa. ¿Está bien eliminar la referencia del puntero a la memoria unitaria de esta manera? En realidad, no estamos leyendo nada, pero no me queda claro si el compilador lo sabe.

Supuse que la variante unaligned de ptr::write podría ser necesaria para un código genérico superior a MaybeUninit<T> ya que no todos los tipos tendrán campos alineados.

No es necesario write_unaligned . El compilador maneja la alineación de campos por usted. Y el as *mut bool tampoco debería ser necesario, ya que el compilador puede inferir que necesita convertir el &mut en un *mut . Creo que esta coerción inferida es la razón por la que es segura / válida. Si quieres ser explícito y hacer as *mut _ , eso también debería estar bien. Si desea guardar el puntero en una variable, entonces es necesario convertirlo en un puntero.

@scottjmaddox ¿ ptr::write sigue siendo seguro incluso si la estructura es #[repr(packed)] ? ptr::write dice que el puntero debe estar alineado correctamente, por lo que supongo que se requiere ptr::write_unaligned en los casos en que está escribiendo un código genérico que necesita manejar representaciones empaquetadas (aunque para ser honesto, no estoy seguro Puedo pensar en un ejemplo de "código genérico sobre MaybeUninit<T> " que no sabría si el campo estaba alineado correctamente o no).

@nicoburns

lo que sugirió que lanzar inmediatamente una referencia a un puntero (& mut foo como * mut T) en realidad se compila simplemente para crear un puntero.

Lo que compila es distinto de la semántica que el compilador puede usar para realizar esta compilación. Incluso si es una operación no operativa en IR, aún puede tener un efecto semántico, como afirmar suposiciones adicionales al compilador. @scottjmaddox tiene razón en qué operaciones están en juego aquí, pero la parte crítica de la pregunta es la creación de la referencia mutable que ocurre antes e independientemente de la coerción ref-to-ptr. Entonces @mjbshaw es técnicamente correcto acerca de la seguridad general que requiere ptr::write_unaligned cuando el argumento es un argumento genérico desconocido.

No recuerdo dónde leí esto (¿nomicon? ¿Una de las publicaciones del blog de

¿De qué manera esta variante de escritura en campos no inicializados exhibe un comportamiento menos indefinido que get_mut ()? En el punto del código donde se evalúa el primer argumento de ptr :: write, el código ha creado un & mut _ en el campo interno que debería ser tan indefinido como la referencia a toda la estructura que de otro modo se crearía. ¿No debería permitirse al compilador asumir que esto ya está en estado inicializado?

¡Muy buena pregunta! Estas preocupaciones son una de las razones por las que abrí https://github.com/rust-lang/rfcs/pull/2582. Con ese RFC aceptado, el código que mostré no crea un &mut , crea un *mut .

@mjbshaw Touché. Sí, supongo que tienes razón sobre la posibilidad de que la estructura esté empaquetada y, por lo tanto, necesite ptr::write_unaligned . No lo había considerado antes, principalmente porque todavía no he usado estructuras empaquetadas en óxido. Esto probablemente debería ser una pelusa recortada, si aún no lo está.

Editar: no vi una pelusa clippy relevante, así que envié un problema: https://github.com/rust-lang/rust-clippy/issues/3659

Abrí un PR para cancelar mem::zeroed : https://github.com/rust-lang/rust/pull/57825

Abrí un problema en el repositorio de RFC para bifurcar la discusión sobre la puesta a cero segura de la memoria, de modo que podamos desaprobar mem::zeroed en algún momento una vez que tengamos una mejor solución a ese problema: https://github.com / rust-lang / rfcs / issues / 2626

¿Sería posible estabilizar const uninitialized , as_ptr y
as_mut_ptr por delante del resto de la API? Me parece muy probable que estos
se estabilizarán como están ahora. Además, el resto de la API se puede construir
arriba de as_ptr y as_mut_ptr , por lo que una vez estabilizado sería posible
tener un rasgo MaybeUninitExt en crates.io que proporciona, en estable, la API
que actualmente se está discutiendo permitir que más personas (por ejemplo, usuarios solo estables)
dar su opinión al respecto.

En incrustado, en lugar de un asignador global (inestable), usamos variables estáticas,
mucho Sin MaybeUninit no hay forma de tener memoria no inicializada en
variables estáticas en estable. Esto nos impide colocar capacidad fija
colecciones en variables estáticas e inicializando variables estáticas en tiempo de ejecución, en
costo cero. La estabilización de este subconjunto de la API desbloqueará estos casos de uso.

Para darle una idea de lo importante que es esto para la comunidad integrada, hicimos
[una encuesta] preguntando a la comunidad sobre sus puntos débiles y sus necesidades. Estabilizador
MaybeUninit resultó ser la segunda cosa más solicitada para estabilizar (detrás
const fn con límites de rasgos) y, en general, terminó en el séptimo lugar de docenas de
solicitudes relacionadas con rust-lang / *. Después de una mayor deliberación dentro del grupo de trabajo, chocamos
su prioridad para, en general, el tercer lugar debido a su impacto esperado en el ecosistema.

(En una nota más personal, soy el autor de un marco de concurrencia integrado
que se beneficiaría de usar internamente MaybeUninit (uso de memoria en
las aplicaciones se podrían reducir en un 10-50% sin cambios en el código de usuario). yo
podría proporcionar una función de carga solo por la noche para esto, pero después de años de
solo por la noche incrustado y solo recientemente lo hizo estable, siento que
proporcionar una función solo por la noche sería un mensaje incorrecto para enviar a mis usuarios
así que estoy esperando ansiosamente que esta API se estabilice).

@japaric Eso ciertamente evitaría las discusiones de nombres alrededor de into_inner y amigos. Sin embargo, todavía me preocupa la discusión semántica, por ejemplo, sobre las personas que hacen let r = &mut *foo.as_mut_ptr(); y, por lo tanto, afirman que tienen una referencia válida, mientras que aún no estamos seguros de cuáles son los requisitos de validez para las referencias, es decir, estamos aún no estoy seguro de si tener una referencia a datos no válidos es insta-UB. Para un ejemplo concreto:

let x: MaybeUninit<!> = MaybeUninit::uninitialized();
let r: &! = &*x.as_ptr() // is this UB?

Esta discusión comenzó recientemente en el UCG WG.

Mi esperanza era que pudiera estabilizar MaybeUninit en un "paquete" único y coherente con una historia adecuada para los datos no inicializados, de modo que la gente solo tenga que volver a aprender estas cosas una vez, en lugar de publicarlas por partes. pieza y tal vez tener que cambiar algunas de las reglas en el camino. Pero tal vez no sea una buena idea, ¿y es más importante que obtengamos algo para mejorar el status quo?

Pero de cualquier manera, creo que no deberíamos estabilizar nada antes de que aceptemos https://github.com/rust-lang/rfcs/pull/2582 , para que podamos al menos decirle a la gente con certeza que lo siguiente no es UB:

let x: MaybeUninit<(!, u32)> = MaybeUninit::uninitialized();
let r1: *const ! = &(*x.as_ptr()).1; // immediately coerced to raw ptr, no UB
let r2 = &(*x.as_ptr()).1 as *const !; // immediately cast to raw ptr, no UB

(Tenga en cuenta que, como de costumbre, ! es una pista falsa aquí, y todos los ejemplos en esta publicación son iguales, en términos de UB, si usamos bool lugar).

Mi esperanza era que pudiera estabilizar MaybeUninit en un "paquete" único y coherente con una historia adecuada para los datos no inicializados, de modo que las personas solo tengan que volver a aprender estas cosas una vez, en lugar de publicarlas pieza por pieza y tal vez tener que hacerlo. cambie algunas de las reglas en el camino.

Encuentro este argumento muy convincente.

Creo que la necesidad más inmediata es tener un mensaje claro sobre cómo manejar la memoria no inicializada sin UB. Si eso es simplemente "usar punteros sin procesar y ptr::read_unaligned y ptr::write_unaligned ", entonces está bien, pero definitivamente necesitamos una forma bien definida de obtener punteros sin procesar a valores de pila no inicializados y a campos de estructura / tupla . rust-lang / rfcs # 2582 (más algo de documentación) parece satisfacer las necesidades inmediatas, mientras que MaybeUninit no.

@scottjmaddox ¿cómo es ese RFC pero sin MaybeUninit nada bueno para la memoria no inicializada (pila)?

@RalfJung Supongo que depende de si lo siguiente es UB o no:

let x: bool = mem::uninitialized();
ptr::write(&x as *mut bool, false);
assert_eq!(x, false);

Mi suposición implícita era que rust-lang / rfcs # 2582 haría que el ejemplo anterior fuera válido y bien definido. No es ese el caso?

@scottjmaddox

let x: bool = mem::uninitialized();

Esto es UB. No tiene nada que ver con referencias.

Mi suposición implícita era que rust-lang / rfcs # 2582 haría que el ejemplo anterior fuera válido y bien definido.

Estoy completamente sorprendido por esto. Ese RFC se trata solo de referencias. ¿Por qué asume que cambia algo sobre los valores booleanos?

@RalfJung

Esto es UB. No tiene nada que ver con referencias.

La documentación de mem :: uninitialized () dice:

Omite las comprobaciones normales de inicialización de memoria de Rust pretendiendo producir un valor de tipo T , sin hacer nada en absoluto.

La documentación no dice nada sobre T* .

@kpp ¿Qué intentas decir? No hay * y no & en esa línea de código:

let x: bool = mem::uninitialized();

¿Por qué afirma que esta línea es UB?

Porque un bool siempre debe ser true o false , y este no lo es. Consulte también https://github.com/rust-rfcs/unsafe-code-guidelines/blob/master/reference/src/glossary.md#validity -and-safety-invariant.

@kpp para que esa declaración tenga un comportamiento definido mem::uninitialized necesitaría materializar un _valid_ bool .

En todas las plataformas actualmente admitidas, bool tiene solo dos valores _valid_, true (patrón de bits: 0x1 ) y false (patrón de bits: 0x0 ).

mem::uninitialized , sin embargo, produce un patrón de bits donde todos los bits tienen el valor uninitialized . Este patrón de bits no es 0x0 ni 0x1 , por lo tanto, el bool resultante es _invalido_ y el comportamiento no está definido.

Para definir el comportamiento, necesitaríamos cambiar la definición de bool para admitir tres valores válidos: true , false o uninitialized . Sin embargo, no podemos hacer eso, porque T-lang y T-compiler ya RFC dijeron que bool es idéntico al _Bool C y no podemos romper esa garantía (esto permite bool para usarse de manera portátil en C FFI).

Podría decirse que C no tiene exactamente la misma definición de validez que Rust, pero las "representaciones de trampas" de C se acercan mucho. En pocas palabras, no hay mucho que se pueda hacer en C con un _Bool cuyo valor no representa true o false sin invocar un comportamiento indefinido.

Si está en lo correcto, entonces el siguiente código de seguridad también debe ser UB:

let x: bool;
x = true;

Que obviamente no lo es.

Si está en lo correcto, el siguiente código seguro también debe ser UB:

let x: bool; no inicializa x a un uninitialized patrón de bits, no inicializa x en absoluto. El x = true; inicializa x (nota: si no inicializa x antes de usarlo, obtendrá un error de compilación).

Esto es diferente del comportamiento de C, donde, según el contexto, _Bool x; inicializa x a un valor _indeterminado_.

No, allí el compilador sabe que x no está inicializado.

El problema con mem::uninitialized es que está inicializando una variable, en lo que respecta al seguimiento de inicialización del compilador.

let x: bool; sí solo ni siquiera reserva ningún espacio para almacenar x , solo reserva un nombre. let x = foo; reserva algo de espacio y lo inicializa usando foo . let x: bool = mem::uninitialized(); reserva 1 byte de espacio para x pero lo deja sin inicializar, y eso es un problema.

Esta es una manera tan fácil de disparar la API diseñada para su pierna que debe documentarse tanto en mem :: uninitialized como en intrinsics :: uninit con una especialización para mem :: uninitializedentrar en pánico durante la compilación.

¿También significa que inicializar cualquier estructura con un bool con mem :: uninitialized también es UB?

@kpp

¿También significa que inicializar cualquier estructura con un bool con mem :: uninitialized también es UB?

Sí, como probablemente esté descubriendo, mem::uninitialized hace que sea trivial dispararse en el pie, iría tan lejos como para decir que es casi imposible usarlo correctamente. Es por eso que estamos tratando de desaprobarlo en favor de MaybeUninit , que es un poco más detallado de usar, pero tiene la ventaja de que, debido a que es una unión, puede inicializar valores "por partes" sin materializar realmente el valor en sí mismo en un estado _invalid_. El valor solo tiene que ser completamente _valido_ para cuando uno llama into_inner() .

Quizás le interese leer las secciones del nomicon sobre inicialización (des) marcada y sin marcar: https://doc.rust-lang.org/nomicon/checked-uninit.html Cubren cómo la inicialización let x: bool; funciona en óxido seguro. Por favor, complete los problemas si la explicación no es clara o si hay algo que no comprende. También tenga en cuenta que la mayoría de las explicaciones son "no normativas" ya que aún no han pasado por el proceso de RFC. El Grupo de trabajo sobre directrices sobre códigos inseguros intentará presentar un RFC que documente y garantice el comportamiento actual en algún momento de este año.

Esta es una manera tan fácil de disparar la API diseñada para su pierna que debe documentarse tanto en mem :: uninitialized como en intrinsics :: uninit

El problema es que actualmente no hay una forma correcta de hacer esto, por lo que estamos trabajando arduamente para estabilizar MaybeUninit para que estas funciones puedan tener su documentación reemplazada por un gordo "DO NOT USE".


Discusiones como esta y temas como este me hacen estar cada vez más de acuerdo con @japaric en que deberíamos sacar algo de la puerta lo antes posible. Básicamente, necesitamos esta y esta lista de casillas de verificación para marcarlas, diría yo. Entonces tenemos suficiente juntos para proporcionar algunos patrones básicos.

¿Sería posible estabilizar const sin inicializar, as_ptr y
as_mut_ptr por delante del resto de la API? Me parece muy probable que estos
se estabilizarán como están ahora.

+1 para esto. Sería genial tener esta funcionalidad disponible en estable. Permitiría a las personas experimentar con una variedad de diferentes API de nivel superior (y potencialmente seguras) además de esta básica de bajo nivel. Y parece que este aspecto de la API es bastante poco controvertido.

Además, me gustaría sugerir que get_ref y get_mut nunca se estabilicen y se eliminen por completo. Normalmente, trabajar con referencias es más seguro que trabajar con punteros sin procesar (y, por lo tanto, las personas pueden verse tentadas a usar estos métodos en as_ptr y as_mut_ptr aunque estén marcados como inseguros), pero en este caso son estrictamente más peligrosos que los métodos de puntero sin procesar, ya que pueden causar UB, mientras que los métodos de puntero no pueden.

Si la regla es "nunca se creó una referencia a la memoria no inicializada", entonces creo que deberíamos ayudar a las personas a cumplir con esta regla haciendo que solo sea posible crear dicha referencia al hacerlo explícitamente, en lugar de tener un método auxiliar que lo haga internamente. .

Suponiendo https://github.com/rust-lang/rfcs/pull/2582 , estamos completamente seguros de que (1) ni siquiera es UB aunque (2) lo sea, y (1) también contiene una eliminación de vallas de un puntero que apunta a la memoria no inicializada?

(1) unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).bar) as *mut bool, true); }
(2) let x: bool = mem::uninitialized();

Y si es así, ¿cuál es la lógica detrás de eso (con suerte, podemos poner parte de la discusión sobre este tema en la documentación de MaybeUninit)? Supongo que en (1) el valor desreferenciado siempre permanece como un "rvalue" y nunca se convierte en un "lvalue", mientras que en (2) el bool inválido se convierte en un "lvalue" y, por lo tanto, debe materializarse en la memoria. (No estoy muy seguro de cuál es el término correcto para esto en Rust, pero he visto estos términos usados ​​para C ++).

¿Y otros piensan que valdría la pena crear un RFC para la sintaxis de acceso al campo en punteros sin procesar que evalúe directamente en un puntero sin procesar al campo para evitar esta confusión en primer lugar?

Si la regla es "nunca se creó una referencia a la memoria no inicializada"

No creo que esa deba ser la regla, pero podría serlo. Se está discutiendo ahora mismo en la UCG.

¿Estamos completamente seguros de que (1) ni siquiera es UB aunque (2) lo sea, y (1) también contiene un puntero que apunta a una memoria no inicializada?

¡Buena pregunta! Pero sí, lo estamos - básicamente por necesidad de corte. Piense en &mut foo as *mut bool como &raw mut foo , una expresión atómica de tipo *mut bool . Aquí no hay ninguna referencia, solo un ptr sin procesar para la memoria no inicializada, y eso definitivamente está bien.

let x: bool = mem::uninitialized();

Esto es UB. No tiene nada que ver con referencias.

Mi suposición implícita era que rust-lang / rfcs # 2582 haría que el ejemplo anterior fuera válido y bien definido.

Estoy completamente sorprendido por esto. Ese RFC se trata solo de referencias. ¿Por qué asume que cambia algo sobre los valores booleanos?

@RalfJung Supongo que pensé que no era UB porque el valor indefinido no era observable porque se sobrescribió inmediatamente con un valor bool válido. Pero supongo que ese no es el caso.

Para ejemplos más complicados, en los que el valor en x implementa Drop, se requeriría un puntero sin procesar para sobrescribir el valor, y es por eso que pensé que rfc 2582 era necesario para evitar UB.

Supongo que pensé que no era UB porque el valor indefinido no era observable porque se sobrescribió inmediatamente con un valor bool válido. Pero supongo que ese no es el caso.

La semántica procede enunciado por enunciado (mirando el MIR). Cada declaración debe tener sentido. let x: bool = mem::uninitialized(); materializa un booleano malo, y no importa lo que suceda después, no debes materializar un booleano malo.

Entiendo que el valor de x no es válido, pero ¿eso requiere un comportamiento indefinido? Puedo ver cómo podría, en general, sacarse de contexto. Pero en el contexto de ese ejemplo en particular, ¿el comportamiento no está bien definido? Supongo que mi problema básico es que no entiendo completamente el significado de "comportamiento indefinido".

Queremos que el compilador pueda confiar en ciertos invariantes. Estos son invariantes solo si siempre se mantienen. Una vez que comenzamos a agregar excepciones, se convierte en un desastre.

Tal vez esté esperando algo más de la forma " inspeccionar un valor requiere que se mantenga la invariante de validez". Aquí, "inspeccionar" un bool lo estaría usando en un if . Esa es una especificación razonable, pero menos útil: ahora el compilador tiene que probar que el valor está realmente "inspeccionado" antes de que pueda asumir el invariante.

¿Eso requiere un comportamiento indefinido?

Elegimos lo que es y no es un comportamiento indefinido. Eso es parte del diseño de un lenguaje. El comportamiento indefinido casi nunca es "necesario" per se, pero es necesario permitir más optimizaciones. Así que el arte aquí es encontrar una definición de comportamiento indefinido (por más contradictorio que pueda parecer ^^) que permita las optimizaciones deseadas y se ajuste a las expectativas (inseguras) de los programadores.

No entiendo completamente el significado de "comportamiento indefinido".

Escribí una publicación de blog sobre eso , pero la respuesta corta es que el comportamiento indefinido es un contrato entre usted y el compilador , y el contrato dice que es su obligación asegurarse de que no ocurra ningún comportamiento indefinido. Es una obligación de prueba. "Desreferenciar un puntero NULL es UB" equivale a decir "cada vez que se desreferencia un puntero, se espera que el programador demuestre que este puntero no puede ser NULL". Esto ayuda al compilador a entender el código, porque cada vez que se desreferencia un puntero, el compilador ahora puede deducir "¡ajá! Aquí el programador demostró que el puntero no es NULL, por lo que puedo usar esa información para optimizaciones y generación de código. Gracias , programador! "

Lo que dice exactamente el contrato depende del lenguaje de programación. Existen limitaciones, por supuesto (por ejemplo, estamos limitados por LLVM). En nuestro caso, la UCG cree (de acuerdo con lo que escuchamos de los equipos de lenguaje y compilador) que queremos que el contrato contenga la siguiente cláusula: "Cada vez que se crea un rvalue, el programador tiene que demostrar que este rvalue siempre satisfacer la validez invariante ". No existe una ley de física o de computadoras que nos obligue a incluir esta cláusula en el contrato, pero se considera un compromiso razonable entre muchas opciones diferentes.

En particular, ya emitimos información para LLVM que no podríamos emitir legítimamente con un contrato más débil. Podríamos decidir cambiar lo que le decimos a LLVM, por supuesto, pero si la elección es entre "el código inseguro debe usar MaybeUninit siempre que se trate de memoria no inicializada" y " todo el código se puede optimizar menos", lo primero parece como la mejor opción.

Tomando tu ejemplo:

let x: bool = mem::uninitialized();

Este código es UB en rustc hoy. Si observa el LLVM IR (no optimizado) por mem::uninitialized::<bool>() , esto es lo que obtiene:

; core::mem::uninitialized
; Function Attrs: inlinehint nonlazybind uwtable
define zeroext i1 @_ZN4core3mem13uninitialized17h6c99c480737239c2E() unnamed_addr #0 !dbg !5 {
start:
  %tmp_ret = alloca i8, align 1
  %0 = load i8, i8* %tmp_ret, align 1, !dbg !14, !range !15
  %1 = trunc i8 %0 to i1, !dbg !14
  br label %bb1, !dbg !14

bb1:                                              ; preds = %start
  ret i1 %1, !dbg !16
}
; snip
!15 = !{i8 0, i8 2}

Básicamente, esta función asigna 1 byte en la pila y luego carga ese byte. Sin embargo, la carga está marcada con !range , lo que le dice a LLVM que el byte debe estar entre 0 <= x <2, es decir, solo puede ser 0 o 1. LLVM asumirá que esto es cierto y el comportamiento no está definido si se viola esta restricción.

En resumen, el problema no son tanto las variables no inicializadas en sí mismas, sino el hecho de que está copiando y moviendo valores que violan sus restricciones de tipo.

¡Gracias a ambos por la exposición! ¡Ahora está mucho más claro!

Supongo que mi problema básico es que no entiendo completamente el significado de "comportamiento indefinido".

Esta serie de publicaciones de blog (que tiene ejemplos bastante interesantes / aterradores en la segunda publicación) es bastante útil, creo: http://blog.llvm.org/2011/05/what-every-c-programmer-should-know .html

Siento que esto realmente necesita una buena documentación. El cambio aquí es probablemente algo bueno por varias razones que puedo enumerar y probablemente otras que no puedo. Pero el uso correcto de la memoria no inicializada (y otros usos de la inseguridad) puede ser notablemente contrario a la intuición. El Nomicon tiene una sección sobre uninitialized (que presumiblemente se actualizaría para hablar sobre este tipo), pero no parece expresar toda la complejidad del problema.

(No es que sea voluntario para escribir esa documentación. Nomino ... a quien sepa más sobre esto que yo).

Una idea interesante de https://github.com/rust-lang/rust/issues/55422#issuecomment -433943803: Podríamos convertir métodos como into_inner en funciones, de modo que tenga que escribir MaybeUninit::into_inner(foo) lugar de foo.into_inner() - eso documenta mucho más claramente lo que está sucediendo.

En https://github.com/rust-lang/rust/pull/58129 estoy agregando algunos documentos, devuelvo un &mut T de set y cambiando el nombre de into_inner a into_initialized .

Creo que después de esto, y una vez que se resuelva https://github.com/rust-lang/rust/pull/56138 , podríamos proceder con la estabilización de partes de la API (los constructores, as_ptr , as_mut_ptr , set , into_initialized ).

¿Por qué MaybeUninit::zeroed() es const fn ? ( MaybeUninit::uninitialized() es un const fn )

EDITAR: ¿se puede realmente convertir en const fn usando Rust nocturno?

¿Por qué MaybeUninit::zeroed() es const fn ? ( MaybeUninit::uninitialized() es un const fn )

@gnzlbg Lo intenté , pero requiere uno de los siguientes:

Lo único que me preocupa más acerca de pasar pronto a la estabilización es la falta total de retroalimentación de las personas que realmente usan este tipo. Parece que todo el mundo está esperando que esto se estabilice antes de empezar a usarlo. Eso es un problema, porque significa que notaremos problemas de API demasiado tarde.

@ rust-lang / libs ¿cuáles son las condiciones habituales en las que utilizará una función en lugar de un método? Me pregunto si algunas de las operaciones aquí deberían ser funciones para que la gente tenga que escribir, por ejemplo, MaybeUninit::as_ptr(...) . Me preocupa que esto haga explotar el código para que se vuelva ilegible, pero OTOH, algunas funciones en ManuallyDrop hicieron exactamente esto.

@RalfJung Tengo entendido que los métodos se evitan en cosas que se reducen a parámetros genéricos, para evitar ocultar métodos del tipo de usuario, por lo tanto ManuallyDrop::take .

Dado que MaybeUninit<T> nunca será Deref<Target = T> , creo que los métodos son apropiados aquí.

Solicite comentarios y los recibirá. Usé MaybeUninit para implementar una nueva funcionalidad en std recientemente.

  1. En sys / sgx / ext / arch.rs lo uso en combinación con el ensamblaje en línea. De hecho, usé get_mut incorrectamente, las referencias de pensamiento y los punteros sin procesar serían equivalentes (corregidos en 928efca1). Ya estaba en un bloque inseguro, así que al principio no noté la diferencia.
  2. En sys / sgx / rwlock.rs , lo estoy usando para asegurarme de que el patrón de bits de un const fn new() sea ​​el mismo que un inicializador de matriz en un archivo de encabezado C. Estoy usando zeroed seguido de set para intentar asegurarme de que los bits "no me importa" sean 0. No sé si este es el uso correcto, pero parece funcionar bien .
  1. Estaría muy confundido si out.get_mut() as *mut _ ! = out.as_mut_ptr() . Realmente parece C ++ ish. Espero que se solucione de alguna manera.

¿Cuál es el punto de get_mut() ?

Una cosa que me preguntaba recientemente era si se garantizaba que MaybeUninit<T> tuviera el mismo diseño que T , y si algo así podría usarse para inicializar parcialmente los valores en el montón y luego convertirlo en un valor inicializado, por ejemplo, algo como ( campo de juego completo )

struct Foo {
    x: i32,
}

let mut partial: Box<MaybeUninit<Foo>> = Box::new(MaybeUninit::uninitialized());
let complete: Box<Foo> = unsafe {
    ptr::write(&mut (*partial.as_mut_ptr()).x, 5);
    mem::transmute(partial)
};

según Miri, este ejemplo funciona (aunque ahora me doy cuenta de que no sé si transmutar cuadros de tipos con diseño idéntico es en sí mismo correcto).

@ Nemo157 ¿por qué necesita el mismo diseño de memoria cuando tiene into_inner ?

@Pzixel para evitar copiar el valor después de la inicialización, imagine que contiene un búfer de 100 MB que provocará un desbordamiento de la pila si se asigna en la pila. Aunque, al escribir un caso de prueba , parece que esto requiere una API adicional fn uninit_boxed<T>() -> Box<MaybeUninit<T>> para permitir asignar un cuadro no inicializado sin tocar la pila.

Usando box sintaxis para permitir la asignación del espacio de almacenamiento dinámico no inicializado se puede ver que transmutador como esto funciona, al intentar utilizar into_initialized provoca un desbordamiento de pila: parque infantil

@ Nemo157 ¿ Quizás sea mejor hacer cumplir el compilador para optimizar la copia? Creo que debería hacerlo de todos modos, pero podría haber un atributo para garantizar que las compilaciones lo hagan.

@ Nemo157

Una cosa que me preguntaba recientemente era si se garantizaba que MaybeUninit<T> tuviera el mismo diseño que T , y si algo así podría usarse para inicializar parcialmente los valores en el montón y luego convertirlo en un valor inicializado,

Creo que esto está garantizado y que su código es válido, con un par de advertencias:

  • Dependiendo del tipo que esté usando (y especialmente en el código genérico), es posible que necesite ptr::write_unaligned .
  • Si hay más campos, y solo algunos de ellos están inicializados, no debe transmutar a T hasta que todos los campos estén completamente inicializados .

Este también es un caso de uso que me interesa, ya que creo que podría combinarse con un proc-macro para proporcionar una abstracción de constructor in situ segura.

@Pzixel Si tiene el mismo diseño de memoria, puede evitar copiar toda la estructura de datos una vez que la haya construido. Por supuesto, el compilador puede omitir la copia, y puede que no importe para estructuras pequeñas. Pero definitivamente es bueno tenerlo.

@nicoburns sí, lo veo ahora. Solo estoy hablando de que puede haber algún atributo, por ejemplo, #[same_layout] o #[elide_copying] , o ambos, o algo más, para asegurarse de que funcione de la misma manera que transmute . O tal vez cambie la implementación de into_constructed para evitar copias adicionales. Espero que este sea un comportamiento predeterminado, no solo para los tipos inteligentes que leen los documentos sobre el diseño. Quiero decir, tengo mi código que llama a into_constructed y obtengo una copia adicional, pero @ Nemo157 solo llama a transmute y está bien. No hay ninguna razón por la que into_constructed no pueda hacer lo mismo.

Estaría muy confundido si out.get_mut() as *mut _ ! = out.as_mut_ptr() . Realmente parece C ++ ish. Espero que se solucione de alguna manera.

¿Cuál es el punto de get_mut() ?

Hice un punto similar arriba de que get_mut() y get_ref() son potencialmente confusos / facilitan invocar accidentalmente un comportamiento indefinido (porque dan la ilusión de ser alternativas más seguras a as_ptr() y as_mut_ptr() , pero de hecho son menos seguros que esos métodos).

Creo que no están en el subconjunto de la API que @RalfJung propuso estabilizar (ver: https://www.ralfj.de/blog/2019/02/12/all-hands-recap.html)

@RalfJung Con respecto a su propuesta para un método ptr::freeze() (https://www.ralfj.de/blog/2019/02/12/all-hands-recap.html):

¿Tendría sentido tener un método similar para construir MaybeUninit ? ( MaybeUninit::frozen() , MaybeUninit::abitrary() o similar). Intuitivamente, parece que dicha memoria tendría el mismo rendimiento que la memoria realmente no inicializada para muchos casos de uso, sin tener el costo de escribir en la memoria como zeroed . Quizás incluso podría recomendarse sobre el constructor uninitialized menos que las personas estén realmente seguras de que necesitan memoria no inicializada.

En esa nota, ¿cuáles son los casos de uso en los que realmente necesita memoria "no inicializada" en lugar de memoria "congelada"?

@Pzixel

1. I'd be very confused if `out.get_mut() as *mut _` != `out.as_mut_ptr()`. Looks really C++ish. I hope it would be fixed somehow.

Célebre. La razón por la que algunas personas proponen esto es que puede ser útil declarar &mut ! deshabitado (por ejemplo, tener ese valor es UB). Sin embargo, con MaybeUninit::<!>::uninitiailized().get_mut() , hemos creado ese valor. Es por eso que as_mut_ptr es menos peligroso: evita crear una referencia.

@nicoburns (Tenga en cuenta que freeze no es mi idea, solo fui parte de la discusión y me gusta mucho la propuesta).

Creo que _no_ están en el subconjunto de la API que @RalfJung propuso estabilizar

Correcto. Y de hecho, tal vez no deberíamos tenerlos en absoluto.

¿Tendría sentido tener un método similar para construir MaybeUninit ? ( MaybeUninit::frozen() , MaybeUninit::abitrary() o similar).

¡Si! Iba a proponer agregar esto una vez que un MaybeUninit básico sea estable y ptr::freeze haya aterrizado.

En esa nota, ¿cuáles son los casos de uso en los que realmente necesita memoria "no inicializada" en lugar de memoria "congelada"?

Esto necesita más estudio y evaluación comparativa, la expectativa es que podría costar el rendimiento porque LLVM no hará optimizaciones que de otra manera podría hacer.

(Volveré a los otros comentarios también, una vez que tenga tiempo).

@Pzixel es capaz de construir objetos directamente en la memoria preasignada no es trivial, Rust tenía dos RFC aceptadas para implementar tal cosa (¡hace más de 4 años!), Pero desde entonces no han sido aceptadas y la mayor parte de la implementación se ha eliminado. (excepto la sintaxis box que utilicé anteriormente). Si desea obtener más detalles, el hilo i.rl.o sobre la eliminación sería el mejor lugar para comenzar.

Como @nicoburns menciona, MaybeUninit podría potencialmente usarse como un bloque de construcción para una solución basada en una biblioteca menos ergonómica para el mismo problema, muy útil como una forma de comenzar a experimentar con el concepto y ver qué tipo de API permite la construcción. Eso solo depende de si MaybeUninit puede proporcionar las garantías necesarias para crear una solución de este tipo.

@ Nemo157 Solo sugiero usarlo en un solo lugar, nada para tratar con un caso genérico no trivial.

@jethrogb ¡ Muchas gracias! Entonces, ¿parece que la API funciona bien para usted en este momento?

2. En sys / sgx / rwlock.rs , lo estoy usando para asegurarme de que el patrón de bits de un const fn new() sea ​​el mismo que un inicializador de matriz en un archivo de encabezado C.

Woah, eso es una locura. ^^ Pero supongo que debería funcionar, es un const fn sin argumentos después de todo, así que siempre debería devolver lo mismo ...

Una cosa que me preguntaba recientemente era si se garantizaba que MaybeUninit<T> tuviera el mismo diseño que T , y si algo así podría usarse para inicializar parcialmente los valores en el montón y luego convertirlo en un valor inicializado

En la lista de cosas que deberíamos agregar eventualmente hay algo como

fn into_initialized_box(Box<MaybeUninit<T>>) -> Box<T>

que transmuta el Box .

Pero sí, creo que deberíamos permitir tales transmutaciones. ¿Existe un precedente para decir en los documentos "puedes transmutar esto de la siguiente manera"? Creo que generalmente preferimos agregar métodos de ayuda en lugar de que las personas realicen sus propias transmutaciones.

  • Dependiendo del tipo que esté utilizando (y especialmente en el código genérico), es posible que necesite ptr::write_unaligned .

En código genérico no se puede acceder a los campos. Creo que si puede acceder a los campos, normalmente sabrá si la estructura está empaquetada, y si no lo está, entonces ptr::write es suficientemente bueno. (¡Sin embargo, no uses la asignación porque eso podría caer! Sigo olvidándome de eso ...)

Aunque, al escribir un caso de prueba , parece que esto requiere una API adicional fn uninit_boxed<T>() -> Box<MaybeUninit<T>> para permitir asignar un cuadro no inicializado sin tocar la pila.

Eso es un error , pero dado que ese error puede ser difícil de solucionar, también podría ser una buena idea ofrecer un constructor separado para esto. Sin embargo, no estoy seguro de cómo implementarlo. Y luego probablemente también queremos algo como zeroed_box que evite poner a cero una ranura de pila y luego memcpying, y así sucesivamente ... No me gusta toda esta duplicación. : /

Así que propondría que después / en paralelo a la estabilización inicial, algunas personas que tienen casos de uso para memoria no inicializada en el montón (básicamente, mezclando Box y MaybeUninit ) se reúnan y diseñen el mínimo posible extensión de API para eso. @eddyb también expresó interés en esto. Eso no está realmente relacionado con simplemente desaprobar mem::uninitialized más, así que creo que debería tener su propio lugar para la discusión, fuera de estos problemas de seguimiento (demasiado grandes ya).

Mi propio comentario: en general, estoy contento con MaybeUninit<T> . No tengo grandes quejas. Es menos una pistola que mem::uninitialized , lo cual es bueno. Los métodos const new y uninitialized son buenos. Desearía que más métodos fueran constantes, pero según tengo entendido, muchos de ellos requieren que se haga más progreso en const fn en general antes de que puedan hacerse const .

Me gustaría una garantía más sólida que el "mismo diseño" por T y MaybeUninit<T> . Me gustaría que fueran compatibles con ABI (efectivamente, #[repr(transparent)] , aunque sé que ese atributo no se puede aplicar a las uniones) y FFI-safe (es decir, si T es FFI-safe , entonces MaybeUninit<T> debería ser seguro para FFI). (Tangencialmente, desearía que pudiéramos usar #[repr(transparent)] en uniones que tienen solo un campo de tamaño positivo (como podemos para estructuras))

De hecho, confío en el ABI de MaybeUninit<T> en mi proyecto para ayudar con una optimización (pero no de una manera insegura, así que no se asuste). Me complace entrar en detalles si alguien está interesado, pero mantendré este comentario breve y omitiré los detalles por ahora.

@mjbshaw ¡Gracias!

Ojalá pudiéramos usar #[repr(transparent)] en uniones que solo tienen un campo de tamaño positivo (como podemos hacer para las estructuras).

Una vez que exista ese atributo, agregarlo a MaybeUninit sería una obviedad. Y, de hecho, la lógica para esto ya se ha implementado en rustc ( MaybeUninit<T> de-facto es compatible con ABI con T , pero no lo garantizamos).

Todo lo que se necesita es que alguien escriba un RFC y lo revise, y agregue algunas comprobaciones que aseguren que las uniones repr(transparent) solo tengan un campo que no sea ZST. ¿Le gustaría intentarlo? :RE

Todo lo que se necesita es que alguien escriba un RFC y lo revise, y agregue algunas comprobaciones que aseguren que las uniones repr(transparent) solo tengan un campo que no sea ZST. ¿Le gustaría intentarlo? :RE

@RalfJung ¡ Pide y recibirás!

Cc https://github.com/rust-lang/rust/pull/58468

Esto deja solo la API que creo que podemos estabilizar razonablemente en maybe_uninit , y mueve el resto a puertas de funciones separadas.

Bien, todos los RP preparatorios aterrizaron, y into_inner se fue.

Sin embargo, realmente me gustaría que https://github.com/rust-lang/rfcs/pull/2582 sea ​​aceptado antes de estabilizar, de lo contrario, ni siquiera tenemos una forma de inicializar una estructura campo por campo - y ese parece un caso de uso principal para MaybeUninit . Estamos muy cerca de tener todas las casillas necesarias para que FCP comience.

Acabo de convertir mi código para usar MaybeUninit . Hay bastantes lugares donde podría haber usado un método take que funciona en &mut self lugar de self . Actualmente estoy usando x.as_ptr().read() pero siento que x.take() o x.take_initialized() serían mucho más claros.

@Amanieu Esto se siente muy similar al método into_inner . ¿Quizás podamos intentar evitar la duplicación aquí?

😉

El método take de Option tiene otra semántica. x.as_ptr().read() no cambia el valor interno de x, pero Option::take intenta reemplazar el valor. Podría ser engañoso para mí.

@ qwerty19106 x.as_ptr().read() en un MaybeUninit _semantically_ saca el valor y deja la envoltura sin inicializar nuevamente, simplemente sucede que el valor no inicializado dejado atrás tiene el mismo patrón de bits que el valor que se eliminó .

Actualmente estoy usando x.as_ptr().read() pero siento que x.take() o x.take_initialized() serían mucho más claros.

Encuentro eso curioso, ¿podrías explicar por qué?

En mi opinión, un método similar a take es algo engañoso porque, a diferencia de take y into_initialized , no protege contra la toma dos veces. En realidad, para los tipos Copy (y de hecho para los valores Copy como None as Option<Box<T>> ), ¡tomar dos veces está completamente bien! Entonces, la analogía con take realmente no se sostiene, desde mi perspectiva.

Podríamos llamarlo read_initialized() , pero en ese momento me pregunto seriamente si eso es realmente más claro que as_ptr().read() .

x.as_ptr().read() en un MaybeUninit _ semánticamente_ saca el valor y deja la envoltura sin inicializar nuevamente, simplemente sucede que el valor no inicializado dejado atrás tiene el mismo patrón de bits que el valor que se tomó.

MaybeUninit no tiene realmente un invariante semántico útil, por lo que no estoy seguro de estar completamente de acuerdo con esa afirmación. TBH No estoy convencido de que sea útil considerar las operaciones en MaybeUninit de otra manera que no sea solo su efecto operacional en bruto.

@RalfJung hmm, tal vez "semánticamente" sea la palabra incorrecta aquí. En términos de cómo un usuario debe usar el tipo, debe asumir que el valor no está inicializado nuevamente después de leerlo (a menos que sepa concretamente que el tipo es Copy ).

Si solo observa el efecto operacional en bruto, obtendrá interacciones extrañas como esta en las que puede violar invariantes de seguridad de otras API inseguras sin leer técnicamente la memoria no inicializada. (Tenía la esperanza de que Miri todavía rastreara 0 lecturas de longitud de memoria no inicializada, pero no parece que sea así).

@RalfJung En todos mis casos, esto implica un static mut en el que se coloca un valor y luego se saca. Como no puedo consumir estática, no puedo usar into_uninitialized .

@Amanieu, lo que estaba preguntando es, ¿por qué crees que x.take_initialized() es más claro que x.as_ptr().read() ?

@ Nemo157

Tenía la esperanza de que Miri todavía rastreara 0 lecturas de longitud de memoria no inicializada, pero no parece tan

Una lectura de longitud 0 de memoria no inicializada nunca es UB, entonces, ¿por qué Miri se preocuparía por ellos?

Si solo observa el efecto operacional en bruto, obtendrá interacciones extrañas como esta en las que puede violar invariantes de seguridad de otras API inseguras sin leer técnicamente la memoria no inicializada.

Claro, puede violar invariantes de seguridad sin tener que leer la memoria no inicializada. También puede usar MaybeUninit::zeroed().into_initialized() para eso. No veo el problema.
La "interacción extraña" aquí es que creó dos valores de un tipo que no tenía derecho a crear. Se trata de la invariante de seguridad de Spartacus y no tiene nada que ver con las invariantes de validez.

Es por eso que creo que read_initialized() transmite mejor lo que sucede: leemos los datos y afirmamos que están inicializados correctamente (lo que incluye asegurarnos de que realmente se nos permite crear este valor en ese tipo). Esto no tiene ningún efecto sobre el patrón de bits que aún se almacena en MaybeUninit .

@RalfJung Básicamente estoy tratando MaybeUninit como Option , pero sin la etiqueta. De hecho, anteriormente estaba usando la caja de opciones sin etiquetar exactamente para este propósito, y tiene un método take para extraer el valor de la unión.

@Amanieu @shepmaster Agregué un read_initialized en https://github.com/rust-lang/rust/pull/58660. Sigo pensando que es un nombre mejor que take_initialized . ¿Esto satisface tus necesidades?

Ese PR también agrega ejemplos a algunos de los otros métodos, ¡comentarios bienvenidos!

Estoy contento con read_initialized .

Mientras estaba en eso, también hice MaybeUninit<T>: Copy if T: Copy . No parece una buena razón para no hacer eso.

Hm, ¿quizás get_initialized sería un mejor nombre? Después de todo, complementa set .

¿O tal vez set debería cambiarse de nombre a write ? Eso también lograría coherencia.

He estado convirtiendo mi código para usar MaybeUninit y he descubierto que trabajar con porciones no inicializadas es muy poco ergonómico. Creo que esto podría mejorarse si tuviéramos funciones para lo siguiente:

  • Conversión segura de &mut [T] a &mut [MaybeUninit<T>] . Esto efectivamente permite emular los parámetros &out usando &mut [MaybeUninit<T>] , que es útil, por ejemplo, para read .
  • Conversión insegura de &mut [MaybeUninit<T>] a &mut [T] (y lo mismo para &[T] ), que se utilizará una vez que hayamos llamado .set en cada elemento del segmento.

Las API que tengo se parecen a esto:

// The returned slice is truncated to the number of elements actually read.
fn read<T>(out: &mut [MaybeUninit<T>]) -> Result<&mut [T]>;

Estoy de acuerdo en que trabajar con cortes no es ergonómico actualmente, y es por eso que agregué first_ptr y first_ptr_mut . Pero eso probablemente esté lejos de ser la mejor API.

Sin embargo, preferiría que pudiéramos concentrarnos en enviar primero la "API central" y luego ver la interacción con las porciones (y con Box ).

Me gusta la idea de cambiar el nombre de set a write , proporcionando coherencia con ptr::write .

En la misma línea, ¿ read_initialized realmente mejor que solo read ? Si la preocupación es sobre el uso accidental que se oculta, quizás conviértalo en una función en lugar de un método, es decir, MaybeUninit::read(&mut v) ? Lo mismo se podría hacer con write , es decir, MaybeUninit::write(&mut v) para mantener la coherencia. La compensación en ambos casos es entre usabilidad y explicidad, y si la explicidad se considera mejor en un caso, no veo por qué sería diferente en el otro.

Independientemente, hasta que se resuelvan estas API, apoyo firmemente la estabilización con una API mínima, es decir, new , uninitialized , zeroed , as_ptr , as_mut_ptr , y tal vez get_ref y get_mut .

y tal vez get_ref y get_mut .

Estos solo deberían estabilizarse una vez que resolvamos https://github.com/rust-rfcs/unsafe-code-guidelines/issues/77 , y parece que podría llevar un tiempo ...

estabilizándose con una API mínima, es decir, new , uninitialized , zeroed , as_ptr , as_mut_ptr

Mi plan era que into_initialized , set / write y read_initialized fueran parte de ese conjunto mínimo. ¿Pero tal vez no debería ser así? set / write y read_initialized se pueden implementar fácilmente con el resto, por lo que ahora también me estoy inclinando por no estabilizarlos en el primer lote. Pero tener algo como into_initialized desde el principio es deseable, en mi opinión.

quizás convertirlo en una función en lugar de un método, es decir, MaybeUninit::read(&mut v) ? Lo mismo se podría hacer con write , es decir, MaybeUninit::write(&mut v) para mantener la coherencia.

De lo que se discutió aquí antes, solo usamos el enfoque de función explícita para evitar problemas con instancias Deref . No creo que debamos introducir precedencia por otra razón para usar una función en lugar de un método.

¿Es read_initialized realmente mejor que solo read ?

¡Buena pregunta! No lo sé. Esto fue por simetría con into_initialized . Pero into_inner es un método común en el que uno puede perder la descripción general de qué tipo se llama, read es mucho menos común. ¿Y tal vez debería ser initialized lugar de into_initialized ? Tantas opciones ...

De lo que se discutió aquí antes, solo usamos el enfoque de función explícita para evitar problemas con instancias Deref . No creo que debamos introducir precedencia por otra razón para usar una función en lugar de un método.

Excepto que ptr::read y ptr::write son funciones, no métodos. Entonces, la precedencia ya está establecida a favor de MaybeUninit::read y MaybeUninit::write .

Editar : Bien, aparentemente también hay métodos read y write en punteros ... Nunca los noté antes ... Pero consumen el puntero, lo que realmente no tiene sentido para MaybeUninit .

Tantas opciones ...

Convenido. Hasta que haya mucho más desprendimiento de bicicletas en los otros métodos, creo que solo new , uninitialized , zeroed , as_ptr , as_mut_ptr están realmente listos para la estabilización.

Excepto que ptr::read y ptr::write son funciones, no métodos. Entonces la precedencia ya está establecida

No forman parte de una estructura de datos, por supuesto, son funciones independientes. Y como usted observa, hoy en día también existen como métodos.

Pero consumen el puntero

Los punteros crudos son Copy , por lo que en realidad no se consume nada.

Los punteros crudos son Copy , por lo que en realidad no se consume nada.

Buen punto...

Bueno, v.as_ptr().read() ya es bastante conciso y claro. El as_ptr seguido de read debería hacer que se destaque como algo en lo que pensar detenidamente, mucho más que into_initialized . Personalmente, estoy a favor de exponer solo as_ptr y as_mut_ptr , al menos por ahora. Y new , uninitialized y zeroed , por supuesto.

@Amanieu ¿Qué hay de algo más parecido a lo que tiene Cell , donde hay conversiones seguras por &mut MaybeUninit<[T]> hacia y desde &mut [MaybeUninit<T>] ?

Eso permitiría lo siguiente, lo que me parece bastante natural:

fn read<T>(out: &mut MaybeUninit<[T]>) -> Result<&mut [T]> {
    let split = out.as_mut_slice_of_uninit();
    // ... operate on split ...
    return Some(unsafe { split[0..n].as_uninit_mut_slice().get_mut() })
}

También parece que representa con mayor precisión la semántica para la persona que llama. La función que toma un &mut [MaybeUninit<T>] me parecería que podría tener alguna lógica distintiva para saber cuáles están bien y cuáles no. Tomando &mut MaybeUninit<[T]> , por otro lado expresa que no se va a distinguir entre las celdas cuando se trata de qué datos ya están en ellas.

(Los nombres de los métodos, por supuesto, están sujetos a rotura de bicicletas; simplemente imité lo que hace Cell ).

@eternaleye MaybeUninit<[T]> no es un tipo válido porque las uniones no pueden ser DST.

Mmm, cierto

Hasta que haya mucho más desprendimiento de bicicletas en los otros métodos, creo que solo new , uninitialized , zeroed , as_ptr , as_mut_ptr están realmente listos para la estabilización.

Bueno, creo que deberíamos aceptar este RFC antes de estabilizar cualquier cosa; de lo contrario, ni siquiera tenemos una forma autorizada para inicializar una estructura campo por campo, que parece ser el mínimo.

Entonces, mientras esperamos los experimentos , podemos hablar un poco sobre los nombres de lo que actualmente se llama set , read_initialized y into_initialized . Se han sugerido los siguientes cambios de nombre:

  1. set -> write . La mejor metáfora para .as_ptr().read() parece ser "leer", no "obtener", pero luego el complemento ( .as_ptr_mut().write() ) debería ser "escribir", no "establecer".
  2. read_initialized -> read . Coincide muy bien con write , pero no es seguro. ¿Es eso (más la documentación) una advertencia suficiente de que debe asegurarse manualmente de que los datos ya están inicializados? Hubo mucho acuerdo en que un into_inner inseguro no es suficiente, por lo que le cambié el nombre a into_initialized .
  3. into_initialized -> initialized . Si tenemos read_initialized y into_initialized , eso tiene una buena consistencia IMO, pero si es read , entonces into_initialized sobresale un poco. El nombre del método es bastante largo. Aún así, la mayoría de las operaciones de consumo se llaman into_* , por lo que sé.

¿Alguna objeción para (1)? Y principalmente me apoyo en (3). Para (2) estoy indeciso: read es más fácil de escribir, pero read_initialized IMO funciona mejor cuando se lee dicho código, y el código se lee y revisa con más frecuencia de lo que se escribe. Parece agradable mencionar el lugar donde realmente asumimos que las cosas se inicializarán.

¿Pensamientos, opiniones?

Bueno, creo que deberíamos aceptar este RFC antes de estabilizar cualquier cosa; de lo contrario, ni siquiera tenemos una forma autorizada para inicializar una estructura campo por campo, que parece ser el mínimo.

¿Es aquí donde coloco un enchufe por offset_of! ? :)

Tenga en cuenta que read_initialized es un superconjunto estricto de into_initialized (toma &self lugar de self ). ¿Tiene mucho sentido apoyar a ambos?

¿Es aquí donde coloco un enchufe por offset_of! ? :)

Si puede estabilizarlo antes de que se acepte mi RFC, seguro. ;)

¿Tiene mucho sentido apoyar a ambos?

En mi opinión, sí. into_initialized es más seguro ya que evita usar el mismo valor dos veces y, por lo tanto, debe preferirse a read_initialized siempre que sea posible.

Así que @nikomatsakis hizo este punto antes, pero no lo convirtió en un bloqueador duro.

Acabo de portar una gran cantidad de código para usar MaybeUninit<T> y into_initialized y lo encuentro innecesariamente detallado. El código ya es mucho más detallado que antes, donde estaba "incorrectamente" usando mem::uninitialized .

Creo que MaybeUninit<T> debería llamarse simplemente Uninit<T> , porque para todos los propósitos prácticos, si obtiene un MaybeUninit<T> desconocido, debe asumir que no está inicializado, por lo que Uninit<T> resumiría eso correctamente. Además, into_uninitialized solo debe ser into_init() o similar por razones de coherencia.

También podríamos llamar al tipo Uninitialized<T> y al método into_initialized , pero usar una abreviatura para el tipo y la forma larga para el método o viceversa es una inconsistencia dolorosa. Idealmente, debería recordar que "las API de Rust usan abreviaturas / formas largas" y eso es todo.

Debido a que las abreviaturas pueden ser ambiguas para diferentes personas, prefiero usar formas largas en todas partes y terminarlo. Pero usar una mezcla es, en mi opinión, lo peor de ambos mundos. Rust tiende a usar abreviaturas con más frecuencia que las formas más largas, por lo que no tendría nada en contra de Uninit<T> como abreviatura y .into_init() como otra abreviatura del método.

No me gusta into_initialized() , porque parece que se está realizando una transformación para inicializar el valor. Prefiero mucho take_initialized() . Me doy cuenta de que la firma de tipo se aparta de otros métodos take , pero creo que es mucho más claro, semánticamente, y creo que la claridad semántica debe reemplazar la consistencia de préstamo / movimiento. Otras alternativas que aún no tienen prioridad de ser préstamos mutables podrían ser move_initialized o consume_initialized .

En cuanto a set() vs write() , prefiero fuertemente write() para invocar la similitud con as_ptr().write() , para lo cual sería un alias.

Y finalmente, si va a haber un take_initialized() o similar, entonces prefiero read_initialized() sobre read() debido a lo explícito del primero.

Editar : pero para aclarar, creo que seguir con as_ptr().write() y as_ptr().read() es aún más claro y es más probable que active los circuitos mentales PELIGRO PELIGRO .

@gnzlbg teníamos un FCP para el nombre del tipo, no estoy seguro de si deberíamos volver a abrir esa discusión.

Sin embargo, me gusta la propuesta de usar "init" de manera consistente, como en MaybeUninit::uninit() y x.into_init() .

No me gusta into_initialized() , porque parece que se está realizando una transformación para inicializar el valor.

into métodos into_vec .

Estoy bien con take_initialized(&mut self) (además de into_init), pero creo que debería revertir el estado interno a undef .

revertir el estado interno

https://github.com/rust-lang/rust/issues/53491#issuecomment -437811282

esto no debería cambiar el contenido de self en absoluto. La propiedad justa se transfiere, por lo que ahora está efectivamente en el mismo estado que cuando se construyó sin inicializar.

Muchas de estas cosas ya se han discutido en los más de 200 comentarios ocultos.

Muchas de estas cosas ya se han discutido en los más de 200 comentarios ocultos.

He estado siguiendo la discusión por un tiempo y podría estar equivocado, pero no creo que este punto haya sido mencionado antes. En particular, el comentario que cita no sugiere "revertir el estado interno a undef ", sino que lo hace equivalente a ptr::read (que es dejar el estado interno sin cambios). Lo que sugiero es el equivalente conceptual de mem::replace(self, MaybeUninit::uninitialized()) .

el equivalente conceptual de mem::replace(self, MaybeUninit::uninitialized()) .

Debido al significado de undef , eso es equivalente a read : https://rust.godbolt.org/z/e0-Gyu

@scottmcm no, no lo es. Con read , lo siguiente es legal:

let mut x = MaybeUninit::<u32>::uninitialized();
x.set(13);
let x1 = unsafe { x.read_initialized() };
// `u32` is `Copy`, so we may read multiple times.
let x2 = unsafe { x.read_initialized() };
assert_eq!(x1, x2);

Con el take , esto sería ilegal ya que x2 sería undef .

El hecho de que dos funciones generen el mismo ensamblado no significa que sean equivalentes.

Sin embargo, no veo ningún beneficio al sobrescribir el contenido con undef . Simplemente introduce más formas para que las personas se disparen en el pie. @jethrogb, no has dado ninguna motivación, ¿podrías explicar por qué crees que es una buena idea?

Estoy bien con take_initialized(&mut self) (además de into_init), pero creo que debería revertir el estado interno a undef .

Proponía take_initialized(self) lugar de into_initialized(self) , porque creo que el nombre anterior describe con mayor precisión la operación. Nuevamente, entiendo que take generalmente toma &mut self y into típicamente toma self , pero creo que la nomenclatura semánticamente precisa es más importante que la escritura constante nombrar. Sin embargo, quizás deba usarse un nombre diferente, como move_initialized o transmute_initialized .

Y, de nuevo, en cuanto a v.write() y v.read_initialized() , no veo ningún valor positivo sobre v.as_ptr().write() y v.as_ptr().read() . Los dos últimos parecen menos propensos a ser mal utilizados.

Y, de nuevo, en cuanto a v.write() y v.read_initialized() , no veo ningún valor positivo sobre v.as_ptr().write() y v.as_ptr().read() . Los dos últimos parecen menos propensos a ser mal utilizados.

v.write() (o v.set() o como sea que lo llamemos estos días) es seguro. v.as_ptr().write() requiere un bloque unsafe , que es un poco molesto. Aunque estoy de acuerdo con v.read_init() vs v.as_ptr().read() . v.read_init() parece superfluo.

Estaba proponiendo take_initialized (self) en lugar de into_initialized (self), porque creo que el nombre anterior describe con mayor precisión la operación. Nuevamente, entiendo que la toma típicamente toma un yo & mut y en típicamente toma un yo, pero creo que una denominación semánticamente precisa es más importante que una denominación escrita de forma consistente.

Creo firmemente que into_init(ialized) también semánticamente es más preciso aquí; después de todo, consume MaybeUninit .

@mjbshaw Ah, sí, así es. No me di cuenta de que ... Está bien, bueno, en ese caso revoco todos mis comentarios anteriores sobre set / write . Quizás set tenga más sentido; Cell y Pin ya definen los métodos set . La principal diferencia sería que MaybeUninit::set no eliminaría ningún valor almacenado previamente; quizás eso esté aún más cerca de write ... No sé. De cualquier manera, la documentación es bastante clara.

@RalfJung Está bien, olvídate de take... entonces. ¿Qué tal un nombre nuevo, como move... , consume... o transmute... o algo así? Creo que into_init(ialized) es demasiado confuso; también yo, implica que el valor se está inicializando, cuando en realidad estamos afirmando implícitamente que ya se inicializó.

cuando realmente estamos afirmando implícitamente que ya fue inicializado.

Creo que vale la pena volver a decir que lo único que into_init afirma es que el valor satisface el _ invariante de validez_ de T , que no debe confundirse con T siendo "inicializado" en cualquier sentido general de la palabra.

Por ejemplo:

pub mod foo {
    pub struct AlwaysTrue(bool);
    impl AlwaysTrue { 
        pub fn new() -> Self { Self(true) }
        /// It is impossible to initialize `AlwaysTrue` to false
        /// and unsafe code can rely on `is_true` working properly:
        pub fn is_true(x: bool) -> bool { x == self.0 }
    }
}

pub unsafe fn improperly_initialized() -> foo::AlwaysTrue {
    let mut v: MaybeUninit<foo::AlwaysTrue> = MaybeUninit::uninitialized();
    // let v = v.into_init(); // UB: v is invalid
    *(v.as_mut_ptr() as *mut u8) = 3; // OK
    // let v = v.inti_init(); // UB v is invalid
    *(v.as_mut_ptr() as *mut bool) = false; // OK
    let v = v.into_init(); // OK: v is valid, even though AlwaysTrue is false
    v
}

Aquí el valor de retorno de improperly_initialized se "inicializa" en el sentido de que satisface el _ invariante de validez_ de T , pero no en el sentido de que satisface el _ invariante de seguridad_ de T , y la distinción es sutil pero importante, porque en este caso esta distinción es lo que requiere que improperly_initialized se declare como unsafe fn .

Cuando la mayoría de los usuarios hablan de que algo se está "inicializando", normalmente no tienen la semántica "válida pero QuizásUsafe" de MaybeUninit::into_init .

Si quisiéramos ser terriblemente detallados sobre estos, podríamos tener Invalid<T> y Unsafe<T> , tener Invalid<T>::into_valid() -> Unsafe<T> y requerir que los usuarios escriban uninit.into_valid().into_safe() . Entonces, por encima de improperly_initialized devolvería Unsafe<T> , y solo después de que el usuario establezca correctamente el valor de AlwaysTrue en true pueden realmente obtener la T segura:

// note: this is now a safe fn
fn improperly_uninitialized() -> Unsafe<foo::AlwaysTrue>;
fn initialized() -> foo::AlwaysTrue {
    let mut v: Unsafe<foo::AlwaysTrue> = improperly_uninitialized();
    unsafe { v.as_mut_ptr() as *mut bool } = true;
    unsafe { v.into_safe() }
}

Tenga en cuenta que esto permite que improperly_uninitialized convierta en un fn seguro, porque ahora el invariante de que el AlwaysTrue no es seguro no está codificado en "comentarios" alrededor de la función, sino en el tipos.

No sé si vale la pena seguir este enfoque dolorosamente insoportable. MaybeUninit objetivo de MaybeUninit correctamente. De lo contrario, la gente podría escribir fn improperly_uninitialized() -> AlwaysTrue como un fn seguro y simplemente devolver un AlwaysTrue inseguro porque bueno, lo "inicializaron".

Una cosa que también se podría hacer con Invalid<T> y Unsafe<T> es tener dos rasgos, ValidityCheckeable y UnsafeCheckeable , con dos métodos, ValidityCheckeable::is_valid(Invalid<Self>) y UnsafeCheckeable::is_safe(Unsafe<Self>) , y tienen los métodos Invalid::into_valid y Unsafe::into_safe assert_validity! y assert_safety! .

En lugar de escribir el invariante de seguridad en un comentario, podría simplemente escribir el código para la verificación.

Creo que vale la pena volver a señalar que lo único que into_init afirma es que el valor satisface la invariante de validez de T, que no debe confundirse con T "inicializado" en ningún sentido general de la palabra.

Esto es correcto. OTOH, creo que "inicializado" es un proxy razonable para esto en una primera explicación.

De lo contrario, la gente podría escribir fn incorrectamente_uninitialized () -> AlwaysTrue como un fn seguro, y simplemente devolver un AlwaysTrue inseguro porque bueno, lo "inicializaron".

Creo que podemos hacer un punto razonable de que esto no está "inicializado" correctamente. Estoy de acuerdo en que necesitamos una documentación adecuada de cómo estos dos invariantes interactúan en algún lugar (y no estoy seguro de cuál sería el mejor lugar), pero también creo que la intuición de la mayoría de la gente dirá que improperly_uninitialized no es un bien función para exportar. "Romper los invariantes de otras personas" es un concepto que, creo, surge de forma natural cuando se piensa en "todas las funciones seguras que estoy exportando deben ser tales que el código seguro no pueda utilizarlas para causar estragos".

Una cosa que también se puede hacer con Invalide insegurotiene dos rasgos, ValidityCheckeable y UnsafeCheckeable, con dos métodos, ValidityCheckeable :: is_valid (Invalid) y UnsafeCheckeable :: is_safe (Inseguro), y tener los métodos Invalid :: into_valid y Unsafe :: into_safe assert_validity! y assert_safety! en ellos.

En la gran mayoría de los casos, no se podrá comprobar el invariante de seguridad. Incluso la invariante de validez probablemente no se pueda verificar para referencias. (Bueno, esto depende un poco de cómo factorizamos las cosas).

@scottjmaddox

¿Qué tal un nombre nuevo, como mover ..., consumir ..., transmutar ... o algo así? Creo que into_init (ialized) es demasiado confuso; también yo, implica que el valor se está inicializando, cuando en realidad estamos afirmando implícitamente que ya se inicializó.

¿Cómo move_init transmite una "afirmación" más que into_init ?

assert_init(italized) se ha sugerido previamente.

Sin embargo, observe que read o read_initialized o as_ptr().read tampoco dicen nada acerca de afirmar nada.

Si quisiéramos ser terriblemente prolijos sobre estos, podríamos tener Invalid<T> y Unsafe<T> , tener Invalid<T>::into_valid() -> Unsafe<T> , y requerir que los usuarios escriban uninit.into_valid().into_safe() . Luego, por encima de improperly_initialized devolvería Unsafe<T> , y solo después de que el usuario establezca correctamente el valor de AlwaysTrue en true pueden realmente obtener la T segura:

@gnzlbg Oye, eso es bastante ingenioso. Me gusta que esto arroje la distinción en las caras de los usuarios de una manera ineludible. Probablemente sea un buen momento para enseñar. "validez" y "seguridad" que harán que la gente lo piense dos veces? uninit.into_valid().into_safe() no es tan detallado de todos modos en comparación con uninit.assume_initialized() o lo que sea. Por supuesto, para hacer esta distinción, necesitaremos llegar a un acuerdo en torno al modelo en primer lugar. 😅 Creo que deberíamos investigar un poco más este modelo.

assert_init(italized) se ha sugerido previamente.

@RalfJung También tenemos assume_initialized debido a @eternaleye (creo). Consulte https://github.com/rust-lang/rust/issues/53491#issuecomment -440730699 con una lista de justificaciones que son bastante convincentes.

TBH Siento que tener dos tipos es demasiado detallado.

@RalfJung ¿Podemos profundizar en eso? posiblemente con algunas comparaciones de ejemplos que cree que muestran el alto grado de verbosidad?

Hmm ... si estamos considerando API más detalladas, entonces

uninit.into_inner(uninit.assert_initialized());

podría funcionar bastante bien semánticamente. El primer método devuelve un token que registra su afirmación. El segundo método devuelve el tipo interno, pero requiere que hayas afirmado que es válido.

Sin embargo, no estoy del todo convencido de que valga la pena el esfuerzo adicional, ya que la abstracción podría confundir más a las personas y, por lo tanto, es probable que cometan errores.

También hemos asumido_inicializado debido a @eternaleye (creo). Vea # 53491 (comentario) con una lista de justificaciones que son bastante convincentes.

Justa. assume_initialized suena bien.

¿O quizás sea assume_init ? Eso probablemente debería ser consistente con el constructor, MaybeUninit::uninit() vs MaybeUninit::uninitialized() - y ese está programado para estabilizarse con el primer lote, por lo que deberíamos hacer esa llamada pronto.

@nicoburns No veo el beneficio que obtendríamos al agregar una indirección a través de un token aquí.

¿Podemos profundizar en eso? posiblemente con algunas comparaciones de ejemplos que cree que muestran el alto grado de verbosidad?

Bueno, está claro que es más detallado que "solo" MaybeUninit , ¿verdad? Hay mucha carga mental adicional (tener que entender dos tipos), existe el doble desenvolvimiento y eso significa que tengo que elegir qué tipo usar. Así que hay un costo adicional aquí que creo que debe justificar.

De hecho, generalmente dudo de la utilidad de Unsafe . Desde la perspectiva del compilador, sería completamente un NOP; el compilador nunca asume que sus datos satisfacen el invariante de seguridad. Desde la perspectiva de la implementación de la biblioteca, dudo mucho que la legibilidad del código mejore si, en la implementación Vec , transmutamos cosas en Unsafe<Vec<T>> siempre que violemos temporalmente el invariante de seguridad. Y desde la perspectiva de la enseñanza, dudo que alguien se sorprenda cuando crea un Vec<T> que es válido pero inseguro, le da eso a un código seguro, y luego todo explota.
Compare esto con MaybeUninit que se necesita desde la perspectiva del compilador, y donde el hecho de que incluso deba tener cuidado con los bool "malos" en su propio código privado puede resultar una sorpresa para algunos. .

Dado su costo significativo, creo que Unsafe necesita una motivación mucho más fuerte. No veo cómo ayudaría realmente a prevenir errores o mejorar la legibilidad del código.

Puedo ver los argumentos para el cambio de nombre MaybeUninit a MaybeInvalid . Sin embargo, "inválido" es extremadamente vago (¿inválido para qué ?). He visto a personas confundidas por mi distinción entre "válido" y "seguro". Se podría suponer que un " Vec válido" es válido para cualquier tipo de uso. "no inicializado" al menos desencadena básicamente las asociaciones adecuadas para la mayoría de las personas. ¿Quizás deberíamos cambiar el nombre de "invariante de validez" a "invariante de inicialización" o algo así?

Además, la mera presencia de Unsafe<T> puede ser engañosa (al implicar erróneamente que todos los valores que no están incluidos en él son seguros) a menos que adoptemos una convención generalizada fuerte contra los valores inseguros fuera de este contenedor. Este sería un gran proyecto que requeriría otro RFC y un consenso más amplio de la comunidad. Espero que sea algo controvertido ( @RalfJung dio algunas buenas razones en su contra arriba), y con argumentos más débiles de su lado que MaybeUninit ya que no hay UB involucrado; es esencialmente una pregunta de estilo. Como tal, soy escéptico sobre si tal convención alguna vez será universal en la comunidad de Rust, incluso si se acepta un RFC y se actualizan la biblioteca estándar y los documentos.

Entonces, en mi opinión, cualquiera que quiera ver que suceda la convención tiene un pez más grande para freír que la eliminación de bicicletas de la API MaybeUninit , y sugeriría no retrasar más su estabilización para esperar la resolución de ese proceso. Si estabilizamos las conversiones de MaybeUninit<T> -> T , las generaciones futuras de Rust aún podrían escribir MaybeUninit<Unsafe<T>> para indicar los datos que primero no están inicializados y que posiblemente aún no sean seguros después de ser inicializados.

@RalfJung

¿O quizás es assume_init ? Eso probablemente debería ser consistente con el constructor, MaybeUninit::uninit() vs MaybeUninit::uninitialized() - y _ ese_ está programado para estabilizarse con el primer lote, por lo que deberíamos hacer esa llamada pronto.

Si podemos tener una coherencia de 3 vías con el tipo, el constructor y la función -> T , sería aún mejor. Como el tipo no tiene el sufijo -ialized , creo que ::uninit() y .assume_init() es probablemente el camino a seguir.

Bueno, está claro que es más detallado que "solo" MaybeUninit , ¿verdad?

Depende ... creo que foo.assume_init().assume_safe() (o foo.init().safe() si uno se inclina a ser breve) no es todo lo que ya. También podemos ofrecer la combinación como foo.assume_init_safe() si es necesario. La combinación todavía tiene la ventaja de que detalla los dos supuestos.

Hay mucha carga mental adicional (tener que entender dos tipos), existe el doble desenvolvimiento y eso significa que tengo que elegir qué tipo usar. Así que hay un costo adicional aquí que creo que debe justificar.

Es de esperar que la complejidad provenga de tener que comprender los conceptos subyacentes detrás de la validez y la seguridad. Una vez hecho esto, no creo que haya mucha complejidad mental adicional. Siento que es importante transmitir los conceptos subyacentes.

De hecho, generalmente dudo de la utilidad de Unsafe . Desde la perspectiva del compilador, sería completamente un NOP; el compilador nunca asume que sus datos satisfacen el invariante de seguridad.

Por supuesto; Estoy de acuerdo en que desde un compilador POV es inútil. Cualquier utilidad de la distinción es como una especie de interfaz de "tipos de sesión".

Dado su costo significativo, creo que Unsafe necesita una motivación mucho más fuerte. No veo cómo ayudaría realmente a prevenir errores o mejorar la legibilidad del código.

El aspecto que me llamó la atención fue el de la enseñabilidad. Creo que es probable que ocurran errores cuando la gente piensa que .assume_init() significa que "OK; he comprobado la validez invariante y ahora tengo un buen T ". El esquema actual de MaybeUninit<T> es poco útil de esta manera. Sin embargo, no estoy casado con Unsafe<T> y Invalid<T> como nombres. Simplemente creo que la separación en dos tipos, cualquiera que sea su nombre, puede ser útil desde el punto de vista educativo. ¿Quizás hay otras formas, como reforzar la documentación, que puedan compensar esto dentro del marco actual?

_Puedo_ ver los argumentos para cambiar el nombre de MaybeUninit a MaybeInvalid . Sin embargo, "inválido" es extremadamente vago (¿inválido para _ qué_?), He visto personas confundidas por mi distinción entre "válido" y "seguro"; uno podría suponer que un " Vec válido" es válido para cualquier tipo de uso. "no inicializado" al menos desencadena básicamente las asociaciones adecuadas para la mayoría de las personas. ¿Quizás deberíamos cambiar el nombre de "invariante de validez" a "invariante de inicialización" o algo así?

Definitivamente estoy de acuerdo con que "validez" y "seguridad" son confusas debido a la forma en que suena "válido". He sido partidario de "invariante de máquina" como reemplazo de "validez" y "invariante de sistema de tipos" para "seguridad".

@rkruppe

Entonces, en mi opinión, cualquiera que quiera ver que suceda la convención tiene peces más grandes para freír que eliminar la API MaybeUninit , y sugeriría no retrasar más su estabilización para esperar la resolución de ese proceso. Si estabilizamos las conversiones de MaybeUninit<T> -> T , las generaciones futuras de Rust aún podrían escribir MaybeUninit<Unsafe<T>> para indicar los datos que primero no están inicializados y que posiblemente aún no sean seguros después de ser inicializados.

Buenos puntos, especialmente re. MaybeUninit<Unsafe<T>> ; Probablemente también podría agregar algún alias de tipo para que el nombre del tipo sea menos detallado.

Si podemos tener consistencia de 3 vías con el tipo, el constructor y la función -> T, eso sería aún mejor. Como el tipo no tiene el sufijo -ializado, creo que :: uninit () y .assume_init () es probablemente el camino a seguir.

Convenido. Estoy un poco triste por perder el prefijo into , pero no veo una buena manera de retenerlo.

Entonces, ¿qué pasa con read / read_init entonces? ¿Es la similitud con ptr::read suficiente para activar "es mejor que te asegures de que esto esté realmente inicializado"? Qué read_init tienen problema similar a into_init , donde suena como que hace que sea inicializado en lugar de tener que como un supuesto? ¿Debería quizás assume_init ser como read ahora?

Es de esperar que la complejidad provenga de tener que comprender los conceptos subyacentes detrás de la validez y la seguridad. Una vez hecho esto, no creo que haya mucha complejidad mental adicional. Siento que es importante transmitir los conceptos subyacentes.

¿Podría dar un ejemplo de código si algo en Vec usa esto correctamente para reflejar cuándo se violan las invariantes de Vec ? Creo que sería extremadamente detallado y completamente oscuro lo que realmente sucede.

Creo que agregar un tipo como este es la forma incorrecta de transmitir el concepto subyacente.

Creo que es probable que ocurran errores cuando la gente piensa que .assume_init () significa que "OK; he comprobado la validez invariante y ahora tengo una buena T".

Me parece muy poco probable que alguien diga "Inicialicé este Vec<i32> escribiéndolo lleno de 0xFF , ahora está inicializado, eso significa que puedo presionar". Me gustaría ver al menos una indicación, mejores datos sólidos, de que esto es en realidad un error que la gente comete.
En mi experiencia, la gente tiene una intuición bastante sólida de que cuando entregan datos a un código desconocido o llaman a las operaciones de la biblioteca en algunos datos, entonces es necesario mantener las invariantes de la biblioteca.

Las cosas se han calmado un poco aquí. Entonces, ¿qué pasa con el siguiente plan?

  • Preparo un PR para desaprobar MaybeUninit::uninitialized y le cambio el nombre a MaybeUninit::uninit .
  • Una vez que ha aterrizado (requiere actualizar stdsimd, por lo que hay algo de tiempo aquí si la gente piensa que este no es el camino a seguir), preparo un PR para estabilizar MaybeUninit::{new, uninit, zeroed, as_ptr, as_mut_ptr} .

Esto deja abierta la pregunta alrededor de set / write , into_init[ialized] / assume_init[ialized] y read[_init[italized]] . Actualmente, me inclino por assume_init , write y read , pero he cambiado de opinión sobre esto antes. Desafortunadamente, no tengo una buena idea de cómo tomar una decisión aquí.

  • Una vez que ha aterrizado

¿Significa eso que habrá un período en el que no habrá ninguna vía para crear un valor no inicializado sin (a) una advertencia de depreciación o (b) utilizando características inestables? Esa no es una práctica sostenible.

Al desaprobar algo que no planeamos eliminar de manera efectiva, es necesario que haya un reemplazo estable disponible cada vez que se agregue la advertencia de desaprobación. De lo contrario, las personas simplemente agregarán una anotación para ignorar la advertencia y seguir adelante con sus vidas.

¿Significa eso que habrá un período en el que no habrá ninguna vía para crear un valor no inicializado sin (a) una advertencia de depreciación o (b) utilizando características inestables?

Estoy confundido. Propongo desaprobar un método inestable e introducir otro método inestable en su lugar.

Observe que estaba hablando de MaybeUninit::uninitialized , no de mem::uninitialized .

Desafortunadamente, no tengo una buena idea de cómo tomar una decisión aquí.

@RalfJung Simplemente hágalo (y

Simplemente hágalo (y réame si lo desea) como lo hizo antes con los otros PR de renombrado y si alguien se opone, podemos lidiar con eso en FCP. :)

Bueno, voy a esperar un poco porque estos no tienen que ser parte de la estabilización inicial.

desaprobar un método inestable e introducir otro método inestable en su lugar

Ah, te pillo. Continúe entonces.

Muy bien, haciendo los cambios de nombre en https://github.com/rust-lang/rust/pull/59284 :

uninitialized -> uninit
into_initialized -> asumir_init
read_initialized -> leer
establecer -> escribir

Me gustan los nombres propuestos recientemente. Estoy un poco preocupado por el uso indebido de read , pero parece mucho menos probable que el uso indebido de into_initialized , principalmente debido a la asociación con ptr::read . En general, creo que el nuevo nombre es totalmente aceptable para la estabilización.

Preparo un PR para estabilizar MaybeUninit :: {new, uninit, zeroed, as_ptr, as_mut_ptr}.

¿Alguna posibilidad de que esto se convierta en 1.35-beta (disponible en ~ 2 semanas)?

Estoy un poco en conflicto acerca de impulsar esto dado lo completamente en el aire que https://github.com/rust-lang/rfcs/pull/2582 todavía está. : / Sin ese RFC, la inicialización gradual de una estructura aún no es posible, pero la gente lo hará de todos modos.
OTOH, MaybeUninit ha esperado lo suficiente. Y no es como si el código para la inicialización gradual que las personas escriben actualmente sea mejor que lo que escribirían con MaybeUninit .

Dicho esto, https://github.com/rust-lang/rust/pull/59284 ni siquiera ha aterrizado todavía, por lo que tendríamos que apresurarnos para poner esto en 1.35. TBH Preferiría esperar un ciclo más para que la gente tenga al menos algo de tiempo para jugar con los nombres de los nuevos métodos y ver cómo se sienten.

¿Existe alguna posibilidad de que las funciones de construcción en MaybeInit puedan ser const ?

init y new son const . zeroed no lo es, necesitamos algunas extensiones de lo que pueden hacer las funciones const antes de que pueda ser const .

Quería proporcionar algunos comentarios sobre MaybeUninit , los cambios de código reales se pueden ver aquí https://github.com/Thomasdezeeuw/mio-st/pull/71. En general, mi experiencia (limitada) con la API fue positiva.

El único pequeño problema que encontré fue que devolver &mut T en MaybeUninit::set conduce a tener que usar let _ = ... (https://github.com/Thomasdezeeuw/mio-st/pull/ 71 / files # diff-1b9651542d08c6eca04e6025b1c6fd53R116), que es un poco incómodo pero no un gran problema.

También tengo que agregar API que me gustaría cuando trabaje con matrices unitarias, a menudo en combinación con C.

  1. Un método para ir de &mut [MaybeUninit<T>] a &mut [T] sería bueno, el usuario debe asegurarse de que todos los valores en el segmento se inicialicen correctamente
  2. Una función o macro de inicializador de matriz pública, como uninitialized_array , también sería una adición muy buena.

Quería proporcionar algunos comentarios sobre MaybeUninit

¡Muchas gracias!

devolver & mut T en MaybeUninit :: set lleva a tener que usar let _ = ...

¿Por qué eso? Puede simplemente "desechar" los valores de retorno, de hecho, los ejemplos en los documentos no lo hacen let _ = ... . ( write / set aún no tiene un ejemplo ... pero en realidad es más o menos lo mismo que read , tal vez debería simplemente enlazar).

foo.write(bar); funciona bien sin let .

trabajar con matrices unitarias

Sí, definitivamente es un área de interés futuro.

@RalfJung

devolver & mut T en MaybeUninit :: set lleva a tener que usar let _ = ...

¿Por qué eso? Puede simplemente "desechar" los valores de retorno, de hecho, los ejemplos en los documentos no lo hacen let _ = ... . ( write / set aún no tiene un ejemplo ... pero en realidad es más o menos lo mismo que read , tal vez debería simplemente enlazar).

He habilitado la advertencia para unused_results , por lo que sin let _ = ... produciría una advertencia. Olvidé que no es el predeterminado.

Ah, no sabía nada de esa advertencia. Interesante.

Ese podría ser un argumento para hacer que write no devuelva una referencia y proporcione un método separado para eso si hay más demanda.

Una función o macro de inicializador de matriz pública, como uninitialized_array , también sería una adición muy buena.

Esto sería solo [MaybeUninit::uninit(); EVENTS_CAP] . Consulte https://github.com/rust-lang/rust/issues/49147.

Olvidé que no es el predeterminado.

Ese podría ser un argumento para hacer que write no devuelva una referencia y proporcione un método separado para eso si hay más demanda.

¿Parece un nicho? Si hay más demanda en el futuro, podemos agregar un método que no devuelva una referencia.

¿Parece un nicho?

Sí, hay toneladas de métodos que establecen un valor y luego devuelven una referencia mutable.

@Centril Je, no creo haber visto tu comentario aquí cuando escribí esto en otro lugar: https://github.com/rust-lang/rust/issues/54542#issuecomment -478261027

Eliminando las antiguas funciones renombradas en desuso en https://github.com/rust-lang/rust/pull/59912.

Después de eso, supongo que lo siguiente que hay que hacer es proponer la estabilización ...: tada:

Estoy un poco en conflicto acerca de impulsar esto dado lo completamente en el aire que todavía está rust-lang / rfcs # 2582 . : / Sin ese RFC, la inicialización gradual de una estructura aún no es posible, pero la gente lo hará de todos modos.
OTOH, MaybeUninit ha esperado lo suficiente. Y no es como si el código para la inicialización gradual que las personas escriben actualmente sea mejor que lo que escribirían con MaybeUninit .

Después de eso, supongo que lo siguiente que hay que hacer es proponer la estabilización ... 🎉

@RalfJung ¿Cómo está el estado de la documentación aquí? Si podemos mitigar "la gente lo hará de todos modos" con algunos documentos claros que me ayudarían a dormir mejor ... :)

Al leer los documentos de MaybeUninit , en particular el de assume_init , no está claro en la sección "Seguridad" que si está llamando a mu.assume_init() y luego devuelve ese resultado en una caja fuerte fn , entonces también debe respetar las invariantes de seguridad. Antes de estabilizar, sería bueno mejorar esos documentos y proporcionar fragmentos de invariantes de seguridad proporcionados por la biblioteca que también deben respetarse al usar MaybeUninit .

¿Cómo está el estado de la documentación aquí? Si podemos mitigar "la gente lo hará de todos modos" con algunos documentos claros que me ayudarían a dormir mejor ... :)

Probablemente agregaré una sección sobre la inicialización gradual de estructuras, diciendo que eso no es compatible actualmente. La gente que lea esto dirá "WTF, ¿en serio?".

TBH Encuentro esto bastante frustrante. :( Creo que era muy posible que tuviéramos algún consejo para eso a estas alturas y me entristece que no pudiéramos hacerlo.

No está claro en la sección "Seguridad" que si está llamando a mu.assume_init () y luego devuelve ese resultado en una fn segura, entonces también debe mantener los invariantes de seguridad. Antes de estabilizar, sería bueno mejorar esos documentos y proporcionar fragmentos con invariantes de seguridad proporcionados por la biblioteca que también deben respetarse al usar MaybeUninit.

Básicamente, está sugiriendo convertir esto en documentos que expliquen la idea completa de los invariantes de tipo de datos y cómo se resuelve en Rust. Creo que MaybeUninit es el lugar equivocado para eso; eso haría que parezca que esta preocupación es específica de MaybeUninit cuando en realidad no lo es. Las cosas que está preguntando deben explicarse en algún lugar de nivel más alto como el Nomicon. Planeo enfocar los documentos de MaybeUninit en el problema central de este tipo. No dude en ampliarlos si cree que es útil. :)

Básicamente, está sugiriendo convertir esto en documentos que expliquen la idea completa de los invariantes de tipo de datos y cómo se resuelve en Rust.

Esto es un poco fuerte ... Solo estoy sugiriendo un "Oh, __por cierto__, recuerda que el invariante de seguridad también importa" en algún lugar estratégico en la documentación de MaybeUninit<T> . No estoy sugiriendo que agreguemos una novela. ;) Esa novela puede residir en Nomicon, pero es probable que la mayoría de las personas que usan MaybeUninit<T> interactúen principalmente con la documentación de la biblioteca estándar.

De acuerdo, intenté incorporar todo eso en el PR de estabilización: https://github.com/rust-lang/rust/pull/60445

Me encontré con un uso de mem::uninitialized en la documentación de la biblioteca de estándares, realmente no sabía dónde más señalar que el último ejemplo de core::ptr::drop_in_place necesita ser actualizado (también un poco irónico que exhibe la otra forma de UB que solo sería sancionada por https://github.com/rust-lang/rfcs/pull/2582, así que personalmente la eliminaría).

@HeroicKatora ¡gracias! Incorporé la solución para eso en https://github.com/rust-lang/rust/pull/60445.

Realmente no podemos hacer nada sobre el campo ref-to-unaligned-field actualmente, aunque no estoy seguro de si eliminar el documento es una buena idea.

Tal vez agregue el rasgo PartialUninit (o PartialInit ) que inicializaría los datos parcialmente basados ​​en metadatos.

Ejemplo: MODULEENTRY32W .
El primer campo ( dwSize ) debe ser inicializado por el tamaño de la estructura ( size_of::<MODULEENTRY32W>() ).

pub trait PartialUninit: Sized {
    fn uninit() -> MaybeUninit<Self>;
}

impl<T> PartialUninit for T {
    default fn uninit() -> MaybeUninit<Self> {
        MaybeUninit::uninit()
    }
}

impl PartialUninit for MODULEENTRY32W {
    unsafe fn uninit() -> MaybeUninit<MODULEENTRY32W> {
        let uninit = MaybeUninit { uninit: () };
        uninit.get_mut().dwSize = size_of::<MODULEENTRY32W>();
        uninit
    }
}

¿Cómo crees que?

@kgv Me temo que no entiendo tu sugerencia. ¿Quizás algún contexto adicional que explique qué problema está tratando de resolver podría ayudar? ¿Y quizás un ejemplo más completo de la solución sugerida?

@scottjmaddox arreglado . ¿Está más claro?

@kgv, ¿cuál es el problema que esto está resolviendo (a diferencia de que alguien simplemente escriba una función auxiliar para esto)? No veo por qué libstd tiene que hacer algo aquí.

Tenga en cuenta que la inicialización parcial de estructuras basada en asignaciones solo funciona para tipos que no necesitan eliminarse. uninit.get_mut().foo = bar lo contrario bajará foo , que es UB.

@RalfJung El problema que estoy tratando de resolver: trabajo unificado con estructuras FFI, algunos campos de los cuales no dependen de self (solo Self o no dependen de nada (constante)), por ejemplo, uno de los campos tiene un tamaño de Self .

@kgv Tengo que estar de acuerdo con @RalfJung aquí en que tal caso de uso se maneja mejor con un módulo auxiliar o caja.

El PR de estabilización aterrizó, justo a tiempo para la beta. :) Han pasado alrededor de 8 meses desde que comencé a investigar la situación en torno a los sindicatos y la memoria no inicializada, y finalmente tenemos algo que (muy probablemente) se enviará en 6 semanas. ¡Qué viaje! Muchas gracias a todos los que ayudaron con eso. :RE

Por supuesto, estamos lejos de terminar. Hay https://github.com/rust-lang/rfcs/pull/2582 por resolver. libstd todavía tiene bastantes usos de mem::uninitialized (principalmente en código específico de la plataforma) que necesitan ser trasladados. La API estable que tenemos ahora es mínima: necesitamos averiguar qué hacer con read y write , y deberíamos idear API que ayuden a trabajar con matrices y cajas de MaybeUninit . Y tenemos mucho que explicar para mover lentamente todo el ecosistema lejos de mem::uninitialized .

Pero llegaremos allí, y este primer paso fue probablemente el más importante. :)

y deberíamos crear API que ayuden a trabajar con matrices y cajas de MaybeUninit .

@RalfJung Con ese fin; tal vez ahora es el momento de comenzar a trabajar en https://github.com/rust-lang/rust/issues/49147? = P

Además, probablemente deberíamos dividir y cerrar este problema de seguimiento en favor de otros más pequeños para los bits restantes.

Con ese fin; tal vez ahora es el momento de empezar a trabajar en el # 49147? = P

¿Acabas de ser voluntario? ;) (Me temo que no tendré tiempo para eso.)

probablemente deberíamos dividir y cerrar este problema de seguimiento en favor de otros más pequeños para los bits restantes.

Eso se lo dejo a los expertos en procesos. Pero tiendo a estar de acuerdo.

¿Acabas de ser voluntario? ;) (Me temo que no tendré tiempo para eso.)

Qué he hecho ... = D - Ya tengo un proyecto en el que estoy trabajando, por lo que probablemente me llevará algo de tiempo. ¿Quizás alguien más esté interesado? (si es así, salte al tema del seguimiento)

Eso se lo dejo a los expertos en procesos. Pero tiendo a estar de acuerdo.

Ese sería yo ...;) Intentaré dividirlo y cerrarlo pronto.

@RalfJung sobre su declaración de que let x: bool = mem::uninitialized(); es UB, la pregunta es ¿por qué las primitivas inválidas se consideran así? Según tengo entendido, debe leer un valor para observar que no es válido para activar UB. Pero si no lo lee, ¿entonces qué?

Siento que incluso crear un valor es algo malo, pero me gustaría saber las razones por las que el óxido no lo permite de todos modos. Parece que no hay ningún daño si no observa un estado inválido. ¿Es solo por los primeros errores o quizás por otra cosa?

¿Existe algún caso real en el compilador cuando se basa en estos supuestos?

Por ejemplo, anotamos funciones como foo(x: bool) diciéndole a LLVM que x es un valor booleano válido. Eso hace que UB pase un bool que no sea true o false incluso si la función originalmente no miraba x . Esto es útil porque a veces el compilador quiere introducir usos de variables no utilizadas anteriormente (en particular, esto sucede cuando se mueven declaraciones fuera de bucles sin probar que el bucle se tomó al menos una vez).

AFAIK también establecemos (o queremos establecer) algunas de estas anotaciones dentro de una función, no solo en los límites de la función. Y podríamos encontrar más lugares en el futuro donde dicha información pueda ser útil. Podríamos cubrir eso con una definición inteligente de "usar una variable" (un término que usó sin definirlo, y de hecho no es fácil de definir), pero creo que cuando se trata de UB en código inseguro, es importante tener reglas simples donde podamos.

Por lo tanto, queremos asegurarnos de que incluso en el código inseguro, los tipos en el código tengan algún significado. Eso sólo puede hacerse mediante el tratamiento de la memoria sin inicializar de manera adecuada con un tipo dedicado, en lugar de la "Yolo" enfoque ad-hoc de mentir al compilador sobre el contenido de una variable ( "Yo reclamo que este es un bool , pero realmente no lo inicializaré ").

Por ejemplo, anotamos funciones como foo (x: bool) diciéndole a LLVM que x es un booleano válido. Eso hace que UB pase un bool que no sea verdadero o falso incluso si la función originalmente no miró x. Esto es útil porque a veces el compilador quiere introducir usos de variables no utilizadas anteriormente (en particular, esto sucede cuando se mueven declaraciones fuera de bucles sin probar que el bucle se tomó al menos una vez).

Esto puede considerarse un uso. Estoy preguntando acerca de ingresar un valor y nunca leerlo / pasarlo en ningún lugar antes de que se sobrescriba con un valor válido.
No veo ningún caso de uso útil para generar valor de una manera tan matizada, pero me pregunto.

En pocas palabras, mi pregunta es si este código es UB (de acuerdo con los documentos - it it), y si es así, ¿qué se puede romper exactamente si lo escribo?

let _: bool = unsafe { mem::unitialized };

Otra pregunta sobre el tema en sí: sabemos que tenemos la sintaxis box que le permite asignar memoria directamente en el montón, y siempre funciona a diferencia de Box::new() que a veces apila la memoria. Entonces, si hago box MaybeUninit::new() y luego lo lleno, ¿cómo podría convertir Box<MaybeUninit<T>> en Box<T> ? ¿Debo escribir transmutaciones o qué? Quizás simplemente me perdí este punto en la documentación.

@Pzixel ya hemos hablado de las interacciones entre Box y MaybeUninit en este hilo : smile:

@Centril tiene un subtema para discutir que podría ser bueno cuando dividas esto.

Sí, recuerdo esa discusión, pero no recuerdo ninguna API específica.

En pocas palabras, quiero tener algo como

fn into_inner<A,T>(value: A<MaybeUninit<T>>) -> A<T> { unsafe { std::mem::transmute() } }

Pero no creo que exista tal API, y parece que no podría implementarse sin el soporte del compilador en este punto de la evolución del lenguaje.


Lo pensé un poco más y parece que debería funcionar en cualquier nivel de anidación. Entonces Vec<Result<Option<MaybeUninit<u8>>>> debería tener into_inner método que devuelve Vec<Result<Option<u8>>>

Supuse que get_ref y get_mut se estabilizarían al mismo tiempo (todas las características apuntan a este problema). ¿Hay alguna razón para no hacerlo? Son agradables y son la única indicación de que se permite realizar la acción que realizan (lo que obviamente debería ser cierto).

Esto puede considerarse un uso.

Entonces let x: bool = mem::uninitialized() no está usando bool (¡aunque se asigna a x !), Pero

fn id(x: bool) -> bool { x }
let x: bool = id(mem::uninitialized());

lo usa? Qué pasa

fn uninit() -> bool { mem::uninitialized() }
let x: bool = uninit();

¿Es útil la devolución aquí?

Esto rápidamente se vuelve muy sutil. Entonces, la respuesta que creo que deberíamos estar dando es que cada asignación (realmente cada copia, como en, cada asignación después de bajar a MIR) es un uso, y eso incluye la asignación en let x: bool = mem::uninitialized() .


Supuse que get_ref y get_mut se estabilizarían al mismo tiempo (todas las características apuntan a este problema). ¿Hay alguna razón para no hacerlo? Son agradables y son la única indicación de que se permite realizar la acción que realizan (lo que obviamente debería ser cierto).

Esto está bloqueado al resolver https://github.com/rust-lang/unsafe-code-guidelines/issues/77 : ¿es seguro tener un &mut bool que apunte a una memoria no inicializada? Creo que la respuesta debería ser "sí", pero la gente no está de acuerdo.

Esto está bloqueado al resolver rust-lang / unsafe-code-Guidelines # 77

No creo que sea necesario bloquear. Puede estabilizarlo y decir "es UB usar esto si la memoria no está inicializada" y luego suavizar el requisito si determinamos que está bien. Es un buen método para la post-inicialización.

y luego suavizar el requisito

Lo que significa que si codifico con la documentación de la versión futura, pero alguien compila mi código utilizando la versión anterior (compatible con API) del compilador, ¿ahora hay UB?

@Gankro

No creo que sea necesario bloquear. Puede estabilizarlo y decir "es UB usar esto si la memoria no está inicializada" y luego suavizar el requisito si determinamos que está bien. Es un buen método para la post-inicialización.

Eso me parece muy divertido. ¿Por qué no escribir simplemente &mut *foo.as_mut_ptr() ? Una vez que haya inicializado todo, ¿por qué no funcionaría? OIA, ahora me pregunto si dices

la única indicación de que está permitido realizar la acción que realizan

porque ¿por qué no sería así? Si enumeramos exhaustivamente todo lo que puede hacer una vez que inicializó el valor, será una lista larga. ^^

@hepmaster

Lo que significa que si codifico con la documentación de la versión futura, pero alguien compila mi código utilizando la versión anterior (compatible con API) del compilador, ¿ahora hay UB?

Eso es cierto hoy si la gente hace &mut *foo.as_mut_ptr() . No veo forma de evitarlo.

Además, solo existe UB si realmente tenemos que cambiar algo al hacer esa documentación. De lo contrario, nos encontramos en la situación extraña en la que habría habido UB si ese mismo código se hubiera ejecutado con el mismo compilador antes de hacer una garantía, pero ahora que hacemos la garantía, ya no hay UB. UB es una propiedad no solo del compilador sino también de la especificación, y la especificación puede cambiar retroactivamente. ;)

Bien, estaba asumiendo que el proceso era

  • estabilícelo con un requisito estricto pero sin sentido de implementación ahora
  • proceda a trabajar en el modelo de memoria y lo que ha
  • una vez que el modelo está hecho

    • si necesita ser UB, genial, deja los documentos igual, agrega optimizaciones si es útil

    • Si no necesita ser UB, genial, déjalo en los documentos y dale por terminado

@RalfJung

¿Es útil la devolución aquí?

Sí, devolver un valor o pasarlo a cualquier lugar es un uso.

Esto rápidamente se vuelve muy sutil. Entonces, la respuesta que creo que deberíamos estar dando es que cada asignación (realmente cada copia, como en, cada asignación después de bajar a MIR) es un uso, y eso incluye la asignación en let x: bool = mem :: uninitialized ().

Parece válido.

De todos modos, eso es sobre arbitrart MaybeUninit anidando? ¿Se puede transmutar de forma segura sin que sea necesario que el usuario escriba la transmutación para cada tipo de contenedor?

@Pzixel No estoy seguro de haber entendido su pregunta, pero creo que se está discutiendo en https://github.com/rust-lang/rust/issues/61011.

Vi que el método MaybeUninit::write() aún no estabilizado no es unsafe aunque puede omitir la caída de llamadas en un T ya presente, que habría asumido que no era seguro. ¿Existe un precedente de que esto se considere seguro?

https://doc.rust-lang.org/nomicon/leaking.html#leaking
https://doc.rust-lang.org/nightly/std/mem/fn.forget.html

forget no está marcado como unsafe , porque las garantías de seguridad de Rust no incluyen una garantía de que los destructores siempre se ejecutarán.

¿Podríamos agregar un método MaybeUninit<T> -> NonNull<T> a MaybeUninit ? AFAICT el puntero devuelto por MaybeUninit::as_mut_ptr() -> *mut T nunca es nulo. Eso reduciría la rotación de tener que interactuar con API que usan NonNull<T> , desde:

let mut x = MaybeUninit<T>::uninit();
foo(unsafe { NonNull::new_unchecked(x.as_mut_ptr() });

a:

let mut x = MaybeUninit<T>::uninit();
foo(x.ptr());

el puntero devuelto por MaybeUninit :: as_mut_ptr () -> * mut T nunca es nulo.

Esto es correcto.

Generalmente (y creo que he visto a @Gankro decir esto), NonNull funciona bastante bien "en reposo" pero cuando en realidad se usan punteros, uno quiere llegar a un puntero sin procesar lo antes posible. Eso es mucho más legible.

Sin embargo, agregar un método que devuelva NonNull parece correcto. Sin embargo, ¿cómo debería llamarse? ¿Existe precedencia?

Hay un precedente con https://github.com/rust-lang/rust/issues/47336 pero el nombre no es bueno y no estoy seguro de que vayamos a estabilizar este método.

¿Ha ocurrido la ejecución del cráter mencionada en https://github.com/rust-lang/rust/pull/60445#issuecomment -488818677?

La idea de 3 meses de tiempo disponible que menciona @centril no se materializa para las personas que quieren estar libres de advertencias en todas las

¿Podría posponerse tal vez la desaprobación a 1.40.0?

Las advertencias de obsolescencia no siempre están aisladas de la caja responsable de ellas. Por ejemplo, cuando una caja expone una macro que usa std::mem::uninitialized internamente, los usos de cajas de terceros aún invocan la advertencia de obsolescencia. Me di cuenta de esto hoy cuando compilé uno de mis proyectos con el compilador nocturno. Aunque el código no contiene una sola mención de uninitialized , recibí la advertencia de desaprobación porque invocaba la macro implement_vertex glium.

Ejecutar cargo +nightly test en glium master me da más de 1400 líneas de salida, en su mayoría compuestas por advertencias de depreciación de la función uninitialized (cuento la advertencia 200 veces, pero es probable que tenga un límite como el número que rg "uninitialized" | wc -l salidas es 561).

¿Cuáles son las preocupaciones restantes que bloquean la estabilización del resto de los métodos? Hacer todo a través de *foo.as_mut_ptr() vuelve muy tedioso y, a veces (para write ) implica más bloques unsafe de los necesarios.

@SimonSapin Para emular write , puede reemplazar el MaybeUninit completo sin inseguro usando *val = MaybeUninit::new(new_val) donde val: &mut MaybeUninit<T> y new_val: T o podría usar std::mem::replace si desea el valor anterior.

@ est31 estos son buenos puntos. Estaría bien rechazando la desaprobación con una versión o dos.

¿Alguna objeción?

Ya dijimos en la publicación de blog de la versión 1.36.0:

Como QuizásUninites la alternativa más segura, comenzando con Rust 1.38, la función mem :: uninitialized quedará obsoleta.

Como tal, creo que deberíamos evitar el flip-flopper en este caso, ya que no envía un buen mensaje y es confuso. Además, la fecha de desactivación también debería ser ampliamente conocida, dado que se ha mencionado en la publicación del blog.

Tal vez sea tarde para volver a la depreciación de uninitialized . Pero tal vez podríamos decidir una política para emitir advertencias de desaprobación en Nightly después de que el reemplazo haya estado en el canal estable durante algún tiempo.

Por ejemplo, Firefox se comprometió a requerir una nueva versión de Rust dos semanas después de su lanzamiento .

Ya dijimos en la publicación de blog de la versión 1.36.0:

No estoy de acuerdo en que la mención de una fecha en una publicación de blog sea un grado tan férreo. Está en un repositorio y podemos enviar una edición.

Como tal, creo que deberíamos evitar el flip-flopper en este caso, ya que no envía un buen mensaje y es confuso.

"flip-floppery" es algo malo, pero cambiar de opinión basándonos en datos y comentarios no es eso.

No me importa mucho de una forma u otra la decisión real, pero no creo que la gente se confunda con la propuesta. Aquellos que hayan visto la publicación del blog o la advertencia de desaprobación pueden pasar a lo nuevo. A las personas que no lo han hecho no les interesarán otros pocos lanzamientos.

"flip-floppery" es algo malo, pero cambiar de opinión basándonos en los datos y la retroalimentación no es eso.

Totalmente de acuerdo. No veo que se envíe un mal mensaje diciendo "oye, nuestro calendario de desactivación fue demasiado agresivo, movimos las cosas hacia atrás con un lanzamiento". En realidad, todo lo contrario.
De hecho, IIRC mencioné durante el aterrizaje del PR de estabilización que el precedente es desaprobar 3 lanzamientos en el futuro y no 2, pero por alguna razón elegimos 2. Tres lanzamientos significan 1 lanzamiento completo entre estable-se-libera-con-el -anuncio-de-deprecación y obsoleto-en-la-noche, parece un buen momento para que la gente rastree todas las noches. 6 semanas es un eón, ¿verdad? ;)

Así que planeo enviar un PR mañana que cambie la versión obsoleta a 1.39.0. También puedo enviar un PR para actualizar esa publicación de blog si la gente cree que es importante.

Así que planeo enviar un PR mañana que cambie la versión obsoleta a 1.39.0. También puedo enviar un PR para actualizar esa publicación de blog si la gente cree que es importante.

Aceptaré la 1.39, pero a más tardar. También deberá actualizar las notas de la versión además de la publicación del blog.

PR enviado para cambiar el calendario de desactivación: https://github.com/rust-lang/rust/pull/62599.

@SimonSapin

¿Cuáles son las preocupaciones restantes que bloquean la estabilización del resto de los métodos? Hacer todo a través de * foo.as_mut_ptr () se vuelve muy tedioso y, a veces (para escribir) involucra más bloques inseguros de los necesarios.

Para as_ref / as_mut , honestamente quería esperar hasta saber si las referencias tienen que apuntar a datos inicializados. De lo contrario, la documentación de esos métodos es muy preliminar.

Por read / write , estoy bien estabilizándolos si todos están de acuerdo en que los nombres y las firmas tienen sentido. Creo que esto debería coordinarse con ManuallyDrop::take/read , y tal vez también debería haber ManuallyDrop::write ?

Honestamente, quería esperar hasta que sepamos si las referencias tienen que apuntar a datos inicializados.

¿Qué se necesita para que el Grupo de trabajo sobre directrices sobre códigos inseguros y el Equipo de idiomas lleguen a una decisión sobre este tema? ¿Espera que sea más probable que suceda en unas pocas semanas, meses o años?

Mientras tanto, as_mut ser inestable no impide que los usuarios escriban &mut *manually_drop.as_mut_ptr() qué es cuando necesitan hacer algo.

¿Qué se necesita para que el Grupo de trabajo sobre directrices sobre códigos inseguros y el Equipo de idiomas lleguen a una decisión sobre este tema? ¿Espera que sea más probable que suceda en unas pocas semanas, meses o años?

Meses, quizás años.

Mientras tanto, el hecho de que as_mut sea inestable no impide que los usuarios escriban & mut * manualmente_drop.as_mut_ptr () qué es cuando necesitan hacer algo.

Sí, lo sé. La esperanza es empujar a la gente a retrasar la parte &mut tanto como sea posible y trabajar con punteros sin procesar. Por supuesto, sin https://github.com/rust-lang/rfcs/pull/2582 eso a menudo es difícil.

La documentación de MaybeUninit parece un lugar privilegiado para al menos discutir que se trata de una ambigüedad en la semántica del lenguaje y que los usuarios deberían asumir conservadoramente que no está bien.

Cierto, esa sería la otra opción.

Incluso con una suposición conservadora, as_mut es válido después de que el valor se haya inicializado por completo.

Una forma de ser conservador con las matrices es usar MaybeUninit<[MaybeUninit<Foo>; N]> . Los envoltorios externos permiten crear la matriz con una sola llamada uninit() . (Creo que el literal [expr; N] requiere Copy ?) Los envoltorios internos hacen que sea seguro incluso en el supuesto conservador usar la conveniencia de slice::IterMut para atravesar la matriz, y luego inicialice los valores Foo uno por uno.

@SimonSapin ve la uninitialized_array! en libcore .

@RalfJung tal vez uninit_array! sería un mejor nombre.

@Stargateur Por supuesto , esto definitivamente no se estabilizará con su nombre actual. Es de esperar que no se estabilice nunca si https://github.com/rust-lang/rust/issues/49147 sucede pronto (TM).

@RalfJung Ugh, eso es mi culpa, estaba bloqueando las relaciones públicas sin una gran razón: https://github.com/rust-lang/rust/pull/61749#issuecomment -512867703

@eddyb, esto funciona para libcore, ¡yay! Pero de alguna manera, cuando trato de usar la función en liballoc, no se compila a pesar de que configuro la bandera. Consulte https://github.com/rust-lang/rust/commit/4c2c7e0cc9b2b589fe2bab44173acc2170b20c09.

Building stage1 std artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
   Compiling alloc v0.0.0 (/home/r/src/rust/rustc.2/src/liballoc)
error[E0277]: the trait bound `core::mem::MaybeUninit<K>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<K>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:109:19
    |
109 |               keys: uninit_array![_; CAPACITY],
    |                     -------------------------- in this macro invocation
    |
    = help: consider adding a `where core::mem::MaybeUninit<K>: core::marker::Copy` bound
    = note: the `Copy` trait is required because the repeated element will be copied

error[E0277]: the trait bound `core::mem::MaybeUninit<V>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<V>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:110:19
    |
110 |               vals: uninit_array![_; CAPACITY],
    |                     -------------------------- in this macro invocation
    |
    = help: consider adding a `where core::mem::MaybeUninit<V>: core::marker::Copy` bound
    = note: the `Copy` trait is required because the repeated element will be copied

error[E0277]: the trait bound `core::mem::MaybeUninit<collections::btree::node::BoxedNode<K, V>>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<collections::btree::node::BoxedNode<K, V>>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:162:20
    |
162 |               edges: uninit_array![_; 2*B],
    |                      --------------------- in this macro invocation
    |
    = help: the following implementations were found:
              <core::mem::MaybeUninit<T> as core::marker::Copy>
    = note: the `Copy` trait is required because the repeated element will be copied

error: aborting due to 3 previous errors

Misterio resuelto: los usos de la expresión repetida en libcore en realidad eran para tipos que son copia.

Y la razón por la que no funciona en liballoc es que MaybeUninit::uninit no se puede promocionar.

@RalfJung ¿ Quizás abrir un PR eliminando usos de la macro donde es completamente innecesario?

Respecto a maybe_uninit_ref

Para as_ref / as_mut, honestamente quería esperar hasta que sepamos si las referencias tienen que apuntar a datos inicializados. De lo contrario, la documentación de esos métodos es muy preliminar.

Inestables get_ref / get_mut son definitivamente recomendables por eso; sin embargo, hay casos en los que se puede usar get_ref / get_mut cuando MaybeUninit ha sido init: para obtener un manejo seguro de los datos (ahora conocidos inicializados) mientras se evita cualquier memcpy (en lugar de assume_init , que puede activar un memcpy ).

  • esto puede parecer una situación particularmente específica, pero la principal razón por la que la gente (quiere) utilizar datos no inicializados es precisamente por este tipo de ahorro económico.

Debido a esto, imagino que assume_init_by_ref / assume_init_by_mut podría ser bueno tener (dado que into_inner se ha llamado assume_init , parece plausible que el ref / ref mut getters también obtienen un nombre especial para reflejar esto).

Hay dos / tres opciones para esto, relacionadas con la interacción Drop :

  1. Exactamente la misma API que get_ref y get_mut , lo que puede provocar pérdidas de memoria cuando hay pegamento;

    • (Variante): misma API que get_ref / get_mut , pero con un límite de Copy ;
  2. API de estilo de cierre, para garantizar la caída:

impl<T> MaybeUninit<T> {
    /// # Safety
    ///
    ///   - the contents must have been initialised
    unsafe
    fn assume_init_with_mut<R, F> (mut self: MaybeUninit<T>, f: F) -> R
    where
        F : FnOnce(&mut T) -> R,
    {
        if mem::needs_drop::<T>().not() {
            return f(unsafe { self.get_mut() });
        }
        let mut this = ::scopeguard::guard(self, |mut this| {
            ptr::drop_in_place(this.as_mut_ptr());
        });
        f(unsafe { MaybeUninit::<T>::get_mut(&mut *this) })
    }
}

(Donde la lógica de scopeguard se puede volver a implementar fácilmente, por lo que no hay necesidad de depender de ella)


Estos podrían estabilizarse más rápido que get_ref / get_mut , dado el requisito explícito de assume_init .

Inconvenientes

Si se eligiera una variante de la opción .1 , y get_ref / get_mut se pudiera usar sin la situación assume_init , entonces esta API se volvería casi estrictamente inferior (Digo casi porque con la API propuesta, leer de la referencia estaría bien, lo que nunca puede ser en el caso de get_ref y get_mut )

Similar a lo que escribió @danielhenrymantilla sobre get_{ref,mut} , estoy empezando a pensar que read probablemente debería cambiarse el nombre a read_init o read_assume_init o menos, algo que indica que esto solo se puede hacer después de que se complete la inicialización.

@RalfJung Tengo una pregunta sobre esto:

fn foo<T>() -> T {
    let newt = unsafe { MaybeUninit::<T>::zeroed().assume_init() };
    newt
}

Por ejemplo, llamamos foo<NonZeroU32> . ¿Esto activa UB cuando declaramos una función foo (porque tiene que ser válida para todos los T s o cuando la instanciamos con un tipo que activa UB? Lo siento si es un lugar incorrecto para hacer una pregunta.

El código

Entonces, foo::<i32>() está bien. Pero foo::<NonZeroU32>() es UB.

La propiedad de ser válida para todas las formas posibles de llamar se llama "solidez", ver también la referencia . El contrato general en Rust es que la superficie API segura de una biblioteca debe ser sólida. Esto para que los usuarios de una biblioteca no tengan que preocuparse por UB. Toda la historia de seguridad de Rust se basa en bibliotecas con API sólidas.

@RalfJung gracias.

Entonces, si entiendo correctamente, esta función no es sólida (y, por lo tanto, no es válida), pero si la marcamos como unsafe entonces este cuerpo se vuelve válido y sólido.

@Pzixel, si lo marca como inseguro, la solidez ya no es un concepto que se aplique. "¿Es este sonido" solo tiene sentido como una pregunta para un código seguro.

Sí, debe marcar la función unsafe porque algunas entradas pueden activar UB. Pero incluso si lo hace, esas entradas aún activan UB, por lo que la función aún no debe llamarse de esa manera. Nunca está bien activar UB, ni siquiera en código inseguro.

Sí, por supuesto, lo entiendo. Solo quería concluir que la función parcial debe estar marcada como unsafe . Tiene sentido para mí, pero no lo pensé antes de que respondieras.

Dado que la discusión sobre este problema de seguimiento es tan larga ahora, ¿podemos dividirla en algunos otros problemas de seguimiento para cada característica de MaybeUninit que aún es inestable?

  • maybe_uninit_extra
  • maybe_uninit_ref
  • maybe_uninit_slice

Parece razonable. También está https://github.com/rust-lang/rust/issues/63291.

Cerrando esto a favor de un meta-problema que rastrea MaybeUninit<T> más general: # 63566

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