Rust: Rastreamento de problema para RFC 1892, "Suspensão de uso não inicializado em favor de um novo tipo MaybeUninit"

Criado em 19 ago. 2018  ·  382Comentários  ·  Fonte: rust-lang/rust

NOVO PROBLEMA DE TRACKING = https://github.com/rust-lang/rust/issues/63566

Este é um problema de rastreamento para o RFC "Substituir uninitialized em favor de um novo MaybeUninit type" (rust-lang / rfcs # 1892).

Passos:

  • [x] Implementar o RFC (cc @ rust-lang / libs)
  • [x] Ajuste a documentação (em https://github.com/rust-lang/rust/pull/60445)
  • [x] RP de estabilização (em https://github.com/rust-lang/rust/pull/60445)

Perguntas não resolvidas:

  • Devemos ter um setter seguro que retorna &mut T ?
  • Devemos renomear MaybeUninit ?
  • Devemos renomear into_inner ?
  • MaybeUninit<T> ser Copy para T: Copy ?
  • Devemos permitir a chamada de get_ref e get_mut (mas não a leitura das referências retornadas) antes que os dados sejam inicializados? (Também conhecido como: "As referências a dados não inicializados insta-UB ou apenas UB ao serem lidos?") Devemos renomeá-lo de forma semelhante a into_inner ?
  • Podemos fazer into_inner (ou o que quer que seja chamado) entrar em pânico quando T está desabitado, como mem::uninitialized faz atualmente? (feito)
  • Parece que não queremos descontinuar mem::zeroed .
B-RFC-approved C-tracking-issue E-mentor T-lang T-libs

Comentários muito úteis

mem::zeroed() é útil para certos casos de FFI em que se espera que você zere um valor com memset(&x, 0, sizeof(x)) antes de chamar uma função C. Acho que essa é uma razão suficiente para mantê-lo indefinido.

Todos 382 comentários

cc @RalfJung

[] Implementar o RFC

Posso ajudar a implementar o RFC.

Incrível, posso ajudar na revisão :)

Gostaria de alguns esclarecimentos sobre esta parte do RFC:

Faça com que a chamada não inicializada em um tipo vazio acione um pânico em tempo de execução que também imprime a mensagem de reprovação.

Apenas mem::uninitialized::<!>() entrar em pânico? Ou isso também deve abranger structs (e talvez enums?) Que contêm o tipo vazio (por exemplo, (!, u8) )?

AFAIK só fazemos a geração de código realmente prejudicial para ! . A maioria dos outros usos de mem::uninitialized são igualmente incorretos, mas o compilador não os explora.

Então, eu faria isso por ! apenas, mas também por mem::zeroed . (Esqueci de corrigir essa parte quando adicionei zeroed ao RFC, ao que parece.)

Podemos começar fazendo o seguinte:
https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/intrinsic.rs#L184 -L198

verifique se fn_ty.ret.layout.abi é Abi::Uninhabited e no mínimo emite uma armadilha, por exemplo: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/ operand.rs # L400 -L403

Depois de ver a armadilha (ou seja, intrinsics::abort ) em ação, você pode ver se há alguma maneira legal de provocar pânico. Pode ser complicado por causa do desenrolamento, precisaremos colocá-los em um caso especial aqui: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L445 - L447

Para entrar em pânico, você precisa de algo assim: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L360 -L407
(você pode ignorar o braço EvalErrorKind::BoundsCheck )

@eddyb Obrigado pelas dicas.


Agora estou corrigindo (vários) avisos de depreciação e me sinto (muito) tentado a apenas executar sed -i s/mem::uninitialized()/mem::MaybeUninit::uninitialized().into_inner()/g mas acho que não entenderia ... Ou tudo bem se eu souber que o valor é concreto (Copiar) tipo? por exemplo, let x: [u8; 1024] = mem::uninitialized(); .

Isso seria exatamente errado, sim. ^^

Pelo menos por agora, gostaria de considerar mem::MaybeUninit::uninitialized().into_inner() UB para todos os tipos de não sindicalizados. Observe que Copy certamente não é suficiente; ambos bool e &'static i32 são Copy e seu snippet se destina a ser insta-UB para eles. Podemos querer uma exceção para "tipos onde todos os padrões de bits estão corretos" (tipos inteiros, essencialmente), mas eu me oporia a fazer tal exceção porque undef não é um padrão de bits normal. É por isso que o RFC diz que você precisa inicializar totalmente antes de chamar into_inner .

Também diz isso por get_mut , mas a discussão RFC trouxe o desejo de algumas pessoas de relaxar a restrição aqui. Essa é uma opção com a qual eu poderia viver. Mas não por into_inner .

Receio que todos esses usos de uninitialized terão que ser analisados ​​com mais cuidado e, de fato, essa era uma das intenções da RFC. Gostaríamos que o ecossistema mais amplo fosse mais cuidadoso aqui, se todos usarem into_inner imediatamente, então o RFC não teria valor.

Gostaríamos que o ecossistema mais amplo fosse mais cuidadoso aqui, se todos usarem into_inner imediatamente, o RFC não teria valor.

Isso me dá uma ideia ... talvez devêssemos lint (grupo: "correção") para esse tipo de código? cc @ oli-obk

Agora estou corrigindo (vários) avisos de depreciação

Devemos apenas enviar Nightly com esses avisos, uma vez que a substituição recomendada está disponível pelo menos no Stable. Veja uma discussão semelhante em https://github.com/rust-lang/rust/pull/52994#issuecomment -411413493

@RalfJung

Podemos querer uma exceção para "tipos onde todos os padrões de bits estão corretos" (tipos inteiros, essencialmente)

Você já participou da discussão sobre isso antes, mas vou postar aqui para circular mais amplamente: isso já é algo para o qual já temos muitos casos de uso existentes em fúcsia, e temos uma característica para isso ( FromBytes ) e uma macro de derivação para esses tipos. Também havia um Pré-RFC interno para adicioná-los à biblioteca padrão (cc @gnzlbg @joshlf).

Eu me oporia a fazer tal exceção porque undef não é um padrão de bits normal.

Sim, este é um aspecto em que mem::zeroed() é significativamente diferente de mem::uninitialized() .

@cramertj

Você já participou da discussão sobre isso antes, mas vou postar aqui para circular mais amplamente: isso já é algo para o qual temos muitos casos de uso existentes em fúcsia e temos uma característica para isso (FromBytes) e uma macro derivar para esses tipos. Também havia um Pré-RFC interno para adicioná-los à biblioteca padrão (cc @gnzlbg @joshlf).

Essas discussões foram sobre maneiras de permitir memcpy s seguros entre os tipos, mas acho que é muito ortogonal se a memória que está sendo copiada é inicializada ou não - se você colocar memória não inicializada, obterá memória não inicializada.

O consenso também era que não seria bom para qualquer abordagem discutida permitir a leitura de bytes de preenchimento, que são uma forma de memória não inicializada, no Rust seguro. Ou seja, se você colocar a memória inicializada, não poderá retirar a memória não inicializada.

IIRC, ninguém ali sugeriu ou discutiu qualquer abordagem na qual você pudesse inserir memória não inicializada e retirar a memória inicializada, então não estou entendendo o que essas discussões têm a ver com esta. Para mim, eles são completamente ortogonais.

Para esclarecer um pouco mais a questão, o LLVM define os dados não inicializados como Poison, que é diferente de "algum padrão de bits arbitrário, mas válido". Ramificar com base em um valor de Poison ou usá-lo para calcular um endereço que é então desreferenciado é UB. Portanto, infelizmente, "tipos em que todos os padrões de bits estão corretos" ainda não são seguros para construir porque usá-los sem inicializá-los separadamente será UB.

Certo, desculpe, eu deveria ter esclarecido o que quis dizer. Eu estava tentando dizer que "tipos em que todos os padrões de bits estão bem" já é algo que estamos interessados ​​em definir por outras razões. Como @RalfJung disse acima,

Eu me oporia a fazer tal exceção porque undef não é um padrão de bits normal.

Graças a Deus tem gente que sabe ler, porque aparentemente eu não sei ...

Certo, então o que eu quis dizer é: nós definitivamente temos tipos em que todos os padrões de bits inicializados estão corretos - todos os tipos i* e u* , ponteiros brutos, acho que f* também e, em seguida, tuplas / estruturas consistindo apenas em tais tipos.

O que é uma questão em aberto é em quais circunstâncias quais desses tipos podem ser não inicializados , isto é, veneno. Minha própria resposta preferida é "nunca".

O consenso também era que não seria bom para qualquer abordagem discutida permitir a leitura de bytes de preenchimento, que são uma forma de memória não inicializada, no Rust seguro. Ou seja, se você colocar a memória inicializada, não poderá retirar a memória não inicializada.

Ler bytes de preenchimento como MaybeUninit<u8> deve funcionar.

O consenso também era que não seria bom para qualquer abordagem discutida permitir a leitura de bytes de preenchimento, que são uma forma de memória não inicializada, no Rust seguro. Ou seja, se você colocar a memória inicializada, não poderá retirar a memória não inicializada.

Lendo bytes de preenchimento como MaybeUninitdeve estar bem.

A discussão resumida foi sobre fornecer uma característica, Compatible<T> , com um método seguro fn safe_transmute(self) -> T que "reinterpreta" / "memcpys" os bits de self em um T . A garantia desse método é que se self for inicializado corretamente, o T resultante também estará. Foi proposto que o compilador preenchesse implementações transitivas automaticamente, por exemplo, se houver um impl Compatible<V> for U e um impl Compatible<W> for V então há um impl Compatible<W> for U (ou porque foi fornecido manualmente ou o compilador o gera automaticamente - como isso poderia ser implementado foi completamente modificado à mão).

Foi proposto que deveria ser unsafe para implementar o traço: se você implementá-lo para um T que tem bytes de preenchimento onde Self tem campos, então está tudo bem, pelo menos até que você tente usar T e o comportamento do seu programa acabe dependendo do conteúdo da memória não inicializada.

Não tenho ideia do que isso tem a ver com MaybeUninit<u8> , talvez você pudesse explicar melhor?

A única coisa que posso imaginar é que poderíamos adicionar um implante de cobertor: unsafe impl<T> Compatible<[MaybeUninit<u8>; size_of::<T>()]> for T { ... } já que transmutar qualquer tipo em [MaybeUninit<u8>; N] de seu tamanho é seguro para todos os tipos. Eu não sei o quão útil tal impl seria, dado que MaybeUninit é uma união, e quem usa [MaybeUninit<u8>; N] não tem ideia se um elemento particular da matriz foi inicializado ou não .

@gnzlbg naquela época você estava falando sobre FromBits<T> for [u8] . É aí que eu digo que devemos usar [MaybeUninit<u8>] .

Discuti essa proposta com @nikomatsakis na RustConf e ele me incentivou a avançar com um RFC. Eu ia fazer isso em algumas semanas, mas se houver interesse, posso tentar fazer um neste fim de semana. Isso seria útil para esta discussão?

@joshlf de qual proposta você está falando?

@RalfJung

@gnzlbg naquela época você estava falando sobre FromBitspara [u8]. É aí que eu digo que temos que usar [MaybeUninit] em vez de.

Te peguei, concordo totalmente aqui. Tínhamos esquecido completamente que também queríamos fazer isso 😆

@joshlf de qual proposta você está falando?

Uma proposta FromBits / IntoBits . TLDR: T: FromBits<U> significa que qualquer padrão de bits que seja U válido corresponde a um T válido. U: IntoBits<T> significa a mesma coisa. O compilador infere automaticamente ambos para todos os pares de tipos dadas certas regras, e isso desbloqueia muitos benefícios divertidos que atualmente requerem unsafe . Há um rascunho deste RFC aqui que escrevi há algum tempo, mas pretendo alterar grandes partes dele, então não tome esse texto como nada mais do que um guia aproximado.

@joshlf Acho que esse par de características seria mais desenvolvido em cima dessa discussão do que parte dela. AFAIK, temos duas questões em aberto em termos de validade:

  • Ele recurse nas referências abaixo? Eu acho cada vez mais forte que não, à medida que vemos mais exemplos. Portanto, provavelmente devemos adaptar os documentos MaybeUninit::get_mut acordo (não é realmente UB usar isso antes de concluir a inicialização, mas é UB desreferenciá- lo antes de concluir a inicialização). No entanto, primeiro temos que tomar essa decisão para validade, e não tenho certeza de qual é o local certo para isso. Provavelmente um RFC dedicado?
  • Um u8 (e outros tipos inteiros, ponto flutuante, ponteiro bruto) precisa ser inicializado, ou seja, é MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Acho que sim, mas principalmente com base no pressentimento de que queremos manter no mínimo os lugares onde permitimos poison / undef . No entanto, eu poderia ser persuadido do contrário se houver muitos usos para esse padrão (e espero usar miri para ajudar a determinar isso).

Ele recurse nas referências abaixo?

@RalfJung você pode mostrar um exemplo do que você quer dizer com "referências abaixo recorrentes"?

Um u8 (e outros tipos inteiros, ponto flutuante, ponteiro bruto) precisa ser inicializado, ou seja, é MaybeUinit:: uninitialized (). into_inner () insta-UB?

O que acontece se não for UB instantâneo? O que posso fazer com esse valor? Posso combinar com ele? Em caso afirmativo, o comportamento do programa é determinístico?

Eu sinto que se eu não conseguir igualar o valor sem apresentar o UB, então reinventamos mem::uninitialized . Se eu puder combinar o valor e o mesmo branch for sempre usado em todas as arquiteturas, níveis de opção, etc., reinventamos mem::zeroed (e estamos fazendo uso de MaybeUninit digite um pouco discutível). Se o comportamento do programa não é determinístico, e muda com os níveis de otimização, entre arquiteturas, dependendo de fatores externos (como se o sistema operacional deu ao processo páginas zeradas), etc., então eu sinto que estaríamos introduzindo uma grande arma no língua.

Um u8 (e outros tipos inteiros, ponto flutuante, ponteiro bruto) precisa ser inicializado, ou seja, é MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Acho que sim, mas principalmente com base no pressentimento de que queremos manter no mínimo os locais onde permitimos poison / undef . No entanto, eu poderia ser persuadido do contrário se houver muitos usos para esse padrão (e espero usar miri para ajudar a determinar isso).

FWIW, dois dos benefícios de não ser UB são que a) ele se alinha com o que o LLVM faz e, b) permite mais flexibilidade em otimizações. Também parece mais consistente com sua proposta recente para definir a segurança na hora do uso, não na hora da construção.

O que acontece se não for UB instantâneo? O que posso fazer com esse valor? Posso combinar com ele? Em caso afirmativo, o comportamento do programa é determinístico?

Eu sinto que se eu não conseguir igualar o valor sem apresentar UB, então nós reinventamos mem::uninitialized . Se eu puder combinar o valor e o mesmo branch for sempre usado em todas as arquiteturas, níveis de opção, etc., reinventamos mem::zeroed (e estamos fazendo uso de MaybeUninit digite um pouco discutível). Se o comportamento do programa não é determinístico, e muda com os níveis de otimização, entre arquiteturas, dependendo de fatores externos (como se o sistema operacional deu ao processo páginas zeradas), etc., então eu sinto que estaríamos introduzindo uma grande arma no língua.

Por que você deseja ser capaz de corresponder a algo que não foi inicializado? Defini-lo como UB para ramificar ou indexar com base em valores não inicializados oferece ao LLVM mais espaço para otimizar, então não acho que amarrar mais suas mãos seja uma boa ideia, especialmente se não houver um caso de uso convincente.

Por que você deseja ser capaz de corresponder a algo que não foi inicializado?

Não disse que queria, afirmei que, se isso não puder ser feito, não entendo a diferença entre MaybeUinit<u8>::uninitialized().into_inner() e apenas mem::uninitialized() .

@RalfJung você pode mostrar um exemplo do que você quer dizer com "referências abaixo recorrentes"?

Essencialmente, a questão é se permitimos o seguinte:

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

Se decidirmos que uma referência é válida apenas se apontar para algo válido (isso é o que quero dizer com "referências abaixo recorrentes"), esse código é UB.

O que acontece se não for UB instantâneo? O que posso fazer com esse valor? Posso combinar com ele? Em caso afirmativo, o comportamento do programa é determinístico?

Você não pode inspecionar um u8 não inicializado de forma alguma. match pode fazer muitas coisas, tanto vincular nomes quanto testar a igualdade; o primeiro está certo, mas o último não. Mas você pode escrever de volta na memória.

Essencialmente, é isso que a miri implementa atualmente.

Eu sinto que se eu não conseguir igualar no valor sem apresentar UB, então nós reinventamos mem :: uninitialized.

Porquê isso? O maior problema com mem::uninitialized era em torno de tipos que têm restrições para quais são seus valores válidos. Poderíamos decidir que u8 não tem tais restrições, então mem::uninitialized() estava certo para u8 . Era quase impossível usar corretamente em código genérico, então é melhor se livrar totalmente dele.
De qualquer forma, ainda não é correto passar um u8 não inicializado para o código seguro, mas pode ser correto usá-lo cuidadosamente em código não seguro.

Você também não pode "combinar" um &mut apontando para dados inválidos. IOW, acho que o bool exemplo que dei acima é bom, mas o seguinte certamente não é:

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

Isso está usando match para fazer uma desreferência normal do ponteiro.

FWIW, dois dos benefícios de não ser UB são que a) ele se alinha com o que o LLVM faz e, b) permite mais flexibilidade em otimizações. Também parece mais consistente com sua proposta recente para definir a segurança na hora do uso, não na hora da construção.

Quais otimizações isso permitiria?
Observe que o LLVM faz otimizações em código essencialmente não tipado, portanto, nada disso é uma preocupação aqui. Estamos falando apenas sobre otimizações MIR aqui.

Estou essencialmente vindo da perspectiva de que devemos permitir o mínimo possível até que tenhamos um uso claro. Sempre podemos permitir mais coisas mais tarde, mas não o contrário. Dito isso, alguns bons usos de fatias de bytes que podem ultrapassar qualquer dado surgiram recentemente, o que pode ser um argumento suficiente para fazer isso pelo menos para u* e i* .

Se decidirmos que uma referência é válida apenas se apontar para algo válido (isso é o que quero dizer com "referências abaixo recorrentes"), esse código é UB.

Peguei vocês.

O maior problema com mem :: uninitialized era em torno de tipos que têm restrições para quais são seus valores válidos.

mem::uninitialized também tem o problema que você apontou acima: criar uma referência a um valor não inicializado pode ser um comportamento indefinido (ou não). Então é o seguinte UB?

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

Achei que uma das razões para a introdução de MaybeUninit era evitar esse problema, tendo sempre a união inicializada (por exemplo, para unidade), o que permite que você faça uma referência a ela e altere seu conteúdo, por exemplo, definindo o campo ativo para u8 e dando a ele um valor via ptr::write sem introduzir UB.

É por isso que estou um pouco confuso. Não vejo como into_inner é melhor do que:

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

Ambos parecem bombas-relógio de comportamento indefinido para mim.

Quais otimizações isso permitiria?
Observe que o LLVM faz otimizações em código essencialmente não tipado, portanto, nada disso é uma preocupação aqui. Estamos falando apenas sobre otimizações MIR aqui.

Se dissermos que a memória indefinida tem algum valor e, portanto, você tem permissão para ramificá-la de acordo com a semântica Rust, então não podemos reduzi-la para a versão de undefined do LLVM, porque seria incorreto.

Estou essencialmente vindo da perspectiva de que devemos permitir o mínimo possível até que tenhamos um uso claro. Sempre podemos permitir mais coisas mais tarde, mas não o contrário.

Isso é justo.

Dito isso, alguns bons usos de fatias de bytes que podem ultrapassar qualquer dado surgiram recentemente, o que pode ser um argumento suficiente para fazer isso pelo menos para u* e i* .

Algum desses casos de uso inclui ter fatias de bytes que contêm valores não inicializados?

Um lugar onde um &mut [u8] não inicializado-mas-não-veneno pode ser valioso é em Read::read - gostaríamos de evitar a necessidade de zerar o buffer só porque algum estranho Read impl poderia lê-lo em vez de apenas escrever nele.

Um lugar onde um &mut [u8] não inicializado-mas-não-veneno pode ser valioso é em Read::read - gostaríamos de evitar a necessidade de zerar o buffer só porque algum estranho Read impl poderia lê-lo em vez de apenas escrever nele.

Entendo, então a ideia é que MaybeUninit representaria um tipo que foi inicializado, mas com conteúdo indefinido, enquanto outros tipos de dados não inicializados (por exemplo, campos de preenchimento) ainda seriam totalmente não inicializados no sentido de veneno do LLVM?

Eu não acho que seria necessário se aplicar ao MaybeUninit em geral. Em teoria, poderia haver alguma API para "congelar" o conteúdo de indefinido para definido-mas-arbitrário.

Se dissermos que a memória indefinida tem algum valor e, portanto, você tem permissão para ramificá-la de acordo com a semântica Rust, então não podemos reduzi-la para a versão de undefined do LLVM, porque seria incorreto.

Essa nunca foi a proposta. É e continuará sendo UB para ramificar em poison .

A questão é se é UB meramente "ter" um poison em um u8 .

Algum desses casos de uso inclui ter fatias de bytes que contêm valores não inicializados?

Fatias são como referências, então &mut [u8] de dados não inicializados estão bem, contanto que sejam apenas gravados (assumindo que essa é a solução que tomamos para validade de referência).

@sfackler

Um lugar em que um & mut não inicializado-mas-não-veneno [u8] pode ser valioso é para Read :: read - gostaríamos de ser capazes de evitar a necessidade de zerar o buffer só porque algum impl estranho de Read poderia ler dele em vez de apenas escrever nele.

Bem, sem &out você só poderá fazer isso se conhecer o impl. A questão não é se o código seguro deve lidar com poison em u8 (não é, esse não é um uso correto de código seguro!), A questão é se o código inseguro pode lidar com isso cuidadosamente deste jeito. (Veja aquela postagem do blog que eu queria escrever hoje sobre a distinção entre invariantes de segurança e invariantes de validade ...)

Talvez eu esteja atrasado, mas sugiro alterar a assinatura do método set() para retornar &mut T . Desta forma, seria seguro escrever um código completamente seguro trabalhando com MaybeUninit (pelo menos em algumas situações).

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

Isso é praticamente uma garantia estática de que init() inicializará o valor ou divergirá. (Se ele tentasse retornar outra coisa, o tempo de vida estaria errado e &'static mut u8 seria impossível no código seguro.) Talvez pudesse ser usado como parte da API do placer no futuro.

@Kixunil Já foi assim antes, e eu concordo que é bom. Acabei de achar o mesmo set confuso para uma função que retorna algo.

@Kixunil

Isso é praticamente uma garantia estática de que init() inicializará o valor ou divergirá. (Se tentasse retornar algo diferente, o tempo de vida estaria errado e &'static mut u8 seria impossível no código seguro.)

Não exatamente; você pode obter um com Box::leak .

Em uma base de código que escrevi recentemente, criei um esquema semelhante; é um pouco mais complicado, mas fornece uma garantia estática verdadeira de que a referência fornecida foi inicializada. Ao invés de

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

eu tenho

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

O truque é que Uninitialized e DidInit são invariáveis ​​em seus parâmetros de vida, então não há como reutilizar DidInit com um parâmetro de vida diferente, mesmo por exemplo 'static .

DidInit impls Deref e DerefMut , então um código seguro pode usá-lo como uma referência, como no seu exemplo. Mas a garantia de que foi realmente a referência original transmitida que foi inicializada, e não alguma outra referência aleatória, é útil para código não seguro . Isso significa que você pode definir inicializadores estruturalmente:

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 função inicializa um ponteiro para struct Foo inicializando cada um de seus campos, por sua vez, usando os callbacks de inicialização fornecidos pelo usuário. Requer que os callbacks retornem DidInit s, mas não se preocupa com seus valores; o fato de eles existirem é o suficiente. Uma vez que todos os campos foram inicializados, ele sabe que todo o Foo é válido - então ele chama did_init() no Uninitialized<'a, Foo> , que é um método inseguro que apenas o converte para o tipo DidInit , que init_foo então retorna.

Eu também tenho uma macro que automatiza o processo de escrever tais funções, e a versão real é um pouco mais cuidadosa com destruidores e pânicos (embora precise de melhorias).

De qualquer forma, gostaria de saber se algo assim poderia ser implementado na biblioteca padrão.

Link do parque

(Observação: DidInit<'a, T> é na verdade um alias de tipo para &'a mut _DidInitMarker<'a, T> , para evitar problemas de vida com DerefMut .)

A propósito, embora a abordagem vinculada acima ignore os destruidores, uma abordagem ligeiramente diferente seria tornar DidInit<‘a, T> responsável por executar o destruidor de T . Nesse caso, teria que ser uma estrutura, não um alias; e ele só poderia distribuir referências a T que vivem enquanto DidInit si, não por todo ’a (caso contrário, você poderia continuar acessando-o após a destruição).

+1 por incluir um método para fornecer o comportamento que eu havia solicitado anteriormente em set , mas não há problema em estar disponível por meio de outro nome.

Alguma boa ideia para qual poderia ser esse nome? set_and_as_mut ? ^^

set_and_borrow_mut ?

insert / insert_mut ? O tipo Entry tem um método or_insert um tanto semelhante (mas OccupiedEntry também tem insert que retorna o valor antigo, então não é nada parecido).

Existe uma razão realmente convincente para ter dois métodos separados? Parece simples ignorar o valor de retorno e imagino que a função seria marcada como #[inline] portanto, não esperaria nenhum custo real de tempo de execução.

Existe uma razão realmente convincente para ter dois métodos separados? Parece bastante simples ignorar o valor de retorno

Acho que a única razão é que ver set retornar algo é bastante surpreendente.

Talvez eu esteja faltando alguma coisa, mas o que poderia nos salvar de termos um valor inválido? Quero dizer se nós

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

e se some_native_function não operar e não inicializar o valor? Ainda é UB? Como isso poderia ser tratado?

@Pzixel, tudo isso é coberto pela documentação da API para MaybeUninit .

Se some_native_function é um NOP, nada acontece; se posteriormente você usar foo_ref.value (ou melhor, foo_ref.as_mut() pois você só pode usar a API pública), isso é UB porque a função só pode ser chamada depois que tudo for inicializado.

MaybeUninit não evita ter valores inválidos - se pudesse, seria seguro, mas isso não é possível. No entanto, isso torna o trabalho com valores inválidos menos do que uma arma de fogo, porque agora a informação de que o valor pode ser inválido está codificada no tipo, para o compilador e o programador verem.

Eu queria documentar uma conversa IRC que tive com @sfackler a respeito de um problema hipotético que poderia surgir no futuro.

A questão principal é se mem::zeroed é uma representação válida na memória para a proposta de implementação atual de MaybeUninit<NonZeroU8> . Em minha opinião, no estado “uninit” o valor é apenas preenchimento, que o compilador pode usar para qualquer propósito, e no estado "valor", todos os valores possíveis exceto mem::zeroed são válidos (por causa de NonZero ).

Um sistema de layout de tipo futuro com empacotamento discriminante de enum mais avançado (do que temos agora) pode então armazenar um discriminante no preenchimento do estado "não inicial" / memória zerada no estado "valor". Nesse sistema hipotético, o tamanho de Option<MaybeUninit<NonZeroU8>> é 1, enquanto atualmente é 2. Além disso, nesse sistema hipotético, Some(MaybeUninit::uninitialized()) seria indistinguível de None . Acho que provavelmente podemos consertar isso alterando a implementação de MaybeUninit (mas não sua API pública) assim que mudarmos para esse sistema.

Não vejo diferença entre NonZeroU8 e &'static i32 este respeito. Ambos são tipos em que "0" não é válido . Portanto, para ambos, MaybeUninit<T>::zeroed().into_inner() é insta-UB.

Se Option<Union> pode fazer otimizações de layout depende de qual é a validade de uma união. Isso ainda não foi decidido para todos os casos, mas há um consenso geral de que para uniões que possuem uma variante do tipo () , qualquer padrão de bits é válido e, portanto, nenhuma otimização de layout é possível. Isso cobre MaybeUninit . Portanto, Option<MaybeUninit<NonZeroU8>> nunca terá tamanho 1.

há um consenso geral de que para uniões que possuem uma variante do tipo (), qualquer padrão de bits é válido e, portanto, nenhuma otimização de layout é possível.

Este é um caso especial para “sindicatos que têm uma variante do tipo ()”? A estabilização desse recurso implicitamente estabiliza essa parte da Rust ABI? Que tal um union contendo struct UnitType; ou struct NewType(()); ? E quanto a struct Padded (abaixo)? Que tal um union contendo struct Padded ?

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

Minha formulação foi terrivelmente específica porque esta é literalmente a única coisa sobre a qual tenho certeza de que temos um acordo geral. :) Acho que gostaríamos de tornar isso dependente apenas do tamanho (ou seja, todos os ZSTs receberiam isso), mas, na verdade, acho que essa variante nem deveria ser necessária e os sindicatos nunca obterão otimizações de layout por padrão (mas eventualmente os usuários podem optar por usar atributos). Mas esta é apenas minha opinião.

Teremos uma discussão apropriada para avaliar o consenso atual e talvez chegar a um acordo sobre mais coisas em uma das próximas discussões no repo UCG , e você é bem-vindo para se juntar a ela quando isso acontecer.

A estabilização desse recurso implicitamente estabiliza essa parte da Rust ABI?

Estamos falando de invariantes de validade aqui, não de layout de dados (que presumo que você se refira quando menciona a ABI). Portanto, nada disso estabilizaria qualquer ABI. Estes são relacionados, mas distintos, e de fato existe atualmente uma discussão em curso sobre a ABI dos sindicatos .

Estes são relacionados, mas distintos, e de fato existe atualmente uma discussão em curso sobre a ABI dos sindicatos.

AFAICT essa discussão é apenas sobre a representação da memória dos sindicatos e não inclui como os sindicatos são passados ​​através dos limites de funções e outras coisas que podem ser relevantes para uma ABI. Não acho que o objetivo do repo UCG seja criar um ABI para Rust.

Bem, o objetivo é definir coisas suficientes para interoperar com C. Coisas como "Rust bool e C bool são compatíveis com ABI".

Mas, de fato, para repr(Rust) , acho que não há planos para definir uma chamada de função ABI - mas isso deveria ser uma declaração explícita em qualquer forma que o documento resultante assumir, não apenas uma omissão.

Estou curioso para saber se há algum argumento contra a otimização de layout Option<Foo> onde Foo é definido assim:

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

@Kixunil, você poderia levantar isso em https://github.com/rust-rfcs/unsafe-code-guidelines/issues/13? Sua pergunta realmente não está relacionada a MaybeUninit .

Quero saber qual seção conterá variáveis estáticas sem inicialização?
Em "C" posso escrever uint8_t a[100]; em alto nível de arquivo, e sei que um símbolo será colocado na seção .bss . Se eu escrever uint8_t a[100] = {}; o símbolo será posta à seção .data (que será copiada do Flash para a RAM antes de principal).

É um pequeno exemplo em Rust que usou 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();

Qual seção será contém A e B símbolos?

PS Eu sei sobre a mutilação de símbolos, mas não importa para esta questão.

@ qwerty19106 Em seu exemplo, tanto a quanto b estarão em .bss . O LLVM trata os valores de undef , como MaybeUninit::uninitialized() , como zeros ao selecionar em qual seção uma variável irá.

Se A::new() inicializou len para 1, então b teria terminado em .data . Se static contiver qualquer valor diferente de zero, a variável entrará em .data . O preenchimento é tratado como valor zero.

Isso é o que o LLVM faz. Rust não faz promessas ~ garantias ~ (*) sobre em qual seção do linker uma variável static irá entrar. Ela apenas herda o comportamento do LLVM.

(*) A menos que você use #[link_section]

Curiosidade: em algum momento, o LLVM considerou undef como um valor diferente de zero, então a variável a em seu exemplo terminou em .data . Consulte # 41315.

Obrigado @japaric pela sua resposta. Foi muito útil para mim.

Eu tenho uma nova ideia.
Pode-se usar a seção .init_array para inicializar variáveis mut estáticas antes de chamar main .

Esta é uma prova de conceito:

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

O código de teste :

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 iniciado em 5

Exemplo completo : playground

Pergunta não resolvida :
Não consegui usar concat_idents para gerar a_init_fn e A_INIT_VAR . Parece que o # 1628 ainda não está pronto para uso.

Este teste não é muito útil. Mas pode ser útil em embarcado para inicializar estruturas complicadas (será colocado em .bss , permitindo assim a economia de FLASH ).

Por que o rustc não usa a seção .init_array ? É uma seção padronizada do formato ELF ( link ).

@ qwerty19106 Porque a vida antes de main () é considerada uma característica inadequada e foi explicitamente desinvendida da semântica de Rust.

Ok, é um bom design de idioma.

Mas em # [no_std] não temos uma boa alternativa agora (talvez eu estivesse pesquisando mal).

Podemos usar spin :: Once , mas é muito caro ( Ordering :: SeqCst em cada obtenção de referência).

Eu gostaria de ter uma verificação de tempo de compilação no incorporado .

é muito caro ( Ordering::SeqCst em cada referência obtida).

Isso não parece certo para mim. Não se supõe que todas as abstrações "uma vez" sejam relaxadas no acesso e sincronizadas na inicialização? Ou estou pensando em outra coisa?
cc @Amanieu @alexcrichton

@ qwerty19106 :

Quando você diz "embutido", está se referindo a bare-metal? É importante notar que .init_array não é, de fato, parte do formato ELF em si ¹ - Nem mesmo faz parte do System V ABI ² que o estende; apenas .init é. Você não encontrará .init_array até chegar à atualização de rascunho da ABI do System V , da qual a ABI do Linux herda.

Como resultado, se você estiver executando em bare metal, .init_array pode nem mesmo funcionar de forma confiável para seu caso de uso - afinal, ele é implementado em não bare-metal por código no carregador dinâmico e / ou libc. A menos que seu gerenciador de inicialização assuma a responsabilidade de executar o código referenciado em .init_array , ele não fará nada.

1: Consulte a página 28, figura 1-13 "Seções especiais"
2: Consulte a página 63, figura 4-13 "Seções especiais (continuação)"

@eddyb Você precisa de uma carga de Acquire no mínimo ao ler Once . Esta é uma carga normal no x86 e uma barreira load + no ARM.

A implementação atual usa load(SeqCst) , mas na prática isso gera o mesmo conjunto de load(Acquire) em todas as arquiteturas.

(Você se importaria de mover essas discussões para outro lugar? Elas não têm mais nada a ver com MaybeUninit vs mem :: uninitialized. Ambos se comportam da mesma forma que o LLVM - gerar undef. O que acontece com aquele undef posteriormente não é o assunto aqui. )

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

@eddyb Você precisa de um Acquire no mínimo ao ler o
Once . Esta é uma carga normal no x86 e uma barreira load + no ARM.

A implementação atual usa load(SeqCst) , mas na prática isso
gera o mesmo conjunto de load(Acquire) em todas as arquiteturas.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente ou visualize-o no GitHub:
https://github.com/rust-lang/rust/issues/53491#issuecomment -420825802

MaybeUninit aterrissou no mestre e estará na próxima noite. :)

https://github.com/rust-lang/rust/issues/54470 propõe o uso de Box<[MaybeUninit<T>]> em RawVec<T> . Para habilitar esta e possivelmente outras combinações interessantes com caixas e fatias com menos transmutes, talvez pudéssemos adicionar mais algumas APIs à biblioteca padrão?

Em particular, para alocar sem inicializar (acho que Box::new(MaybeUninit::uninitialized()) ainda copiaria size_of::<T>() bytes de preenchimento?):

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

Em core::slice / std::slice , pode ser usado depois de pegar uma sub-fatia:

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

Acho que Box :: new (MaybeUninit :: uninitialized ()) ainda copiaria size_of ::() bytes de preenchimento

Não deveria, e existe um teste codegen destinado a testar isso.

Os bytes de preenchimento não precisam ser copiados, pois sua representação de bits não importa (qualquer coisa que observe a representação de bits é UB de qualquer maneira).

Ok, talvez Box::new_uninit seja desnecessário? A versão da fatia é diferente, entretanto, uma vez que Box::new requer T: Sized .

Eu gostaria de defender que MaybeUninit::zeroed seja um const fn . Existem alguns usos relacionados a FFI que eu teria para ele (por exemplo, um estático que deve ser inicializado em zero) e eu acredito que outros podem achar útil. Eu ficaria feliz em oferecer meu tempo para const-ify a função zeroed .

@mjbshaw, você precisará usar #[rustc_const_unstable(feature = "const_maybe_uninit_zeroed")] para isso, já que zeroed faz coisas que não passam na verificação de min_const_fn (https://github.com/rust-lang/ rust / issues / 53555) o que significa que a constância de MaybeUninit::zeroed será instável mesmo se a função for estável.

A implementação / estabilização disso poderia ser dividida em algumas etapas, a fim de tornar o tipo MaybeUninit disponível para o ecossistema mais amplo mais cedo? As etapas podem ser:

1) adicionar MaybeUninit
2) converter todos os usos de mem :: uninitialized / zeroed e deprecate

@scottjmaddox

adicionar MaybeUninit

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

Agradável! Então, o plano é estabilizar o MaybeUninit o mais rápido possível?

A próxima etapa é descobrir por que https://github.com/rust-lang/rust/pull/54668 regride o desempenho tão mal (em alguns benchmarks). Não terei muito tempo para ver isso esta semana, porém, ficaria feliz se alguém pudesse dar uma olhada. : D

Também não acho que devemos nos apressar. Nós erramos a última API para lidar com dados não inicializados, não vamos nos apressar e estragar novamente. ;)

Dito isso, eu também prefiro não adicionar atrasos desnecessários, para que possamos finalmente descontinuar o antigo footgun. :)

Ah, e algo mais me ocorreu ... com https://github.com/rust-lang/rust/pull/54667 pousado, as APIs antigas na verdade protegem contra algumas das piores armas de fogo. Eu me pergunto se poderíamos conseguir um pouco disso por MaybeUninit também? Não está bloqueando a estabilização, mas poderíamos tentar encontrar uma maneira de fazer MaybeUninit::into_inner entrar em pânico quando chamados em um tipo desabitado. Em compilações de depuração, eu também poderia imaginar *x em pânico quando x: &[mut] T com T desabitado.

Atualização de status: para progredir com https://github.com/rust-lang/rust/pull/54668, provavelmente precisamos de alguém para ajustar a computação de layout para uniões. @eddyb deseja ser mentor, mas precisamos de alguém para fazer a implementação. :)

Acho que um método que sai do invólucro, substituindo-o por um valor não inicializado, seria útil:

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

Devo enviar isso?

@shepmaster Parece muito semelhante ao método into_inner . Talvez possamos tentar evitar a duplicação aqui?

Além disso, "substituir por" é provavelmente a imagem errada aqui, isso não deve alterar o conteúdo de self alguma. Apenas a propriedade é transferida, de modo que agora está efetivamente no mesmo estado em que estava quando foi construída não inicializada.

mude o conteúdo de self em tudo

Claro, então a implementação seria basicamente ptr::read , mas do ponto de vista do uso, eu encorajaria enquadrá-lo como a substituição de um valor válido por um valor não inicializado.

evitar duplicação

Não tenho nenhuma objeção forte, pois espero que a implementação de um chame o outro. Só não sei qual seria o estado final.

Acho que into_inner é um nome de função muito inocente. As pessoas, provavelmente sem ler os documentos com muito cuidado, ainda fazem MaybeUninit::uninitialized().into_inner() . Podemos alterar o nome para algo como was_initialized_unchecked ou de forma que indique que você só deve chamar isso depois que os dados foram inicializados?

Acho que provavelmente o mesmo se aplica a take .

Embora seja um pouco estranho, algo como unchecked_into_initialized pode funcionar?

Ou esses métodos devem ser totalmente removidos e os documentos dar exemplos com x.as_ptr().read() ?

@SimonSapin into_inner consome self que é bom.

Mas para take de @shepmaster , fazer as_mut_ptr().read() faria o mesmo ... embora, claro, por que você se importaria com um ponteiro mutável?

Que tal take_unchecked e into_inner_unchecked ?

Acho que seria um plano de backup, mas preferiria que pudesse indicar que você deve ter inicializado.

Colocar a ênfase de que ele deve ser inicializado e uma descrição do que ele faz (desembrulhar / into_inner / etc.) Em um nome fica um tanto complicado, então que tal fazer o primeiro com assert_initialized e deixar o último implícito na assinatura? Possível unchecked_assert_initialized para evitar a implicação de uma verificação de tempo de execução como assert!() tem.

Possível unchecked_assert_initialized para evitar a implicação de uma verificação de tempo de execução como assert! ().

Já diferenciamos entre suposições e afirmações por meio de intrinsics::assume(foo) vs assert!(foo) , então talvez assume_initialized ?

assume é uma API instável, um exemplo estável de assumir vs afirmar é unreachable_unchecked vs unreachable e get_unchecked vs get . Portanto, acho que unchecked é o termo certo.

Eu diria que foo_unchecked só faz sentido quando há um foo , caso contrário, a natureza pura da função sendo unsafe indica para mim que algo "diferente" está acontecendo em.

Esta malha de bicicletas é claramente da cor errada

Com esta API específica, já vimos e continuaremos a ver os programadores presumirem que a insegurança é porque "dados não inicializados são lixo, então você pode causar UB se manuseá-los sem cuidado", em vez do pretendido "é UB chamar isso em dados não inicializados, ponto final ". Não sei ao certo se um indiscutivelmente redundante ⚠️ como unchecked ajudará com isso, mas prefiro errar por ser mais desconcertante (= mais propenso a fazer com que as pessoas perguntem ou leiam os documentos muito cuidado).

@RalfJung

Acho que into_inner é um nome de função muito inocente. As pessoas, provavelmente sem ler os documentos com muito cuidado, ainda fazem MaybeUninit::uninitialized().into_inner() . Podemos alterar o nome para algo como was_initialized_unchecked ou de forma que indique que você só deve chamar isso depois que os dados foram inicializados?

Eu _ realmente _ gosto dessa idéia; Sinto fortemente que diz a coisa certa sobre a semântica e que isso é potencialmente perigoso.

@rkruppe

Colocar a ênfase de que ele deve ser inicializado e uma descrição do que ele faz (desembrulhar / into_inner / etc.) Em um nome fica um tanto complicado, então que tal fazer o primeiro com assert_initialized e deixar o último implícito na assinatura? Possível unchecked_assert_initialized para evitar a implicação de uma verificação de tempo de execução como assert!() tem.

Não tenho escrúpulos em nomes longos e pesados ​​para coisas perigosas. Se isso faz com que mais pessoas pensem duas vezes, então até was_initialized_into_inner_unchecked está totalmente certo para mim. Tornar não ergonômico (dentro do razoável) escrever código não seguro é um recurso, não um bug;)

Lembre-se de que a grande maioria das pessoas provavelmente usará um IDE com alguma forma de preenchimento automático, portanto, um nome longo é um pequeno obstáculo.

Eu não me importo com a ergonomia de usar essa função, mas acho que além de um certo ponto, os nomes tendem a ser lidos em vez de lidos, e esse nome realmente deve ser lido para entender o que está acontecendo. Além disso, espero que esta função seja discutida / explicada quase tão frequentemente quanto é realmente usada (uma vez que é relativamente nicho e muito sutil), e enquanto digitar identificadores longos no código-fonte pode ser bom (por exemplo, graças a IDEs), digitá-los fora de a memória em um sistema de bate-papo é ... menos agradável (estou meio brincando sobre esse ponto, mas só pela metade).

@shepmaster Sure; Eu uso um IDE com preenchimento automático também; mas acho que um nome mais longo com unchecked dentro, incluindo dentro de um bloco unsafe , daria pelo menos uma pausa extra.

@rkruppe

digitá-los de memória em um sistema de bate-papo é ... menos agradável (estou meio brincando sobre esse ponto, mas apenas pela metade).

Eu faria essa troca. Se um nome for um pouco especial, isso pode torná-lo ainda mais memorável. ;)

Qualquer um (ou nomes semelhantes que incluem as mesmas conotações 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ão bem por mim.

E quanto a initialized_into_inner ? Ou initialized_into_inner_unchecked , se você acha que unchecked é realmente necessário, embora eu tendo a concordar com @shepmaster que unchecked só é necessário para distinguir de alguma outra variante _checked_ do mesmo funcionalidade, onde as verificações de tempo de execução não estão acontecendo.

Ao implementar manualmente um gerador de auto-empréstimo , acabei usando ptr::drop_in_place(maybe_uninit.as_mut_ptr()) várias vezes, parece que isso funcionaria bem como um método inerente unsafe fn drop_in_place(&mut self) em MaybeUninit .

Existe um precedente com ManuallyDrop::drop .

Eu diria que foo_unchecked só faz sentido quando há um foo correspondente, caso contrário, a natureza pura da função ser insegura indica para mim que algo "diferente" está acontecendo.

Não acho que não ter uma versão segura seja um bom motivo para remover o sinal de alerta da versão não segura.

remova o sinal de aviso da versão insegura

Sendo um pouco hiperbólico, quando uma função unsafe não deveria ter _unchecked presa no final então? De que adianta ter dois avisos que dizem a mesma coisa?

Essa é uma pergunta justa. :) Mas acho que a resposta é "quase nunca", e realmente lamento que tenhamos offset como função insegura em ponteiros, o que de forma alguma expressa que é inseguro. Não precisa ser literalmente unchecked , mas IMO deve haver algo . Quando estou de qualquer maneira em um bloco inseguro e acidentalmente escrevo .offset vez de .wrapping_offset , fiz uma promessa ao compilador que não pretendia fazer.

como função insegura em ponteiros que de forma alguma expressa que é inseguro

Isso resume meu espanto neste estágio.

@shepmaster, então você não acha que é realista alguém editar o código dentro de um bloco unsafe (talvez um grande, talvez dentro de um grande unsafe fn que implicitamente tem um unsafe block), e não estar ciente de que a chamada que eles estão adicionando é unsafe ?

alguém editará o código dentro de um bloco unsafe [...] e não estará ciente de que a chamada que está adicionando é unsafe

Desculpe, não pretendia descartar essa possibilidade e ela parece real. Minha opinião é que adicionar qualificadores ao nome de uma função para indicar que ela não é segura porque o qualificador unsafe não nos ajuda parece indicar uma falha mais profunda.

Talvez seja uma falha que não podemos consertar de forma compatível com versões anteriores e adicionar palavras aos nomes é a única solução possível, mas espero que não seja o caso.

talvez um grande, talvez dentro de um grande unsafe fn que implicitamente tem um bloco unsafe

Já me perguntaram por que Rust permite que variáveis ​​sejam sombreadas, porque sombreamento é claramente uma má ideia quando você tem uma função de várias centenas de linhas. Pessoalmente, desprezo muito esses casos porque acredito que esse código é geralmente aceito como uma forma inadequada para começar.

Agora, se algum aspecto do Rust força os blocos inseguros a serem maiores do que "precisam" ser, talvez isso também indique uma questão mais fundamental.


À parte, eu me pergunto se IDEs + RLS podem identificar qualquer função marcada como insegura e destacá-la especialmente. Meu editor já destaca a palavra-chave unsafe , por exemplo.

Agora, se algum aspecto do Rust força os blocos inseguros a serem maiores do que "precisam" ser, talvez isso também indique uma questão mais fundamental.

Bem, há https://github.com/rust-lang/rfcs/pull/2585;)

À parte, eu me pergunto se IDEs + RLS podem identificar qualquer função marcada como insegura e destacá-la especialmente.

Isso seria bom! Porém, nem todo mundo só lê código em IDEs - ou seja, as revisões geralmente não são feitas em IDEs.

Agora, se algum aspecto do Rust força os blocos inseguros a serem maiores do que "precisam" ser, talvez isso também indique uma questão mais fundamental.

Acho que métodos inseguros em cadeias são um dos maiores exemplos - dividir um método no meio em uma ligação let pode ser bastante anti-ergonômico, mas a menos que você faça isso, toda a cadeia é coberta.

Não exatamente 'força', mas definitivamente 'motiva'.

l existe rust-lang / rfcs # 2585 ;)

Sim, mas não mencionei porque não ajudaria no seu caso também. As pessoas sempre podem adicionar um bloco unsafe ao redor de todo o corpo (como é mencionado nos comentários) e então você está de volta ao mesmo problema: chamadas de funções inseguras "sorrateiramente".

Nem todo mundo só lê código em IDEs, no entanto

Sim, é por isso que coloquei isso de lado. Suponho que deveria ter afirmado isso mais claramente.


Acho que meu problema é que, efetivamente , você está defendendo isso:

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

Isso permite que você veja claramente todas as funções não seguras ao ler o código. A repetição me irrita muito e indica que algo está abaixo do ideal.

Isso permite que você veja claramente todas as funções não seguras ao ler o código. A repetição me irrita muito e indica que algo está abaixo do ideal.

Compreendo. Você também pode ver essa repetição como a implementação do princípio dos 4 olhos, em que o compilador fornece dois olhos. ;)

@shepmaster Eu acho que isso está ficando um pouco fora do caminho, mas IMO o ponto original é que não está claro quais são as invariáveis desse método - ou seja, quando o código unsafe realmente não é UB , com o nome mais simples.

Concordo que "desmarcado" não é a melhor opção, mas tem precedente como "invariantes de violação fácil".

Isso me faz desejar que tivéssemos uma convenção de nomes nos moldes de initialized_or_ub .

Eu acho que isso está ficando um pouco fora do caminho

Eu estava prestes a dizer isso. Já disse minha parte (e aparentemente ninguém concorda comigo), então vou deixá-la mentir; vocês escolhem o que quiserem.

tivemos uma convenção de nomenclatura nos moldes de initialized_or_ub

Você quer dizer maybe_uninit(ialized) ? Algo que pudesse ser amplamente aplicado a um conjunto de métodos relacionados de alguma forma? 😇

Não, quero dizer como unwrap_or_else - colocando o que acontece no "caso infeliz" no nome do método.

@eddyb Ei, isso não é tão ruim ... .initialized_or_unsound talvez?

Em geral, adicionar informações de tipo a nomes de identificadores é considerado um antipadrão (por exemplo, foo_i32 , bar_mutex , baz_iterator ) porque é para isso que os tipos existem.

No entanto, quando se trata de funções, embora unsafe parte do tipo fn , adicionando _unchecked , _unsafe , _you_better_know_what_you_are_doing parece ser bastante comum.

Eu me pergunto, por que esse é o caso?

Além disso, para sua informação, há um problema (https://github.com/rust-analyzer/rust-analyzer/issues/190) em rust-analyzer para expor se as funções são unsafe ou não. Editores e IDEs devem ser capazes de enfatizar operações que requerem unsafe dentro de blocos de unsafe , que incluem não apenas chamar unsafe funções (independentemente de serem sufixadas com um identificador como, por exemplo, _unchecked ou não), mas também desreferenciando ponteiros brutos, etc.

Indiscutivelmente, rust-analyzer ainda não pode fazer isso (EDITAR: intellij-Rust meio que pode: https://github.com/intellij-rust/intellij-rust/issues/3013#issuecomment-440442306), mas se o a intenção é deixar claro que chamar isso dentro de um bloco unsafe requer unsafe , realce de sintaxe é uma alternativa possível para sufixar isso com qualquer coisa. Quer dizer, se você realmente quer isso agora, provavelmente pode adicionar o nome desta função como uma "palavra-chave" ao seu marcador de sintaxe em alguns minutos e encerrar o dia.

@gnzlbg

Em geral, adicionar informações de tipo a nomes de identificadores é considerado um antipadrão (por exemplo, foo_i32 , bar_mutex , baz_iterator ) porque é para isso que os tipos existem.

Claro, a notação húngara é geralmente considerada um antipadrão. Eu concordaria. No entanto, em geral, a segurança não é levada em consideração nessas discussões e eu acho que, dado o perigo que o UB apresenta, há boas razões para abrir uma exceção aqui.

No entanto, quando se trata de funções, embora unsafe parte do tipo fn , adicionando _unchecked , _unsafe , _you_better_know_what_you_are_doing parece ser bastante comum.

Eu me pergunto, por que esse é o caso?

Simplificando: inseguro. Quando a insegurança está envolvida, a redundância é imo seu amigo. Isso se aplica tanto ao código quanto ao hardware crítico de segurança e outros.

Além disso, para sua informação, há um problema ( analisador de ferrugem / analisador de ferrugem # 190 ) em rust-analyzer para expor se as funções são unsafe ou não. Editores e IDEs devem ser capazes de enfatizar operações que requerem unsafe dentro de blocos de unsafe , que incluem não apenas chamar unsafe funções (independentemente de serem sufixadas com um identificador como, por exemplo, _unchecked ou não), mas também desreferenciando ponteiros brutos, etc.

Indiscutivelmente, rust-analyzer ainda não pode fazer isso, mas se a intenção é deixar claro que chamar isso dentro de um bloco unsafe requer unsafe , o realce de sintaxe é uma alternativa possível ao sufixo isso com qualquer coisa.

Tudo isso é incrível. No entanto, como @RalfJung observou: "Nem todo mundo só lê código em IDEs - ou seja, as revisões geralmente não são feitas em IDEs." Parece-me improvável que o GitHub incorpore um analisador de ferrugem em sua interface do usuário para mostrar se uma função / operação chamada não é segura ou não.

Se a compensação for entre ser feio e estar sujeito ao uso incorreto (e, portanto, doentio) de unsafe , acho que devemos sempre preferir a primeira opção. Muito pode ser dito sobre fazer com que um programador _ tenha_ que fazer uma pausa e pensar consigo mesmo, "espere, estou fazendo isso certo?"

Por exemplo, se você deseja usar uma operação criptográfica insegura em Mundane, você deve:

  • Importar do módulo insecure
  • Escreva allow(deprecated) ou apenas viva com os avisos do compilador emitidos sempre que você usar essa operação
  • Escreva um código semelhante a let mut hash = InsecureSha1::default(); hash.insecure_write(bytes); ...

Tudo está documentado aqui com mais detalhes. Não acho que precisamos ser _tão_ agressivos em todas as circunstâncias, mas acho que a filosofia certa é que, se a operação é perigosa o suficiente para se preocupar, então não deve haver nenhuma maneira de um programador perder a gravidade do que eles estão fazendo.

Sugestão totalmente séria

Uma vez que estamos 95% preocupados com as pessoas que usam indevidamente esse tipo e apenas 5% com nomes longos, vamos começar renomeando o tipo para MaybeUninitialized . Os 7 caracteres extras valem a pena.

Principalmente sugestões sérias

  1. Renomeie-o para MaybeUninitializedOrUndefinedBehavior para realmente aplicá-lo aos usuários finais.

  2. Opte por este tipo para não ter métodos e tudo pode ser uma função associada, reforçando o ponto em cada chamada de função, conforme desejado:

    MaybeUninitializedOrUndefinedBehavior::into_inner(value)
    

Sugestão boba

MaybeUninitializedOrUndefinedBehaviorReadTheDocsAllOfThemYesThisMeansYou

Bem ... na verdade, ter um nome longo como MaybeUninitializedOrUndefinedBehavior no tipo parece errado para mim. É a operação .into_inner() que precisa do bom nome, porque essa é a parte potencialmente problemática que precisa de atenção extra. Não ter métodos pode ser uma boa ideia embora. MaybeUninit::initialized_or_undefined(foo) parece bastante claro.

IMO, não deveríamos sair do nosso caminho para tornar as operações inseguras não ergonômicas como esta. Precisamos de nomes ergonômicos e maneiras de escrever códigos não seguros corretos. Se o confundirmos com um monte de nomes excessivamente longos e utilitários e conversões pouco claras, isso desencorajará os usuários a escreverem códigos não seguros corretos e tornará os códigos inseguros mais difíceis de ler e validar.

Lembre-se de que a grande maioria das pessoas provavelmente usará um IDE com alguma forma de preenchimento automático, portanto, um nome longo é um pequeno obstáculo.

Até que o RLS esteja mais funcional, esse não é o meu caso, pelo menos.

Eu acho que a maioria de nós concorda que

  • Nomes mais descritivos são bons

  • Nomes menos ergonômicos são ruins

e a questão é sobre como resolver as coisas quando elas estão em tensão.

Mesmo assim, acho que into_inner em particular é um nome ruim para esse método (para usar um termo sofisticado, não está na fronteira de Pareto). A convenção geral é que temos um into_inner quando um Foo<T> contém exatamente um T e você deseja retirá-lo. Mas isso não é verdade para MaybeUninit<T> . Ele contém zero ou um T s.

Portanto, uma opção menos ruim, pelo menos, seria chamá-lo de unwrap , ou talvez unwrap_unchecked .

Eu também acho que from_initialized ou from_initialized_unchecked soam bem, embora "de" geralmente apareça nos nomes de métodos estáticos.

Talvez unwrap_initialized_unchecked ficasse bem?

Chame-o de take_initialized e torne-o &mut self vez de self . O nome deixa claro que espera que o valor interno seja inicializado. No contexto de MaybeUninit , unsafe e o fato de que ele não retorna um Option / Result também deixa claro que esta operação não está verificada.

Levar uma &mut self parece uma footgun que torna mais fácil perder a noção se você semanticamente saiu de MaybeUninit .

Nome alternativo: já que ele realmente está movendo a propriedade, assim como métodos chamados into implicam, talvez into_initialized_unchecked ?

Fazer com que ele mude parece uma arma de fogo que torna mais fácil perder a noção se você saiu semanticamente do MaybeUninit.

No entanto, é um método que foi solicitado e, desde que você controle o contrário, para que isso não aconteça duas vezes, está tudo bem.

E não parece valer a pena ter a variante emprestada e a consumidora.

Eu gosto de take_initialized , ou da variante mais explícita take_initialized_unchecked .

vamos começar renomeando o tipo para MaybeUninitialized

Alguém quer preparar um PR?

para preparar um PR?

Posso usar minhas incríveis sed habilidades desde que sugeri ;-)

Acho que seria uma melhoria chamar o método into_inner algo que enfatize que ele assume que foi inicializado, mas acho que a adição de unchecked é supérflua e inútil. Temos uma maneira de informar aos usuários que funções inseguras não são seguras: geramos um erro do compilador se eles não os envolverem em um bloco inseguro.

EDITAR: take_initialized parece bom

E quanto a assume_initialized ? Este:

  • Conecta-se ao modelo de 'obrigação de prova'
  • Visceralmente se conecta a "suposições são arriscadas"
  • Só precisa de duas palavras
  • Descreve o significado semântico da operação
  • Lê muito naturalmente
  • Muito parecido com o LLVM assume intrínseco, é UB se incorretamente assumido

Alguém quer preparar um PR?

Deixa pra lá. A equipe de libs decidiu que não valia a pena.

Existe uma razão pela qual MaybeUninit<T> não é Copy quando T: Copy ?

@tommit porque MaybeUninit<T> depende de ManuallyDrop<T> , nós, programadores, devemos garantir que o valor interno seja descartado quando nossas estruturas estiverem fora do escopo. Se ele implementar Copy , acho que pode ser mais difícil para os recém-chegados do Rust lembrar-se de descartar o valor interno T toda vez, do próprio struct ou de suas cópias. Dessa forma, pode produzir vazamentos de memória menos visíveis do que não esperamos.

@ luojia65 Não tenho certeza se essa linha de raciocínio se aplica quando T si é Copy , independentemente do que ManuallyDrop e MaybeUninit façam.

Não acho que haja razão. Ninguém pensou em adicionar #[derive(Copy)] ;)

Uma observação de talvez um aspecto um tanto sutil disso:
Eu acredito que mesmo que MaybeUninit<T> deva ser Copy quando T: Copy , MaybeUninit<T> não deveria ser Clone quando T: Clone e T não é Copy .

Sim, definitivamente não podemos simplesmente chamar clone .

Sempre esqueço que Copy: Clone ...

Tudo bem, podemos implementar Clone for MaybeUninit<T> where T: Copy base no retorno de *self .

Fiz o meu melhor para atualizar a descrição do problema com todas as perguntas que surgiram aqui. Me avise se eu perdi alguma coisa!

A documentação para ManuallyDrop::drop diz

Esta função executa o destruidor do valor contido e, portanto, o valor agrupado agora representa os dados não inicializados. Cabe ao usuário deste método garantir que os dados não inicializados não sejam realmente usados.

Alguma sugestão de como melhorar esse texto para que não possa ser confundido com o tipo de "não inicialização" que MaybeUninit trata?

Em meus termos, um ManuallyDrop<T> descartado não é mais um T seguro , mas é um T válido ... pelo menos no que diz respeito às otimizações de layout.

"obsoleto" / "inválido", talvez? Ele é inicializado .

FWIW, acho que o texto é claro (pelo menos para mim), certificando-se de que um
o objeto não cair duas vezes é um problema de “segurança”. Se tivéssemos um pequeno documento
no UCG que define “segurança”, provavelmente deve-se criar um hyperlink. Você poderia
adicionar que T deve ser "válido" e hiperlink definição "válida", mas desde que
ainda não tenho essas definições escritas em lugar nenhum ... não sei. Eu
não acho que devemos parafraseá-los em todos os documentos.

Podemos estabilizar o MaybeUninit antes de descontinuar os não inicializados?

@RalfJung Eu diria que o lugar está "mudado de". FWIW devemos usar o mesmo tipo de terminologia em std::ptr::read , mas também não está muito claro.

@bluss , nunca devemos descontinuar nada amplamente usado sem um “melhor
solução ”/“ caminho de migração ”para os usuários atuais.

Os avisos de suspensão de uso devem ser: “X está obsoleto, use Y no lugar”. Se nós
não tem um Y e X é amplamente utilizado ... então devemos considerar esperar
o aviso de suspensão de uso até que tenhamos um Y.

Caso contrário, estaríamos enviando uma mensagem muito estranha.

@cramertj "inválido" não é uma boa escolha, pois ainda (deve!) satisfazer a invariante de validade.

Se tivéssemos um pequeno documento no UCG definindo “segurança”, provavelmente deveríamos criar um hiperlink para ele. Você poderia adicionar que T deve ser uma definição "válida" e um hiperlink "válida", mas uma vez que não temos essas definições escritas em nenhum lugar ainda ...

Devemos definitivamente fazer isso quando tivermos algo: D

@RalfJung Não acho que "invariante de validade" esteja no léxico da maioria (quase todos?) Dos usuários do Rust - acho que referir-se a "dados inválidos" coloquialmente é aceitável (o ManuallyDrop<T> não é mais utilizável como a T ). Dizer que ele precisa manter certas invariáveis ​​de representação que o compilador usa para otimizações não o torna menos inválido.

Não acho que "invariante de validade" esteja no léxico da maioria (quase todos?) Dos usuários do Rust

Muito justo, o termo não é oficial (ainda). Mas devemos escolher um termo oficial para isso eventualmente, e então devemos evitar tais confrontos. Podemos dizer que dados "válidos" são o que chamo de "seguros" em minha postagem, mas então precisamos de outra palavra para o que chamo de "válidos".

@shepmaster escreveu há um tempo

Acho que um método que sai do invólucro, substituindo-o por um valor não inicializado, seria útil:

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

Acho que minha maior preocupação com isso é que, com essa função, é terrivelmente fácil copiar acidentalmente dados não copiados. Caso você precise disso, é realmente tão ruim fazer maybe_uninit.as_ptr().read() ?

Acho que posso ter sugerido que algo como take substituísse algo como into_inner . Não acho mais que seja uma boa ideia: na maioria das vezes, a restrição adicional de que into_inner consome self é realmente útil.

@RalfJung No final, todos os métodos de MaybeUninit não são seguros e são apenas invólucros de conveniência em torno de as_ptr . No entanto, espero que take seja uma das operações mais comuns, uma vez que MaybeUninit é efetivamente apenas um Option onde a tag é gerenciada externamente. Isso é útil em muitos casos, por exemplo, uma matriz em que nem todos os elementos são inicializados (por exemplo, uma tabela hash).

Em https://github.com/rust-lang/rust/pull/57045 , estou propondo adicionar duas novas operações 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
    }

Veja aquele PR para motivação e discussão.

Ao remover zeroed parece que ele é substituído apenas por MaybeUninit::zeroed().into_inner() que se torna uma forma equivalente de escrever a mesma coisa. Não há mudança prática. Com valores não iniciados, em vez disso, temos a mudança prática de todos os dados não inicializados sendo mantidos armazenados em valores do tipo MaybeUninit ou união equivalente.

Por esse motivo, consideraria manter std::mem::zeroed como está, já que é uma função amplamente usada em FFI. A suspensão de uso faria com que ele emitisse avisos altos, o que é quase o mesmo que removê-lo, e pelo menos muito irritante - isso também pode levar a um número crescente de #[allow(deprecated)] que pode ocultar outras questões mais importantes.

Este exercício de esclarecimento do modelo e das diretrizes de Rust para o código marcado com unsafe é muito útil, mas vamos evitar mudanças como para zeroed onde está apenas reformulando o mesmo efeito prático usando uma nova maneira de dizê-lo .

@bluss Meu entendimento (que pode estar errado) é que std::mem:zeroed é tão perigoso quanto std::mem::uninitialized , e tem a mesma probabilidade de resultar em UB. Talvez esteja sendo usado para inicializar matrizes de bytes, que seriam melhor inicializadas com vec![0; N] ou [0; N] , caso em que talvez uma regra rustfix pudesse ser adicionada para automatizar a alteração? Fora da inicialização de matrizes de bytes ou inteiros, no entanto, meu entendimento é que há uma boa chance de que o uso de std::mem::zeroed pode levar ao UB.

@scottjmaddox É muito fácil invocar UB com std::mem:zeroed , mas ao contrário de std::mem::uninitialized , existem alguns tipos para os quais std::mem:zeroed é perfeitamente válido (por exemplo, tipos nativos, muitos relacionados a FFI struct s, etc.). Como muitas funções unsafe , zeroed() não deve ser usado levianamente, mas não é tão problemático quanto uninitialized() . Eu ficaria triste em ter que usar MaybeUninit::zeroed().into_inner() vez de std::mem:zeroed() pois não há nenhuma diferença entre os dois em termos de segurança e a versão MaybeUninit é mais pesada e um pouco menos legível (e quando devo usar código inseguro, valorizo ​​muito a legibilidade).

@mjbshaw

ao contrário de std :: mem :: uninitialized, existem alguns tipos para os quais std :: mem: zeroed é perfeitamente válido (por exemplo, tipos nativos,

Existem alguns tipos para os quais mem::uninitialized é perfeitamente seguro ( por exemplo, unit ), enquanto existem alguns tipos "nativos" (por exemplo, bool , &T , etc. .) para o qual mem::zeroed invoca um comportamento indefinido.


Parece haver um equívoco aqui de que MaybeUninit é de alguma forma sobre memória não inicializada (e eu posso ver por que: "Não inicializada" está em seu nome).

O perigo que estamos tentando prevenir é o causado pela criação de um valor _inválido_, se o valor _inválido_ contém todos zeros, ou bits não inicializados, ou algo mais (por exemplo, bool de um padrão de bits não é true ou false ), realmente não importa - mem::zeroed e mem::uninitialized ambos podem ser usados ​​para criar um valor _inválido_ e, portanto, são quase igualmente perigoso do meu ponto de vista.

OTOH MaybeUninit::zeroed() e MaybeUninit::uninitialized() são métodos _seguros_ porque retornam union . MaybeUninit::into_inner é unsafe , e chamá-lo só é _seguro_ se a pré-condição de que os bits atuais em MaybeUninit<T> representam um valor _válido_ de T for satisfeita. Se o padrão de bits for _inválido_, o comportamento é indefinido. Não importa se o padrão de bits é inválido porque contém todos os zeros, bits não inicializados ou qualquer outra coisa.

@RalfJung Estou começando a achar que o nome MaybeUninit pode ser um pouco enganador. Talvez devêssemos renomeá-lo para MaybeInvalid ou algo parecido para transmitir melhor o problema que ele resolve e os perigos que evita. EDITAR: seguindo as sugestões de @Centril que postei sobre a questão das bicicletas.


EDIT: FWIW, acho que ter uma maneira ergonômica (por exemplo, sem usar diretamente MaybeUninit ) para criar memória zerada com segurança seria útil, mas mem::zeroed não é. Poderíamos adicionar um traço Zeroed semelhante a Default que só é implementado para tipos para os quais o padrão de bits de todos os zeros é válido ou algo parecido, como uma forma de obter um efeito semelhante ao mem::zeroed agora, mas sem suas armadilhas.

Em geral, acho que não devemos descontinuar a funcionalidade até que um caminho de migração para os usuários atuais para uma solução melhor esteja em vigor. MaybeUninit é uma solução melhor do que mem::zeroed aos meus olhos, embora possa não ser perfeito (é mais seguro, mas não é tão ergonômico), então não haveria problema em desaprovar mem::zeroed assim que MaybeUninit cair, mesmo se quando isso acontecer não tivermos um substituto mais ergonômico no lugar.

Talvez devêssemos renomeá-lo para MaybeInvalid ou algo parecido para melhor transmitir o problema que ele resolve e os perigos que evita.

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

@gnzlbg

existem alguns tipos "nativos" (por exemplo, bool

Contanto que bool seja seguro para FFI (o que geralmente é considerado, apesar do RFC 954 ter sido rejeitado e, em seguida, não oficialmente aceito), deve ser seguro usar mem::zeroed para ele.

, &T , etc.) para o qual mem::zeroed invoca um comportamento indefinido.

Sim, mas esses tipos que têm UB para mem::zeroed também têm UB para MaybeUninit::zeroed().into_inner() (tive o cuidado de incluir intencionalmente .into_inner() em meu comentário original). MaybeUninit não adiciona nada se o usuário chamar imediatamente .into_inner() (que é precisamente o que eu e muitos outros faríamos se mem::zeroed fosse preterido, porque estou usando apenas mem::zeroed para tipos que são seguros para zero).

Contanto que o bool seja seguro para FFI (o que geralmente é considerado, apesar da RFC 954 ter sido rejeitada e depois não oficialmente aceita), deve ser seguro usar mem :: zeroed para ele.

Eu não queria entrar em detalhes sobre isso, mas bool é seguro para FFI no sentido de que é definido como igual a _Bool C. No entanto, os valores true e false de C _Bool não são definidos no padrão C (embora possam ser algum dia, talvez em C20), portanto, se mem::zeroed cria um bool válido ou não é tecnicamente definido pela implementação.

Sim, mas esses tipos que têm UB para mem :: zeroed também têm UB para MaybeUninit :: zeroed (). Into_inner () (tive o cuidado de incluir intencionalmente .into_inner () em meu comentário original). MaybeUninit não adiciona nada se o usuário chamar imediatamente .into_inner () (que é precisamente o que eu e muitos outros faríamos se mem :: zeroed fosse descontinuado, porque estou usando mem :: zeroed apenas para tipos que são seguros para zero) .

Eu realmente não entendo que ponto você está tentando fazer aqui. MaybeUninit adiciona a opção de chamar ou não chamar into_inner , que mem::zeroed não tem, e há valor nisso, já que é a operação que pode introduzir um comportamento indefinido ( construir a união como não inicializada ou zerada é seguro).

Por que alguém traduziria cegamente mem::zeroed para MayeUninit + into_inner ? Essa não é a maneira apropriada de "consertar" o aviso de descontinuação de mem::zeroed , e silenciar o aviso de descontinuação tem o mesmo efeito e um custo muito menor.

A maneira apropriada de passar de mem::zeroed para MaybeUninit é avaliar se é seguro chamar into_inner , caso em que pode-se apenas fazer isso e escrever um comentário explicando por que isso é seguro, ou apenas continue trabalhando com MaybeUninit como um union até que chamar into_inner se torne seguro (pode ser necessário alterar muito código até que seja o caso, quebrar API muda para retornar MaybeUninit vez de T s, etc.).

Eu não queria entrar em detalhes sobre isso, mas bool é seguro para FFI no sentido de que é definido como igual a _Bool C. No entanto, os valores true e false de C's _Bool are not defined in the C standard (although they might be some day, maybe in C20), so whether mem :: zerado creates a valid bool` ou não é tecnicamente definido pela implementação .

Desculpas por continuar a tangente, mas C11 requer que todos os bits definidos para zero represente o valor 0 para tipos inteiros (consulte a seção 6.2.6.2 "Tipos inteiros", parágrafo 5) (que inclui _Bool ) . Além disso, os valores de true e false são definidos explicitamente (consulte a seção 7.18 "Tipo booleano e valores <stdbool.h> ").

Eu realmente não entendo que ponto você está tentando fazer aqui. MaybeUninit adiciona a opção de chamar ou não chamar into_inner , que mem::zeroed não tem, e há valor nisso, já que é a operação que pode introduzir um comportamento indefinido ( construir a união como não inicializada ou zerada é seguro).

Há um valor em MaybeUninit e MaybeUninit::zeroed . Nós dois concordamos nisso. Não estou defendendo que MaybeUninit::zeroed seja removido. Meu ponto é que também há valor em std::mem::zeroed .

Existem alguns tipos para os quais mem :: uninitialized é perfeitamente seguro (por exemplo, unidade), enquanto existem alguns tipos "nativos" (por exemplo, bool, & T, etc.) para os quais mem :: zeroed invoca um comportamento indefinido.

Este é um arenque vermelho. Só porque zeroed e uninitialized são válidos para algum subconjunto de tipos não os torna comparáveis ​​no uso real. Você precisa olhar para o tamanho desses subconjuntos. O número de tipos para os quais mem::uninitialized é válido é muito pequeno (na verdade, são apenas tipos de tamanho zero?), E ninguém realmente escreveria um código que fizesse isso (por exemplo, para ZSTs você apenas usaria o construtor de tipo). Por outro lado, existem muitos tipos para os quais mem::zeroed é válido. mem::zeroed é válido para pelo menos os seguintes tipos (espero ter entendido direito):

  • todos os tipos inteiros (incluindo bool , como mencionado acima)
  • todos os tipos de ponteiro brutos
  • Option<T> onde T aciona a otimização do layout enum. T inclui:

    • NonZeroXXX (todos os tipos inteiros)

    • NonNull<U>

    • &U

    • &mut U

    • fn -pointers

    • qualquer array de qualquer tipo nesta lista

    • qualquer struct onde qualquer campo é um tipo nesta lista.

  • Qualquer matriz struct ou union consistindo apenas em tipos nesta lista.

Sim, tanto uninitialized quanto zeroed lidam com valores potencialmente inválidos. No entanto, os programadores usam esses primitivos de maneiras muito diferentes.

O padrão comum para mem::uninitialized é:

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

Se você não está escrevendo o uso de valores não inicializados dessa maneira, provavelmente está cometendo um grande erro.

O uso mais comum de mem::zeroed hoje é para os tipos descritos acima, e isso é perfeitamente válido. Eu concordo totalmente com @bluss que não vejo nenhum ganho de prevenção de footgun substituindo mem::zeroed() todos os lugares por MaybeUninit::zeroed().into_inner() .

Para resumir, o uso comum de uninitialized é para tipos que podem ter valores inválidos. O uso comum de zeroed é para tipos que são válidos se zerados.

Um traço Zeroed ou similar (por exemplo, Pod , mas note que T: Zeroed não implica T: Pod ), como foi sugerido, parece uma coisa boa para adicionar o futuro, mas não vamos descontinuar fn zeroed<T>() -> T até que tenhamos um fn zeroed2<T: Zeroed>() -> T estável.

@mjbshaw

Desculpas por continuar na tangente, mas C11 exige que

De fato! É apenas bool do C ++ que deixa os valores válidos não especificados! Obrigado por me corrigir, vou enviar um PR para o UCG com essa garantia.

@jethrogb

Você precisa olhar para o tamanho desses subconjuntos. O número de tipos para os quais mem::uninitialized é válido é muito pequeno (na verdade, são apenas tipos de tamanho zero?), E ninguém realmente escreveria um código que fizesse isso (por exemplo, para ZSTs você apenas usaria o construtor de tipo).

Nem mesmo é correto para todos os ZSTs se você considerar a privacidade com a qual é possível ter ZSTs como uma espécie de "prova de trabalho" ou "token para recurso" ou apenas "testemunha de prova" em geral. Um exemplo 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 é uma espécie de tangente, mas não tenho certeza se seu código é realmente um exemplo de um tipo para o qual chamar uninitialized cria um valor inválido. Você está usando um código inseguro para violar as invariáveis ​​internas que Id deve sustentar. Há muitas maneiras de fazer isso, por exemplo transmute(()) ou ponteiros brutos de conversão de tipo.

@jethrogb Meus únicos pontos são que a) por favor, seja mais cuidadoso com o texto, b) privacidade não parece suficientemente fundamentada em discussões sobre o que os valores válidos são. Parece-me que "violar as invariáveis ​​internas" e "valor inválido" são a mesma coisa; há uma condição secundária aqui "se A != B então Id<A, B> está desabitado.".

Parece-me que "violar as invariáveis ​​internas" e "valor inválido" são a mesma coisa; há uma condição secundária aqui "se A != B então Id<A, B> está desabitado.".

As invariantes "impostas pelo código da biblioteca" são diferentes das invariantes "impostas pelo compilador" de várias maneiras, consulte a postagem do blog de . Nessa terminologia, seu exemplo Id tem uma invariante de segurança e mem::zeroed ou outras maneiras de sintetizar genericamente Id<A, B> não podem ser seguras , mas não é UB imediato apenas construa um Id errado com mem::zeroed ou mem::uninitialized porque Id não tem invariante de validade . Embora os autores de códigos inseguros certamente precisem manter os dois tipos de invariantes em mente, há alguns motivos pelos quais essas discussões se concentram principalmente na validade:

  • Os invariantes de segurança são definidos pelo usuário, raramente formalizados e podem ser arbitrariamente complicados, então há pouca esperança de raciocinar genericamente sobre eles ou o compilador / linguagem ajudando a manter qualquer invariante de segurança particular.
  • Romper a invariante de segurança pode ocasionalmente ser necessário (internamente em uma biblioteca de som), então mesmo se pudéssemos descartar mecanicamente mem::zeroed::<T>() base na invariante de segurança de T , podemos não querer.
  • Da mesma forma, as consequências de invariantes de validade quebrados são de algumas maneiras piores do que um invariante de segurança quebrado (menos chance de depurá-lo porque todo o inferno começa imediatamente, e muitas vezes o comportamento real resultante do UB é menos compreensível porque todo o compilador e otimizador fatores nele, enquanto a invariante de segurança só é explorada diretamente pelo código no mesmo módulo / caixa).

Depois de ler o comentário de @jethrogb , concordo que mem::zeroed não deve ser descontinuado com a introdução de MaybeUninit .

@jethrogb Small nit:

qualquer array de qualquer tipo nesta lista
qualquer estrutura onde qualquer campo é um tipo nesta lista.

Não tenho certeza se isso é um erro de digitação simples ou uma diferença semântica, mas acho que você precisa superar esses dois marcadores - não acredito que seja necessariamente o caso de None de, por exemplo, Option<[&u8; 2]> tem zeros bit a bit como uma representação válida (poderia, por exemplo, usar [0, 24601] como a representação do caso None - apenas um dos valores internos deve assumir uma representação de nicho - cc @ eddyb para me verificar isso). Duvido que façamos isso hoje, mas não parece completamente impossível que algo assim possa aparecer no futuro.

@jethrogb

O uso mais comum de mem :: zeroed hoje é para os tipos descritos acima, e isso é perfeitamente válido.

Existe uma fonte para isso?

Por outro lado, existem muitos tipos para os quais mem :: zeroed é válido.

Também existem infinitos casos em que pode ser usado incorretamente.

Eu entendo que, para aqueles que usam mem::zeroed intensa e correta, adiar a suspensão de uso até que uma solução mais ergonômica esteja disponível é uma alternativa muito atraente.

Eu prefiro reduzir ou eliminar o número de usos incorretos de mem::zeroed mesmo que isso acarrete um custo ergonômico temporário. Uma reprovação avisa os usuários que o que eles estão fazendo potencialmente invoca um comportamento indefinido (particularmente novos usuários que o usam pela primeira vez), e temos uma solução sólida para o que fazer, o que torna o aviso acionável.

Eu uso MaybeUninit frequência e é menos ergonômico do que mem::zeroed e mem::uninitialized , mas não tem sido dolorosamente anti-ergonômico para mim. Se MaybeUninit é tão doloroso quanto alguns comentários nesta discussão afirmam, então uma biblioteca e / ou RFC para uma alternativa segura mem::zeroed aparecerá em nenhum momento (nada está bloqueando ninguém aqui AFAICT).

Alternativamente, os usuários podem ignorar o aviso e continuar usando mem::zeroed , isso é com eles, nunca podemos remover mem::zeroed de libcore qualquer maneira.

Mas as pessoas que usam mem::zeroed pesadamente devem inspecionar ativamente se todos os seus usos estão corretos de qualquer maneira. Particularmente aqueles que usam mem::zeroed pesadamente, aqueles que o usam em código genérico, aqueles que o usam como uma alternativa "menos assustadora" a mem::uninitialized etc. pode ser um comportamento indefinido.

@bluss

Ao remover zerado, parece que ele é substituído apenas por MaybeUninit :: zeroed (). Into_inner () que se torna uma forma equivalente de escrever a mesma coisa. Não há mudança prática. Com os valores não iniciados, temos, em vez disso, a mudança prática de todos os dados não inicializados sendo mantidos armazenados em valores do tipo MaybeUninit ou união equivalente.

Isso é verdade quando estamos falando sobre números inteiros, mas quando olhamos, por exemplo, os tipos de referência, mem::zeroed() se torna um problema.

No entanto, concordo que é muito mais provável que as pessoas realmente percebam que mem::zeroed::<&T>() é um problema, do que pessoas percebendo que mem::uninitialized::<bool>() é um problema. Portanto, talvez faça sentido manter mem::zeroed() .

Observe, entretanto, que ainda podemos decidir que mem::uninitialized::<u32>() está bem - se permitirmos bits não inicializados em tipos inteiros, mem::uninitialized() torna-se válido para quase todos os "tipos POD". Não acho que devemos permitir isso, mas ainda temos que ter essa discussão.

O número de tipos para os quais mem :: uninitialized é válido é muito pequeno (na verdade, são apenas tipos de tamanho zero?), E ninguém realmente escreveria um código que fizesse isso (por exemplo, para ZSTs, você apenas usaria o tipo construtor).

FWIW, algum código de iterador de fatia na verdade precisa criar um ZST em código genérico sem ser capaz de escrever um construtor de tipo. Ele usa mem::zeroed() / MaybeUninit::zeroed().into_inner() para isso.

mem::zeroed() é útil para certos casos de FFI em que se espera que você zere um valor com memset(&x, 0, sizeof(x)) antes de chamar uma função C. Acho que essa é uma razão suficiente para mantê-lo indefinido.

@Amanieu Isso parece desnecessário. A construção Rust correspondente a memset é write_bytes .

mem :: zeroed () é útil para certos casos FFI

Além disso, a última vez que verifiquei, mem::zeroed era a maneira idiomática de inicializar estruturas libc com campos privados ou dependentes de plataforma.

@RalfJung O código completo em questão é geralmente Type x; memset(&x, 0, sizeof(x)); e a primeira parte não tem um grande equivalente em Rust. Usar MaybeUninit para este padrão causa muito ruído de linha (e muito pior codegen sem otimizações) quando a memória nunca é realmente inválida após memset .

Tenho uma pergunta sobre o design de MaybeUninit : Existe alguma maneira de escrever para um único campo de T contido dentro de um MaybeUninit<T> forma que você possa, com o tempo, escrever para todos os campos e terminam com um tipo válido / inicializado?

Suponha que tenhamos uma estrutura como a seguinte:

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

Gerar uma referência & mut Foo e, em seguida, gravar nela aciona o 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(); }
}

Usar um ponteiro bruto em vez de uma referência evita esse 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(); }
}

Ou existe alguma outra maneira em que esse padrão pode ser implementado sem acionar UB? Intuitivamente, parece-me que, enquanto eu não estiver lendo memória não inicializada / inválida, então tudo deve estar bem, mas vários dos comentários neste tópico me levam a duvidar disso.

Meu caso de uso para essa funcionalidade seria para um padrão de construtor no local para tipos em que alguns dos campos devem ser especificados pelo usuário (e não têm um padrão sensato), mas alguns dos campos têm um padrão valor.

Existe alguma maneira de escrever em um único campo do T contido em um MaybeUninitde forma que você pudesse, com o tempo, gravar em todos os campos e acabar com um tipo válido / inicializado?

Sim. Usar

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

Você não deve usar get_mut() para isso, é por isso que os documentos para get_mut dizem que o valor deve ser inicializado antes de chamar este método. Podemos relaxar essa regra no futuro, que está sendo discutida em https://github.com/rust-rfcs/unsafe-code-guidelines/.

@RalfJung *(uninit.as_mut_ptr()).bar = val1; arriscaria perder o valor anterior em bar , que poderia não ter sido inicializado? Eu acho que é preciso fazer

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

@scottjmaddox ah, certo. Esqueci-me de Drop . Vou atualizar o post.

De que forma essa variante de gravação em campos não inicializados exibe menos comportamento indefinido do que get_mut() ? No ponto de código onde o primeiro argumento para ptr::write é avaliado, o código criou um &mut _ para o campo interno que deve ser tão indefinido quanto a referência a toda a estrutura que de outra forma seria criada. O compilador não deveria assumir que já estava no estado inicializado?

Isso não exigiria um novo método de projeção de ponteiro que não exigisse &mut _ intermediários expostos?


Exemplo ligeiramente interessante:

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

Mas se o compilador pode assumir &mut _ como uma representação válida, ele pode simplesmente jogar fora ptr::write ? Se passarmos da declaração, o conteúdo não era 0 mas o único outro bool válido é true/1 . Portanto, pode assumir que este é um ambiente autônomo se passarmos da afirmação. Uma vez que o valor não é acessado antes, após o reordenamento podemos acabar com isso? Não parece que o llvm explora isso agora, mas não tenho certeza se isso seria garantido.


Se, em vez disso, criarmos nossos próprios MaybeUninit dentro da função, obteremos uma realidade ligeiramente diferente. No playground , em vez disso, descobrimos que ele assume que a declaração nunca pode ser acionada, presumivelmente porque assume que str::ptr::write é a única gravação em inner portanto, deve ter acontecido antes de lermos previous ? Isso parece um pouco suspeito de qualquer maneira. Para apoiar essa teoria, observe o que acontece quando você altera a escrita do ponteiro para false .


Sei que esse problema de rastreamento pode não ser o melhor lugar para essa pergunta.

@RalfJung @scottjmaddox Obrigado por suas respostas. Essas nuances são exatamente porque perguntei.
@HeroicKatora Sim, estava pensando sobre isso.

Talvez o encantamento correto seja 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()); }
}

( playground )

Eu li um comentário no Reddit (que infelizmente não consigo mais encontrar) que sugeria que lançar imediatamente uma referência a um ponteiro ( &mut foo as *mut T ) na verdade compila apenas para criar um ponteiro. No entanto, o bit *uninit_foo.as_mut_ptr() me preocupa. Posso cancelar a referência do ponteiro para a memória unitializada desta forma? Na verdade, não estamos lendo nada, mas não está claro para mim se o compilador sabe disso.

Achei que a variante unaligned de ptr::write pode ser necessária para código genérico acima de MaybeUninit<T> pois nem todos os tipos terão campos alinhados?

Não há necessidade de write_unaligned . O compilador lida com o alinhamento de campo para você. E o as *mut bool também não deveria ser necessário, já que o compilador pode inferir que ele precisa coagir o &mut a *mut . Acho que essa coerção inferida é a razão pela qual é segura / válida. Se você quiser ser explícito e fazer as *mut _ , isso também deve servir. Se você deseja salvar o ponteiro em uma variável, é necessário forçá-lo a se tornar um ponteiro.

@scottjmaddox ptr::write ainda é seguro mesmo se a estrutura for #[repr(packed)] ? ptr::write diz que o ponteiro deve estar alinhado corretamente, então presumo que ptr::write_unaligned é necessário nos casos em que você está escrevendo algum código genérico que precisa lidar com representações compactadas (embora, para ser honesto, não tenha certeza Posso pensar em um exemplo de "código genérico acima de MaybeUninit<T> " que não saberia se o campo estava alinhado corretamente ou não).

@nicoburns

que sugeriu que lançar imediatamente uma referência a um ponteiro (& mut foo como * mut T) na verdade compila apenas para criar um ponteiro.

O que ele compila é diferente da semântica que o compilador tem permissão para usar para realizar essa compilação. Mesmo que seja um ambiente autônomo em IR, ele ainda pode ter um efeito semântico, como afirmar suposições adicionais para o compilador. @scottjmaddox está correto ao @mjbshaw está tecnicamente correto sobre a segurança geral que exige ptr::write_unaligned quando o argumento é um argumento genérico desconhecido.

Não me lembro onde li isso (nomicon? Uma das postagens do blog de quase certo de que o acesso ao campo via desreferência de ponteiro bruto, referência e conversão imediata da referência em um ponteiro (por meio de coerção ou fundição) é um caso especial.

De que forma essa variante de gravação em campos não inicializados exibe menos comportamento indefinido do que get_mut ()? No ponto de código onde o primeiro argumento para ptr :: write é avaliado, o código criou um & mut _ para o campo interno que deve ser tão indefinido quanto a referência para a estrutura inteira que seria criada de outra forma. O compilador não deveria assumir que já estava no estado inicializado?

Muito boa pergunta! Essas preocupações são um dos motivos pelos quais abri https://github.com/rust-lang/rfcs/pull/2582. Com esse RFC aceito, o código que mostrei não cria um &mut , ele cria um *mut .

@mjbshaw Touché. Sim, suponho que você esteja certo sobre a possibilidade de o struct ser empacotado e, portanto, precisar de ptr::write_unaligned . Eu não havia considerado isso antes, principalmente porque ainda não usei estruturas compactadas em ferrugem. Provavelmente deve ser um fiapo cortante, se ainda não for.

Edit: Eu não vi um clippy lint relevante, então enviei um problema: https://github.com/rust-lang/rust-clippy/issues/3659

Abri um PR para cancelar o uso de mem::zeroed : https://github.com/rust-lang/rust/pull/57825

Abri um problema no repositório RFC para bifurcar a discussão sobre zeragem de memória segura, para que possamos suspender mem::zeroed em algum ponto, assim que tivermos uma solução melhor para esse problema: https://github.com / rust-lang / rfcs / issues / 2626

Seria possível estabilizar const uninitialized , as_ptr e
as_mut_ptr à frente do resto da API? Parece-me muito provável que estes
serão estabilizados como estão agora. Além disso, o resto da API pode ser construído em
topo de as_ptr e as_mut_ptr , então, uma vez estabilizado, seria possível
tem um traço MaybeUninitExt em crates.io que fornece, no estável, a API
que está sendo discutido, permitindo que mais pessoas (por exemplo, usuários estáveis ​​apenas)
dê feedback sobre isso.

Em embarcado, em vez de um alocador global (instável), usamos variáveis ​​estáticas,
muito . Sem MaybeUninit não há como ter memória não inicializada em
variáveis ​​estáticas em estável. Isso nos impede de colocar capacidade fixa
coleções em variáveis ​​estáticas e inicialização de variáveis ​​estáticas em tempo de execução, em
custo zero. Estabilizar esse subconjunto da API desbloqueará esses casos de uso.

Para lhe dar uma noção de como isso é importante para a comunidade incorporada,
[uma pesquisa] perguntando à comunidade sobre seus pontos fracos e necessidades. Estabilizador
MaybeUninit saiu como a segunda coisa mais solicitada para estabilizar (atrás
const fn com limites de traço) e, no geral, terminou em 7º lugar entre dezenas de
solicitações relacionadas a rust-lang / *. Após mais deliberação dentro do WG, encontramos
sua prioridade para, em geral, o terceiro lugar devido ao seu impacto esperado no ecossistema.

(Em uma nota mais pessoal, sou o autor de uma estrutura de simultaneidade incorporada
que se beneficiaria do uso interno de MaybeUninit (uso de memória em
os aplicativos podem ser reduzidos em 10-50% com nenhuma alteração no código do usuário). Eu
poderia fornecer um recurso de carga apenas noturno para isso, mas depois de anos de
incorporado apenas à noite e apenas recentemente tornando-o estável, sinto que
fornecer um recurso somente noturno seria a mensagem errada para enviar aos meus usuários
por isso estou esperando ansiosamente que esta API seja estabilizada.)

@japaric Isso certamente evitaria as discussões sobre nomes em torno de into_inner e amigos. No entanto, ainda estou preocupado com a discussão semântica, por exemplo, sobre as pessoas que fazem let r = &mut *foo.as_mut_ptr(); e, portanto, afirmam que têm uma referência válida, embora não tenhamos certeza de quais são os requisitos de validade das referências - ou seja, estamos ainda não tenho certeza se ter uma referência a dados inválidos é insta-UB. Para um exemplo concreto:

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

Esta discussão começou recentemente no UCG WG.

Minha esperança era que poderia estabilizar MaybeUninit em um "pacote" único e coerente com uma história adequada para dados não inicializados, de modo que as pessoas só precisassem reaprender essas coisas uma vez, em vez de liberá-lo pedaço por- peça e talvez tenha que mudar algumas das regras ao longo do caminho. Mas talvez não seja uma boa ideia, e é mais importante que façamos algo para melhorar o status quo.

Mas, de qualquer forma, acho que não devemos estabilizar nada antes de aceitarmos https://github.com/rust-lang/rfcs/pull/2582 , para que possamos pelo menos dizer às pessoas com certeza que o seguinte não é 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

(Observe que, como de costume, ! é uma pista falsa aqui, e todos os exemplos neste post serão iguais, em termos de UB, se usarmos bool vez disso.)

Minha esperança era que poderia estabilizar MaybeUninit em um "pacote" único e coerente com uma história adequada para dados não inicializados, de modo que as pessoas só precisassem reaprender essas coisas uma vez, em vez de liberá-las peça por peça e talvez ter que mudar algumas das regras ao longo do caminho.

Acho esse argumento muito convincente.

Acho que a necessidade mais imediata é ter uma mensagem clara sobre como lidar com a memória não inicializada sem UB. Se atualmente é simplesmente "usar ponteiros brutos e ptr::read_unaligned e ptr::write_unaligned ", então tudo bem, mas definitivamente precisamos de alguma maneira bem definida de obter ponteiros brutos para valores de pilha não inicializados e para campos de estrutura / tupla . rust-lang / rfcs # 2582 (mais alguma documentação) parece atender às necessidades imediatas, enquanto MaybeUninit não.

@scottjmaddox como essa RFC é, mas sem MaybeUninit qualquer boa para memória não inicializada (pilha)?

@RalfJung Suponho que depende se o seguinte é UB:

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

Minha suposição implícita era que rust-lang / rfcs # 2582 tornaria o exemplo acima válido e bem definido. Não é esse o caso?

@scottjmaddox

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

Este é o UB. Não tem nada a ver com referências.

Minha suposição implícita era que rust-lang / rfcs # 2582 tornaria o exemplo acima válido e bem definido.

Estou totalmente surpreso com isso. Essa RFC trata apenas de referências. Por que você acha que isso muda alguma coisa nos booleanos?

@RalfJung

Este é o UB. Não tem nada a ver com referências.

A documentação para mem :: uninitialized () diz:

Ignora as verificações normais de inicialização de memória de Rust fingindo produzir um valor do tipo T , sem fazer nada.

A documentação não diz nada sobre T* .

@kpp O que você está tentando dizer? Não há * e não & naquela linha de código:

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

Por que você afirma que esta linha é UB?

Porque um bool deve ser sempre true ou false , e este não é. Consulte também https://github.com/rust-rfcs/unsafe-code-guidelines/blob/master/reference/src/glossary.md#validity -and-safety-invariant.

@kpp para que essa declaração tenha comportamento definido mem::uninitialized precisaria materializar um _valid_ bool .

Em todas as plataformas atualmente suportadas, bool tem apenas dois valores _válidos, true (padrão de bits: 0x1 ) e false (padrão de bits: 0x0 ).

mem::uninitialized , entretanto, produz um padrão de bits em que todos os bits têm o valor uninitialized . Este padrão de bits não é 0x0 nem 0x1 , portanto, o bool resultante é _inválido_ e o comportamento é indefinido.

Para definir o comportamento, precisaríamos mudar a definição de bool para suportar três valores válidos: true , false ou uninitialized . Não podemos, no entanto, fazer isso, porque o T-lang e o T-compilador já fizeram RFC que bool é idêntico ao _Bool e não podemos quebrar essa garantia (isso permite bool para ser usado portavelmente em C FFI).

Discutivelmente, C não tem exatamente a mesma definição de validade que Rust, mas C "representações de armadilhas" chegam muito perto. Em resumo, não há muito que se possa fazer em C com _Bool cujo valor não representa true ou false sem invocar um comportamento indefinido.

Se você estiver correto, o seguinte código de segurança também deve ser UB:

let x: bool;
x = true;

O que obviamente não é.

Se você estiver correto, o seguinte código seguro também deve ser UB:

let x: bool; não inicializa x para um padrão de bits uninitialized , ele não inicializa x alguma. O x = true; inicializa x (nota: se você não inicializar x antes de usá-lo, você obterá um erro de compilação).

Isso é diferente do comportamento de C, onde, dependendo do contexto, _Bool x; inicializa x com um valor _indeterminado_.

Não, aí o compilador sabe que x não foi inicializado.

O problema com mem::uninitialized é que ele está inicializando uma variável, no que diz respeito ao rastreamento de inicialização do compilador.

let x: bool; não reserva por si mesmo nenhum espaço para x ser armazenado, apenas reserva um nome. let x = foo; reserva algum espaço e inicializa-o usando foo . let x: bool = mem::uninitialized(); reserva 1 byte de espaço para x mas deixa-o não inicializado, e isso é um problema.

Esta é uma maneira tão fácil de disparar sua API projetada para a perna que deve ser documentada em mem :: uninitialized e intrinsics :: uninit com uma especialização para mem :: uninitializedentrar em pânico durante a compilação.

Isso também significa que inicializar qualquer struct com um bool nele com mem :: uninitialized é UB também?

@kpp

Isso também significa que inicializar qualquer struct com um bool nele com mem :: uninitialized é UB também?

Sim - como você provavelmente está descobrindo, mem::uninitialized torna trivial atirar no próprio pé, eu diria que é quase impossível de usar corretamente. É por isso que estamos tentando descontinuá-lo em favor de MaybeUninit , que é um pouco mais prolixo de usar, mas tem a vantagem de que, por ser uma união, você pode inicializar valores "por partes" sem realmente materializar o próprio valor em um estado _invalid_. O valor só precisa ser totalmente _válido_ no momento em que se chama into_inner() .

Você pode estar interessado em ler as seções do nomicon sobre inicialização marcada e não verificada (des): https://doc.rust-lang.org/nomicon/checked-uninit.html Eles cobrem como a inicialização let x: bool; funciona em ferrugem segura. Por favor, preencha os problemas se a explicação não for clara ou houver algo que você não entende. Também tenha em mente que a maioria das explicações lá são "não normativas", uma vez que ainda não passaram pelo processo de RFC. O WG de Diretrizes de Código Inseguro tentará enviar uma documentação RFC e garantir o comportamento atual ainda este ano.

Esta é uma maneira tão fácil de disparar a API projetada para sua perna que deve ser documentada em mem :: uninitialized e intrinsics :: uninit

O problema é que atualmente não há uma maneira correta de fazer isso - é por isso que estamos trabalhando duro para estabilizar MaybeUninit para que essas funções possam ter sua documentação substituída por um "NÃO USE" gordo.


Discussões como essa e questões como essa me fazem concordar cada vez mais com @japaric que devemos lançar algo o mais rápido possível. Basicamente, precisamos disso e dessa lista de caixas de seleção para ser marcada, eu diria. Então, temos o suficiente juntos para fornecer alguns padrões básicos.

Seria possível estabilizar const não inicializado, as_ptr e
as_mut_ptr à frente do resto da API? Parece-me muito provável que estes
serão estabilizados como estão agora.

1 para isso. Seria ótimo ter essa funcionalidade disponível no stable. Isso permitiria que as pessoas experimentassem uma variedade de diferentes APIs de nível superior (e potencialmente seguras) além desta de baixo nível. E parece que esse aspecto da API é bastante incontroverso.

Além disso, gostaria de sugerir que get_ref e get_mut nunca são estabilizados e são totalmente removidos. Normalmente, trabalhar com referências é mais seguro do que trabalhar com ponteiros brutos (e, portanto, as pessoas podem ficar tentadas a usar esses métodos sobre as_ptr e as_mut_ptr , embora sejam marcados como inseguros), mas neste caso eles são estritamente mais perigosos do que os métodos de ponteiro brutos, pois podem causar UB, enquanto os métodos de ponteiro não.

Se a regra é "nunca foi criada uma referência para a memória não inicializada", então acho que devemos ajudar as pessoas a cumprir essa regra, tornando possível criar tal referência explicitamente fazendo isso, em vez de ter um método auxiliar que o faça internamente .

Assumindo https://github.com/rust-lang/rfcs/pull/2582 , estamos completamente certos de que (1) não é nem UB, embora (2) seja, e (1) também contém um derefencing de um ponteiro que aponta para memória não inicializada?

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

E em caso afirmativo, qual é a lógica por trás disso (esperançosamente podemos colocar um pouco da discussão sobre este assunto na documentação do MaybeUninit)? Estou supondo algo como porque em (1) o valor não referenciado sempre permanece um "rvalue" e nunca se torna um "lvalue", enquanto em (2) o bool inválido se torna um "lvalue" e, portanto, realmente tem que ser materializado na memória (Não tenho certeza de qual é o termo correto para isso em Rust, mas já vi esses termos usados ​​para C ++).

E os outros acham que valeria a pena criar um RFC para a sintaxe de acesso ao campo em ponteiros brutos que avaliam diretamente em um ponteiro bruto para o campo, a fim de evitar essa confusão em primeiro lugar?

Se a regra for "nunca foi criada uma referência à memória não inicializada"

Não acho que deva ser a regra, mas pode ser. Está sendo discutido agora no UCG.

estamos completamente certos de que (1) não é nem UB, embora (2) seja, e (1) também contém um derefencing de um ponteiro que aponta para uma memória não inicializada?

Boa pergunta! Mas sim, estamos - basicamente por necessidade de cisalhamento. Pense em &mut foo as *mut bool como &raw mut foo , uma expressão atômica do tipo *mut bool . Não há nenhuma referência aqui, apenas um ptr bruto para a memória não inicializada - e isso está definitivamente correto.

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

Este é o UB. Não tem nada a ver com referências.

Minha suposição implícita era que rust-lang / rfcs # 2582 tornaria o exemplo acima válido e bem definido.

Estou totalmente surpreso com isso. Essa RFC trata apenas de referências. Por que você acha que isso muda alguma coisa nos booleanos?

@RalfJung Suponho que pensei que não era UB porque o valor indefinido não era observável porque foi substituído imediatamente por um valor bool válido. Mas acho que não é esse o caso?

Para exemplos mais complicados, nos quais o valor em x implementa Drop, um ponteiro bruto seria necessário para sobrescrever o valor, e é por isso que pensei que rfc 2582 era necessário para evitar UB.

Suponho que pensei que não era UB porque o valor indefinido não era observável porque foi substituído imediatamente por um valor bool válido. Mas acho que não é esse o caso?

A semântica prossegue declaração por declaração (olhando para o MIR). Cada afirmação deve fazer sentido. let x: bool = mem::uninitialized(); materializa um booleano ruim e não importa o que aconteça depois - você não deve materializar um booleano ruim.

Eu entendo que o valor de x é inválido, mas isso exige um comportamento indefinido? Eu posso ver como isso poderia, em geral, tirado do contexto. Mas, no contexto desse exemplo específico, o comportamento não está bem definido? Suponho que meu problema básico é que não entendo totalmente o significado de "comportamento indefinido".

Queremos que o compilador seja capaz de confiar em certos invariantes. Essas são invariantes apenas se sempre forem válidas . Quando começamos a adicionar exceções, fica uma bagunça.

Talvez você esteja esperando algo mais na forma " inspecionar um valor requer que a invariante de validade seja mantida". Aqui, "inspecionar" um bool seria usá-lo em um if . Essa é uma especificação razoável, mas menos útil: agora o compilador tem que provar que o valor é realmente "inspecionado" antes de poder assumir a invariante.

isso exige um comportamento indefinido?

Escolhemos o que é e o que não é um comportamento indefinido. Isso faz parte do projeto de uma linguagem. O comportamento indefinido dificilmente é "necessário" per se - mas é necessário permitir mais otimizações. Portanto, a arte aqui é encontrar uma definição de comportamento indefinido (por mais contraditório que pareça ^^) que possibilite as otimizações desejadas e esteja em conformidade com as expectativas (inseguras) dos programadores.

Não entendo totalmente o significado de "comportamento indefinido".

Eu escrevi uma postagem no blog sobre isso , mas a resposta curta é que o comportamento indefinido é um contrato entre você e o compilador - e o contrato diz que é sua obrigação garantir que nenhum comportamento indefinido ocorra. É uma obrigação de prova. "desreferenciar um ponteiro NULL é UB" é equivalente a dizer "toda vez que um ponteiro é desreferenciado, espera-se que o programador prove que esse ponteiro não pode ser NULL". Isso ajuda o compilador a entender o código, porque toda vez que um ponteiro é desreferenciado, o compilador agora pode deduzir "aha! Aqui o programador provou que o ponteiro não é NULL e, portanto, posso usar essa informação para otimizações e geração de código. Obrigado , programador! "

O que exatamente o contrato diz depende da linguagem de programação. Existem restrições, é claro (por exemplo, somos restringidos pelo LLVM). No nosso caso, o UCG acredita (de acordo com o que ouvimos das equipes de linguagem e compiladores) que queremos que o contrato contenha a seguinte cláusula: "Cada vez que um rvalue é criado, o programador deve provar que este rvalue sempre será satisfazer a invariante de validade. " Não há nenhuma lei da física ou dos computadores que nos obrigue a ter essa cláusula no contrato, mas é considerado um meio-termo razoável entre muitas opções diferentes.

Em particular, emitimos informações para o LLVM que não poderíamos emitir legitimamente com um contrato mais fraco. Poderíamos decidir mudar o que dizemos ao LLVM, é claro - mas se a escolha for entre "código inseguro deve usar MaybeUninit sempre que lidar com memória não inicializada" e " todo código pode ser menos otimizado", o primeiro parece como a melhor escolha.

Tomando seu exemplo:

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

Este código é UB em rustc hoje. Se você olhar para o (não otimizado) LLVM IR para mem::uninitialized::<bool>() , isto é o que você obtém:

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

Essencialmente, essa função aloca 1 byte na pilha e, em seguida, carrega esse byte. No entanto, a carga é marcada com !range , que diz ao LLVM que o byte deve estar entre 0 <= x <2, ou seja, só pode ser 0 ou 1. LLVM assumirá que isso é verdade, e o comportamento é indefinido se esta restrição for violada.

Em resumo, o problema não é tanto as próprias variáveis ​​não inicializadas, mas o fato de que você está copiando e movendo valores que violam suas restrições de tipo.

Obrigado a ambos pela exposição! Está muito mais claro agora!

Suponho que meu problema básico é que não entendo totalmente o significado de "comportamento indefinido".

Esta série de postagens de blog (que tem exemplos bastante interessantes / assustadores na segunda postagem) é bastante útil, eu acho: http://blog.llvm.org/2011/05/what-every-c-programmer-should-know .html

Eu sinto que isso realmente precisa de uma boa documentação. A mudança aqui é provavelmente uma coisa boa por vários motivos que posso listar e provavelmente outros que não posso. Mas o uso correto de memória não inicializada (e outros usos de não seguro) pode ser incrivelmente contra-intuitivo. O Nomicon tem uma seção sobre uninitialized (que presumivelmente seria atualizada para falar sobre esse tipo), mas não parece expressar toda a complexidade do problema.

(Não que eu esteja me oferecendo para escrever tal documentação. Eu indico ... quem sabe mais sobre isso do que eu.)

Uma ideia interessante de https://github.com/rust-lang/rust/issues/55422#issuecomment -433943803: poderíamos transformar métodos como into_inner em funções, para que você tenha que escrever MaybeUninit::into_inner(foo) vez de foo.into_inner() - isso documenta muito mais claramente o que está acontecendo.

Em https://github.com/rust-lang/rust/pull/58129 Estou adicionando alguns documentos, retorne &mut T de set e renomeie into_inner para into_initialized .

Acho que depois disso, e assim que https://github.com/rust-lang/rust/pull/56138 for resolvido, poderíamos prosseguir com a estabilização de partes da API (os construtores, as_ptr , as_mut_ptr , set , into_initialized ).

Por que não é MaybeUninit::zeroed() a const fn ? ( MaybeUninit::uninitialized() é um const fn )

EDIT: pode realmente ser feito um const fn usando ferrugem noturno?

Por que não é MaybeUninit::zeroed() a const fn ? ( MaybeUninit::uninitialized() é um const fn )

@gnzlbg Eu tentei , mas requer um dos seguintes:

A única coisa que me preocupa mais em relação à estabilização em breve é ​​a total falta de feedback das pessoas que realmente usam esse tipo. Parece que todos estão esperando que isso se estabilize antes de começarem a usá-lo. Isso é um problema, porque significa que notaremos problemas de API tarde demais.

@ rust-lang / libs quais são as condições usuais sob as quais você usará uma função em vez de um método? Estou pensando se algumas das operações aqui devem ser funções para que as pessoas tenham que escrever, por exemplo, MaybeUninit::as_ptr(...) . Estou preocupado que isso vá explodir o código e torná-lo ilegível - mas, OTOH, algumas funções em ManuallyDrop fizeram exatamente isso.

@RalfJung Meu entendimento é que métodos são evitados em coisas que não seguem parâmetros genéricos, para evitar métodos ocultos do tipo do usuário - portanto, ManuallyDrop::take .

Como MaybeUninit<T> nunca será Deref<Target = T> , acho que os métodos são apropriados aqui.

Peça feedback e você receberá. Usei MaybeUninit para implementar uma nova funcionalidade em std recentemente.

  1. Em sys / sgx / ext / arch.rs eu o uso em combinação com o assembly embutido. Na verdade, usei get_mut incorretamente, pensando que referências e ponteiros brutos seriam equivalentes (corrigidos em 928efca1). Eu já estava em um bloqueio inseguro, então não notei realmente a diferença no início.
  2. Em sys / sgx / rwlock.rs , estou usando-o para garantir que o padrão de bits de um const fn new() seja o mesmo de um inicializador de array em um arquivo de cabeçalho C. Estou usando zeroed seguido por set para tentar ter certeza de que os bits “não me importo” são 0. Não sei se este é o uso correto, mas parece funcionar bem .
  1. Eu ficaria muito confuso se out.get_mut() as *mut _ ! = out.as_mut_ptr() . Parece realmente C ++ ish. Espero que seja consertado de alguma forma.

Qual é o objetivo de get_mut() ?

Uma coisa que eu estava me perguntando recentemente era se MaybeUninit<T> tinha a garantia de ter o mesmo layout de T , e se algo assim poderia ser usado para inicializar parcialmente valores no heap e então transformá-lo em um totalmente valor inicializado, por exemplo, algo como ( playground 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)
};

de acordo com Miri, esse exemplo funciona (embora, agora, eu perceba que não sei se a transmutação de caixas de tipos com layout idêntico é por si só correta).

@ Nemo157 por que você precisa do mesmo layout de memória quando você tem into_inner ?

@Pzixel para evitar a cópia do valor após a inicialização, imagine que ele contém um buffer de 100 MB que causará um estouro de pilha se alocado na pilha. Embora, ao escrever um caso de teste , pareça que isso requer uma API extra fn uninit_boxed<T>() -> Box<MaybeUninit<T>> para permitir a alocação de uma caixa não inicializada sem tocar na pilha.

Usando a sintaxe box para permitir a alocação do espaço de heap não inicializado, você pode ver que transmutar assim funciona, enquanto a tentativa de usar into_initialized causa um estouro de pilha: playground

@ Nemo157 Talvez seja melhor aplicar o compilador para otimizar a cópia? Acho que deveria funcionar de qualquer maneira, mas pode haver um atributo para garantir que a compilação o faça.

@ Nemo157

Uma coisa que eu estava me perguntando recentemente era se MaybeUninit<T> tinha a garantia de ter o mesmo layout de T , e se algo assim poderia ser usado para inicializar parcialmente valores no heap e então transformá-lo em um totalmente valor inicializado,

Acredito que isso é garantido e que seu código é válido, com algumas ressalvas:

  • Dependendo do tipo que você está usando (e principalmente no código genérico), você pode precisar de ptr::write_unaligned .
  • Se houver mais campos e apenas alguns deles forem inicializados, você não deve transmutar para T até que todos os campos sejam totalmente inicializados .

Este também é um caso de uso no qual estou interessado, pois acredito que poderia ser combinado com uma macro proc para fornecer uma abstração de construtor in-loco segura.

@Pzixel Se tiver o mesmo layout de memória, você pode evitar copiar toda a estrutura de dados depois de construí-la. É claro que o compilador pode eliminar a cópia e isso pode não importar para estruturas pequenas. Mas é definitivamente bom ter.

@nicoburns sim, eu vejo agora. Estou apenas falando que pode haver algum atributo, por exemplo, #[same_layout] ou #[elide_copying] , ou ambos, ou outra coisa, para garantir que funcione da mesma maneira que transmute . Ou talvez mude a implementação de into_constructed para evitar cópias extras. Eu esperava que este fosse um comportamento padrão, não apenas para caras espertos que lêem a documentação sobre layout. Quer dizer, tenho meu código que chama into_constructed e recebo uma cópia extra, mas @ Nemo157 apenas chama transmute e ele está bem. Não há razão para que into_constructed não possa fazer a mesma coisa.

Eu ficaria muito confuso se out.get_mut() as *mut _ ! = out.as_mut_ptr() . Parece realmente C ++ ish. Espero que seja consertado de alguma forma.

Qual é o objetivo de get_mut() ?

Fiz um ponto semelhante acima de que get_mut() e get_ref() são potencialmente confusos / tornam mais fácil invocar acidentalmente um comportamento indefinido (porque dão a ilusão de serem alternativas mais seguras para as_ptr() e as_mut_ptr() , mas são na verdade menos seguros do que esses métodos).

Eu acredito que eles não estão no subconjunto da API que @RalfJung propôs estabilizar (ver: https://www.ralfj.de/blog/2019/02/12/all-hands-recap.html)

@RalfJung Em relação à sua proposta para um método ptr::freeze() (https://www.ralfj.de/blog/2019/02/12/all-hands-recap.html):

Faria sentido ter um método semelhante para construir MaybeUninit ? ( MaybeUninit::frozen() , MaybeUninit::abitrary() ou semelhante). Intuitivamente, parece que tal memória teria um desempenho tão bom quanto a memória não inicializada para muitos casos de uso, sem ter o custo de gravar na memória como zeroed . Talvez pudesse até ser recomendado em vez do construtor uninitialized menos que as pessoas realmente tenham certeza de que precisam de memória não inicializada?

Nessa nota, quais são os casos de uso em que você realmente precisa de memória "não inicializada" em vez de memória "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.

Notado. A razão pela qual algumas pessoas propõem isso é que pode ser útil declarar &mut ! desabitado (como em, ter esse valor é UB). No entanto, com MaybeUninit::<!>::uninitiailized().get_mut() , criamos esse valor. É por isso que as_mut_ptr é menos perigoso - evita a criação de uma referência.

@nicoburns (Observe que freeze não é ideia minha, apenas fiz parte da discussão e gostei muito da proposta.)

Eu acredito que eles _não_ estão no subconjunto da API que @RalfJung propôs estabilizar

Corrigir. E, de fato, talvez não devêssemos simplesmente tê-los.

Faria sentido ter um método semelhante para construir MaybeUninit ? ( MaybeUninit::frozen() , MaybeUninit::abitrary() ou semelhante).

Sim! Eu ia propor adicionar isso assim que um MaybeUninit básico estiver estável e ptr::freeze tiver pousado.

Nessa nota, quais são os casos de uso em que você realmente precisa de memória "não inicializada" em vez de memória "congelada"?

Isso precisa de mais estudos e benchmarking, a expectativa é que isso possa custar desempenho porque o LLVM não fará otimizações que poderia fazer de outra forma.

(Voltarei aos outros comentários também, assim que tiver tempo.)

@Pzixel ser capaz de construir objetos diretamente na memória pré-alocada não é trivial, Rust tinha duas RFCs aceitas para implementar tal coisa (mais de 4 anos atrás!), Mas desde então não foram aceitas e a maior parte da implementação foi removida (exceto a sintaxe box que usei acima). Se você quiser mais detalhes, o tópico i.rl.o sobre a remoção seria o melhor lugar para começar.

Como @nicoburns menciona, MaybeUninit poderia ser usado como um bloco de construção para uma solução baseada em biblioteca menos ergonômica para o mesmo problema, muito útil como uma forma de começar a experimentar o conceito e ver que tipo de APIs ele permite construir. Isso depende apenas se MaybeUninit pode fornecer as garantias necessárias para a construção de tal solução.

@ Nemo157 Sugiro apenas usá-lo em um lugar, nada para lidar com caso genérico não trivial.

@jethrogb Muito obrigado! Parece que a API funciona bem para você agora?

2. Em sys / sgx / rwlock.rs , estou usando-o para garantir que o padrão de bits de const fn new() seja o mesmo de um inicializador de array em um arquivo de cabeçalho C.

Uau, isso é loucura. ^^ Mas acho que deve funcionar, é um const fn sem argumentos afinal, então sempre deve retornar a mesma coisa ...

Uma coisa que eu estava me perguntando recentemente era se MaybeUninit<T> tinha a garantia de ter o mesmo layout de T e se algo assim poderia ser usado para inicializar parcialmente valores no heap e então transformá-lo em um valor inicializado

Na lista de coisas que devemos adicionar eventualmente está algo como

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

que transmuta o Box .

Mas sim, acho que devemos permitir tais transmutações. Existe precedente para dizer nos documentos "você pode transmutar isso da seguinte maneira"? Acho que geralmente preferimos adicionar métodos auxiliares em vez de pessoas fazendo seus próprios transmutes.

  • Dependendo do tipo que você está usando (e principalmente no código genérico), você pode precisar de ptr::write_unaligned .

No código genérico, você não pode acessar os campos. Eu acho que se você pode acessar os campos, você geralmente sabe se a estrutura está compactada e, se não estiver, ptr::write é bom o suficiente. (Não use a atribuição, porque isso pode cair! Eu sempre me esqueço disso ...)

Embora, ao escrever um caso de teste , pareça que isso requer uma API extra fn uninit_boxed<T>() -> Box<MaybeUninit<T>> para permitir a alocação de uma caixa não inicializada sem tocar na pilha.

Isso é um bug , mas como esse bug pode ser difícil de consertar, também pode ser uma boa ideia oferecer um construtor separado para isso. Não tenho certeza de como implementá-lo, no entanto. E provavelmente também queremos algo como zeroed_box que evita zerar um slot de pilha e, em seguida, memcpying, e assim por diante ... Não gosto de toda essa duplicação. : /

Portanto, eu proporia que após / em paralelo à estabilização inicial, algumas pessoas que têm casos de uso para memória não inicializada no heap (basicamente, misturando Box e MaybeUninit ) se reúnam e projetem o mínimo possível extensão de API para isso. @eddyb também expressou interesse nisso. Isso não está realmente relacionado a apenas descontinuar mem::uninitialized , então eu acho que isso deve ter seu próprio lugar para discussão, fora desses (já muito grandes) problemas de rastreamento.

Meu próprio feedback: geralmente fico feliz com MaybeUninit<T> . Eu não tenho grandes reclamações. É menos uma footgun do que mem::uninitialized , o que é bom. Os métodos const new e uninitialized são bons. Eu gostaria que mais métodos fossem constantes, mas, pelo que entendi, muitos deles requerem mais progresso em const fn em geral antes que possam ser feitos const .

Eu gostaria de uma garantia mais forte do que "mesmo layout" para T e MaybeUninit<T> . Gostaria que fossem compatíveis com ABI (efetivamente, #[repr(transparent)] , embora eu saiba que esse atributo não pode ser aplicado a sindicatos) e seguros para FFI (ou seja, se T for seguro para FFI , então MaybeUninit<T> deve ser seguro para FFI). (Tangencialmente, gostaria que pudéssemos usar #[repr(transparent)] em uniões que têm apenas um campo de tamanho positivo (como podemos para structs))

Na verdade, conto com a ABI de MaybeUninit<T> em meu projeto para ajudar na otimização (mas não de uma forma insegura, então não entre em pânico). Fico feliz em entrar em detalhes se alguém estiver interessado, mas vou manter este comentário breve e omitir os detalhes por enquanto.

@mjbshaw Obrigado!

Gostaria de poder usar #[repr(transparent)] em uniões que têm apenas um campo de tamanho positivo (como podemos para structs).

Uma vez que esse atributo existe, adicioná-lo a MaybeUninit seria um acéfalo. E, de fato, a lógica para isso já foi implementada em rustc ( MaybeUninit<T> de-facto é compatível com ABI com T , mas não garantimos isso.)

Basta alguém escrever uma RFC e vê-la passar, e adicionar algumas verificações que garantam que repr(transparent) unions tenham apenas um campo não-ZST. Você gostaria de tentar? : D

Basta alguém escrever um RFC e vê-lo passar, e adicionar algumas verificações para garantir que repr(transparent) unions tenham apenas um campo não-ZST. Você gostaria de tentar? : D

@RalfJung Peça e receberá!

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

Isso deixa apenas a API, acho que podemos estabilizar razoavelmente em maybe_uninit , e move o resto para portas de recursos separadas.

Ok, todos os PRs preparatórios chegaram e into_inner se foi.

No entanto, eu realmente gostaria que https://github.com/rust-lang/rfcs/pull/2582 fosse aceito antes da estabilização, caso contrário, não temos nem mesmo uma maneira de inicializar uma estrutura campo a campo - e esse parece ser um caso de uso principal para MaybeUninit . Estamos muito próximos de ter todas as caixas necessárias para o FCP iniciar.

Acabei de converter meu código para usar MaybeUninit . Existem alguns lugares onde eu poderia ter usado um método take que funciona em &mut self vez de self . Atualmente, estou usando x.as_ptr().read() mas acho que x.take() ou x.take_initialized() seria muito mais claro.

@Amanieu Parece muito semelhante ao método into_inner . Talvez possamos tentar evitar a duplicação aqui?

😉

O método take de Option tem outra semântica. x.as_ptr().read() não altera o valor interno de x, mas Option::take tenta substituir o valor. Pode ser enganoso para mim.

@ qwerty19106 x.as_ptr().read() em um MaybeUninit _semantically_ retira o valor e deixa o invólucro não inicializado novamente, apenas acontece que o valor não inicializado deixado para trás tem o mesmo padrão de bits que o valor que foi retirado .

No momento estou usando x.as_ptr().read() mas acho que x.take() ou x.take_initialized() seria muito mais claro.

Acho isso curioso, você poderia explicar por quê?

Na minha opinião, um método semelhante a take é um tanto enganoso porque, ao contrário de take e into_initialized , ele não protege contra pegadas duas vezes. Na verdade, para Copy tipos (e de fato para Copy valores como None as Option<Box<T>> ), pegar duas vezes é completamente bom! Então, a analogia com take realmente não é válida, da minha perspectiva.

Poderíamos chamá-lo de read_initialized() , mas nesse ponto estou seriamente me perguntando se isso é de fato mais claro do que as_ptr().read() .

x.as_ptr().read() em um MaybeUninit _semantically_ remove o valor e deixa o invólucro não inicializado novamente, apenas acontece que o valor não inicializado deixado para trás tem o mesmo padrão de bits do valor que foi retirado.

MaybeUninit realmente não tem uma invariante semântica útil, então não tenho certeza se concordo totalmente com essa afirmação. TBH Não estou convencido de que seja útil considerar as operações em MaybeUninit de qualquer outra forma que não apenas seu efeito operacional bruto.

@RalfJung hmm, talvez "semanticamente" seja a palavra errada aqui. Em termos de como um usuário deve usar o tipo, você deve assumir que o valor não foi inicializado novamente depois de lê-lo (a menos que você saiba concretamente que o tipo é Copy ).

Se você olhar apenas para o efeito operacional bruto, obterá interações estranhas como essa, em que pode violar invariantes de segurança de outras APIs inseguras sem ler tecnicamente a memória não inicializada. (Eu esperava que Miri ainda rastreasse leituras de comprimento 0 de memória não inicializada, mas não parece).

@RalfJung Em todos os meus casos, isso envolve um static mut no qual um valor é colocado e, posteriormente, retirado. Como não posso consumir um estático, não posso usar into_uninitialized .

@Amanieu, o que eu estava perguntando é: por que você acha que x.take_initialized() é mais claro do que x.as_ptr().read() ?

@ Nemo157

Eu esperava que Miri ainda rastreasse 0 comprimento de leituras de memória não inicializada, mas não parece que

Uma leitura de comprimento 0 de memória não inicializada nunca é UB, então por que Miri se importaria com isso?

Se você olhar apenas para o efeito operacional bruto, obterá interações estranhas como essa, em que pode violar invariantes de segurança de outras APIs inseguras sem ler tecnicamente a memória não inicializada.

Claro, você pode violar invariantes de segurança sem nunca ler a memória não inicializada. Você também pode usar MaybeUninit::zeroed().into_initialized() para isso. Eu não vejo o problema.
A "interação estranha" aqui é que você criou dois valores de um tipo que você não tinha o direito de criar. Isso é tudo sobre a invariante de segurança de Spartacus e não tem nada a ver com invariantes de validade.

É por isso que acho que read_initialized() transmite melhor o que acontece: lemos os dados e afirmamos que eles foram inicializados corretamente (o que inclui garantir que realmente temos permissão para criar esse valor naquele tipo). Isso não tem efeito no padrão de bits ainda armazenado em MaybeUninit .

@RalfJung Estou essencialmente tratando MaybeUninit como um Option , mas sem a tag. Na verdade, eu estava usando anteriormente a caixa de opções sem etiqueta exatamente para esse propósito e ela tem um método take para extrair o valor da união.

@Amanieu @shepmaster Eu adicionei read_initialized em https://github.com/rust-lang/rust/pull/58660. Ainda acho que é um nome melhor do que take_initialized . Isso atende às suas necessidades?

Esse PR também adiciona exemplos a alguns dos outros métodos, feedback bem-vindo!

Estou feliz com read_initialized .

Enquanto fazia isso, também ganhei MaybeUninit<T>: Copy if T: Copy . Não parece um bom motivo para não fazer isso.

Hm, talvez get_initialized fosse um nome melhor? Afinal, isso complementa set .

Ou talvez set deva ser renomeado para write ? Isso também alcançaria consistência.

Tenho convertido meu código para usar MaybeUninit e descobri que trabalhar com fatias não inicializadas é muito anti-ergonômico. Acho que isso poderia ser melhorado se tivéssemos funções para o seguinte:

  • Conversão segura de &mut [T] em &mut [MaybeUninit<T>] . Isso efetivamente permite que &out parâmetros sejam emulados usando &mut [MaybeUninit<T>] , que é útil, por exemplo, para read .
  • Conversão insegura de &mut [MaybeUninit<T>] em &mut [T] (e o mesmo para &[T] ), a ser usada uma vez que tenhamos chamado .set em cada elemento da fatia.

As APIs que tenho se parecem com isto:

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

Concordo que trabalhar com fatias não é ergonômico atualmente, e é por isso que adicionei first_ptr e first_ptr_mut . Mas provavelmente está longe de ser a melhor API.

No entanto, eu preferiria que pudéssemos nos concentrar em enviar a "API principal" primeiro e, em seguida, olhar para a interação com as fatias (e com Box ).

Gosto da ideia de renomear set para write , fornecendo consistência com ptr::write .

Na mesma linha, read_initialized realmente melhor do que apenas read ? Se a preocupação for sobre o uso acidental que fica oculto, talvez torne-o uma função em vez de um método, ou seja, MaybeUninit::read(&mut v) ? O mesmo poderia ser feito para write , ou seja, MaybeUninit::write(&mut v) para consistência. A compensação em ambos os casos é entre usabilidade e explicitação, e se explicitação é considerada melhor em um caso, não vejo por que seria diferente no outro.

Independentemente disso, até que essas APIs sejam elaboradas, eu apóio fortemente a estabilização com uma API mínima, ou seja, new , uninitialized , zeroed , as_ptr , as_mut_ptr e talvez get_ref e get_mut .

e talvez get_ref e get_mut .

Eles só devem ser estabilizados depois que resolvermos https://github.com/rust-rfcs/unsafe-code-guidelines/issues/77 , e parece que pode demorar um pouco ...

estabilizando com uma API mínima, ou seja, new , uninitialized , zeroed , as_ptr , as_mut_ptr

Meu plano era into_initialized , set / write e read_initialized fazer parte desse conjunto mínimo. Mas talvez não devesse ser? set / write e read_initialized podem ser facilmente implementados com o resto, então agora estou inclinado a não estabilizá-los no primeiro lote. Mas ter algo como into_initialized desde o início é desejável, IMO.

talvez torná-lo uma função em vez de um método, ou seja, MaybeUninit::read(&mut v) ? O mesmo poderia ser feito para write , ou seja, MaybeUninit::write(&mut v) para consistência.

Pelo que foi discutido aqui antes, usamos apenas a abordagem de função explícita para evitar problemas com Deref instâncias. Não acho que devemos introduzir precedência por outro motivo para usar uma função em vez de um método.

é read_initialized realmente melhor do que apenas read ?

Boa pergunta! Eu não sei. Isso era para simetria com into_initialized . Mas into_inner é um método comum em que se pode perder a visão geral sobre o tipo de chamada, read é muito menos comum. E talvez devesse ser apenas initialized vez de into_initialized ? Tantas opções ...

Pelo que foi discutido aqui antes, usamos apenas a abordagem de função explícita para evitar problemas com Deref instâncias. Não acho que devemos introduzir precedência por outro motivo para usar uma função em vez de um método.

Exceto que ptr::read e ptr::write são funções, não métodos. Portanto, a precedência já está definida em favor de MaybeUninit::read e MaybeUninit::write .

Edit : Ok, aparentemente há read e write métodos em ponteiros também ... Nunca percebi isso antes ... Mas eles consomem o ponteiro, o que realmente não faz sentido para MaybeUninit .

Tantas opções ...

Acordado. Até que haja muito mais derramamento de bicicletas nos outros métodos, acho que apenas new , uninitialized , zeroed , as_ptr , as_mut_ptr estão realmente prontos para a estabilização.

Exceto ptr::read e ptr::write são funções, não métodos. Portanto, a precedência já está definida

Eles não fazem parte de uma estrutura de dados, é claro que são funções independentes. E, como você observa, hoje em dia eles também existem como métodos.

Mas eles consomem o ponteiro

Os ponteiros brutos são Copy , então nada é realmente consumido.

Os ponteiros brutos são Copy , então nada é realmente consumido.

Bom ponto ...

Bem, v.as_ptr().read() já é bastante conciso e claro. O as_ptr seguido por read deve fazer com que ele se destaque como algo para se pensar cuidadosamente, muito mais do que into_initialized . Pessoalmente, sou a favor de apenas expor as_ptr e as_mut_ptr , pelo menos por enquanto. E, new , uninitialized e zeroed , é claro.

@Amanieu Que tal algo mais parecido com o que Cell tem, onde há conversões seguras para &mut MaybeUninit<[T]> de e para &mut [MaybeUninit<T>] ?

Isso permitiria o seguinte, o que parece bastante natural para mim:

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

Também parece que representa com mais precisão a semântica para o chamador. A função assumindo um &mut [MaybeUninit<T>] pareceria, para mim, como se pudesse ter alguma lógica distinta para quais estão ok e quais não são. Por outro lado, tomar &mut MaybeUninit<[T]> expressa que não vai fazer distinção entre as células quando se trata de quais dados já estão nelas.

(Os nomes dos métodos estão, obviamente, sujeitos a bicicletas - eu apenas imitei o que Cell faz.)

@eternaleye MaybeUninit<[T]> não é um tipo válido porque as uniões não podem ser DSTs.

Mm certo

Até que haja muito mais derramamento de bicicletas nos outros métodos, acho que apenas new , uninitialized , zeroed , as_ptr , as_mut_ptr estão realmente prontos para a estabilização.

Bem, acho que devemos aceitar este RFC antes de estabilizar qualquer coisa - caso contrário, não temos nem mesmo uma forma autorizada de inicializar uma estrutura campo a campo, o que parece ser o mínimo.

Enquanto esperamos pelos experimentos , podemos pedalar um pouco sobre os nomes do que atualmente é chamado de set , read_initialized e into_initialized . As seguintes renomeações foram sugeridas:

  1. set -> write . A melhor metáfora para .as_ptr().read() parece ser "ler", não "obter", mas o complemento ( .as_ptr_mut().write() ) deve ser "escrever", não "definir".
  2. read_initialized -> read . Combina bem com write , mas não é seguro. Isso (mais a documentação) é um aviso suficiente para que você tenha que se certificar manualmente de que os dados já foram inicializados? Houve um consenso de que um into_inner inseguro não é suficiente, e é por isso que o renomeei para into_initialized .
  3. into_initialized -> initialized . Se tivermos read_initialized e into_initialized , isso tem uma boa consistência IMO - mas se for read , então into_initialized sobressai um pouco. O nome do método é bastante longo. Ainda assim, a maioria das operações de consumo são chamadas de into_* , pelo que eu sei.

Alguma objeção para (1)? E estou mais inclinado contra (3). Para (2) estou indeciso: read é mais fácil de digitar, mas read_initialized IMO funciona melhor ao ler esse código - e o código é lido e revisado com mais frequência do que escrito. Parece bom chamar o lugar onde realmente assumimos que as coisas foram inicializadas.

Pensamentos, opiniões?

Bem, acho que devemos aceitar este RFC antes de estabilizar qualquer coisa - caso contrário, não temos nem mesmo uma forma autorizada de inicializar uma estrutura campo a campo, o que parece ser o mínimo.

É aqui que coloco um plug-in para offset_of! ? :)

Observe que read_initialized é um superconjunto estrito de into_initialized (leva &self vez de self ). Faz muito sentido apoiar os dois?

É aqui que coloco um plug-in para offset_of! ? :)

Se você conseguir estabilizar isso antes que meu RFC seja aceito, com certeza. ;)

Faz muito sentido apoiar os dois?

IMO sim. into_initialized é mais seguro, pois evita o uso do mesmo valor duas vezes e, portanto, deve ser preferível a read_initialized sempre que possível.

Então, @nikomatsakis meio que fez isso antes, mas não o tornou um bloqueador rígido.

Acabei de portar muitos códigos para usar MaybeUninit<T> e into_initialized e acho que é desnecessariamente prolixo. O código já é muito mais detalhado do que antes, onde estava "incorretamente" usando mem::uninitialized .

Eu acho que MaybeUninit<T> deve ser chamado apenas de Uninit<T> , porque para todos os efeitos práticos, se você receber um MaybeUninit<T> desconhecido, você deve assumir que ele não foi inicializado, então Uninit<T> resumiria isso corretamente. Além disso, into_uninitialized só deve ser into_init() ou similar por razões de consistência.

Também poderíamos chamar o tipo Uninitialized<T> e o método into_initialized , mas usar uma abreviatura para o tipo e a forma longa para o método ou vice-versa é uma inconsistência dolorosa. Idealmente, eu deveria apenas lembrar que "APIs Rust usam abreviações / formulários longos" e é isso.

Como as abreviações podem ser ambíguas para pessoas diferentes, prefiro usar apenas formulários longos em todos os lugares e encerrar o dia. Mas usar uma mistura IMO é o pior dos dois mundos. Rust tende a usar abreviações com mais freqüência do que formas mais longas, então eu não teria nada contra Uninit<T> como uma abreviatura e .into_init() como outra abreviatura para o método.

Não gosto de into_initialized() , porque parece que há uma transformação em andamento para inicializar o valor. Eu prefiro muito mais take_initialized() . Sei que a assinatura de tipo é diferente de outros métodos take , mas acho que é muito mais claro, semanticamente, e acredito que a clareza semântica deve substituir a consistência emprestar / mover. Outras alternativas que ainda não têm precedência de serem mutáveis ​​podem ser move_initialized ou consume_initialized .

Quanto a set() vs write() , sou fortemente favorável a write() para invocar a semelhança com as_ptr().write() , para o qual seria um apelido.

E, finalmente, se houver um take_initialized() ou similar, então eu prefiro read_initialized() vez de read() devido à explicitação do primeiro.

Editar : mas para esclarecer, acho que ficar com as_ptr().write() e as_ptr().read() é ainda mais claro e mais provável de acionar os circuitos mentais de PERIGO, PERIGO .

@gnzlbg temos um FCP para o nome do tipo, não tenho certeza se devemos reabrir essa discussão.

No entanto, gosto da proposta de usar "init" consistentemente, como em MaybeUninit::uninit() e x.into_init() .

Não gosto de into_initialized() , porque parece que há uma transformação em andamento para inicializar o valor.

into métodos into_vec .

Estou bem com um take_initialized(&mut self) (além de um into_init), mas acho que deve reverter o estado interno para undef .

reverter o estado interno de volta

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

isso não deve mudar o conteúdo de self alguma. Apenas a propriedade é transferida, de modo que agora está efetivamente no mesmo estado em que estava quando foi construída não inicializada.

Muitas dessas coisas já foram discutidas nos mais de 200 comentários ocultos.

Muitas dessas coisas já foram discutidas nos mais de 200 comentários ocultos.

Venho acompanhando a discussão há algum tempo e posso estar enganado, mas não pense que esse ponto foi tocado antes. Em particular, o comentário que você cita não sugere "reverter o estado interno de volta para undef ", mas tornando-o equivalente a ptr::read (que é deixar o estado interno inalterado). O que estou sugerindo é o equivalente conceitual de mem::replace(self, MaybeUninit::uninitialized()) .

o equivalente conceitual de mem::replace(self, MaybeUninit::uninitialized()) .

Por causa do significado de undef , isso é equivalente a read : https://rust.godbolt.org/z/e0-Gyu

@scottmcm não, não é. Com read , o seguinte é 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);

Com o take proposto, isso seria ilegal, pois x2 seria undef .

Só porque duas funções geram a mesma montagem não significa que sejam equivalentes.

No entanto, não vejo benefício em substituir o conteúdo com undef . Isso apenas apresenta mais maneiras de as pessoas darem um tiro no próprio pé. @jethrogb você não deu nenhuma motivação, poderia explicar por que acha que essa é uma boa ideia?

Estou bem com um take_initialized(&mut self) (além de um into_init), mas acho que deve reverter o estado interno para undef .

Eu estava propondo take_initialized(self) vez de into_initialized(self) , porque acredito que o nome anterior descreve com mais precisão a operação. Novamente, eu entendo que take normalmente leva &mut self e into normalmente leva self , mas acredito que nomear semanticamente preciso é mais importante do que digitar consistentemente nomeação. Talvez um nome diferente deva ser usado, como move_initialized ou transmute_initialized .

E, novamente, quanto a v.write() e v.read_initialized() , não vejo nenhum valor positivo acima de v.as_ptr().write() e v.as_ptr().read() . Os dois últimos parecem menos prováveis ​​de serem mal utilizados.

E, novamente, quanto a v.write() e v.read_initialized() , não vejo nenhum valor positivo acima de v.as_ptr().write() e v.as_ptr().read() . Os dois últimos parecem menos prováveis ​​de serem mal utilizados.

v.write() (ou v.set() ou o que quer que seja que estamos chamando hoje em dia) é seguro. v.as_ptr().write() requer um bloco unsafe , o que é irritante. Embora eu concorde sobre v.read_init() vs v.as_ptr().read() . v.read_init() parece supérfluo.

Eu estava propondo take_initialized (self) em vez de into_initialized (self), porque acredito que o nome anterior descreve com mais precisão a operação. Novamente, eu entendo que o take normalmente leva um self & mut e normalmente leva um self, mas acredito que nomear semanticamente preciso é mais importante do que nomear consistentemente.

Eu sinto fortemente que into_init(ialized) também semanticamente é mais preciso aqui - ele consome MaybeUninit , afinal.

@mjbshaw Ah, sim, é verdade. Não percebi que ... Ok, bem, nesse caso revogo todos os meus comentários anteriores sobre set / write . Talvez set faça mais sentido; Cell e Pin já definem set métodos. A principal diferença seria que MaybeUninit::set não descartaria nenhum valor armazenado anteriormente; talvez isso ainda esteja mais perto de write ... Não sei. De qualquer forma, a documentação é bastante clara.

@RalfJung Ok, esqueça take... então. Que tal um novo nome, como move... , consume... ou transmute... ou algo assim? Acho que into_init(ialized) é muito confuso; também para mim, isso implica que o valor está sendo inicializado, quando na verdade estamos implicitamente afirmando que ele já foi inicializado.

quando, na verdade, estamos implicitamente afirmando que ele já foi inicializado.

Acho que vale a pena ressaltar novamente que a única coisa que into_init afirma é que o valor satisfaz a _validação invariante_ de T , que não deve ser confundida com T sendo "inicializado" em qualquer sentido geral da palavra.

Por exemplo:

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
}

Aqui, o valor de retorno de improperly_initialized é "inicializado" no sentido de que satisfaz a _variância de validade_ de T , mas não no sentido de que satisfaça a _invariante de segurança_ de T , e a distinção é sutil, mas importante, porque, neste caso, é essa distinção que requer que improperly_initialized seja declarado como unsafe fn .

Quando a maioria dos usuários fala sobre algo sendo "inicializado", eles normalmente não têm a semântica "válido, mas MaybeUnsafe" de MaybeUninit::into_init .

Se quiséssemos ser extremamente verborrágicos sobre isso, poderíamos ter Invalid<T> e Unsafe<T> , ter Invalid<T>::into_valid() -> Unsafe<T> e exigir que os usuários escrevessem uninit.into_valid().into_safe() . Então, acima de improperly_initialized retornaria Unsafe<T> , e somente após o usuário definir corretamente o valor de AlwaysTrue para true eles podem realmente obter o seguro T:

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

Observe que isso permite que improperly_uninitialized se torne um fn seguro, porque agora o invariante de que AlwaysTrue não é seguro não está codificado em "comentários" em torno da função, mas no tipos.

Não sei se vale a pena seguir essa abordagem dolorosamente excruciante. MaybeUninit objetivo é comprometer, permitir aos usuários lidar com memória não inicializada e inválida, mas sem colocar essas distinções na cara do usuário. Pessoalmente, acho que não podemos esperar que os usuários conheçam essas distinções, a menos que as coloquemos explicitamente em seus rostos, e é preciso conhecer essa distinção para poder usar MaybeUninit corretamente. Caso contrário, as pessoas podem escrever fn improperly_uninitialized() -> AlwaysTrue como um fn seguro e apenas retornar um AlwaysTrue inseguro porque bem, eles o "inicializaram".

Uma coisa que também se pode fazer com Invalid<T> e Unsafe<T> é ter duas características, ValidityCheckeable e UnsafeCheckeable , com dois métodos, ValidityCheckeable::is_valid(Invalid<Self>) e UnsafeCheckeable::is_safe(Unsafe<Self>) , e ter os Invalid::into_valid e Unsafe::into_safe métodos assert_validity! e assert_safety! neles.

Em vez de escrever a invariante de segurança em um comentário, você poderia apenas escrever o código para a verificação.

Acho que vale a pena ressaltar novamente que a única coisa que into_init afirma é que o valor satisfaz a invariante de validade de T, que não deve ser confundida com T sendo "inicializado" em qualquer sentido geral da palavra.

Isto está certo. OTOH, sinto que "inicializado" é um proxy razoável para isso em uma primeira explicação.

Caso contrário, as pessoas podem escrever fn improperly_uninitialized () -> AlwaysTrue como um fn seguro e apenas retornar um AlwaysTrue inseguro porque, bem, eles o "inicializaram".

Acho que podemos dizer que isso não foi "inicializado" corretamente. Concordo que precisamos de uma documentação adequada de como essas duas invariáveis ​​interagem em algum lugar (e não tenho certeza de qual seria o melhor lugar), mas também acho que a intuição da maioria das pessoas dirá que improperly_uninitialized não é um ok função para exportar. "Quebrar invariantes de outras pessoas" é um conceito que, eu acho, surge naturalmente quando você pensa sobre "todas as funções seguras que estou exportando devem ser tais que o código seguro não pode usá-las para causar estragos".

Uma coisa que também pode ser feita com inválidoe insegurotem duas características, ValidityCheckeable e UnsafeCheckeable, com dois métodos, ValidityCheckeable :: is_valid (Invalid) e UnsafeCheckeable :: is_safe (Unsafe), e têm os métodos Invalid :: into_valid e Unsafe :: into_safe assert_validity! e assert_safety! neles.

Na grande maioria dos casos, a invariante de segurança não será verificável. Mesmo a invariante de validade provavelmente não é verificável para referências. (Bem, isso depende um pouco de como fatoramos as coisas.)

@scottjmaddox

Que tal um novo nome, como mover ..., consumir ... ou transmutar ... ou algo assim? Acho que into_init (ialized) é muito confuso; também para mim, isso implica que o valor está sendo inicializado, quando na verdade estamos implicitamente afirmando que ele já foi inicializado.

Como move_init transmite uma "afirmação" mais do que into_init ?

assert_init(italized) foi sugerido anteriormente.

No entanto, observe que read ou read_initialized ou as_ptr().read também não dizem nada sobre afirmar algo.

Se quiséssemos ser extremamente verborrágicos sobre isso, poderíamos ter Invalid<T> e Unsafe<T> , ter Invalid<T>::into_valid() -> Unsafe<T> e exigir que os usuários escrevessem uninit.into_valid().into_safe() . Então, acima de improperly_initialized retornaria Unsafe<T> , e somente após o usuário definir corretamente o valor de AlwaysTrue para true eles podem realmente obter o seguro T:

@gnzlbg Ei, isso é muito legal. Gosto que isso lance a distinção no rosto do usuário de uma forma inevitável. Provavelmente é um bom momento de ensino wrt. "validade" e "segurança" que fará as pessoas pensarem duas vezes? uninit.into_valid().into_safe() não é tão prolixo de qualquer maneira em comparação com uninit.assume_initialized() ou outros enfeites. É claro que, para fazer essa distinção, precisamos primeiro encontrar um acordo em torno do modelo. 😅 Acho que devemos investigar este modelo um pouco mais.

assert_init(italized) foi sugerido anteriormente.

@RalfJung Também temos assume_initialized devido a @eternaleye (eu acho). Consulte https://github.com/rust-lang/rust/issues/53491#issuecomment -440730699 com uma lista de justificativas que são bastante convincentes.

TBH Acho que ter dois tipos é muito prolixo.

@RalfJung Podemos nos aprofundar nisso? possivelmente com algumas comparações de exemplos que você acha que mostram o alto grau de verbosidade?

Hmm ... se estamos considerando APIs mais detalhadas, então

uninit.into_inner(uninit.assert_initialized());

poderia funcionar muito bem semanticamente. O primeiro método retorna um token que registra sua asserção. O segundo método retorna o tipo interno, mas requer que você tenha declarado que é válido.

Não estou totalmente convencido de que vale a pena o esforço extra, pois a abstração pode apenas deixar as pessoas mais confusas e, portanto, propensas a cometer erros.

Também assumimos_inicializado devido a @eternaleye (eu acho). Veja # 53491 (comentário) com uma lista de justificativas que são bastante convincentes.

Justo. assume_initialized parece bom para mim.

Ou talvez seja assume_init ? Isso provavelmente deve ser consistente com o construtor, MaybeUninit::uninit() vs MaybeUninit::uninitialized() - e que se está programado para ser estabilizado com o primeiro lote, de modo que devemos fazer essa chamada em breve.

@nicoburns Não vejo o benefício que ganharíamos em adicionar uma indireção por meio de um token aqui.

Podemos cavar mais fundo nisso? possivelmente com algumas comparações de exemplos que você acha que mostram o alto grau de verbosidade?

Bem, está claro que é mais prolixo do que "apenas" MaybeUninit , certo? Há muita carga mental adicional (ter que entender dois tipos), há o desembrulhamento duplo, e isso significa que tenho que escolher qual tipo usar. Portanto, há alguns custos adicionais que acho que você precisa justificar.

Na verdade, geralmente duvido da utilidade de Unsafe . Da perspectiva do compilador, seria inteiramente um NOP; o compilador nunca assume que seus dados satisfazem a invariante de segurança. De uma perspectiva de implementação de biblioteca, duvido muito que a legibilidade do código melhore se, na implementação de Vec , transmutarmos as coisas para Unsafe<Vec<T>> sempre que violarmos temporariamente a invariante de segurança. E do ponto de vista do ensino, duvido que alguém se surpreenda ao criar um Vec<T> válido mas inseguro, atribuí-lo a algum código seguro e então tudo explodirá.
Compare isso com MaybeUninit que é necessário do ponto de vista do compilador, e onde o fato de você precisar ter cuidado com bool "ruins" em seu próprio código privado pode ser uma surpresa para alguns .

Dado seu custo significativo, acho que Unsafe precisa de uma motivação muito mais forte. Não vejo como isso realmente ajudaria a prevenir bugs ou melhorar a legibilidade do código.

Eu posso ver os argumentos para renomear MaybeUninit a MaybeInvalid . No entanto, "inválido" é extremamente vago (inválido para quê ?), Tenho visto pessoas confusas com a minha distinção entre "válido" e "seguro" - pode-se supor que um "válido Vec " é válido para qualquer tipo de uso. "não inicializado", pelo menos, aciona basicamente as associações certas para a maioria das pessoas. Talvez devêssemos renomear "invariante de validade" para "invariante de inicialização" ou assim?

Além disso, a mera presença de Unsafe<T> pode ser enganosa (por implicar erroneamente que todos os valores não incluídos nele são seguros), a menos que adotemos uma convenção amplamente difundida contra valores inseguros fora deste invólucro. Este seria um grande projeto, exigindo outro RFC e um consenso mais amplo da comunidade. Espero que seja um tanto controverso ( @RalfJung deu algumas boas razões contra isso acima), e com argumentos mais fracos do que MaybeUninit uma vez que não há UB envolvido - é essencialmente uma questão de estilo. Como tal, não acredito que tal convenção algum dia seja universal na comunidade Rust, mesmo que um RFC seja aceito e a biblioteca padrão e os documentos sejam atualizados.

Então, IMO, qualquer pessoa que quiser ver essa convenção acontecer tem coisas maiores para fritar do que andar de bicicleta na API MaybeUninit , e eu sugeriria não atrasar ainda mais sua estabilização para esperar a resolução desse processo. Se estabilizarmos as conversões de MaybeUninit<T> -> T , as gerações futuras do Rust ainda poderão gravar MaybeUninit<Unsafe<T>> para indicar dados que não foram inicializados pela primeira vez e, possivelmente, ainda não são seguros após serem inicializados.

@RalfJung

Ou talvez seja assume_init ? Isso provavelmente deve ser consistente com o construtor, MaybeUninit::uninit() vs MaybeUninit::uninitialized() - e _tesse_ está programado para ser estabilizado com o primeiro lote, então devemos fazer essa chamada em breve.

Se pudermos ter consistência de 3 vias com o tipo, construtor e a função -> T , isso seria ainda melhor. Como o tipo não tem o sufixo -ialized , acho que ::uninit() e .assume_init() é provavelmente o caminho a percorrer.

Bem, está claro que é mais prolixo do que "apenas" MaybeUninit , certo?

Depende ... Acho foo.assume_init().assume_safe() (ou foo.init().safe() , se um está inclinado a ser breve) não é tudo o que mais tempo. Também podemos oferecer a combinação de foo.assume_init_safe() se necessário. A combinação ainda tem a vantagem de definir as duas premissas.

Há muita carga mental adicional (ter que entender dois tipos), há o desembrulhamento duplo, e isso significa que tenho que escolher qual tipo usar. Portanto, há alguns custos adicionais que acho que você precisa justificar.

Esperançosamente, a complexidade vem de ter que entender os conceitos subjacentes por trás de validade e segurança. Feito isso, não acho que haja muita complexidade mental adicional. Acho que os conceitos subjacentes são importantes para transmitir isso.

Na verdade, geralmente duvido da utilidade de Unsafe . Da perspectiva do compilador, seria inteiramente um NOP; o compilador nunca assume que seus dados satisfazem a invariante de segurança.

Certo; Eu concordo que de um ponto de vista do compilador é inútil. Qualquer utilidade dessa distinção é como uma espécie de interface de "tipos de sessão".

Dado seu custo significativo, acho que Unsafe precisa de uma motivação muito mais forte. Não vejo como isso realmente ajudaria a prevenir bugs ou melhorar a legibilidade do código.

O aspecto que me chamou a atenção foi a capacidade de ensino. Eu acho que erros acontecem quando as pessoas pensam que .assume_init() significa que "OK; eu verifiquei a invariante de validade e agora tenho um bom T ". O esquema atual de MaybeUninit<T> é meio inútil dessa maneira. No entanto, não sou casado com Unsafe<T> e Invalid<T> como nomes. Eu apenas penso que a separação em dois tipos, quaisquer que sejam seus nomes, pode ser útil educacionalmente. Talvez haja outras maneiras, como reforçar a documentação, que podem compensar isso dentro da estrutura atual?

Eu _posso_ ver os argumentos para renomear MaybeUninit para MaybeInvalid . No entanto, "inválido" é extremamente vago (inválido para _what_?), Tenho visto pessoas confusas com a minha distinção entre "válido" e "seguro" - pode-se supor que um "válido Vec " é válido para qualquer tipo de uso. "não inicializado", pelo menos, aciona basicamente as associações certas para a maioria das pessoas. Talvez devêssemos renomear "invariante de validade" para "invariante de inicialização" ou assim?

Eu definitivamente concordo com "validade" e "segurança" sendo confusas devido à forma como "válido" soa. Tenho sido parcial para "invariante de máquina" como um substituto para "validade" e "invariante de sistema de tipo" para "segurança".

@rkruppe

Então, IMO, qualquer pessoa que quiser ver essa convenção acontecer tem coisas maiores para fritar do que andar de bicicleta na API MaybeUninit , e eu sugeriria não atrasar ainda mais sua estabilização para esperar a resolução desse processo. Se estabilizarmos as conversões de MaybeUninit<T> -> T , as gerações futuras do Rust ainda podem gravar MaybeUninit<Unsafe<T>> para indicar dados que não foram inicializados pela primeira vez e, possivelmente, ainda não são seguros após serem inicializados.

Bons pontos, especialmente re. MaybeUninit<Unsafe<T>> ; Você provavelmente também poderia adicionar algum alias de tipo para tornar o nome do tipo menos prolixo.

Se pudermos ter consistência de 3 vias com o tipo, o construtor e a função -> T, isso seria ainda melhor. Como o tipo não tem o sufixo -ialized, acho que :: uninit () e .assume_init () é provavelmente o caminho a seguir.

Acordado. Estou um pouco triste por perder o prefixo into , mas não vejo uma boa maneira de mantê-lo.

E quanto a read / read_init então? A semelhança com ptr::read suficiente para acionar "é melhor você ter certeza de que foi realmente inicializado"? Será read_init tem problema semelhante para into_init , onde parece que ele faz com que seja inicializado em vez de ter que como uma suposição? Deveria assume_init ser como read agora?

Esperançosamente, a complexidade vem de ter que entender os conceitos subjacentes por trás de validade e segurança. Feito isso, não acho que haja muita complexidade mental adicional. Acho que os conceitos subjacentes são importantes para transmitir isso.

Você poderia dar um exemplo de código se algo em Vec corretamente usando isso para refletir quando invariantes de Vec são violados? Acho que seria extremamente prolixo e totalmente obscuro o que realmente acontece.

Acho que adicionar um tipo como esse é a maneira errada de transmitir o conceito subjacente.

Acho que erros acontecem quando as pessoas pensam que .assume_init () significa que "OK; verifiquei a invariante de validade e agora tenho um bom T".

Acho altamente improvável que alguém diga "Inicializei este Vec<i32> escrevendo-o com 0xFF , agora ele foi inicializado, o que significa que posso empurrá-lo". Eu gostaria de ver pelo menos uma indicação, dados melhores e mais sólidos, de que isso é realmente um erro que as pessoas cometem.
Em minha experiência, as pessoas têm uma intuição bastante sólida de que, quando distribuem dados para código desconhecido ou chamam operações de biblioteca em alguns dados, as invariáveis ​​da biblioteca precisam ser mantidas.

As coisas se acalmaram um pouco aqui. E quanto ao seguinte plano:

  • Eu preparo um PR para descontinuar MaybeUninit::uninitialized e renomeio para MaybeUninit::uninit .
  • Uma vez que isso aconteça (requer atualização stdsimd, então há algum tempo aqui se as pessoas acharem que este não é o caminho a seguir), eu preparo um PR para estabilizar MaybeUninit::{new, uninit, zeroed, as_ptr, as_mut_ptr} .

Isso deixa em aberto a questão em torno de set / write , into_init[ialized] / assume_init[ialized] e read[_init[italized]] . Atualmente, inclino-me para assume_init , write e read , mas já mudei de ideia sobre isso antes. Infelizmente, não tenho uma boa ideia de como tomar uma decisão aqui.

  • Uma vez que pousou

Isso significa que haverá um período em que não haverá maneira de criar um valor não inicializado sem (a) um aviso de descontinuação ou (b) usando recursos instáveis? Essa não é uma prática sustentável.

Ao suspender o uso de algo que não planejamos remover de maneira eficaz, uma substituição estável precisa estar disponível sempre que o aviso de suspensão de uso for adicionado. Caso contrário, as pessoas apenas adicionarão uma anotação para ignorar o aviso e seguir em frente com suas vidas.

Isso significa que haverá um período em que não haverá maneira de criar um valor não inicializado sem (a) um aviso de descontinuação ou (b) usando recursos instáveis?

Estou confuso. Estou propondo descontinuar um método instável e, em vez disso, introduzir outro método instável.

Observe que eu estava falando sobre MaybeUninit::uninitialized , não mem::uninitialized .

Infelizmente, não tenho uma boa ideia de como tomar uma decisão aqui.

@RalfJung Apenas faça (e r? Me se quiser) como você fez antes com os outros PRs renomeados e se alguém contestar, podemos lidar com isso no FCP. :)

Apenas faça (e r? Me se quiser) como você fez antes com os outros PRs de renomeação e se alguém objetar, podemos lidar com isso no FCP. :)

Bem, vou esperar um pouco porque isso não precisa fazer parte da estabilização inicial.

descontinue um método instável e introduza outro método instável

Ah, entendi. Continue então.

Tudo bem, fazendo as renomeações em https://github.com/rust-lang/rust/pull/59284 :

não inicializado -> não inicializado
into_initialized -> assume_init
read_initialized -> read
definir -> escrever

Eu gosto dos novos nomes propostos. Estou um pouco preocupado com a má utilização de read , mas isso parece muito menos provável do que into_initialized sendo mal utilizada, principalmente por causa da associação com ptr::read . No geral, acho que a nova nomenclatura é totalmente aceitável para estabilização.

Eu preparo um PR para estabilizar MaybeUninit :: {new, uninit, zeroed, as_ptr, as_mut_ptr}.

Há alguma chance de isso chegar ao 1.35-beta (em aproximadamente 2 semanas)?

Estou um pouco confuso quanto a empurrar isso, dado que https://github.com/rust-lang/rfcs/pull/2582 ainda está no ar. : / Sem esse RFC, a inicialização gradual de uma estrutura ainda não é possível, mas as pessoas farão de qualquer maneira.
OTOH, MaybeUninit esperou o suficiente. E não é como se o código para inicialização gradual que as pessoas escrevem atualmente seja melhor do que o que escreveriam com MaybeUninit .

Dito isso, https://github.com/rust-lang/rust/pull/59284 ainda nem pousou, então teríamos que nos apressar para colocar isso em 1.35. TBH Prefiro esperar mais um ciclo para que as pessoas tenham pelo menos algum tempo para brincar com os novos nomes dos métodos e ver como se sentem.

Existe alguma chance de que as funções de construção em MaybeInit possam ser const ?

init e new são const . zeroed não é, precisamos de algumas extensões para o que as funções const podem fazer antes que possa ser const .

Eu queria fornecer alguns comentários sobre MaybeUninit , as mudanças reais no código podem ser vistas aqui https://github.com/Thomasdezeeuw/mio-st/pull/71. No geral, minha experiência (limitada) com a API foi positiva.

O único pequeno problema que encontrei foi que retornar &mut T em MaybeUninit::set leva a ter que usar let _ = ... (https://github.com/Thomasdezeeuw/mio-st/pull/ 71 / files # diff-1b9651542d08c6eca04e6025b1c6fd53R116), o que é um pouco estranho, mas não é um grande problema.

Também preciso adicionar APIs que gostaria ao trabalhar com matrizes unitializadas, geralmente em combinação com C.

  1. Um método para ir de &mut [MaybeUninit<T>] para &mut [T] seria bom, o usuário deve garantir que todos os valores na fatia foram devidamente inicializados
  2. Uma função ou macro inicializadora de array público, como uninitialized_array , também seria uma boa adição.

Queria fornecer alguns comentários sobre MaybeUninit

Muito obrigado!

retornar & mut T em MaybeUninit :: set leva a ter que usar let _ = ...

Porquê isso? Você pode simplesmente "descartar" os valores de retorno; na verdade, os exemplos nos documentos não fazem let _ = ... . ( write / set ainda não tem um exemplo ... mas realmente é praticamente o mesmo que read , talvez deva apenas vincular.)

foo.write(bar); funciona perfeitamente sem let .

trabalhando com matrizes unitializadas

Sim, essa é definitivamente uma área de interesse futuro.

@RalfJung

retornar & mut T em MaybeUninit :: set leva a ter que usar let _ = ...

Porquê isso? Você pode simplesmente "descartar" os valores de retorno; na verdade, os exemplos nos documentos não fazem let _ = ... . ( write / set ainda não tem um exemplo ... mas realmente é praticamente o mesmo que read , talvez deva apenas vincular.)

Eu habilitei o aviso para unused_results , então sem let _ = ... ele produziria um aviso. Esqueci que não é o padrão.

Ah, eu não sabia sobre aquele aviso. Interessante.

Isso pode ser um argumento para fazer write não retornar uma referência e fornecer um método separado para isso se houver mais demanda.

Uma macro ou função inicializadora de array público, como uninitialized_array , também seria uma boa adição.

Isso seria apenas [MaybeUninit::uninit(); EVENTS_CAP] . Consulte https://github.com/rust-lang/rust/issues/49147.

Esqueci que não é o padrão.

Isso pode ser um argumento para fazer write não retornar uma referência e fornecer um método separado para isso se houver mais demanda.

Parece um nicho? Se houver mais demanda no futuro, podemos adicionar um método que não retorna uma referência.

Parece um nicho?

Sim, existem muitos métodos que definem um valor e, em seguida, retornam uma referência mutável a ele.

@Centril Heh, acho que não vi seu comentário aqui quando escrevi isso em outro lugar: https://github.com/rust-lang/rust/issues/54542#issuecomment -478261027

Removendo as funções renomeadas antigas e obsoletas em https://github.com/rust-lang/rust/pull/59912.

Depois disso, acho que a próxima coisa a fazer é propor estabilização ...: tada:

Estou um pouco confuso sobre empurrar isso dado o quão completamente no ar o
OTOH, MaybeUninit esperou o suficiente. E não é como se o código para inicialização gradual que as pessoas escrevem atualmente seja melhor do que o que escreveriam com MaybeUninit .

Depois disso, acho que a próxima coisa a fazer é propor uma estabilização ... 🎉

@RalfJung Como está o estado da documentação aqui? Se pudermos mitigar "as pessoas farão isso de qualquer maneira" com alguns documentos claros que me ajudariam a dormir melhor ... :)

Lendo sobre os documentos de MaybeUninit , em particular o de assume_init , não está claro na seção "Segurança" que se você estiver chamando mu.assume_init() e retornando esse resultado em um cofre fn , então você também deve manter as invariáveis ​​de segurança. Antes de estabilizar, seria bom aprimorar esses documentos e fornecer trechos de invariantes de segurança fornecidos pela biblioteca que também devem ser mantidos ao usar MaybeUninit .

Como está o estado da documentação aqui? Se pudermos mitigar "as pessoas farão isso de qualquer maneira" com alguns documentos claros que me ajudariam a dormir melhor ... :)

Provavelmente adicionarei uma seção sobre inicialização gradual de structs, dizendo que isso não é suportado atualmente. Pessoas lendo isso vão ficar tipo "WTF, realmente?".

TBH Acho isso bastante frustrante. :( Acho que foi muito possível para nós sugerir alguns conselhos sobre isso agora e estou triste por não termos sido capazes de fazer isso.

não está claro na seção "Segurança" que se você está chamando mu.assume_init () e, em seguida, retornando esse resultado em um fn seguro, você também deve manter as invariáveis ​​de segurança. Antes de estabilizar, seria bom aprimorar esses documentos e fornecer trechos de invariantes de segurança fornecidos pela biblioteca que também devem ser mantidos ao usar MaybeUninit.

Basicamente, você está sugerindo transformar isso em documentos explicando toda a ideia de invariantes de tipo de dados e como eles se desenvolvem no Rust. Acho que MaybeUninit é o lugar errado para isso; isso faria parecer que essa preocupação é específica de MaybeUninit quando na verdade não é. As coisas que você está perguntando devem ser explicadas em algum lugar de nível mais alto como o Nomicon. Pretendo concentrar a documentação de MaybeUninit na questão central desse tipo. Sinta-se à vontade para expandi-los se achar que isso é útil. :)

Basicamente, você está sugerindo transformar isso em documentos explicando toda a ideia de invariantes de tipo de dados e como eles se desenvolvem no Rust.

Isso é um pouco forte ... Estou apenas sugerindo um "Oh, __ a propósito__, lembre-se que a invariante de segurança também importa" em algum lugar estratégico na documentação de MaybeUninit<T> . Não estou sugerindo que adicionemos um romance. ;) Esse romance pode residir no Nomicon, mas as chances são de que a maioria das pessoas que usam MaybeUninit<T> fará interface principalmente com a documentação da biblioteca padrão.

Tudo bem, tentei incorporar tudo isso ao PR de estabilização: https://github.com/rust-lang/rust/pull/60445

Acabei de descobrir o uso de mem::uninitialized na documentação da biblioteca de padrões, não sabia realmente onde mais observar que o último exemplo de core::ptr::drop_in_place precisa ser atualizado (também meio irônico que exibe a outra forma de UB que só seria sancionada por https://github.com/rust-lang/rfcs/pull/2582, portanto, pessoalmente, eu o removeria).

@HeroicKatora obrigado! Eu incorporei a correção para isso em https://github.com/rust-lang/rust/pull/60445.

Não podemos fazer nada a respeito do campo ref-to-unaligned no momento, mas não temos certeza se remover o documento é uma boa ideia.

Talvez adicione o traço PartialUninit (ou PartialInit ) que inicializaria os dados parcialmente com base nos metadados.

Exemplo: MODULEENTRY32W .
O primeiro campo ( dwSize ) deve ser inicializado pelo tamanho da estrutura ( 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
    }
}

Como você pensa?

@kgv Infelizmente não entendi sua sugestão. Talvez algum contexto extra explicando qual problema você está tentando resolver possa ajudar? E talvez um exemplo mais completo da solução sugerida?

@scottjmaddox fixed . Está mais claro?

@kgv qual é o problema que isso está resolvendo (ao contrário de alguém apenas escrevendo uma função auxiliar para isso)? Não vejo por que libstd tem que fazer nada aqui.

Observe que a inicialização parcial baseada em atribuição de estruturas funciona apenas para tipos que não precisam ser descartados. Caso contrário, uninit.get_mut().foo = bar deixará cair foo , que é UB.

@RalfJung O problema que estou tentando resolver - trabalho unificado com estruturas FFI, alguns campos dos quais não dependem de self (apenas Self ou não dependem de nada (constante)), por exemplo - um dos campos tem o tamanho de Self .

@kgv Tenho que concordar com @RalfJung aqui que tal caso de uso é melhor tratado por um módulo auxiliar ou caixa.

O PR de estabilização pousou, bem a tempo para o beta. :) Já se passaram cerca de 8 meses desde que comecei a examinar a situação em torno dos sindicatos e da memória não inicializada e, finalmente, temos algo que (provavelmente) será enviado em 6 semanas. Que jornada! Muito obrigado a todos que ajudaram com isso. : D

Claro, estamos longe de terminar. Há https://github.com/rust-lang/rfcs/pull/2582 para ser resolvido. libstd ainda tem alguns usos de mem::uninitialized (principalmente em código específico de plataforma) que precisam de portabilidade. A API estável que temos agora é mínima: precisamos descobrir o que fazer com read e write , e devemos criar APIs que ajudem a trabalhar com arrays e caixas de MaybeUninit . E temos muito o que explicar para mover lentamente todo o ecossistema de mem::uninitialized .

Mas chegaremos lá, ans esta primeira etapa foi provavelmente a mais importante. :)

e devemos criar APIs que ajudem a trabalhar com arrays e caixas de MaybeUninit .

@RalfJung Para esse fim; talvez agora seja a hora de começar a trabalhar em https://github.com/rust-lang/rust/issues/49147? = P

Além disso, provavelmente devemos dividir e fechar esse problema de rastreamento em favor de outros menores para os bits restantes.

Para esse fim; talvez agora seja a hora de começar a trabalhar no # 49147? = P

Você acabou de ser voluntário? ;) (Receio não ter tempo para isso.)

provavelmente deveríamos dividir e fechar esse problema de rastreamento em favor de outros menores para os bits restantes.

Vou deixar isso para os especialistas em processo. Mas tendo a concordar.

Você acabou de ser voluntário? ;) (Receio não ter tempo para isso.)

O que eu fiz ... = D - Já tenho um projeto no qual estou trabalhando, então provavelmente vai demorar um pouco. Talvez mais alguém esteja interessado? (em caso afirmativo, pule para o problema de rastreamento)

Vou deixar isso para os especialistas em processo. Mas tendo a concordar.

Esse seria eu ...;) Vou tentar dividir e fechá-lo em breve.

@RalfJung sobre sua afirmação de que let x: bool = mem::uninitialized(); é UB, a questão é por que primitivas inválidas são consideradas assim? Pelo que entendi você tem que ler um valor para observar que é inválido acionar o UB. Mas se você não ler, o que acontecerá?

Eu sinto que mesmo criar um valor é uma coisa ruim, mas eu gostaria de saber as razões pelas quais a ferrugem não permite isso de qualquer maneira? Parece que não há mal nenhum se você não observar um estado inválido. É apenas por causa dos erros iniciais ou talvez algo mais?

Existe algum caso real no compilador quando ele se baseia nessas suposições?

Por exemplo, anotamos funções como foo(x: bool) dizendo ao LLVM que x é um booleano válido. Isso torna UB passar um bool que não é true ou false mesmo que a função originalmente não olhasse para x . Isso é útil porque às vezes o compilador deseja introduzir o uso de variáveis ​​anteriormente não utilizadas (em particular, isso acontece ao mover instruções para fora dos loops sem provar que o loop foi executado pelo menos uma vez).

AFAIK também definimos (ou queremos definir) algumas dessas anotações dentro de uma função, não apenas nos limites da função. E podemos encontrar mais lugares no futuro onde essas informações podem ser úteis. Podemos ser capazes de cobrir isso com uma definição inteligente de "usando uma variável" (um termo que você usou sem defini-lo, e de fato não é fácil de definir), mas acho que quando se trata de UB em código inseguro, é importante ter regras simples onde pudermos.

Portanto, queremos ter certeza de que, mesmo em código não seguro, os tipos no código significam algo. Isso só é possível tratando a memória não inicializada de maneira adequada com um tipo dedicado, em vez da abordagem ad-hoc "yolo" de mentir para o compilador sobre o conteúdo de uma variável ("Eu alego que isso é bool , mas realmente não vou inicializá-lo ").

Por exemplo, anotamos funções como foo (x: bool) informando ao LLVM que x é um booleano válido. Isso torna UB passar um bool que não é verdadeiro ou falso, mesmo se a função originalmente não olhar para x. Isso é útil porque às vezes o compilador deseja introduzir o uso de variáveis ​​anteriormente não utilizadas (em particular, isso acontece ao mover instruções para fora dos loops sem provar que o loop foi executado pelo menos uma vez).

Isso pode ser considerado um uso. Estou perguntando sobre como iniciar um valor e nunca lê-lo / transmiti-lo a qualquer lugar antes de ser substituído por um valor válido.
Não vejo nenhum caso de uso útil para iniciar o valor de uma forma tão matizada, mas apenas imaginando.

Resumindo, minha pergunta é se esse código é UB (de acordo com a documentação - é isso) e, em caso afirmativo, o que exatamente pode quebrar se eu escrever assim?

let _: bool = unsafe { mem::unitialized };

Outra questão sobre o assunto em si: sabemos que temos box sintaxe que permite alocar memória diretamente no heap, e funciona sempre ao contrário de Box::new() que às vezes stackalloc memória. Portanto, se eu fizer box MaybeUninit::new() e preencher, como poderia converter Box<MaybeUninit<T>> em Box<T> ? Devo escrever transmutos ou o quê? Talvez eu simplesmente tenha esquecido esse ponto da documentação.

@Pzixel , na verdade, discutimos as interações entre Box e MaybeUninit já neste tópico : sorria:

@Centril tendo um sub-problema para discutir que pode ser bom quando você dividir isso.

Sim, eu me lembro dessa discussão, mas não me lembro de nenhuma API específica.

Em suma, eu quero ter algo como

fn into_inner<A,T>(value: A<MaybeUninit<T>>) -> A<T> { unsafe { std::mem::transmute() } }

Mas não acho que exista essa API e parece que ela não poderia ser implementada sem o suporte do compilador neste ponto da evolução da linguagem.


Pensei um pouco mais e parece que deveria funcionar em qualquer nível de aninhamento. Então Vec<Result<Option<MaybeUninit<u8>>>> deve ter into_inner método que retorna Vec<Result<Option<u8>>>

Eu estava assumindo que get_ref e get_mut seriam estabilizados ao mesmo tempo (todos os recursos apontam para este problema). Existe uma razão para não o fazer? Eles são legais e são a única indicação de que executar a ação que realizam é permitido (o que obviamente deve ser verdade).

Isso pode ser considerado um uso.

Então let x: bool = mem::uninitialized() não está usando o bool (mesmo que seja atribuído a x !), Mas

fn id(x: bool) -> bool { x }
let x: bool = id(mem::uninitialized());

usa? A respeito

fn uninit() -> bool { mem::uninitialized() }
let x: bool = uninit();

O retorno aqui é uma utilidade?

Isso rapidamente se torna muito sutil. Portanto, a resposta que acho que deveríamos dar é que toda atribuição (na verdade, toda cópia, como em, toda atribuição após a redução para MIR) é um uso, e isso inclui a atribuição em let x: bool = mem::uninitialized() .


Eu estava assumindo que get_ref e get_mut seriam estabilizados ao mesmo tempo (todos os recursos apontam para este problema). Existe uma razão para não o fazer? Eles são legais e são a única indicação de que executar a ação que realizam é ​​permitido (o que obviamente deve ser verdade).

Isso está bloqueado ao resolver https://github.com/rust-lang/unsafe-code-guidelines/issues/77 : é seguro ter um &mut bool que aponta para uma memória não inicializada? Acho que a resposta deveria ser "sim", mas as pessoas discordam.

Isso está bloqueado ao resolver rust-lang / unsafe-code-guidelines # 77

Eu não acho que o bloqueio precisa acontecer. Você pode estabilizá-lo e dizer "é UB usar isso se a memória não foi inicializada" e depois suavizar o requisito se determinarmos que está tudo bem. É um bom método de pós-inicialização.

e depois suavizar o requisito

O que significa que se eu codificar contra a documentação da versão futura, mas alguém compilar meu código usando a versão antiga (compatível com API!) Do compilador, agora existe o UB?

@Gankro

Eu não acho que o bloqueio precisa acontecer. Você pode estabilizá-lo e dizer "é UB usar isso se a memória não foi inicializada" e depois suavizar o requisito se determinarmos que está tudo bem. É um bom método de pós-inicialização.

Isso me parece muito feio. Por que não escrever apenas &mut *foo.as_mut_ptr() ? Depois de ter tudo inicializado, por que isso não funcionaria? IOW, agora estou pensando se você está dizendo

a única indicação de que executar a ação que executam é permitida

porque por que não seria? Se listarmos exaustivamente tudo o que você pode fazer depois de inicializar o valor, será uma longa lista. ^^

@shepmaster

O que significa que se eu codificar contra a documentação da versão futura, mas alguém compilar meu código usando a versão antiga (compatível com API!) Do compilador, agora existe o UB?

Isso é verdade hoje se as pessoas fizerem &mut *foo.as_mut_ptr() . Não vejo como evitá-lo.

Além disso, só haverá UB se realmente tivermos que mudar alguma coisa ao fazer essa documentação. Caso contrário, estamos na situação estranha em que teria havido UB se o mesmo código fosse executado com o mesmo compilador antes de darmos a garantia, mas agora que damos a garantia de que não há mais UB. UB é uma propriedade não apenas do compilador, mas também da especificação, e a especificação pode ser alterada retroativamente. ;)

Certo, eu estava assumindo que o processo foi

  • estabilizá-lo com um requisito estrito, mas sem sentido de implementação agora
  • continue a trabalhar no modelo de memória e o que você
  • uma vez que o modelo está pronto

    • se precisar ser UB, legal, deixe os documentos iguais, adicione otimizações se for útil

    • se não precisa ser UB, legal, tire dos documentos e dê um basta no

@RalfJung

O retorno aqui é uma utilidade?

Sim, retornar um valor ou passá-lo para qualquer lugar é um uso.

Isso rapidamente se torna muito sutil. Portanto, a resposta que acho que deveríamos dar é que toda atribuição (na verdade, toda cópia, como em, toda atribuição após a redução para MIR) é um uso, e isso inclui a atribuição em let x: bool = mem :: uninitialized ().

Parece válido.

De qualquer forma, isso é sobre aninhamento de MaybeUninit arbitrário? Ele poderia ser transmutado com segurança sem exigir que o usuário grave o transmutar para cada tipo de invólucro?

@Pzixel Não tenho certeza se entendi sua pergunta, mas acho que ela está sendo discutida em https://github.com/rust-lang/rust/issues/61011.

Eu vi que o método MaybeUninit::write() ainda não estabilizado não é unsafe embora possa pular a queda de chamadas em um T já presente, que eu teria assumido como inseguro. Existe precedente para que isso seja considerado seguro?

https://doc.rust-lang.org/nomicon/leaking.html#leaking
https://doc.rust-lang.org/nightly/std/mem/fn.forget.html

forget não está marcado como unsafe , porque as garantias de segurança do Rust não incluem uma garantia de que os destruidores sempre funcionarão.

Podemos adicionar um método MaybeUninit<T> -> NonNull<T> a MaybeUninit ? AFAICT o ponteiro retornado por MaybeUninit::as_mut_ptr() -> *mut T nunca é nulo. Isso reduziria a rotatividade de ter que fazer interface com APIs que usam NonNull<T> , de:

let mut x = MaybeUninit<T>::uninit();
foo(unsafe { NonNull::new_unchecked(x.as_mut_ptr() });

para:

let mut x = MaybeUninit<T>::uninit();
foo(x.ptr());

o ponteiro retornado por MaybeUninit :: as_mut_ptr () -> * mut T nunca é nulo.

Isto está certo.

Geralmente (e acho que já vi @Gankro dizer isso), NonNull funciona muito bem "em repouso", mas ao usar ponteiros realmente deseja-se obter um ponteiro bruto o mais rápido possível. Isso é muito mais legível.

No entanto, adicionar um método que retorna NonNull parece correto. Como deveria ser chamado? Existe precedência?

Há precedente com https://github.com/rust-lang/rust/issues/47336, mas o nome não é bom e não tenho certeza se vamos estabilizar esse método.

A execução da cratera mencionada em https://github.com/rust-lang/rust/pull/60445#issuecomment -488818677 aconteceu?

A ideia de 3 meses de tempo disponível que o @centril menciona não se materializa para pessoas que querem estar livres de avisos em todo o beta, estável e noturno. 1.36.0 foi lançado há menos de uma semana e nightly já está emitindo avisos.

A descontinuação poderia ser adiada para 1.40.0?

Os avisos de suspensão de uso nem sempre são isolados da caixa responsável por eles. Por exemplo, quando uma caixa expõe uma macro que usa std::mem::uninitialized internamente, o uso de caixas de terceiros ainda invoca o aviso de depreciação. Percebi isso hoje quando compilei um dos meus projetos com o compilador noturno. Mesmo que o código não contenha uma única menção de uninitialized , recebi o aviso de depreciação porque ele invocou a macro implement_vertex do glium.

Executar cargo +nightly test no glium master me dá mais de 1400 linhas de saída, principalmente compostas de avisos de depreciação da função uninitialized (eu conto o aviso 200 vezes, mas provavelmente é limitado como o número que rg "uninitialized" | wc -l saídas são 561).

Quais são as preocupações restantes que bloqueiam a estabilização do restante dos métodos? Fazer tudo por meio de *foo.as_mut_ptr() torna muito tedioso e às vezes (para write ) envolve mais blocos de unsafe que o necessário.

@SimonSapin Para emular write , você pode substituir o MaybeUninit inteiro sem inseguro usando *val = MaybeUninit::new(new_val) onde val: &mut MaybeUninit<T> e new_val: T ou você pode usar std::mem::replace se você quiser o valor antigo.

@ est31 esses são pontos positivos. Eu ficaria bem em adiar a depreciação em um ou dois lançamentos.

Alguma objeção?

Já dissemos na postagem do blog de lançamento 1.36.0:

Como MaybeUninité a alternativa mais segura, começando com Rust 1.38, a função mem :: uninitialized será descontinuada.

Como tal, acho que devemos evitar flip-floppery neste, pois isso não envia uma boa mensagem e é confuso. Além disso, a data de suspensão de uso também deve ser amplamente conhecida, visto que foi mencionada na postagem do blog.

Talvez seja tarde para voltar à depreciação de uninitialized . Mas talvez pudéssemos decidir sobre uma política para apenas emitir avisos de depreciação no Nightly depois que a substituição estiver no canal Stable por algum tempo?

Por exemplo, o Firefox se comprometeu a exigir uma nova versão do Rust duas semanas após seu lançamento .

Já dissemos na postagem do blog de lançamento 1.36.0:

Eu discordo que a menção de uma data em um post de blog seja um grau tão forte. Ele está em um repositório e podemos enviar uma edição.

Como tal, acho que devemos evitar flip-floppery neste, pois isso não envia uma boa mensagem e é confuso.

"flip-floppery" é uma coisa ruim, mas mudar de ideia com base em dados e feedback não é isso.

Não me importo muito com a decisão real, mas não acredito que as pessoas ficarão confusas com a proposta. Aqueles que viram a postagem do blog ou o aviso de suspensão de uso podem passar para a novidade. Pessoas que não o fizeram não vão se importar com mais alguns lançamentos.

"flip-floppery" é uma coisa ruim, mas mudar de ideia com base em dados e feedback não é isso.

Totalmente de acordo. Não vejo uma mensagem ruim sendo enviada dizendo "ei, nosso cronograma de suspensão de uso era um pouco agressivo demais, mudamos as coisas com um lançamento". Muito pelo contrário, na verdade.
Na verdade, o IIRC I mencionei durante o lançamento do PR de estabilização que o precedente é descontinuar 3 lançamentos no futuro e não 2, mas por alguma razão optamos por 2. Três lançamentos significam 1 lançamento inteiro entre o estábulo-obtém-lançado-com-o -deprecation-anúncio e obsoleto-on-nightly, que parece ser o momento justo para pessoas rastreando todas as noites. 6 semanas é uma eternidade, certo? ;)

Portanto, planejo enviar um PR amanhã que altere a versão obsoleta para 1.39.0. Também posso enviar um PR para atualizar aquela postagem do blog se as pessoas acharem que é importante.

Portanto, planejo enviar um PR amanhã que altere a versão obsoleta para 1.39.0. Também posso enviar um PR para atualizar aquela postagem do blog se as pessoas acharem que é importante.

Vou concordar com 1.39, mas não depois disso. Você também precisará atualizar as notas de lançamento, além da postagem do blog.

PR enviado para programação de suspensão de uso alterada: https://github.com/rust-lang/rust/pull/62599.

@SimonSapin

Quais são as preocupações restantes que bloqueiam a estabilização do restante dos métodos? Fazer tudo através de * foo.as_mut_ptr () se torna muito tedioso e às vezes (para gravação) envolve mais blocos inseguros do que o necessário.

Para as_ref / as_mut , eu honestamente gostaria de esperar até sabermos se as referências têm que apontar para dados inicializados. Caso contrário, a documentação para esses métodos é apenas preliminar.

Por read / write , estou bem estabilizando-os se todos concordarem que os nomes e assinaturas fazem sentido. Acho que isso deve ser coordenado com ManuallyDrop::take/read , e talvez também deva haver ManuallyDrop::write ?

Sinceramente, gostaria de esperar até sabermos se as referências devem apontar para dados inicializados.

O que é necessário para o Unsafe Code Guidelines WG e a equipe de idiomas chegarem a uma decisão sobre esse assunto? Você espera que isso aconteça mais provavelmente em algumas semanas, alguns meses ou alguns anos?

Nesse ínterim, as_mut sendo instável não impede os usuários de escrever &mut *manually_drop.as_mut_ptr() o que é quando eles precisam fazer algo.

O que é necessário para o Unsafe Code Guidelines WG e a equipe de idiomas chegarem a uma decisão sobre esse assunto? Você espera que isso aconteça mais provavelmente em algumas semanas, alguns meses ou alguns anos?

Meses, talvez anos.

Enquanto isso, as_mut sendo instável não impede os usuários de escrever & mut * manualmente_drop.as_mut_ptr () o que é quando eles precisam fazer algo.

Sim eu conheço. A esperança é incentivar as pessoas a atrasar a parte &mut máximo possível e trabalhar com indicadores brutos. Claro que sem https://github.com/rust-lang/rfcs/pull/2582 isso costuma ser difícil.

A documentação sobre MaybeUninit parece ser um lugar principal para, pelo menos, discutir que isso é uma ambigüidade na semântica da linguagem e que os usuários devem presumir de forma conservadora que não está OK.

Verdade, essa seria a outra opção.

Mesmo com uma suposição conservadora, as_mut é válido depois que o valor é totalmente inicializado.

Uma maneira de ser conservador com arrays é usando MaybeUninit<[MaybeUninit<Foo>; N]> . Os wrappers externos permitem criar o array com uma única chamada uninit() . (Eu acho que [expr; N] literal requer Copy ?) Os invólucros internos tornam seguro, mesmo na suposição conservadora, usar a conveniência de slice::IterMut para percorrer a matriz, e em seguida, inicialize os valores Foo um por um.

@SimonSapin veja a macro instável

@RalfJung talvez uninit_array! seja um nome melhor.

@Stargateur Com certeza, isso definitivamente não vai ser estabilizado com seu nome atual. Esperamos que nunca se estabilize se https://github.com/rust-lang/rust/issues/49147 acontecer em breve (TM).

@RalfJung Ugh, a culpa é minha, eu estava bloqueando o PR sem um grande motivo: https://github.com/rust-lang/rust/pull/61749#issuecomment -512867703

@eddyb isso funciona para libcore, yay! Mas de alguma forma, quando tento usar o recurso em liballoc, ele não compila, embora eu defina o sinalizador. 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

Mistério resolvido: os usos da expressão de repetição em libcore na verdade eram para tipos que são cópia.

E o motivo pelo qual não funciona em liballoc é que MaybeUninit::uninit não pode ser promovido.

@RalfJung Talvez abra um PR removendo usos da macro onde é completamente desnecessário?

@eddyb Eu fiz essa parte de https://github.com/rust-lang/rust/pull/62799.

Sobre maybe_uninit_ref

Para as_ref / as_mut, eu honestamente queria esperar até sabermos se as referências devem apontar para dados inicializados. Caso contrário, a documentação para esses métodos é apenas preliminar.

Instáveis get_ref / get_mut são definitivamente aconselháveis ​​por causa disso; no entanto, há casos em que get_ref / get_mut pode ser usado quando o MaybeUninit foi init: para obter um tratamento seguro para os dados (agora conhecidos como inicializados), evitando qualquer memcpy (em vez de assume_init , que pode acionar um memcpy ).

  • isso pode parecer uma situação particularmente específica, mas o principal motivo pelo qual as pessoas (querem) usar dados não inicializados é precisamente para esse tipo de economia barata.

Por causa disso, imagino que assume_init_by_ref / assume_init_by_mut poderia ser bom ter (uma vez que into_inner foi chamado de assume_init , parece plausível que ref / ref mut getters também recebem um nome especial para refletir isso).

Existem duas / três opções para isso, relacionadas à interação de Drop :

  1. Exatamente a mesma API que get_ref e get_mut , o que pode levar a vazamentos de memória quando houver cola;

    • (Variante): mesma API de get_ref / get_mut , mas com um limite de Copy ;
  2. API de estilo de fechamento, para garantir a queda:

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

(Onde a lógica de scopeguard pode ser facilmente reimplementada, então não há necessidade de depender dela)


Eles poderiam ser estabilizados mais rápido do que get_ref / get_mut , dado o requisito explícito de assume_init .

Desvantagens

Se uma variante da opção .1 fosse escolhida, e get_ref / get_mut se tornasse utilizável sem a situação assume_init , então esta API se tornaria quase estritamente inferior (Digo quase porque com a API proposta, ler a referência seria bom, o que nunca pode ser no caso de get_ref e get_mut )

Semelhante ao que @danielhenrymantilla escreveu sobre get_{ref,mut} , estou começando a pensar que read provavelmente deveria ser renomeado para read_init ou read_assume_init ou algo assim, algo que indica que isso só pode ser feito após a conclusão da inicialização.

@RalfJung Tenho uma pergunta sobre isso:

fn foo<T>() -> T {
    let newt = unsafe { MaybeUninit::<T>::zeroed().assume_init() };
    newt
}

Por exemplo, chamamos foo<NonZeroU32> . Isso aciona UB quando declaramos uma função foo (porque ela deve ser válida para todos os T s ou quando a instanciamos com um tipo que aciona UB? Desculpe se é um lugar errado para faça uma pergunta.

O código

Portanto, foo::<i32>() está bem. Mas foo::<NonZeroU32>() é UB.

A propriedade de ser válida para todas as formas possíveis de chamada é chamada de "solidez", consulte também a referência . O contrato geral no Rust é que a superfície API segura de uma biblioteca deve ser sólida. Isso para que os usuários de uma biblioteca não precisem se preocupar com o UB. Toda a história de segurança do Rust se baseia em bibliotecas com APIs de som.

@RalfJung obrigado.

Então, se eu entendi corretamente, esta função não é sólida (e, portanto, inválida), mas se a marcarmos como unsafe então este corpo se torna válido e correto

@Pzixel se você marcar como inseguro, a solidez não é um conceito que se aplica mais. "Isso é som" só faz sentido como uma pergunta para código seguro.

Sim, você deve marcar a função unsafe porque algumas entradas podem acionar UB. Mas mesmo se você fizer isso, essas entradas ainda acionam UB, então a função ainda não deve ser chamada dessa forma. Nunca é normal acionar o UB, nem mesmo em um código inseguro.

Sim, claro, eu entendo isso. Queria apenas concluir que a função parcial deve ser marcada como unsafe . Faz sentido para mim, mas não pensei nisso antes de você responder.

Já que a discussão sobre esse problema de rastreamento está tão longa agora, podemos dividi-la em alguns outros problemas de rastreamento para cada recurso de MaybeUninit que ainda é instável?

  • maybe_uninit_extra
  • maybe_uninit_ref
  • maybe_uninit_slice

Parece razoável. Também há https://github.com/rust-lang/rust/issues/63291.

Fechando isso em favor de um meta-problema que rastreia MaybeUninit<T> mais geral: # 63566

Esta página foi útil?
0 / 5 - 0 avaliações