Rust: Uniões não marcadas (rastreamento de problema para RFC 1444)

Criado em 8 abr. 2016  ·  210Comentários  ·  Fonte: rust-lang/rust

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

Perguntas não resolvidas:

  • [x] A atribuição direta a um campo de união desencadeia uma queda do conteúdo anterior?
  • [x] Ao sair de um campo de um sindicato, os outros são considerados invalidados? ( 1 , 2 , 3 , 4 )
  • [] Em que condições você pode implementar Copy para um sindicato? Por exemplo, e se algumas variantes não forem do tipo cópia? Todas as variantes?
  • [] Que interação existe entre sindicatos e otimizações de layout de enum? (https://github.com/rust-lang/rust/issues/36394)

Questões abertas de alta importação:

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

Comentários muito úteis

@nrc
Bem, o subconjunto é bastante óbvio - "uniões FFI", ou "uniões C" ou "uniões pré-C ++ 11" - mesmo que não seja sintático. Meu objetivo inicial era estabilizar este subconjunto o mais rápido possível (este ciclo, idealmente) para que pudesse ser usado em bibliotecas como winapi .
Não há nada de especialmente duvidoso sobre o subconjunto restante e sua implementação, simplesmente não é urgente e precisa esperar por um período de tempo incerto até que o processo para "Uniões 1.2" RFC seja concluído. Minhas expectativas seriam estabilizar as partes restantes em 1, 2 ou 3 ciclos após a estabilização do subconjunto inicial.

Todos 210 comentários

Posso ter perdido isso na discussão sobre a RFC, mas estou correto em pensar que destruidores de variantes de união nunca são executados? O destruidor de Box::new(1) seria executado neste exemplo?

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

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

@sfackler Meu entendimento atual é que f.g = Box::new(2) _will_ executará o destruidor, mas f = Foo { g: Box::new(2) } _não_. Ou seja, atribuir a um Box<i32> lvalue causará uma queda como sempre, mas atribuir a um Foo lvalue não.

Portanto, uma atribuição a uma variante é como uma afirmação de que o campo era "válido" anteriormente?

@sfackler Para Drop , sim, esse é o meu entendimento. Se eles não eram válidos anteriormente, você precisa usar a forma do construtor Foo ou ptr::write . Em um grep rápido, não parece que o RFC seja explícito sobre esse detalhe. Eu vejo isso como uma instanciação da regra geral que escrever para um Drop lvalue causa uma chamada de destruidor.

Deve uma união & mut com variantes Drop ser um fiapo?

Na sexta-feira, 8 de abril de 2016, Scott Olson [email protected] escreveu:

@sfackler https://github.com/sfackler Para tipos de queda, sim, esse é o meu
compreensão. Se eles não eram válidos anteriormente, você precisa usar o Foo
forma de construtor ou ptr :: write. De um grep rápido, não parece
a RFC é explícita sobre esse detalhe, no entanto.

-
Você está recebendo isto porque está inscrito neste tópico.
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -207634431

Em 8 de abril de 2016, 15:36:22 PDT, Scott Olson [email protected] escreveu:

@sfackler Para Drop , sim, esse é o meu entendimento. Se eles
não eram válidos anteriormente, você precisa usar o formulário do construtor Foo ou
ptr::write . De um grep rápido, não parece que o RFC está
explícito sobre esse detalhe, no entanto.

Eu deveria ter abordado esse caso explicitamente. Acho que ambos os comportamentos são defensáveis, mas acho que seria muito menos surpreendente nunca deixar cair um campo implicitamente. A RFC já recomenda um lint para campos de união com tipos que implementam Drop. Não acho que atribuir a um campo implica que esse campo era válido anteriormente.

Sim, essa abordagem parece um pouco menos perigosa para mim também.

Não descartar ao atribuir a um campo de união faria f.g = Box::new(2) agir de maneira diferente de let p = &mut f.g; *p = Box::new(2) , porque você não pode fazer com que o último caso _não_ caia. Acho que minha abordagem é menos surpreendente.

Também não é um problema novo; unsafe programadores já têm que lidar com outras situações em que foo = bar é UB se foo não foi inicializado e Drop .

Eu, pessoalmente, não pretendo usar tipos Drop com sindicatos. Portanto, vou ceder inteiramente às pessoas que trabalharam com código inseguro análogo na semântica de fazê-lo.

Eu também não pretendo usar tipos Drop em sindicatos, então qualquer forma não importa para mim, contanto que seja consistente.

Não pretendo usar referências mutáveis ​​a sindicatos e provavelmente
apenas aqueles "estranhamente marcados" com Into

Na sexta-feira, 8 de abril de 2016, Peter Atashian [email protected] escreveu:

Eu também não pretendo usar tipos Drop em sindicatos, então de qualquer forma não
importa para mim, desde que seja consistente.

-
Você está recebendo isto porque está inscrito neste tópico.
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -207653168

Parece que este é um bom assunto para levantar como uma questão não resolvida. Ainda não tenho certeza de qual abordagem prefiro.

@nikomatsakis Por mais que eu ache estranho atribuir a um campo de união de um tipo com Drop para exigir a validade anterior desse campo, o caso de referência @tsion mencionado parece quase inevitável. Acho que isso pode ser apenas uma pegadinha associada ao código que desativa intencionalmente o lint para colocar um tipo com Drop em uma união. (E uma breve explicação sobre isso deve estar no texto explicativo desse lint.)

E eu gostaria de reiterar que unsafe programadores geralmente já devem saber que a = b significa drop_in_place(&mut a); ptr::write(&mut a, b) para escrever um código seguro. Não descartar campos de união seria _mais_ exceção a ser aprendida, não menos.

(NB: a queda não acontece quando a é _estaticamente_ conhecido por não ter sido inicializado, como let a; a = b; .)

Mas eu apóio ter um aviso padrão contra Drop variantes em sindicatos que as pessoas têm de #[allow(..)] uma vez que este é um detalhe bastante não óbvio.

@tsion isso não é verdade para a = b e talvez apenas às vezes seja verdade para a.x = b mas certamente é verdade para *a = b . Essa incerteza é o que me deixou hesitante. Por exemplo, isso compila:

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

(embora tentar imprimir x mais tarde falhar, mas considero isso um bug)

@nikomatsakis Esse exemplo é novo para mim. Eu acho que teria considerado um bug o que aquele exemplo compila, dada minha experiência anterior.

Mas não tenho certeza se vejo a relevância desse exemplo. Por que o que eu disse não é verdade para a = b e apenas às vezes para a.x = b ?

Digamos, se x.0 tivesse um tipo com um destruidor, certamente esse destruidor é chamado:

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

Talvez apenas lint contra esse tipo de escrita?

Meu ponto é apenas que = não _sempre_ executa o destruidor; isto
usa algum conhecimento sobre se o alvo é conhecido por ser
inicializado.

Na terça-feira, 12 de abril de 2016 às 04:10:39 PM -0700, Scott Olson escreveu:

@nikomatsakis Esse exemplo é novo para mim. Eu acho que teria considerado um bug o que aquele exemplo compila, dada minha experiência anterior.

Mas não tenho certeza se vejo a relevância desse exemplo. Por que o que eu disse não é verdade para a = b e apenas às vezes para 'ax = b'?

Digamos, se x.0 tivesse um tipo com um destruidor, certamente esse destruidor é chamado:

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

@nikomatsakis

Ele executa o destruidor se o sinalizador de soltar estiver definido.

Mas acho que esse tipo de escrita é confuso de qualquer maneira, então por que não proibi-lo? Você sempre pode fazer *(&mut u.var) = val .

Meu ponto é apenas que = não _sempre_ executa o destruidor; ele usa algum conhecimento sobre se o destino foi inicializado.

@nikomatsakis Já mencionei que:

(NB: a queda não acontece quando a é estaticamente conhecido por já não ter sido inicializado, como seja a; a = b ;.)

Mas eu não considerei a verificação dinâmica de drop flags, então isso é definitivamente mais complicado do que eu pensei.

@tsion

Os sinalizadores de queda são apenas semi-dinâmicos - após o término do zeramento, eles fazem parte do codegen. Eu digo que proibimos esse tipo de escrita porque causa mais confusão do que bem.

Os tipos Drop deveriam ser permitidos em sindicatos? Se estou entendendo as coisas corretamente, o principal motivo para ter uniões no Rust é a interface com o código C que tem uniões, e C nem mesmo tem destruidores. Para todos os outros propósitos, parece que é melhor apenas usar um enum no código Rust.

Há um caso de uso válido para usar uma união para implementar um tipo NoDrop que inibe a queda.

Além de invocar esse código manualmente por meio de drop_in_place ou similar.

Para mim, descartar um valor de campo enquanto escrevo nele é definitivamente errado porque o tipo de opção anterior é indefinido.

Seria possível proibir os configuradores de campo, mas exigir a substituição da união completa? Nesse caso, se o sindicato implementasse Drop, o full union drop seria chamado para o valor substituído conforme o esperado.

Não acho que faça sentido proibir configuradores de campo; a maioria dos usos de sindicatos não deve ter problemas em usá-los, e os campos sem uma implementação Drop provavelmente permanecerão o caso comum. Os sindicatos com campos que implementam Drop produzirão um aviso por padrão, tornando ainda menos provável que acertem acidentalmente neste caso.

Para fins de discussão, pretendo expor referências mutáveis ​​a campos em uniões _e_ colocar tipos arbitrários (possivelmente Drop ) neles. Basicamente, eu gostaria de usar uniões para escrever enums com espaço eficiente personalizado. Por exemplo,

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

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

@nikomatsakis Eu gostaria de propor uma resposta concreta para a questão atualmente listada como não resolvida aqui.

Para evitar uma semântica desnecessariamente complexa, atribuir a um campo de união deve funcionar como atribuir a um campo de estrutura, o que significa descartar o conteúdo antigo. É muito fácil evitar isso se você souber disso, designando para todo o sindicato. Esse comportamento ainda é um pouco surpreendente, mas ter um campo de união que implemente Drop produzirá um aviso, e o texto desse aviso pode mencionar isso explicitamente como uma advertência.

Faria sentido fornecer uma solicitação pull de RFC que altere a RFC1444 para documentar esse comportamento?

@joshtriplett Já que @nikomatsakis está de férias, responderei: Acho que é uma ótima forma de apresentar uma emenda RFC para resolver questões como esta. Freqüentemente, agilizávamos esses PRs RFC quando apropriado.

@aturon Obrigado. Eu preenchi o novo RFC PR https://github.com/rust-lang/rfcs/issues/1663 com estes esclarecimentos.para RFC1444, para resolver esse problema.

( @aturon você pode marcar essa questão não resolvida agora.)

Tenho algumas implementações preliminares em https://github.com/petrochenkov/rust/tree/union.

Status: Implementado (modulo bugs), PR enviado (https://github.com/rust-lang/rust/pull/36016).

@petrochenkov Incrível! Parece ótimo até agora.

Não tenho certeza de como tratar as uniões com campos não Copy no verificador de movimentação.
Suponha que u seja um valor inicializado de union U { a: A, b: B } e agora saímos de um dos campos:

1) A: !Copy, B: !Copy, move_out_of(u.a)
Isso é simples, u.b também é colocado no estado não inicializado.
Verificação de integridade: union U { a: T, b: T } deve se comportar exatamente como struct S { a: T } + alias de campo.

2) A: Copy, B: !Copy, move_out_of(u.a)
Supostamente u.b ainda deve ser inicializado, porque move_out_of(u.a) é simplesmente um memcpy e não muda u.b de nenhuma maneira.

2) A: !Copy, B: Copy, move_out_of(u.a)
Este é o caso mais estranho; supostamente u.b também deve ser colocado no estado não inicializado, apesar de ser Copy . Copy podem ser não inicializados (por exemplo, let a: u8; ), mas alterar seu estado de inicializado para não inicializado é algo novo, AFAIK.

@ retep998
Eu sei que isso é completamente irrelevante para as necessidades da FFI :)
A boa notícia é que não é um bloqueador, vou implementar qualquer comportamento que seja mais simples e enviar PR neste fim de semana.

@petrochenkov meu instinto é que os sindicatos são um "balde de bits", essencialmente. Você é responsável por rastrear se os dados são inicializados ou não e qual é o seu tipo verdadeiro. Isso é muito semelhante ao referente de um ponteiro bruto.

É por isso que não podemos descartar os dados para você e também porque qualquer acesso aos campos não é seguro (mesmo se, digamos, houver apenas uma variante).

Por essas regras, eu esperaria que os sindicatos implementassem Copy se a cópia for implementada para eles. Ao contrário de structs / enums, no entanto, não haveria verificações internas de integridade: você sempre pode implementar a cópia para um tipo de união, se desejar.

Deixe-me dar alguns exemplos para esclarecer:

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

Esta união é afim, porque Copy não foi implementada.

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

Este tipo de união Bar é cópia, porque Copy foi implementado.

Observe que se Bar fosse uma estrutura, seria um erro implementar Copy por causa do tipo do campo x .

Huh, acho que não estou realmente respondendo à sua pergunta, agora que reli. =)

OK, então, eu percebi que não estava respondendo a sua pergunta. Então, deixe-me tentar novamente. Seguindo o princípio do "balde de bits", eu _ainda_ espero que possamos sair de um sindicato à vontade. Mas é claro que outra opção seria tratá-lo como tratamos um *mut T e exigir que você use ptr::read para sair.

EDIT: Não tenho certeza de por que proibiríamos tais movimentos. Pode ter sido necessário fazer com que as mudanças ocorram - ou talvez apenas porque é fácil cometer um erro e parece melhor tornar as "mudanças" mais explícitas? Estou tendo problemas para lembrar a história aqui.

@nikomatsakis

meu instinto é que os sindicatos são um "balde de bits", essencialmente.

Ha, eu, ao contrário, gostaria de dar tantas garantias quanto ao conteúdo sindical que pudermos para uma construção tão perigosa.

A interpretação é que união é um enum para o qual não conhecemos o discriminante, ou seja, podemos garantir que a qualquer momento pelo menos uma das variantes de união tem valor válido (a menos que um código inseguro esteja envolvido).

Todas as regras de emprestar / mover na implementação atual suportam esta garantia, simultaneamente esta é a interpretação mais conservadora, que nos permite seguir o caminho "seguro" (por exemplo, permitindo acesso seguro a sindicatos com campos igualmente digitados, isso pode ser útil ) ou o "balde de bits" no futuro, quando mais experiência com uniões Rust for adquirida.

Na verdade, gostaria de torná-lo ainda mais conservador, conforme descrito em https://github.com/rust-lang/rust/pull/36016#issuecomment -242810887

@petrochenkov

A interpretação é que união é um enum para o qual não conhecemos o discriminante, ou seja, podemos garantir que a qualquer momento pelo menos uma das variantes de união tem valor válido (a menos que um código inseguro esteja envolvido).

Observe que o código inseguro está sempre envolvido, ao trabalhar com um sindicato, uma vez que todo acesso a um campo é inseguro.

A maneira como penso é, eu acho, semelhante. Basicamente, uma união é como um enum, mas pode estar em mais de uma variante simultaneamente. O conjunto de variantes válidas não é conhecido pelo compilador em nenhum ponto, embora às vezes possamos descobrir que o conjunto está vazio (ou seja, o enum não foi inicializado).

Portanto, vejo qualquer uso de some_union.field como basicamente uma afirmação implícita (e insegura) de que o conjunto de variantes válidas atualmente inclui field . Isso parece compatível com o funcionamento da integração do verificador de empréstimo; se você pegar emprestado o campo x e tentar usar y , você está recebendo um erro porque basicamente está dizendo que os dados são simultaneamente x e y (e é emprestado). (Em contraste, com um enum regular, não é possível habitar mais de uma variante por vez, e você pode ver isso em como as regras de empréstimo funcionam ).

De qualquer forma, a questão é que, quando "mudamos" de um campo de uma união, a questão em questão é se podemos deduzir que isso implica que interpretar o valor como as outras variantes não é mais válido. Acho que não seria tão difícil argumentar de qualquer maneira. Eu considero esta uma zona cinzenta.

O perigo de ser conservador é que podemos excluir códigos inseguros que, de outra forma, fariam sentido e seriam válidos. Mas estou bem em começar com mais força e decidir se devo afrouxar mais tarde.

Devemos discutir a questão de quais condições são necessárias para implementar Copy em um sindicato - também, devemos ter certeza de que temos uma lista completa dessas áreas cinzentas listadas acima para ter certeza de que abordamos e documentamos antes da estabilização!

Basicamente, uma união é como um enum, mas pode estar em mais de uma variante simultaneamente.

Um argumento contra a interpretação de "mais de uma variante" é como os sindicatos se comportam em expressões constantes - para esses sindicatos, sempre conhecemos a única variante ativa e também não podemos acessar as variantes inativas porque a transmutação em tempo de compilação é geralmente ruim (a menos que estejamos tentando para transformar o compilador em algum tipo de emulador de destino parcial).
Minha interpretação é que, em tempo de execução, as variantes inativas ainda estão inativas, mas podem ser acessadas se tiverem layout compatível com a variante ativa do union (definição mais restritiva) ou melhor, com o histórico de atribuição de fragmento do union (mais vago, mas mais útil).

devemos ter certeza de que temos uma lista completa dessas áreas cinzentas

Vou alterar a RFC do sindicato em um futuro não tão remoto! A interpretação "enum" tem consequências bem divertidas.

transmutar em tempo de compilação geralmente é ruim (a menos que estejamos tentando transformar o compilador em algum tipo de emulador de destino parcial)

@petrochenkov Este é um dos objetivos do meu projeto Miri . Miri já pode fazer transmutes e várias travessuras de ponteiro cru. Seria uma pequena quantidade de trabalho fazer Miri lidar com os sindicatos (nada de novo no lado do gerenciamento de memória bruta).

E @eddyb está pressionando para substituir a avaliação constante do rustc por uma versão de Miri.

@petrochenkov

Um argumento contra a interpretação de "mais de uma variante" é como os sindicatos se comportam em expressões constantes ...

Qual a melhor forma de oferecer suporte ao uso de uniões em constantes é uma questão interessante, mas não vejo nenhum problema em restringir expressões constantes a um subconjunto do comportamento de tempo de execução (isso é o que sempre fazemos, de qualquer maneira). Ou seja, só porque podemos não ser capazes de suportar totalmente algum transmute em tempo de compilação, não significa que seja ilegal em tempo de execução.

Minha interpretação é que, em tempo de execução, as variantes inativas ainda estão inativas, mas podem ser acessadas se forem compatíveis com o layout da variante ativa da união

Hmm, estou tentando pensar como isso é diferente de dizer que a união pertence a todas essas variantes simultaneamente. Eu realmente não vejo diferença ainda. :)

Eu sinto que essa interpretação tem interações estranhas com movimentos em geral. Por exemplo, se os dados são "realmente" um X e você os interpreta como um Y, mas Y é afim, então ainda é um X?

Independentemente disso, acho que não há problema em que ter uma mudança de qualquer campo consumindo todo o sindicato pode ser visto como consistente com qualquer uma dessas interpretações. Por exemplo, na abordagem de "conjunto de variantes", a ideia é apenas que mover o valor desinicializa todas as variantes existentes (e, claro, a variante usada deve ser uma do conjunto válido). Em sua versão, pareceria "transmutar" naquela variante (e consumir o original).

Vou alterar a RFC do sindicato em um futuro não tão remoto! A interpretação "enum" tem consequências bem divertidas.

Tanta confiança! Você vai tentar;)

Importa-se de revelar mais alguns detalhes sobre quais mudanças concretas você tem em mente?

Importa-se de revelar mais alguns detalhes sobre quais mudanças concretas você tem em mente?

Descrição mais detalhada da implementação (ou seja, melhor documentação), algumas pequenas extensões (como sindicatos vazios e .. em padrões sindicais), duas alternativas principais (contraditórias) de evolução sindical - "espaço vazio" mais inseguro e menos restritivo interpretação e interpretação "enum com discriminante desconhecido" mais segura e mais restritiva - e suas consequências para o verificador de movimentação / inicialização, Copy impls, unsafe ty de acesso de campo, etc.

Também seria útil definir quando acessar um campo de união inativo é UB, por exemplo

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

mas esta é uma área infinitamente complicada.

Parece provável, a semântica entre campos é basicamente um lançamento de ponteiro, certo?
_ (_ () como * u8)

Na quinta-feira, 1 de setembro de 2016, Vadim Petrochenkov [email protected]
escrevi:

Também seria útil definir ao acessar um campo de união inativo
é UB, por exemplo

união U {a: u8, b: ()}
seja u = U {b: ()};
deixe a = ua; // muito provavelmente um UB, equivalente à leitura de mem::uninitialized()

mas esta é uma área infinitamente complicada.

-
Você está recebendo isto porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -244154751,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/ABxXhi68qRITTFW5iJn6omZQQBQgzweNks5qlw4qgaJpZM4IDXsj
.

O acesso em campo não é sempre inseguro?

Na quinta-feira, 1 de setembro de 2016, Vadim Petrochenkov [email protected]
escrevi:

Importa-se de revelar mais alguns detalhes sobre quais mudanças concretas você tem em mente?

Descrição mais detalhada da implementação (ou seja, melhor
documentação), algumas pequenas extensões (como sindicatos vazios e .. em sindicato
padrões), duas alternativas principais (contraditórias) de evolução sindical - mais
interpretação insegura e menos restritiva de "espaço de rascunho" e mais segura
e interpretação mais restritiva "enum com discriminante desconhecido" - e
suas consequências para o verificador de movimentação / inicialização, Copy impls, insegurança
de acesso de campo, etc.

-
Você está recebendo isto porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -244151164,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/ABxXhuHStN8AFhR3KYDU27U29MiMpN5Bks5qlws9gaJpZM4IDXsj
.

O acesso em campo não é sempre inseguro?

Pode ser tornado seguro às vezes, por exemplo

  • a atribuição a campos de união trivialmente destrutíveis é segura.
  • qualquer acesso a campos de union U { f1: T, f2: T, ..., fN: T } (ou seja, todos os campos têm o mesmo tipo) é seguro na interpretação "enum com discriminante desconhecido".

Parece melhor não aplicar condições especiais a isso, do ponto de vista do usuário. Apenas chame de inseguro, sempre.

Atualmente testando o suporte para sindicatos no último rustc do git. Tudo o que experimentei funciona perfeitamente.

Encontrei um caso interessante no verificador de campo morto. Experimente o seguinte código:

#![feature(untagged_unions)]

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

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

Você receberá este erro:

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

Parece que o verificador de dead_code não percebeu a inicialização.

(Já preenchi a PR # 36252 sobre o uso de "campo de estrutura", alterando-o apenas para "campo".)

No momento, as uniões não podem conter campos dimensionados dinamicamente, mas o RFC não especifica esse comportamento de nenhuma maneira:

#![feature(untagged_unions)]

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

Resultado:

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

A palavra-chave contextual não funciona fora do contexto raiz do módulo / caixa:

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

    // does not work
    union Banana {}
}

Parece uma verruga de consistência bem desagradável.

@nagisa
Você está usando alguma versão mais antiga do rustc por acidente?
Acabei de verificar seu exemplo no cercadinho e ele funciona (erros de módulo "união vazia").
Há também uma verificação de teste de passagem para esta situação específica - https://github.com/rust-lang/rust/blob/master/src/test/run-pass/union/union-backcomp.rs.

@petrochenkov ah, usei play.rlo, mas parece que foi revertido para stable ou algo assim. Não se preocupe comigo, então.

Acho que os sindicatos acabarão por precisar apoiar os _seguros campos_, os gêmeos do mal dos campos inseguros desta proposta .
cc https://github.com/rust-lang/rfcs/issues/381#issuecomment -246703410

Acho que faria sentido ter uma forma de declarar "sindicatos seguros", com base em vários critérios.

Por exemplo, uma união contendo exclusivamente campos Copiar não descartáveis, todos com o mesmo tamanho, parece segura; não importa como você acessa os campos, você pode obter dados inesperados, mas não pode encontrar um problema de segurança de memória ou comportamento indefinido.

@joshtriplett Você também precisa se certificar de que não há "buracos" nos tipos de onde você pode ler dados não inicializados. Como eu gosto de dizer: "Dados não inicializados são dados aleatórios imprevisíveis ou sua chave privada SSH, o que for pior."

&T não é copioso e não descartável? Coloque isso em uma união "segura" com usize e você terá um gerador de referência desonesto. Portanto, as regras terão que ser um pouco mais rígidas do que isso.

Por exemplo, uma união contendo exclusivamente campos Copiar não descartáveis, todos com o mesmo tamanho, parece segura; não importa como você acessa os campos, você pode obter dados inesperados, mas não pode encontrar um problema de segurança de memória ou comportamento indefinido.

Sei que este é apenas um exemplo improvisado, não uma proposta séria, mas aqui estão alguns exemplos para ilustrar como isso é complicado:

  • u8 e bool têm o mesmo tamanho, mas a maioria dos valores u8 são inválidos para bool e ignorar isso aciona o UB
  • &T e &U têm o mesmo tamanho e são Copy + !Drop para todos T e U (contanto que ambos ou nenhum sejam Sized )
  • transmutar entre uN / iN e fN atualmente só é possível em código não seguro. Acredito que essas transmutações são sempre seguras, mas isso amplia a linguagem segura, por isso pode ser controverso.
  • Violar a privacidade (por exemplo, trocadilhos entre struct Foo(Bar); e Bar ) também é um grande não, pois a privacidade pode ser usada para manter invariáveis ​​relevantes para a segurança.

@Amanieu Quando estava escrevendo isso, pretendia incluir uma nota sobre não ter nenhum preenchimento interno e, de alguma forma, esqueci de fazê-lo. Obrigado por pegar isso.

@cuviper Eu estava tentando definir "dados simples e antigos", como em algo que contém ponteiros zero. Você está certo, a definição precisaria excluir referências. Provavelmente mais fácil colocar na lista de permissões um conjunto de tipos permitidos e combinações desses tipos.

@rkruppe

u8 e bool têm o mesmo tamanho, mas a maioria dos valores u8 são inválidos para bool e ignorar isso aciona UB

Bom ponto; o mesmo problema se aplica a enums.

& T e & U têm o mesmo tamanho e são Copiar +! Soltar para todos os T e U (desde que ambos ou nenhum tenham o mesmo tamanho)

Eu tinha esquecido disso.

transmutar entre uN / iN e fN atualmente só é possível em código não seguro. Acredito que essas transmutações são sempre seguras, mas isso amplia a linguagem segura, por isso pode ser controverso.

Concordou em ambos os pontos; isso parece aceitável permitir.

Violar privacidade (por exemplo, trocadilhos entre struct Foo (Bar); e Bar) também é um grande não, pois a privacidade pode ser usada para manter invariantes relevantes para a segurança.

Se você não conhece os internos do tipo, não pode saber se os internos atendem aos requisitos (como nenhum preenchimento interno). Portanto, você pode excluir isso exigindo que todos os componentes sejam dados simples, recursivamente, e que você tenha visibilidade suficiente para verificar isso.

transmutar entre uN / iN e fN atualmente só é possível em código não seguro. Acredito que essas transmutações são sempre seguras, mas isso amplia a linguagem segura, por isso pode ser controverso.

Os números de ponto flutuante têm sinalização NaN, que é uma representação de armadilha que resulta em UB.

@ retep998 O Rust funciona em alguma plataforma que não oferece suporte à desativação de armadilhas de ponto flutuante? (Isso não muda o problema do UB, mas, em teoria, poderíamos resolver o problema.)

@petrochenkov

A interpretação é que união é um enum para o qual não conhecemos o discriminante, ou seja, podemos garantir que a qualquer momento pelo menos uma das variantes de união tem valor válido

Acho que cheguei a esta interpretação - bem, _exatamente_ não. Ainda penso nisso, pois há algum conjunto de variantes legais que é determinado no ponto em que você armazena, como sempre fiz. Eu penso em armazenar um valor em uma união como um pouco como colocá-lo em um "estado quântico" - agora ele poderia ser potencialmente transmutado em uma das muitas interpretações legais. Mas eu concordo que quando você sai de uma dessas variantes, você a "forçou" a apenas uma delas e consumiu o valor. Portanto, você não deve ser capaz de usar o enum novamente (se esse tipo não for Copy ). Então 👍, basicamente.

Pergunta sobre #[repr(C)] : como @pnkfelix recentemente apontou para mim, a especificação atual afirma que se um sindicato não for #[repr(C)] , é ilegal armazenar com o campo x e ler com o campo y . Presumivelmente, isso ocorre porque não somos obrigados a iniciar todos os campos com o mesmo deslocamento.

Posso ver alguma utilidade nisso: por exemplo, um desinfetante pode implementar uniões armazenando-as como um enum normal (ou até mesmo uma estrutura ...?) E verificando se você usa a mesma variante que inseriu.

_Mas_ parece uma espécie de footgun, e também uma daquelas garantias reprimidas que nunca seríamos capazes de _realmente_ mudar na prática, porque muitas pessoas estarão contando com ela na selva.

Pensamentos?

@nikomatsakis

A interpretação é que união é um enum para o qual não conhecemos o discriminante, ou seja, podemos garantir que a qualquer momento pelo menos uma das variantes de união tem valor válido

A pior parte são os fragmentos de variante / campo, que são diretamente acessíveis para as uniões.
Considere este código:

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

Todos os campos são Copy , nenhuma propriedade envolvida e o verificador de movimentação está feliz, mas a atribuição parcial ao campo inativo b transforma a união em um estado com 0 variantes válidas. Não pensei em como lidar com isso ainda. Faça essas atribuições UB? Mudar a interpretação? Algo mais?

@petrochenkov

Faça essas atribuições UB?

Essa seria minha suposição, sim. Quando você atribuiu a , a variante b não estava no conjunto de variantes válidas e, portanto, usar u.b.1 (ler ou atribuir) é inválido.

Pergunta sobre # [repr (C)]: como @pnkfelix recentemente apontou para mim, a especificação atual afirma que se uma união não for # [repr (C)], é ilegal armazenar com o campo x e ler com o campo y . Presumivelmente, isso ocorre porque não somos obrigados a iniciar todos os campos com o mesmo deslocamento.

Eu acho que o texto apropriado aqui é que 1) A leitura de campos que não são "compatíveis com layout" (isso é vago) com campos / fragmentos de campo previamente escritos é UB 2) Para #[repr(C)] unions, os usuários sabem quais são os layouts (de documentos ABI) para que eles possam discernir entre UB e não UB 3) Para #[repr(Rust)] os layouts de união não são especificados, então os usuários não podem dizer o que é UB e o que não é, mas NÓS (rustc / libstd + seus testes) têm esse conhecimento sagrado, então podemos separar o joio do trigo e usar #[repr(Rust)] maneira não UB.

4) Depois que as questões de tamanho / avanço e reordenação de campo forem decididas, eu espero que os layouts de estrutura e união sejam gravados e especificados, para que os usuários também conheçam os layouts e possam usar #[repr(Rust)] unions tão livremente quanto #[repr(C)] e o problema irá embora.

@nikomatsakis Na discussão da RFC do sindicato, as pessoas mencionaram que

Há algo que impeça as pessoas de usar #[repr(C)] ? Caso contrário, não vejo necessidade de fornecer qualquer tipo de garantia para #[repr(Rust)] , apenas deixe como "aqui estão os dragões". Provavelmente seria melhor ter um lint avisado por padrão para uniões que não são #[repr(C)] .

@ retep998 Parece-me razoável que repr(Rust) não garante nenhum layout ou sobreposição em particular. Eu apenas sugeriria que repr(Rust) não deveria, na prática, quebrar as suposições das pessoas sobre o uso de memória de um sindicato ("não maior do que o membro maior").

O Rust roda em alguma plataforma que não suporta a desativação de armadilhas de ponto flutuante?

Essa não é uma pergunta válida de se fazer. Em primeiro lugar, o próprio otimizador pode contar com o UB-ness das representações de trap e reescrever o programa de maneiras inesperadas. Além disso, o Rust também não apóia a alteração do ambiente de FP.

Mas parece uma espécie de espingarda, e também uma daquelas garantias reprimidas que nunca seríamos capazes de realmente mudar na prática, porque muitas pessoas dependerão dela na selva.

Pensamentos?

Adicionar um lint ou algo semelhante que inspecione o fluxo do programa e envie uma reclamação ao usuário se a leitura for feita de um campo quando o enum foi provavelmente escrito em algum outro campo ajudaria nisso¹. O lint baseado em MIR daria um pequeno trabalho nisso. Se um CFG não permite tirar nenhuma conclusão sobre a legalidade da carga do campo sindical e o usuário comete um erro, o comportamento indefinido é o melhor que podemos especificar sem ter especificado o Rust repr próprio IMO.

¹: Especialmente eficaz se as pessoas começarem a usar o sindicato como transmutador de um homem pobre por algum motivo.

não deve, na prática, quebrar as suposições das pessoas sobre o uso de memória de um sindicato ("não maior que o membro maior").

Discordo. Pode fazer muito sentido estender repr(Rust) stuff ao tamanho de uma palavra de máquina em algumas arquiteturas, por exemplo.

Um problema que pode ser considerado antes da estabilização é https://github.com/rust-lang/rust/issues/37479. Parece que com a versão mais recente do LLDB, as uniões de depuração podem não funcionar :(

@alexcrichton Funciona com GDB?

Pelo que posso dizer, sim. Os bots do Linux parecem estar executando o teste perfeitamente.

Isso significa que o Rust fornece todas as informações de depuração corretas e o LLDB tem apenas um bug aqui. Não acho que um bug em um dos vários depuradores, não presente em outro, deva bloquear a estabilização disso. O LLDB só precisa ser consertado.

Seria legal ver se conseguiríamos colocar esse recurso no FCP para o ciclo 1.17 (que é o beta de 16 de março). Alguém pode dar um resumo das questões pendentes e da situação atual do recurso para que possamos ver se podemos chegar a um consenso e resolver tudo?

@withoutboats
Meus planos são

  • Aguarde o próximo lançamento (3 de fevereiro).
  • Propor a estabilização dos sindicatos com campos Copy . Isso cobrirá todas as necessidades da FFI - as bibliotecas da FFI serão capazes de usar unions no stable. As uniões "POD" são usadas há décadas em C / C ++ e bem conhecidas (aliasing baseado em tipo de módulo, mas Rust não o tem), também não existem bloqueadores conhecidos.
  • Escreva o RFC "Sindicatos 1.2" até 3 de fevereiro. Ele descreverá a implementação atual dos sindicatos e delineará as direções futuras. O futuro dos sindicatos com campos que não sejam Copy será decidido no processo de discussão desta RFC.

Observe que expor algo como ManuallyDrop ou NoDrop da biblioteca padrão não requer uniões estabilizadoras.

ATUALIZAÇÃO DE STATUS (4 de fevereiro): Estou escrevendo o RFC, mas estou tendo um bloqueio de escritor após cada frase, como de costume, então há uma chance de terminar no próximo fim de semana (11 a 12 de fevereiro) e não neste fim de semana (4 a 5 de fevereiro).
ATUALIZAÇÃO DE STATUS (11 de fevereiro): O texto está 95% pronto, vou enviá-lo amanhã.

@petrochenkov parece um curso de ação muito razoável.

@petrochenkov Isso parece razoável para mim. Também analisei a proposta do seu sindicato 1.2 e fiz alguns comentários; no geral, parece bom para mim.

@joshtriplett Eu estava pensando que, enquanto na reunião @rust-lang / lang conversamos sobre como manter as listas de verificação atualizadas, eu gostaria de ver que - para cada um desses pontos - tomamos uma decisão afirmativa (ou seja, idealmente com @rfcbot). Isso provavelmente sugeriria um problema distinto (ou até mesmo uma emenda RFC). Poderíamos fazer isso com o tempo, mas até então não acho que tenhamos "acertado" definitivamente as respostas às perguntas em aberto. Ao longo dessas linhas, extrair e resumir a conversa relevante em um RFC de alteração ou mesmo apenas um problema para o qual podemos criar um link a partir daqui parece um excelente passo para ajudar a garantir que todos estejam na mesma página - e algo que qualquer pessoa interessada pode fazer , é claro, não apenas membros ou pastores de @rust-lang / lang.

Portanto, enviei a RFC "Unions 1.2" - https://github.com/rust-lang/rfcs/pull/1897.

Agora eu gostaria de propor a estabilização de um subconjunto conservador de união - todos os campos da união devem ser Copy , o número de campos deve ser diferente de zero e a união não deve implementar Drop .
(Não tenho certeza se o último requisito é viável, porque pode ser facilmente contornado envolvendo a união em uma estrutura e implementando Drop para essa estrutura.)
Esses sindicatos cobrem todas as necessidades das bibliotecas FFI, que devem ser as principais consumidoras desse recurso de linguagem.

O texto da RFC "Sindicatos 1.2" realmente não diz nada de novo sobre os sindicatos do tipo FFI, exceto que confirma explicitamente que o tipo de trocadilho é permitido.
EDIT : "Unions 1.2" RFC também fará atribuições para Copy campos trivialmente destrutíveis (consulte https://github.com/rust-lang/rust/issues/32836#issuecomment-281296416, https : //github.com/rust-lang/rust/issues/32836#issuecomment-281748451), isso afeta os sindicatos no estilo FFI também.

Este texto também fornece a documentação necessária para a estabilização.
A seção "Visão geral" pode ser copiada e colada no livro e "Projeto detalhado" na referência.

ping @nikomatsakis

Algo assim realmente precisa ser adicionado como parte da linguagem? Levei cerca de 20 minutos para criar uma implementação de uma união usando um pouco unsafe e ptr::write() .

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


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

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

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


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

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

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

Eu sinto que não seria difícil para alguém escrever uma macro que pode gerar algo assim, exceto garantir que a matriz interna seja do tamanho do tipo maior. Então, em vez de meu totalmente genérico (e terrivelmente inseguro) get::<T>() eles poderiam adicionar uma característica destinada a limitar os tipos que você pode obter e definir. Você pode até adicionar métodos getter e setter específicos se quiser campos nomeados.

Estou pensando que eles podem escrever algo assim:

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

O que quero dizer é que isso é algo que você pode fazer como parte de uma biblioteca, em vez de adicionar sintaxe e complexidade extras a uma linguagem já bastante complexa. Além disso, com macros proc já é bem possível, mesmo que ainda não esteja totalmente estável.

@ Michael-F-Bryan Ainda não temos size_of constantes.

@ Michael-F-Bryan Não é apenas suficiente ter um array [u8] , você também precisa obter o alinhamento correto. Na verdade, já uso macros para lidar com uniões, mas devido à falta das constantes size_of e align_of , tenho que alocar manualmente o espaço correto, mais porque não há concatenação de identidades utilizável em macros declarativas I tem que especificar manualmente os nomes para getters e setters. Até mesmo inicializar uma união é difícil no momento porque eu tenho que inicializá-la primeiro com algum valor padrão e então definir o valor para a variante que eu quero (ou adicionar outro conjunto de métodos para construir a união que é ainda mais detalhista na definição do sindicato). Em geral, é muito mais trabalhoso e sujeito a erros e mais feio do que o suporte nativo para sindicatos. Talvez você deva ler o RFC e a discussão que o acompanha para entender por que esse recurso é tão importante.

E o mesmo para o alinhamento.

Imagino que a concatenação de identidade não seja muito difícil agora que syn existe. Ele permite que você faça operações no AST passado, então você pode pegar dois Idents , extrair sua representação de string ( Ident implementa AsRef<str> ) e, em seguida, criar um novo Ident que é a concatenação dos dois usando Ident::From<String>() .

O RFC menciona muito sobre como as implementações de macro existentes são complicadas de usar, no entanto, com a recente criação de caixas como syn e quote , agora é muito mais fácil fazer macros proc. Acho que isso ajudaria muito a melhorar a ergonomia e a tornar as coisas menos sujeitas a erros.

Por exemplo, você poderia ter um MyUnion::default() que apenas zera o buffer interno da união, então um fn MyUnion::new<T>(value:T) -> MyUnion , onde T tem um traço vinculado garantindo que você só possa inicializar com os tipos corretos .

Em termos de alinhamento e tamanho, você é capaz de usar o módulo mem da biblioteca padrão (ou seja, std :: mem :: align_of () e amigos)? Acho que tudo o que estou propondo dependeria de ser capaz de usá-los no tempo de expansão da macro para descobrir o tamanho e o alinhamento necessários. 99,9% das vezes que as uniões são usadas, é feito com tipos primitivos de qualquer maneira, então eu sinto que você seria capaz de escrever uma função auxiliar que leva o nome de um tipo e retorna seu alinhamento ou tamanho (possivelmente perguntando ao compilador, embora isso seja mais um detalhe de implementação).

Eu admito, o casamento de padrões integrado seria muito bom, mas na maioria das vezes, qualquer união que você usa no FFI ficaria embrulhada em uma fina camada de abstração de qualquer maneira. Portanto, você pode conseguir fazer algumas declarações if / else ou usar uma função auxiliar.

Em termos de alinhamento e tamanho, você é capaz de usar o módulo mem da biblioteca padrão (ou seja, std :: mem :: align_of () e amigos)?

Isso não vai funcionar em nenhum contexto de compilação cruzada.

@ Michael-F-Bryan Todas essas discussões e muitas mais aconteceram na história de https://github.com/rust-lang/rfcs/pull/1444 . Para resumir as respostas às suas preocupações específicas, além das já mencionadas: você teria que reimplementar as regras de preenchimento e alinhamento de cada plataforma / compilador de destino e usar uma sintaxe estranha em todo o seu código FFI (que @ retep998 de fato fez extensivamente para ligações do Windows e pode atestar a estranheza de). Além disso, macros proc atualmente só funcionam para derivar; você não pode estender a sintaxe a outro lugar.

Além disso:

99,9% das vezes que as uniões são usadas, é feito com tipos primitivos de qualquer maneira

Não é verdade. O código C usa extensivamente um padrão de "estrutura de uniões de estruturas", em que a maioria dos campos de união consistem em diferentes tipos de estrutura.

@rfcbot fcp merge por @petrochenkov 's comentário https://github.com/rust-lang/rust/issues/32836#issuecomment -279256434

Não tenho nada a acrescentar, apenas acionando o bot

O membro da equipe @withoutboats propôs fundir isso. A próxima etapa é revisada pelo restante das equipes marcadas:

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

Nenhuma preocupação listada atualmente.

Assim que esses revisores chegarem a um consenso, isso entrará em seu período final para comentários. Se você identificar uma questão importante que não foi levantada em algum ponto deste processo, fale!

Consulte este documento para obter informações sobre quais comandos os membros da equipe marcados podem me dar.

PSA: Vou atualizar o RFC "Uniões 1.2" com mais uma alteração que afeta os sindicatos no estilo FFI - moverei atribuições seguras para campos trivialmente destrutíveis de sindicatos de "Direções futuras" para o RFC adequado.

union.trivially_destructible_field = 10; // safe

Por quê:

  • Atribuições a campos sindicais trivialmente destrutíveis são incondicionalmente seguras, independentemente da interpretação dos sindicatos.
  • Isso removerá aproximadamente metade dos blocos de unsafe relacionados ao sindicato.
  • Será mais difícil fazer mais tarde devido ao grande número potencial de unused_unsafe avisos / erros no código estável.

@petrochenkov Você quer dizer "campos de união trivialmente destrutíveis" ou "uniões com campos totalmente destrutíveis de maneira trivial"?

Você está propondo que todos os comportamentos inseguros ocorram na leitura, onde você escolhe uma interpretação? Por exemplo, ter uma união contendo um enum e outros campos, onde o valor da união contém um discriminante inválido?

Em um nível alto, isso parece plausível. Ele permite algumas coisas que eu consideraria inseguras, mas Rust em geral não, como ignorar destruidores ou vazar memória. Em um nível baixo, eu hesitaria em considerar esse som.

Eu me sinto bem em estabilizar esse subconjunto. Não sei a minha opinião sobre este RFC do Unions 1.2 ainda porque não tive tempo de lê-lo! Não tenho certeza do que penso sobre permitir o acesso seguro aos campos em alguns casos. Eu sinto que nossos esforços para fazer uma noção "mínima" do que é inseguro (apenas dicas de desreferenciamento) foi um erro, em retrospecto, e deveríamos ter declarado uma faixa mais ampla de coisas inseguras (por exemplo, muitos elencos), uma vez que eles interagir de maneiras complexas com o LLVM. Acho que também pode ser o caso aqui. Colocando de outra forma, eu prefiro retirar as regras sobre unsafe em conjunto com mais progresso nas diretrizes de código inseguro.

@joshtriplett
"campos trivialmente destrutíveis", ajustei o texto.

Você está propondo que todos os comportamentos inseguros ocorram na leitura, onde você escolhe uma interpretação?

Sim. A gravação sozinha não pode causar nada de perigoso sem uma leitura subsequente.

EDITAR:

Eu sinto que nossos esforços para fazer uma noção "mínima" do que é inseguro (apenas dicas de desreferenciamento) foram um erro, em retrospecto

Oh.
As gravações seguras estão completamente alinhadas com a abordagem atual de insegurança, mas se você for alterá-la, provavelmente devo esperar.

Não me sinto bem em estabilizar este subconjunto. Normalmente, quando estabilizamos um subconjunto, ele é um subconjunto sintático ou pelo menos um subconjunto bastante óbvio. Este subconjunto parece um pouco complexo para mim. Se há tantos indecisos sobre o recurso que não estamos prontos para estabilizar a implementação atual, prefiro deixar tudo instável por mais um tempo.

@nrc
Bem, o subconjunto é bastante óbvio - "uniões FFI", ou "uniões C" ou "uniões pré-C ++ 11" - mesmo que não seja sintático. Meu objetivo inicial era estabilizar este subconjunto o mais rápido possível (este ciclo, idealmente) para que pudesse ser usado em bibliotecas como winapi .
Não há nada de especialmente duvidoso sobre o subconjunto restante e sua implementação, simplesmente não é urgente e precisa esperar por um período de tempo incerto até que o processo para "Uniões 1.2" RFC seja concluído. Minhas expectativas seriam estabilizar as partes restantes em 1, 2 ou 3 ciclos após a estabilização do subconjunto inicial.

Acho que tenho um argumento final para atribuições de campo seguras.
Atribuição de campo insegura

unsafe {
    u.trivially_destructible_field = value;
}

é equivalente a atribuição segura de união plena

u = U { trivially_destructible_field: value };

exceto que a versão segura é paradoxalmente menos segura porque sobrescreverá u bytes fora de trivially_destructible_field com undefs, enquanto a atribuição de campo tem garantia de deixá-los intactos.

@petrochenkov O extremo disso é size_of_val(&value) == 0 , certo?

A equivalência entre os dois trechos só é verdadeira se o campo em questão for
"trivialmente destrutível", não?

Nesse sentido, fazer atribuições como essas seguras, mas apenas em alguns casos
parece extremamente inconsistente para mim.

Em 22 de fevereiro de 2017, 14:50, "Vadim Petrochenkov" [email protected]
escrevi:

Acho que tenho um argumento final para atribuições de campo seguras.
Atribuição de campo insegura

inseguro {
u.trivially_destructible_field = value;
}

é equivalente a atribuição segura de união plena

u = U {campo destrutível_fivialmente: valor};

exceto que a versão segura é paradoxalmente menos segura porque irá
sobrescrever os bytes de u fora de trivially_destructible_field com undefs,
enquanto a atribuição de campo tem garantia de deixá-los intactos.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment-281660298 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AApc0lUOXLU5xNTfM5PEfEz9nutMZhXUks5rfC8UgaJpZM4IDXsj
.

@eddyb

O extremo disso é size_of_val (& value) == 0, certo?

Sim.

@nagisa
Não entendo por que uma regra simples adicional que elimina grande parte dos falsos positivos é extremamente inconsistente. "apenas alguns casos" cobrem todos os sindicatos FFI em particular.
Acho que "extremamente inconsistente" é uma grande superestimativa. Não tão grande quanto "apenas mut variáveis ​​podem ser atribuídas? Horrível inconsistência!", Mas ainda indo nessa direção.

@petrochenkov considere esse caso:

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

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

Agora, uma vez que adicionar implementações de características geralmente não é uma mudança significativa, o Sr. Somebody Somewhere descobre que pode ser uma boa ideia adicionar a implementação seguinte

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

e lançar uma versão 1.1.0 (semver compatível com 1.0.0 AFAIK) da caixa.

De repente, a caixa do Sr. Outra Pessoa não compila mais:

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

E, portanto, às vezes permitir atribuições seguras para campos de união não é tão trivial quanto apenas mut locais podendo ser mutados.


Ao escrever este exemplo, fiquei inseguro de qual postura devo tomar sobre isso, honestamente. Por um lado, gostaria de preservar a propriedade de que adicionar implementações geralmente não é uma alteração significativa (ignorando casos potenciais de XID). Por outro lado, alterar qualquer campo de união de trivialmente destrutível para não trivialmente destrutível é obviamente uma mudança sempre incompatível que é extremamente fácil de ignorar e a regra proposta tornaria essas incompatibilidades mais visíveis (desde que a atribuição não esteja em um bloco inseguro já).

@nagisa
Este é um bom argumento, não pensei em compatibilidade.

O problema parece resolvível. Para evitar problemas de compatibilidade, faça a mesma coisa que a coerência - evite o raciocínio negativo. Ou seja, substitua "trivialmente destrutível" == "nenhum componente implementa Drop " pela aproximação positiva mais próxima - "implementa Copy ".
Copy não pode cancelar a implementação de Copy com compatibilidade retroativa, e os tipos Copy ainda representam a maioria dos tipos "trivialmente destrutíveis", especialmente no contexto de uniões FFI.

Implementar Drop já não é compatível com versões anteriores e isso não tem nada a ver com o recurso de união:

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

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

O que, por sua vez, soa como implementar Drop drop implícito Copy. E copiar
pode ser confiável.

Na quarta-feira, 22 de fevereiro de 2017 às 10:11, jethrogb [email protected] escreveu:

Implementar o Drop já não é compatível com versões anteriores e
nada a ver com o recurso sindicato:

// Alguém em algum lugar em alguma caixa (v 1.0.0)
struct Apple; // trivialmente destrutível
struct Pineapple {pub na verdade: Apple}

// Outra pessoa em sua caixa dependente
externamente alguma caixa;
fn pineapple_to_apple (abacaxi: abacaxi) -> Maçã {
abacaxi. realmente
}

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

fn pineapple_to_apple (abacaxi: abacaxi) -> Maçã {
banana.realmente // ERRO: não consigo sair do abacaxi
}

-
Você está recebendo isto porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment-281752949 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/ABxXhgbFgRNzYOsU4c6Gu1KFfwdjDHn3ks5rfHpYgaJpZM4IDXsj
.

@jethrogb
Eu queria mencionar este problema, mas não o fiz porque ele tem poucas pré-condições especiais - a estrutura que implementa Drop deve ter um campo público e esse campo não deve ser Copy . O caso sindical afeta todas as estruturas de forma incondicional.

@petrochenkov Eu considero que talvez um argumento de que a criação de um sindicato não seja seguro :)

@petrochenkov qual é o caminho de desenvolvimento e experiência noturna do usuário para estabilizar um subconjunto? Devemos adicionar uma nova porta de recurso primeiro para o subconjunto, para que as pessoas possam obter uma experiência concreta usando o subconjunto antes que ele se estabilize?

@pnkfelix
O que eu presumi é apenas parar de exigir #[feature(untagged_unions)] para este subconjunto, sem novos recursos ou outra burocracia.
Supõe-se que as uniões do tipo FFI sejam o tipo de união mais usado, então um novo recurso significaria quebra garantida logo antes da estabilização, o que, presumo, seria irritante.

Eu gostaria apenas de observar que, como os atributos de alinhamento e compactação ainda não foram implementados (não importa estabilizado), eu realmente não tenho uma grande necessidade de que isso seja estabilizado ainda.

@ retep998
Embalagem? Se você quer dizer #[repr(packed)] então ele é compatível com sindicatos agora (ao contrário de align(>1) atributos).

@petrochenkov #[repr(packed(N))] . Embalagem diferente de 1 é necessária um pouco no winapi. Não é que eu precise que essas coisas tenham suporte específico nos sindicatos, só não quero pular para uma nova versão principal para aumentar meu requisito mínimo de Rust, a menos que possa obter todas essas coisas ao mesmo tempo.

Para esclarecer um pouco a situação:

A proposta FCP atual é apenas para uniões puras- Copy . Esse é o caso, até onde eu sei, basicamente de nenhuma questão pendente além de "Devemos estabilizar?" A discussão desde a moção ao FCP tem sido toda sobre o novo RFC de @petrochenkov .

@nrc e @nikomatsakis , da discussão no IRC, suspeito que vocês dois estão prontos para marcar suas opções, mas vou deixar isso com vocês ;-)

: bell: Agora está entrando em seu período de comentário final , conforme a revisão acima . :Sino:

@petrochenkov
Parece-me que isso se beneficiaria de uma ideia que tive recentemente. (https://internals.rust-lang.org/t/automatic-marker-trait-for-unconditionally-valid-repr-c-types/5054)

Embora não seja proposto com sindicatos em mente, o traço Plain conforme explicado (sujeito a bicicletas) permitiria que qualquer sindicato composto de apenas Plain tipos fosse usado sem qualquer insegurança. Ele codifica a propriedade de que _qualquer_ padrão de bit na memória é igualmente válido, de modo que você pode resolver os problemas de inicialização determinando que a memória seja zerada e garante que as leituras não invoquem UB.

No contexto da FFI, ser Plain conforme definido também é um requisito de fato para que esse código seja válido em muitos casos, enquanto os casos em que tipos não Plain são úteis são raros e difíceis de configurar com segurança.

Deixando de lado a existência factual de uma característica nomeada, pode ser prudente dividir essa característica em duas. Com union não qualificado reforçando os requisitos e permitindo o uso sem insegurança, e unsafe union com requisitos flexíveis de conteúdo e mais dor de cabeça para os usuários. Isso permitiria estabilizar qualquer um dos dois sem impedir o caminho de adicionar o outro no futuro.

@ le-jzr
Isso parece suficientemente ortogonal aos sindicatos.
Eu estimaria aceitar Plain em Rust em um futuro próximo, pois não é muito provável + tornar mais seguros os acessos a campos sindicais é mais ou menos compatível com versões anteriores (não inteiramente devido a lints), então eu não atrasaria os sindicatos devidos para isso.

@petrochenkov Não estou sugerindo atrasar os sindicatos na espera de qualquer encerramento de minha proposta, mas sim considerar a _existência_ de tais possíveis restrições por si só. Tornar mais seguros os acessos aos campos sindicais no futuro pode enfrentar obstáculos, pois cria mais inconsistência na linguagem. Em particular, fazer com que a segurança de um acesso de campo varie de campo para campo parece feio.

Daí minha sugestão de fazer a declaração unsafe union , de modo que a versão incondicionalmente segura de usar possa ser introduzida posteriormente sem adicionar novas palavras-chave. É importante notar que a versão completamente segura de usar é suficiente para a maioria dos casos de uso.

Edit: Tentei esclarecer o que quero dizer.

Outro lugar onde esta possibilidade pode informar o design atual e futuro é a inicialização. Para uma união incondicionalmente segura, é necessário que todo o espaço de memória reservado para ela seja zerado. Mesmo com a versão atual, garantir isso reduziria os potenciais cenários de UB e tornaria o uso de sindicatos mais fácil.

O período de comentários final agora está completo.

Agora que o FCP a ser mesclado está concluído, qual é a próxima etapa aqui? Seria bom estabilizar isso para 1,19.

Seria incrível se alguém pudesse adicionar mais detalhes à mensagem @rfcbot ( código-fonte aqui ). Pode tornar mais fácil para alguém não familiarizado com o processo entrar e mover as coisas.

O caminho é claro para chegar ao 1.19. Alguém quer isso? cc @joshtriplett

A otimização de layout NonZero enum tem garantia de aplicação por meio de um union ? Por exemplo, Option<ManuallyDrop<&u32>> não deve representar None como um ponteiro nulo. Some(ManuallyDrop::new(uninitialized::<[Vec<Foo>; 10]>())).is_some() não deve ler a memória não inicializada.

https://crates.io/crates/nodrop (usado em https://crates.io/crates/arrayvec) tem hacks para lidar com isso.

@SimonSapin
No momento, isso está marcado como uma questão não resolvida no RFC .
Na implementação atual, este programa

#![feature(untagged_unions)]

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

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

estampas

struct 8
optional struct 8
union 8
optional union 16

, ou seja, a otimização não é realizada.
cc https://github.com/rust-lang/rust/issues/36394

É improvável que isso faça 1,19.

@brson

É improvável que isso faça 1,19.

O PR de estabilização é mesclado.

Com as uniões não marcadas agora sendo enviadas em 1.19 (em parte de https://github.com/rust-lang/rust/pull/42068) - há algo restante sobre esse problema ou devemos encerrar?

@jonathandturner
Ainda existe um mundo inteiro de sindicatos com campos não Copy !
O progresso está principalmente bloqueado no RFC de esclarecimento / documentação (https://github.com/rust-lang/rfcs/pull/1897).

Houve algum progresso nas uniões com campos não Copy desde agosto? O RFC do Unions 1.2 parece parado (suponho que seja devido ao período de implementação?)

Permitir tipos de ?Sized em sindicatos - mesmo que apenas para sindicatos de um único tipo - tornaria mais fácil implementar https://github.com/rust-lang/rust/issues/47034 :

`` `ferrugem
união ManualmenteDrop{
valor: T
}

@mikeyhew Você realmente só precisa exigir que no máximo um tipo possa ser removido.

Estou olhando para um código Rust usando union s e não tenho ideia se ele invoca um comportamento indefinido ou não.

A referência [items :: unions] menciona apenas:

Os campos inativos também podem ser acessados ​​(usando a mesma sintaxe) se tiverem um layout suficientemente compatível com o valor atual mantido pelo sindicato. Ler campos incompatíveis resulta em comportamento indefinido.

Mas não consigo encontrar uma definição de "layout compatível" nem em [items :: unions] nem em [type_system :: type_layout] .

Olhando as RFCs, também não consegui encontrar uma definição de "layout compatível", apenas exemplos acenados à mão do que deveria ou não funcionar na RFC 1897: Uniões 1.2 (não mesclado).

O RFC1444: sindicatos só parece permitir a transmutação de um sindicato para suas variantes, desde que não invoque um comportamento indefinido, mas não consigo encontrar nenhum lugar no RFC quando esse é / não é o caso.

As regras _precisas_ que me dizem se um trecho de código usando uniões definiu o comportamento escrito em algum lugar (e qual é o comportamento definido)?

@gnzlbg Para uma primeira aproximação: você não pode acessar o preenchimento, não pode acessar um enum que contém um discriminante inválido, não pode acessar um bool que contém um valor diferente de verdadeiro ou falso, não pode acessar um valor de ponto flutuante inválido ou de sinalização , e algumas outras coisas como essas.

Se você apontar para um código específico envolvendo sindicatos, poderíamos olhar para ele e dizer se ele está fazendo algo indefinido.

Para uma primeira aproximação: você não pode acessar o preenchimento, não pode acessar um enum que contém um discriminante inválido, não pode acessar um bool que contém um valor diferente de verdadeiro ou falso, não pode acessar um valor de ponto flutuante de sinalização ou inválido e algumas outras coisas como essas.

Na verdade, o consenso mais recente é que ler flutuadores arbitrários é bom (# 46012).

Eu adicionaria mais um requisito: as variantes de união de origem e destino são #[repr(C)] e, portanto, todos os seus campos (e recursivamente) se forem estruturas.

@Amanieu , estou corrigido, obrigado.

Então eu acho que as regras não estão escritas em lugar nenhum?

Estou examinando como usar stdsimd com sua nova interface. A menos que estabilizemos alguns extras com ele, será necessário usar sindicatos para fazer trocadilhos com alguns dos tipos simd, como este:

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

AFAIK escrever um campo de união e ler outro é muito parecido com transmute_copy , com as mesmas restrições. O fato de essas restrições ainda serem um pouco nebulosas não é específico do sindicato.

Por falar nisso, a função que você vinculou poderia usar apenas transmute::<__m128d, [f64; 2]> . Embora a versão de união seja discutível melhor, pelo menos uma vez que o transmute que está lá atualmente seja removido: poderia ser apenas A { a }.b[idx] .

@rkruppe Preenchi um problema de clippy para adicionar esse lint: https://github.com/rust-lang-nursery/rust-clippy/issues/2361

a função que você vinculou poderia apenas usar transmutar :: <__ m128d i = "8">

Acho que as regras que procuro são quando a transmutação invoca o comportamento indefinido (então irei procurá-las).

Eu acho que teria me ajudado se a referência de linguagem sobre sindicatos tivesse especificado as regras para sindicatos em termos de transmutação (mesmo que as regras para transmutação não sejam 100% claras ainda) em vez de apenas mencionar "compatibilidade de layout" e deixá-la em que. O salto de "compatibilidade de layout" para "se transmutar não invocar comportamento indefinido, então os tipos são compatíveis com layout e podem ser acessados ​​por meio de trocadilhos" não era óbvio para mim.

Para ficar claro, transmute [_copy] não é "mais primitivo" do que as uniões. Na verdade, transmute_copy é literalmente apenas o ponteiro as casts mais ptr::read . transmute adicionalmente precisa de mem::uninitialized (obsoleto) ou MaybeUninitialized (uma união) ou algo parecido, e é implementado como intrínseco para eficiência, mas também se resume a um tipo- trocadilhos memcpy. A principal razão pela qual fiz a conexão para transmutar é porque é mais antigo e historicamente superenfatizado e, portanto, atualmente temos mais artigos e conhecimento folclórico que se concentra especificamente na transmutação. O verdadeiro conceito subjacente, que dita o que é válido e o que não é (e que uma especificação descreveria), é como os valores são armazenados na memória como bytes e quais sequências de bytes devem ser lidas pelo UB e quais tipos.

Correção: transmute realmente não precisa de armazenamento não inicializado (via intrínseca, ou uniões, ou outro). Eficiência à parte, você pode fazer algo assim (não testado, pode conter erros de digitação embaraçosos):

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

A única parte "mágica" do transmute é que ele pode restringir os parâmetros de tipo para ter o mesmo tamanho em tempo de compilação .

A referência e a RFC do Unions 1.2 são intencionalmente vagas sobre este assunto porque as regras para transmutação em geral não são estabelecidas.
A intenção era "para repr(C) unions, consulte as especificações de ABI de terceiros, para repr(Rust) unions a compatibilidade de layout não é especificada (a menos que seja)".

É tarde demais para revisitar a semântica da verificação de queda de sindicatos com campos de queda?

O problema original é que a adição de ManuallyDrop fez com que Josephine se tornasse insegura, porque (um tanto maliciosamente) dependia de valores emprestados por permanência não tendo seu armazenamento de apoio recuperado sem primeiro executar seu destruidor.

Um exemplo simplificado está em https://play.rust-lang.org/?gist=607e2dfbd51f4062b9dc93d149815695&version=nightly. A ideia é que existe um tipo Pin<'a, T> , com um método pin(&'a self) -> &'a T cuja segurança depende do invariante "depois de chamar pin.pin() , se a memória que suporta o pino for recuperada, então o destruidor do pino deve ter sido executado ".

Este invariante foi mantido por Rust até #[allow(unions_with_drop_fields)] ser adicionado, e usado por ManuallyDrop https://doc.rust-lang.org/src/core/mem.rs.html#949.

O invariante seria restaurado se o verificador de queda considerasse as uniões com campos de queda para ter um implemento de queda. Esta é uma alteração significativa, mas duvido que qualquer código em estado selvagem dependa da semântica atual.

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

Problema de Josephine: https://github.com/asajeffrey/josephine/issues/52

cc: @nox @eddyb @pnkfelix

O problema original é que a adição de ManualmenteDrop fez com que Josephine se tornasse insalubre, porque (um tanto travessamente) dependia de valores emprestados por permanência sem ter seu armazenamento de apoio recuperado sem primeiro executar seu destruidor.

Não há garantia de funcionamento dos destruidores. A ferrugem não garante isso. Ele tenta, mas, por exemplo, std::mem::forget foi transformado em uma função segura.

O invariante seria restaurado se o verificador de queda considerasse as uniões com campos de queda para ter um implemento de queda. Esta é uma alteração significativa, mas duvido que qualquer código em estado selvagem dependa da semântica atual.

Os sindicatos são inseguros principalmente porque você não pode saber qual campo do sindicato é válido. O sindicato não pode ter um Drop impl automático; se você quiser tal impl, precisará escrevê-lo manualmente, levando em consideração todos os meios necessários para saber se o campo de união com Drop impl é válido.

Um esclarecimento aqui: eu não acredito que jamais deve permitir uniões com Drop campos por padrão sem pelo menos um fiapo avisá-by-padrão, se não um fiapo de erro-by-default. unions_with_drop_fields não deve desaparecer como parte do processo de estabilização.

EDIT: opa, não queria clicar em "fechar e comentar".

@joshtriplett sim, Rust não garante que os destruidores serão executados, mas aconteceu (antes do 1.19) manter a invariante de que os valores emprestados por perma somente teriam sua memória recuperada se o destruidor fosse executado. Isso é verdade até na presença de mem::forget , já que você não pode chamá-lo com um valor emprestado por perma.

Era nisso que Joephine estava confiando com malícia, mas não é mais verdade por causa de como o verificador trata unions_with_drop_fields .

Estaria tudo bem se allow(unions_with_drop_fields) fosse considerada uma anotação insegura, esta não seria uma mudança drástica, AFAICT, apenas exigiria deny(unsafe_code) para verificar allow(unions_with_drop_fields) .

@asajeffrey Ainda estou tentando entender a coisa de Pin ... então, se eu seguir o exemplo corretamente, a razão de isso "funcionar" é que fn pin(&'a Pin<'a, T>) -> &'a T força o empréstimo a durar menos de contanto que o tempo de vida 'a anotado no tipo, e esse tempo de vida é, além disso, invariável.

Essa é uma observação interessante! Eu não estava ciente desse truque. Meu pressentimento é que isso funciona "por acidente", ou seja, o Rust seguro não fornece uma maneira de impedir que o destruidor execute, mas isso não faz parte do "contrato". Notavelmente, https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html não lista vazamentos.

IMO, não importa se funciona por acidente ou de propósito. Não havia como evitar que Drop executasse esse truque antes de ManuallyDrop existente (que requer a implementação de um código inseguro), e agora não podemos mais confiar nisso.

A adição de ManuallyDrop basicamente matou aquele comportamento bacana do Rust e dizer que ele não deveria ter sido invocado em primeiro lugar soa como um raciocínio circular para mim. Se ManuallyDrop não permitisse a chamada de Pin::pin , haveria alguma outra maneira de tornar a chamada de Pin::pin incorreta? Acho que não.

Não acho que podemos nos comprometer a preservar todas as garantias que o rustc acidentalmente fornece agora. Não temos ideia do que essas garantias podem ser, então estaríamos estabilizando um porco em uma cutucada (ok, espero que essa expressão faça sentido ... é o que o dicionário me diz que corresponde à minha língua nativa, que seria literalmente traduzido como " o gato na sacola ";) - o que eu quero dizer é que não teríamos ideia do que estaríamos estabilizando).

Além disso, esta é uma faca de dois gumes - cada garantia adicional que decidimos fornecer é algo que o código inseguro deve cuidar. Portanto, descobrir uma nova garantia pode muito bem quebrar o código inseguro existente (sentado silenciosamente em algum lugar em crates.io sem estar ciente) quanto habilitar um novo código inseguro (o último foi o caso aqui).

Por exemplo, é muito concebível que os tempos de vida lexicais permitam um código inseguro que seja quebrado por tempos de vida não lexicais. Atualmente, todas as vidas estão bem aninhadas, talvez haja uma maneira de o código não seguro explorar isso? Apenas com vidas não lexicais pode haver existências que se sobrepõem, mas nenhuma está incluída na outra. Isso torna o NLL uma alteração significativa? Espero que não!

Se ManualmenteDrop não permitisse a chamada de Pin :: pin, haveria alguma outra maneira de tornar a chamada de Pin :: pin incorreta? Acho que não.

Com o código unsafe , haveria. Portanto, ao declarar este Pin som de truque, você está declarando algum código inseguro unsound que seria válido se decidirmos que ManuallyDrop está bem.

O que descrevemos é uma maneira muito ergonômica de integrar Rust com GCs. O que estou tentando dizer é que me parece errado nos dizer que isso foi apenas um acidente que funcionou e que devemos esquecer disso, quando não consigo encontrar nenhum caso de uso para não restringir uniões com Drop campos como descrito por @asajeffrey aqui, e quando esta é realmente a única verruga que quebra Josephine.

Ficarei feliz em esquecê-lo se alguém puder mostrar que ele não era sólido, mesmo sem ManuallyDrop .

O que estou tentando dizer é que me parece errado dizer que foi apenas um acidente que funcionou

Não vejo nenhuma indicação de que esse truque tenha sido "projetado", então acho que é justo chamar de acidente.

e que devemos esquecer isso

Eu deveria ter deixado mais claro que essa parte é apenas minha intuição. Acho que também poderia ser um ponto de ação razoável declarar este um "acidente feliz" e realmente torná-lo uma garantia - se estivermos razoavelmente confiantes de que de fato todos os outros códigos unsafe respeitam esta garantia, e que fornecer essa garantia é mais importante do que o caso de uso ManuallyDrop . Este é um trade-off, semelhante ao pocalipse de vazamento, onde não podemos comer nosso bolo e tê-lo também (não podemos ter Rc com sua API atual e o drop - threads com escopo baseado; não podemos ter ManuallyDrop e Pin ), portanto, temos que tomar uma decisão de qualquer maneira.

Dito isso, acho difícil expressar a garantia real fornecida aqui de maneira precisa, o que me faz pessoalmente inclinar-se mais para o lado " ManuallyDrop está bom" das coisas.

se estivermos razoavelmente confiantes de que de fato todos os outros unsafe código respeitam essa garantia e que fornecer essa garantia é mais importante do que o caso de uso ManuallyDrop . Este é um trade-off, semelhante ao pocalipse de vazamento, onde não podemos comer nosso bolo e tê-lo também (não podemos ter Rc com sua API atual e o drop - threads com escopo baseado; não podemos ter ManuallyDrop e Pin ), portanto, temos que tomar uma decisão de qualquer maneira.

É justo, eu concordo plenamente com isso. Note que se considerarmos o que @asajeffrey descreveu como comportamento indefinido no final, isso pode trazer de volta uma API de thread com escopo baseado em drop .

Pelo que entendi, a proposta de Alan não é remover ManuallyDrop , apenas fazer dropck assumir que (e outras uniões com Drop campos) tem um destruidor. (Acontece que os destruidores não fazem nada, mas sua mera existência afeta os programas que o dropck aceita ou rejeita.)

Ficarei feliz em esquecê-lo se alguém puder mostrar que ele estava incorreto, mesmo sem o ManualmenteDrop.

Não tenho certeza se isso se qualifica, mas aqui está minha primeira tentativa: Uma implementação boba de algo como ManuallyDrop que funciona em pré- union Rust.

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

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

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

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

(Sim, provavelmente tenho que fazer mais algum trabalho para obter o alinhamento correto, mas isso também poderia ser feito sacrificando alguns bytes.)
Playground mostrando esses intervalos Pin : https://play.rust-lang.org/?gist=fe1d841cedb13d45add032b4aae6321e&version=nightly

Isso é o que eu quis dizer com espada de dois gumes acima - pelo que posso ver, meu ManuallyDrop respeita todas as regras que estabelecemos. Portanto, temos duas partes de código inseguro incompatível - ManuallyDrop e Pin . Quem está "certo"? Eu diria que Pin depende de garantias que nunca fizemos e, portanto, está "errado" aqui, mas isso é um julgamento, não uma prova.

Isso é interessante. Em algumas versões do nosso material de fixação, Pin::pin leva um &'this mut Pin<'this, T> , mas não seria irracional para o seu ManuallyDrop ter um DerefMut impl, certo ?

Aqui está um playground que mostra que @RalfJung (sem surpresa) ainda quebra Pin com um método &mut pegando pin .

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

não seria irracional para o seu ManualmenteDrop ter um implante DerefMut, certo?

Sim, acabei de adicionar a API necessária para este exemplo. O óbvio deref_mut deve funcionar muito bem.

Pelo que entendi, a proposta de Alan não é remover o ManualmenteDrop, apenas para fazer dropck assumir que ele (e outras uniões com campos Soltar) tem um destruidor. (Acontece que os destruidores não fazem nada, mas sua mera existência afeta os programas que o dropck aceita ou rejeita.)

Ah, eu tinha sentido falta disso; me desculpe por isso. Adicionar o seguinte ao meu exemplo o mantém funcionando:

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

Somente se eu remover o #[may_dangle] Rust o rejeita. Então, pelo menos, teríamos que criar alguma regra que o código acima viole - apenas dizer "existe algum código com o qual queremos que seja compatível e que seja incompatível" é uma chamada ruim porque o torna praticamente impossível olhar algum código e verificar se ele está correto.


Acho que o que mais me incomoda sobre essa "garantia acidental" é que não vejo uma única boa razão para que isso funcione. A forma como as coisas são conectadas no Rust faz com que isso se mantenha, mas o dropck foi adicionado não para evitar vazamentos, mas para evitar referências incorretas a dados mortos (um problema comum em destruidores). O raciocínio para Pin funcionar não é baseado em "aqui está algum mecanismo no compilador Rust, ou algum tipo de garantia do sistema, que diz muito claramente que dados emprestados permanentemente não podem ser vazados" - é mais baseado em "tentamos muito e não conseguimos vazar dados emprestados de perma, então achamos que está tudo bem". Depender disso para obter solidez me deixa muito nervoso. EDIT: O fato de dropck estar envolvido me deixa ainda mais nervoso porque esta parte do compilador tem um histórico de bugs de integridade desagradáveis. A razão pela qual isso funciona parece ser que empréstimos permanentes estão em desacordo com drop seguros. Isso realmente parece ser "um raciocínio baseado em uma análise exaustiva do caso do que se pode fazer com os dados emprestados por permanentes".

Agora, para ser justo, pode-se dizer coisas semelhantes sobre a mutabilidade interior - acontece que permitir modificações por meio de referências compartilhadas realmente funciona com segurança em alguns casos, se escolhermos a API certa. No entanto, fazer este trabalho realmente exigiu suporte explícito no compilador ( UnsafeCell ) porque ele entra em conflito com otimizações e há um código inseguro que seria válido sem mutabilidade interior, mas não é válido com mutabilidade interior. Outra diferença é que a mutabilidade interior foi uma meta de design desde o início (ou desde muito cedo - isso é muito antes do meu tempo na comunidade Rust), o que não é o caso de "perma-emprestado não vaza". E, finalmente, para a mutabilidade interior, acho que há uma história muito boa sobre "o compartilhamento torna a mutação perigosa , mas não impossível , e a API de referências compartilhadas apenas diz que você não obtém mutabilidade em geral, mas não exclui permitir mais operações para operações específicas tipos ", resultando em um quadro geral coerente. Claro, passei muito tempo pensando sobre referências compartilhadas, então talvez haja uma imagem igualmente coerente para o problema em questão, da qual simplesmente não estou ciente.

Os fusos horários são divertidos, acabei de me levantar! Parece haver dois problemas aqui (invariantes em geral e dropck em particular), então vou colocá-los em comentários separados ...

@RalfJung : sim, este é um problema sobre os invariantes sendo mantidos por Rust inseguro. Para qualquer versão do Rust + std, há mais de uma escolha do invariante I que é mantido usando o raciocínio de garantia confiável. E, de fato, pode haver duas bibliotecas L1 e L2 , que escolheram I1 e I2 incompatíveis, de modo que Rust + L1 é seguro e Rust + L2 é seguro, mas Rust + L1 + L2 não é seguro.

Neste caso, L1 é ManuallyDrop e L2 é Josephine , e é bastante claro que ManuallyDrop vai ganhar, já que é agora em std , que tem restrições de compatibilidade com versões anteriores muito mais fortes do que Josephine.

Curiosamente, as diretrizes em https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html são escritas como "É responsabilidade do programador ao escrever código inseguro que não é possível permitir código seguro exibem estes comportamentos: ... "isto é, é uma propriedade contextual (para todos os contextos seguros C, C [P] não pode dar errado) e, portanto, depende da versão (desde v1.20 de Rust + std tem mais segurança contextos do que v1.18). Em particular, eu diria que a fixação realmente satisfez essa restrição para Rust antes de 1.20, uma vez que não havia contexto seguro C st C [Pinning] deu errado.

No entanto, isso é apenas advocacia de quartel, acho que todos concordam que há um problema com essa definição contextual, daí todas as discussões sobre diretrizes de código inseguras.

Se nada mais, acho que a fixação mostrou um exemplo interessante de invariantes acidentais dando errado.

A coisa particular que as uniões não marcadas (e portanto ManuallyDrop ) fizeram foi na interação com o verificador de queda, em particular ManualDrop age como se sua definição fosse:

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

e então você pode ter uma conversa sobre se isso é permitido ou não :) Na verdade, esta conversa está acontecendo no tópico may_dangle começando em https://github.com/rust-lang/rust/issues/ 34761 # issuecomment -362375924

@RalfJung seu código mostra um caso interessante, onde o tipo de tempo de execução para data é T , mas o tipo de tempo de compilação é [u8; N] . Que tipo conta no que diz respeito a may_dangle ?

Curiosamente, as diretrizes em https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html são escritas como "É responsabilidade do programador ao escrever código inseguro que não é possível permitir código seguro exibem estes comportamentos: ... "ou seja, é uma propriedade contextual

Ah, interessante. Eu concordo que isso claramente não é suficiente - isso faria os threads de escopo originais soarem. Para ser significativo, isso deve (pelo menos) especificar o conjunto de códigos não seguros que o código seguro tem permissão para chamar.

Pessoalmente, acho que a melhor maneira de especificar isso é fornecer os invariantes que devem ser mantidos. Mas estou claramente tendencioso aqui, porque a metodologia que uso para provar coisas sobre Rust requer tal invariante. ;)

Estou um pouco surpreso que a página não contenha algum tipo de isenção de responsabilidade de ser preliminar; ainda não temos certeza de qual será exatamente o limite - como mostra esta discussão. Exigimos um código inseguro para pelo menos fazer o que o documento diz, mas provavelmente teremos que exigir mais.

Por exemplo, os limites do comportamento indefinido e o que o código inseguro pode fazer não são os mesmos. Veja https://github.com/nikomatsakis/rust-memory-model/issues/44 para uma discussão recente sobre esse tópico: Duplicar um &mut T para mem::size_of::<T>() == 0 não leva a nenhum comportamento indefinido diretamente, e ainda é claramente considerado ilegal para código inseguro fazer. O motivo é que outro código inseguro pode depender do respeito pela disciplina de propriedade, e duplicar itens viola essa disciplina.

Se nada mais, acho que a fixação mostrou um exemplo interessante de invariantes acidentais dando errado.

Oh, isso certamente. E eu me pergunto o que podemos fazer para evitar isso no futuro? Talvez coloque um grande aviso em https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html dizendo "só porque um invariante mantém em rustc + libstd, não significa que o código inseguro pode confiar nele; em vez disso, aqui estão alguns invariantes nos quais você pode confiar "?

@RalfJung sim, não acho que alguém ame a definição contextual de "correção", principalmente porque é frágil em relação ao poder de observação dos contextos. Eu ficaria muito mais feliz com uma definição semântica em termos de invariantes.

A única coisa que eu peço é, por favor, podemos nos dar algum espaço de manobra e definir duas invariantes para o raciocínio de garantia de confiança (o código pode contar com R e deve garantir G, onde G implica R). Dessa forma, há algum espaço para fortalecer R e enfraquecer G. Se tivermos apenas um invariante (ou seja, R = G), não conseguiremos alterá-lo!

A verificação constante atualmente não tem campos de união com casos especiais: (cc @solson @ oli-obk)

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

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

fn main() {
    SILLY
}

O código acima produz o erro de avaliação miri "chamando non-const fn std::ptr::drop_in_place::<(std::boxed::Box<std::string::String>, ())> - shim(Some((std::boxed::Box<std::string::String>, ()))) ".

Alterá-lo para forçar o tipo de .to a ser observado pelo verificador de const:

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

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

resulta em "destruidores não podem ser avaliados em tempo de compilação".

O código de implementação relevante está aqui (especificamente a chamada restrict ):
https://github.com/rust-lang/rust/blob/5e4603f99066eaf2c1cf19ac3afbac9057b1e177/src/librustc_mir/transform/qualify_consts.rs#L557

Uma melhor análise de # 41073 revelou que a semântica para quando os destruidores são executados ao atribuir a subcampos de uniões não está suficientemente pronta para estabilização. Veja esse problema para obter detalhes.

É realista descartar totalmente Drop tipos em sindicatos e implementar ManuallyDrop separadamente (como um lang-item)? Pelo que posso dizer, ManuallyDrop parece ser a maior motivação para Drop em sindicatos, mas esse é um caso muito especial.

Na ausência de um traço positivo "sem queda", poderíamos dizer que uma união está bem formada se cada campo for Copy ou da forma ManuallyDrop<T> . Isso contornaria totalmente todas as complicações em torno da eliminação ao atribuir campos de união (onde parece que todas as soluções possíveis estarão cheias de armas surpreendentes), e o ManuallyDrop é um marcador claro para os programadores de que eles têm que lidar com Drop eles próprios aqui. (A verificação pode ser mais inteligente, por exemplo, pode atravessar os tipos de produtos e os tipos nominais que são declarados na mesma caixa. Claro, ter uma forma positiva de dizer "este tipo nunca implementará Drop " seria Agradável.)


A lista de verificação no primeiro post não menciona sindicatos não dimensionados, nem o RFC --- mas ainda temos uma implementação , restrita a sindicatos de variante única. Isso está intimamente relacionado à interação com otimizações de layout porque pressupõe (uma vez que as DSTs de ponteiro fino entrem em cena) que uma união de variante única deve ser "válida" em algum sentido (pode ser descartada, mas não pode ser qualquer padrão de bit estranho).

Isso entra em conflito com a forma como os sindicatos às vezes são usados ​​em C, que é um "ponto de extensão" ( IIRC @joshtriplett foi quem mencionou isso em todas as mãos): um arquivo de cabeçalho pode declarar 3 variantes para um sindicato, mas isso é considerado compatível com o avanço da adição de mais variantes posteriormente (desde que isso não aumente o tamanho da união). O usuário da biblioteca promete não mexer nos dados de união se a tag (colocada em outro lugar) indicar que ele não conhece a variante atual. Essencialmente, se você só sabe uma única variante, que não significa que há apenas uma única variante!

A verificação pode ser mais inteligente, por exemplo, pode atravessar os tipos de produto e os tipos nominais que são declarados na mesma caixa.

Esse predicado já existe, mas é conservador em genéricos por não haver nenhum traço para vincular.
Você pode acessá-lo via std::mem::needs_drop (que usa um intrínseco que rustc implementa).

@eddyb needs_drop levará em consideração a compatibilidade com versões futuras ou ficará feliz em olhar para outras criações para determinar se seus tipos implementam Drop ? O objetivo aqui é ter uma verificação que nunca interrompa as alterações sempre compatíveis, onde, por exemplo, adicionar impl Drop a uma estrutura sem parâmetros de tipo ou tempo de vida e apenas campos privados são sempre compatíveis.

@RalfJung

Isso entra em conflito com a forma como os sindicatos às vezes são usados ​​em C, que é um "ponto de extensão" ( IIRC @joshtriplett foi quem mencionou isso em todas as mãos): um arquivo de cabeçalho pode declarar 3 variantes para um sindicato, mas isso é considerado compatível com o avanço da adição de mais variantes posteriormente (desde que isso não aumente o tamanho da união). O usuário da biblioteca promete não mexer nos dados de união se a tag (colocada em outro lugar) indicar que ele não conhece a variante atual. Crucialmente, se você conhece apenas uma única variante, isso não significa que existe apenas uma única variante!

Esse é um caso muito específico.
Ele afeta apenas as uniões de estilo C (portanto, não há destruidores e tudo é Copy , exatamente o subconjunto disponível no stable) gerado a partir dos cabeçalhos C.
Podemos facilmente adicionar um campo _dummy: () ou _future: () a essas uniões e continuar nos beneficiando de um modelo "enum" mais seguro por padrão. Um sindicato FFI sendo um "ponto de extensão" é algo que precisa ser bem documentado de qualquer maneira.

Em 17 de abril de 2018, às 10:08:54 PDT, Vadim Petrochenkov [email protected] escreveu:

Podemos facilmente adicionar um campo _dummy: () ou _future: () a essas uniões
e continuar se beneficiando de nosso modelo "enum" mais seguro por padrão.

Já vi pessoas falarem sobre tratar os sindicatos como enums, dos quais simplesmente não conhecemos o discriminante, mas, pelo que sei, não conheço nenhum modelo real ou tratamento deles como tal. Na discussão original, mesmo os sindicatos não-FFI queriam o modelo de "múltiplas variantes válidas por vez", incluindo os casos de uso motivadores para querer uniões não-FFI.

Adicionar uma variante () a um sindicato não deve mudar nada, e os sindicatos não devem ser obrigados a fazer isso para obter a semântica que esperam. Os sindicatos devem continuar sendo um saco de bits, com Rust não tendo nenhuma ideia do que eles podem conter a qualquer momento até que um código inseguro os acesse.

Sindicato FFI
ser um "ponto de extensão" é algo que precisa ser bem documentado
de qualquer forma.

Devemos certamente documentar a semântica tão precisamente quanto pudermos.

@RalfJung Não, ele se comporta como auto trait s, expondo todos os detalhes internos.

Atualmente, há alguma discussão sobre "campos ativos" e queda nos sindicatos em https://github.com/rust-lang/rust/issues/41073#issuecomment -380291471

Os sindicatos devem continuar sendo um saco de bits, com Rust não tendo nenhuma ideia do que eles podem conter a qualquer momento até que um código inseguro os acesse.

É exatamente assim que eu esperava que os sindicatos funcionassem. Eles são um recurso avançado para obter desempenho extra e interagir com o código C, onde não existem destruidores.

Para mim, se você quiser descartar o conteúdo de uma união, você deve ~ ter que lançar / transmutar (talvez não possa ser transmutado porque pode ser maior com alguns bits não utilizados no final para outra variante) para o tipo você deseja eliminar ~ pegar ponteiros para os campos que precisam ser eliminados e usar std::ptr::drop_in_place ou usar a sintaxe de campo para extrair o valor.

Se eu não soubesse nada sobre sindicatos, era assim que esperava que funcionassem:

Exemplo - Representando mem::uninitialized como um sindicato

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

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

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

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

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

Vou postar e editar para não perder meu trabalho.
EDIT @SimonSapin forma de eliminar campos.

se você quiser descartar o conteúdo de uma união, deverá lançar / transmutar (talvez não possa ser transmutado porque pode ser maior com alguns bits não utilizados no final para outra variante) para o tipo que deseja descartar ou use a sintaxe do campo para extrair o valor

(Se for apenas para deixá-lo cair, não há necessidade de extrair o valor no sentido de movê-lo, você pode pegar um ponteiro para um dos campos e usar std::ptr::drop_in_place .)

Relacionado: Para constantes, estou argumentando que pelo menos um campo de uma união dentro de uma constante precisa estar correto: https://github.com/rust-lang/rust/pull/51361 (se você tiver um campo ZST que seja sempre verdade)

Vou postar e editar para não perder meu trabalho.

Observe que as edições não se refletem nas notificações por e-mail. Se você vai fazer alterações significativas em seu comentário, considere fazer um novo comentário em vez disso ou adicionalmente.

@derekdreery (e todos os outros) Gostaria de receber seus comentários sobre https://internals.rust-lang.org/t/pre-rfc-unions-drop-types-and-manuallydrop/8025

Relacionado: Para constantes, atualmente estou argumentando que pelo menos um campo de uma união dentro de uma constante precisa estar correto: # 51361

Eu vi a implementação, mas não vi o argumento. ;)

Bem ... o argumento de "não verificar parecia estranho".

Ficarei feliz em implementar qualquer esquema que apresentarmos no verificador de const, mas minha intuição sempre foi que uma variante de união precisa estar totalmente correta.

Caso contrário, as uniões são apenas uma maneira bonita de especificar um tipo com um tamanho e alinhamento específicos e alguma conveniência gerada pelo compilador para transmutar entre um conjunto fixo de tipos.

Acho que os sindicatos são "pacotes de bits não interpretados" com alguma maneira conveniente de acessá-los. Não vejo nada de estranho em não verificá-los.

AFAIK, há na verdade alguns casos de uso @joshtriplett mencionados no all-hands de Berlim, onde a primeira metade da união corresponde a um campo e a segunda metade a algum outro campo.

Acho que os sindicatos são "pacotes de bits não interpretados" com alguma maneira conveniente de acessá-los. Não vejo nada de estranho em não verificá-los.

Sempre achei que essa interpretação fosse um pouco contra o espírito da linguagem.
Em outros lugares, usamos análise estática para evitar armas de fogo, verifique se valores não inicializados ou emprestados não são acessados, mas para sindicatos cuja análise é desativada repentinamente, atire.

Vejo isso como exatamente o propósito de union . Quer dizer, também temos indicadores brutos em que todas as análises estão desabilitadas. Os sindicatos fornecem controle total sobre o layout dos dados, assim como os ponteiros brutos fornecem controle total sobre o acesso à memória. Ambos vão à custa da segurança.

Além disso, isso torna union simples . Acho que ser simples é importante, e ainda mais importante quando está envolvido código inseguro (o que sempre será o caso com sindicatos). Devemos apenas aceitar complexidade extra aqui se ela fornecer benefícios tangíveis.

Não achamos que temos que pagar esse custo pelos sindicatos, já que o modelo bag-of-bits não oferece nenhuma nova oportunidade em comparação com o modelo enum-with-unknown-variant.

A propriedade em questão aqui é pelo menos um fardo para um código inseguro manter, assim como é uma proteção. Não há análise estática que possa evitar todos os erros que podem quebrar essa propriedade, pois queremos usar uniões para tipo inseguro trocadilho 1 , então "enum com variante desconhecida" realmente significa que as uniões de manipulação de código devem ser super cuidadosas com a forma como grava no união ou risco UB instantâneo, sem realmente reduzir a insegurança envolvida na leitura do união, uma vez que a leitura já exige saber (por meio de canais que o compilador não entende) que os bits são válidos para a variante que você está lendo. Na verdade, só podemos alertar os usuários sobre uma união que não é válida para nenhuma de suas variantes quando rodando sob miri, não na grande maioria dos casos em que acontece em tempo de execução.

1 Por exemplo, assumindo que as tuplas são repr (C) para simplificar, union Foo { a: (bool, u8), b: (u8, bool) } permite que você construa algo que é inválido apenas por atribuições de campo.

@rkruppe

união Foo {a: (bool, u8), b: (u8, bool)}

Ei, esse é o meu exemplo :)
E é válido no modelo RFC 1897 (pelo menos um dos fragmentos de "folha" bool -1, u8 -1, u8 -2, bool -2 é válido após quaisquer atribuições parciais).

sindicatos de manuseio de código tem que ser super cuidadoso com a forma como escreve para o sindicato ou arriscar UB

Esse é o ponto do modelo RFC 1897, a verificação estática garante que nenhuma operação segura (como atribuição ou atribuição parcial) pode transformar a união em estado inválido, então você não precisa ser supercuidado o tempo todo e não obter UB instantâneo .
Somente operações inseguras não relacionadas à união, como gravações por meio de ponteiros selvagens, podem tornar uma união inválida.

Por outro lado, sem a verificação de movimento, a união pode ser colocada em um estado inválido com muita facilidade.

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

Esse é o ponto do modelo RFC 1897, a verificação estática garante que nenhuma operação segura (como atribuição ou atribuição parcial) pode transformar a união em um estado inválido, então você não precisa ser super cuidadoso o tempo todo e não obter UB instantâneo .
Somente operações inseguras não relacionadas à união, como gravações por meio de ponteiros selvagens, podem tornar uma união inválida.

Você pode reconhecer automaticamente alguns tipos de gravações como não violando as invariantes extras impostas aos sindicatos, mas ainda são invariantes extras que precisam ser mantidos pelos escritores. Como a leitura ainda não é segura e exige a garantia manual de que os bits sejam válidos para a variante lida, isso não ajuda realmente os leitores, apenas torna a vida dos escritores mais difícil. Nem "pacote de bits" nem "enum com variante desconhecida" ajudam a resolver o difícil problema dos sindicatos: como garantir que ele realmente armazene o tipo de dados que você deseja ler.

Como a verificação de tipo mais sofisticada afetaria Dropping? Se você criar uma união e depois passá-la para C, que assume a propriedade, a ferrugem tentará liberar os dados, talvez causando uma liberação dupla? Ou você sempre implementaria Drop sozinho?

editar seria muito legal se as uniões fossem como "enums onde a variante é verificada estaticamente em tempo de compilação", se eu entendi a sugestão

editar 2 os sindicatos poderiam começar como um saco de bits e depois permitir o acesso seguro, embora sejam compatíveis com versões anteriores?

E é válido sob o modelo RFC 1897 (pelo menos um dos fragmentos "folha" bool-1, u8-1, u8-2, bool-2 é válido após quaisquer atribuições parciais).

Se decidirmos que queremos que isso seja válido, acho que @ oli-obk deve atualizar as verificações de miri para refletir isso - com https://github.com/rust-lang/rust/pull/51361 mesclado, seria rejeitado por miri.

@petrochenkov A parte que não entendo é o que isso nos compra. Obtemos complexidade extra, em termos de implementação (análise estática) e uso (o usuário ainda precisa estar ciente das regras exatas). Essa complexidade extra aumenta o fato de que, quando sindicatos são usados, estamos em um contexto inseguro, então as coisas são naturalmente mais complexas. Acho que devemos ter uma motivação clara de por que essa complexidade extra vale a pena. Não considero "viola de alguma forma o espírito da língua" uma motivação clara.

A única coisa que consigo pensar são as otimizações de layout. Em um modelo de "saco de bits", um sindicato nunca tem nicho. No entanto, acho que é um endereço melhor, dando ao programador mais controle manual sobre o nicho, o que também seria útil em outros casos .

Acho que estou perdendo algo fundamental aqui. Eu concordo com @rkruppe que
o problema difícil com os sindicatos é garantir que o sindicato atualmente armazene
os dados que o programa deseja ler.

Mas AFAIK este problema não pode ser resolvido “localmente” por análise estática. Nós
exigiria pelo menos a análise de todo o programa, e mesmo assim ainda seria
um problema difícil de resolver.

Então ... existe uma solução para este problema em cima da mesa? Ou, o que o
as soluções exatas propostas realmente nos compram? Digamos que recebo um sindicato de C,
sem analisar todo o programa Rust e C, o que pode o proposto
análises estáticas realmente garantem para os leitores?

@gnzlbg acho que a única garantia que teríamos é o que @petrochenkov escreveu acima

a verificação estática garante que nenhuma operação segura (como atribuição ou atribuição parcial) pode transformar a união em um estado inválido

Por outro lado, sem a verificação de movimento, a união pode ser colocada em um estado inválido com muita facilidade.

Sua proposta também não protege contra leituras ruins, não acho que isso seja possível.

Além disso, imaginei um rastreamento "inicializado" muito básico ao longo das linhas de "escrever em qualquer campo inicializa a união". Precisamos de algo de qualquer maneira quando impl Drop for MyUnion for permitido. Para o bem ou para o mal, temos que decidir quando e onde inserir chamadas automáticas para um sindicato. Essas regras devem ser o mais simples possível, porque esse é um código extra que estamos inserindo em um código sutil e inseguro existente. Para uniões que implementam Drop , também imaginei uma restrição semelhante a struct que não permite a gravação em um campo a menos que a estrutura de dados já esteja inicializada.

@derekchiang

Os sindicatos poderiam começar como um saco de bits e depois permitir o acesso seguro ao mesmo tempo que são compatíveis com as versões anteriores?
Não. Uma vez que dizemos que é um saco de bits, pode haver um código inseguro, presumindo que seja permitido.

Eu acho que há valor na verificação de movimento mínimo para ver se uma união foi inicializada. A RFC original especificava explicitamente que inicializar ou atribuir a qualquer campo de união torna toda a união inicializada. Além disso, porém, rustc não deve tentar inferir nada sobre o valor em uma união que o usuário não especifica explicitamente; uma união pode conter qualquer valor, incluindo um valor que não é válido para nenhum de seus campos.

Um caso de uso para isso, por exemplo: considere uma união com tags no estilo C que seja explicitamente extensível com mais tags no futuro. O código C e Rust que lêem essa união não deve assumir que conhece todos os tipos de campo possíveis.

@RalfJung

Talvez eu deva começar na outra direção.

Este código deve funcionar 1) para sindicatos 2) para não sindicatos?

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

Para mim, a resposta é óbvio "não" em ambos os casos, porque essa é toda uma classe de erros que Rust pode e deseja evitar, independentemente da "união" de T .

Isso significa que o verificador de movimento deve ter algum tipo de esquema de acordo com o qual ele implementa esse suporte. Dado que o verificador de movimentação (e verificador de empréstimo) geralmente funcionam no modo por campo, o esquema mais simples para uniões seria "mesmas regras que para structs + (des) inicialização / empréstimo de um campo também (des) inicializa / empresta seus campos irmãos "
Esta regra simples cobre toda a verificação estática.

Então, o modelo enum é simplesmente uma consequência da verificação estática descrita acima + mais uma condição.
Se 1) a verificação de inicialização estiver habilitada e 2) o código não seguro não gravar bytes inválidos arbitrários na área pertencente ao sindicato, então um dos campos "folha" do sindicato é automaticamente válido. Esta é uma garantia dinâmica não verificável (pelo menos para sindicatos com> 1 campos e fora do avaliador const), mas é direcionada a pessoas que leem o código antes de tudo.

Este caso de @joshtriplett , por exemplo

Um caso de uso para isso, por exemplo: considere uma união com tags no estilo C que seja explicitamente extensível com mais tags no futuro. O código C e Rust que lêem essa união não deve assumir que conhece todos os tipos de campo possíveis.

seria muito mais claro para as pessoas que lêem código se o sindicato explicitamente tivesse um campo extra para "possíveis extensões futuras".

Claro, podemos manter a verificação de inicialização estática básica, mas rejeitar a segunda condição e permitir a gravação de dados possivelmente inválidos arbitrários para a união por meio de alguns meios de "terceiros" inseguros sem ser UB instantâneo. Então não teríamos mais aquela garantia dinâmica voltada para as pessoas, só acho que seria uma perda líquida.

@petrochenkov

Este código deve funcionar 1) para sindicatos 2) para não sindicatos?

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

Para mim, a resposta é óbvio "não" em ambos os casos, porque essa é toda uma classe de erros que Rust pode e deseja evitar, independentemente da "união" de T .

Concordo, este nível de verificação de valores não inicializados parece razoável e bastante viável.

Isso significa que o verificador de movimento deve ter algum tipo de esquema de acordo com o qual ele implementa esse suporte. Dado que o verificador de movimentação (e verificador de empréstimo) geralmente funcionam no modo por campo, o esquema mais simples para uniões seria "mesmas regras que para structs + (des) inicialização / empréstimo de um campo também (des) inicializa / empresta seus campos irmãos "
Esta regra simples cobre toda a verificação estática.

Aceito até agora, assumindo que eu entendo as regras para structs.

Então, o modelo enum é simplesmente uma consequência da verificação estática descrita acima + mais uma condição.
Se 1) a verificação de inicialização estiver habilitada e 2) o código não seguro não gravar bytes inválidos arbitrários na área pertencente ao sindicato, então um dos campos "folha" do sindicato é automaticamente válido. Esta é uma garantia dinâmica não verificável (pelo menos para sindicatos com> 1 campos e fora do avaliador const), mas é direcionada a pessoas que leem o código antes de tudo.

Essa condição adicional não é válida para sindicatos.

Este caso de @joshtriplett , por exemplo

Um caso de uso para isso, por exemplo: considere uma união com tags no estilo C que seja explicitamente extensível com mais tags no futuro. O código C e Rust que lêem essa união não deve assumir que conhece todos os tipos de campo possíveis.

seria muito mais claro para as pessoas que lêem código se o sindicato explicitamente tivesse um campo extra para "possíveis extensões futuras".

Não é assim que os sindicatos C funcionam, nem como os sindicatos Rust foram especificados para funcionar. (E eu questionaria se seria mais claro ou simplesmente se corresponde a um conjunto diferente de expectativas.) Mudar isso faria com que as uniões Rust não fossem mais adequadas para alguns dos propósitos para os quais foram projetadas e propostas.

Claro, podemos manter a verificação de inicialização estática básica, mas rejeitar a segunda condição e permitir a gravação de dados possivelmente inválidos arbitrários para a união por meio de alguns meios de "terceiros" inseguros sem ser UB instantâneo. Então não teríamos mais aquela garantia dinâmica voltada para as pessoas, só acho que seria uma perda líquida.

Esses 'meios inseguros de "terceiros" incluem "obter um sindicato da FFI", que é um caso de uso totalmente válido.

Aqui está um exemplo concreto:

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

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

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

Esse é um código totalmente válido que as pessoas podem e irão escrever usando sindicatos.

@petrochenkov

Este código deve funcionar 1) para sindicatos 2) para não sindicatos?
Para mim, a resposta é óbvio "não" em ambos os casos, porque essa é toda uma classe de erros que Rust pode e deseja prevenir, independentemente da "união" -ness de T.

Por mim tudo bem.

o esquema mais simples para uniões seria "mesmas regras que para structs + (des) inicialização / empréstimo de um campo também (des) inicializa / empresta seus campos irmãos".

Uau. As regras de estrutura fazem sentido porque todas são baseadas no fato de que diferentes campos são separados . Você não pode simplesmente invalidar essa suposição básica e ainda usar as mesmas regras. O fato de você precisar de um adendo às regras mostra isso. Eu nunca esperaria que os sindicatos fossem verificados de forma semelhante às structs. De qualquer forma, pode-se esperar que sejam verificados de forma semelhante a enums - mas é claro que isso não pode funcionar, porque enums só podem ser acessados ​​por meio de correspondência.

Se 1) a verificação de inicialização estiver habilitada e 2) o código não seguro não gravar bytes inválidos arbitrários na área pertencente ao sindicato, então um dos campos "folha" do sindicato é automaticamente válido. Esta é uma garantia dinâmica não verificável (pelo menos para sindicatos com> 1 campos e fora do avaliador const), mas é direcionada a pessoas que leem o código antes de tudo.

Eu acho que é extremamente desejável que as suposições básicas de validade sejam verificáveis ​​dinamicamente (dado tipo de informação). Então podemos verificá-los durante o CTFE no miri, podemos até verificá-los durante as execuções "completas" do miri (por exemplo, de uma suíte de teste), podemos eventualmente ter algum tipo de desinfetante ou talvez um modo em que o Rust emita debug_assert! em locais críticos para verificar os invariantes de validade.
Eu acho que a experiência com as regras não verificáveis ​​de C dá ampla evidência de que elas são problemáticas. Normalmente, a primeira etapa para realmente entender e esclarecer quais são as regras é encontrar uma forma dinamicamente verificável de expressá-las. Mesmo para modelos de memória simultânea, variantes "verificáveis ​​dinamicamente" (semântica operacional explicando tudo em termos de execução passo a passo de uma máquina virtual) estão aparecendo e parecem ser a única maneira de resolver problemas abertos de longa data do axiomático modelos que foram usados ​​anteriormente ("ouf of thin air problem" é uma palavra-chave aqui).

Não posso exagerar o quão importante eu acho que é ter regras verificáveis ​​dinamicamente. Eu acho que devemos ter como objetivo ter 0 casos não verificáveis ​​de UB. (Ainda não chegamos lá, mas é o objetivo que deveríamos ter.) Essa é a única forma responsável de ter UB em sua linguagem, todo o resto é um caso de autores de compiladores / linguagens facilitando suas vidas às custas de todos que tem que conviver com as consequências. (Atualmente, estou trabalhando em regras verificáveis ​​dinamicamente para aliasing e acessos de ponteiro bruto.)
Mesmo que esse fosse o único problema, no que me diz respeito, "não verificável dinamicamente" é motivo suficiente para não usar essa abordagem.

Dito isso, não vejo nenhuma razão fundamental para que isso não seja verificável: para cada byte na união, analise todas as variantes para ver quais valores são permitidos para aquele byte nesta variante e obtenha a união (heh;)) de todos desses conjuntos. Uma sequência de bytes é válida para uma união se cada byte for válido de acordo com esta definição.
No entanto, isso é bastante difícil de implementar uma verificação para - de longe, o invariante de validade de tipo básico mais complexo que teríamos em Rust. Isso é uma consequência direta do fato de que essa regra de validade é um tanto complicada de descrever, e é por isso que não gosto dela.

Claro, podemos manter a verificação de inicialização estática básica, mas rejeitar a segunda condição e permitir a gravação de dados possivelmente inválidos arbitrários para a união por meio de alguns meios de "terceiros" inseguros sem ser UB instantâneo. Então não teríamos mais aquela garantia dinâmica voltada para as pessoas, só acho que seria uma perda líquida.

O que essa garantia nos compra ? Onde isso realmente ajuda? Agora, tudo que vejo é que todos têm que trabalhar duro e ter o cuidado de defendê-lo. Eu não vejo o benefício que nós, o povo, tiramos disso.

@joshtriplett

considere uma união marcada no estilo C que é explicitamente extensível com mais marcas no futuro. O código C e Rust que lêem essa união não deve assumir que conhece todos os tipos de campo possíveis.

O modelo proposto por @petrochenkov permite esses casos de uso, adicionando um campo __non_exhaustive: () à união. No entanto, não acho que seja necessário. É concebível que os geradores de ligação possam adicionar esse campo.

@RalfJung

Isso é dinâmico não verificável (pelo menos para uniões com> 1 campos e fora do avaliador const).

Acho que é extremamente desejável que as suposições básicas de validade sejam verificáveis ​​dinamicamente

Um esclarecimento: eu quis dizer não verificável em "por padrão" / "no modo de liberação", é claro que pode ser verificado em "modo lento" com alguma instrumentação extra, mas você já escreveu sobre isso melhor do que eu.

@RalfJung

O modelo proposto por @petrochenkov permite esses casos de uso, adicionando um campo __non_exhaustive: () à união.

Sim, entendi que essa era a proposta.

No entanto, não acho que seja necessário. É concebível que os geradores de ligação possam adicionar esse campo.

Eles poderiam, mas teriam que adicioná-lo sistematicamente a cada sindicato.

Ainda estou para ver um argumento de por que faz sentido interromper os casos de uso primários de uniões em favor de algum caso de uso não especificado que depende da limitação de quais padrões de bits eles podem conter.

@joshtriplett

casos de uso primários de sindicatos

Não é óbvio para mim por que este é o caso de uso principal.
Pode ser verdade para repr(C) sindicatos se você assumir que todos os usos de sindicatos para uniões marcadas / "Rust enum emulation" em FFI assumem extensibilidade (o que não é verdade), mas pelo que tenho visto, usos de repr(Rust) uniões (controle de queda, controle de inicialização, transmutes) não esperam "variantes inesperadas" aparecendo repentinamente neles.

@petrochenkov Eu não disse "quebrar o caso de uso primário", eu disse "quebrar o caso de uso primário". FFI é um dos principais casos de uso de sindicatos.

e obter a união (heh;)) de todos esses conjuntos

Certamente há uma obviedade atraente em uma afirmação de que "os valores possíveis de um sindicato são a união dos valores possíveis de todas as suas variantes possíveis" ...

Verdadeiro. No entanto, essa não é a proposta - todos concordamos que o seguinte deve ser legal:

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

Na verdade, acho que é um bug que isso exija até unsafe .

Então, a união tem que ser considerada bytewise, pelo menos.
Além disso, não acho que a "obviedade atraente" por si só seja um motivo suficientemente bom. Qualquer invariante que decidirmos é um fardo significativo para autores de códigos inseguros, devemos ter vantagens concretas que obteremos em troca.

@RalfJung

Na verdade eu acho que é um bug que requer até inseguro.

Eu não sei sobre a nova implementação do verificador de insegurança baseado em MIR, mas no antigo baseado em HIR foi certamente uma limitação / simplificação do verificador - apenas expressões do formulário expr1.field = expr2 foram analisadas para o campo possível " atribuição "exclusão insegura, todo o resto foi tratado de forma conservadora como" acesso de campo "genérico que não é seguro para os sindicatos.

Respondendo ao comentário em https://github.com/rust-lang/rust/issues/52786#issuecomment -408645420:

Portanto, a ideia é que o compilador ainda não sabe nada sobre o contrato de Wrap<T> e não pode, por exemplo, fazer otimizações de layout. Ok, esta posição está entendida.
Isso significa que internamente, dentro do módulo Wrap , a implementação do módulo Wrap<T> pode, por exemplo, gravar temporariamente "valores inesperados" nele, se não vazá-los para os usuários, e o compilador vai ficar bem com eles.

Não tenho certeza de como exatamente a parte do contrato de Wrap s sobre a ausência de valores inesperados está relacionada à privacidade do campo.

Em primeiro lugar, independentemente dos campos serem privados ou públicos, valores inesperados não podem ser escritos diretamente por meio desses campos. Você precisa de algo como um ponteiro bruto ou código do outro lado do FFI para fazer isso, e isso pode ser feito sem nenhum acesso de campo, apenas tendo um ponteiro para toda a união. Portanto, precisamos abordar isso de alguma outra direção que não seja o acesso a um campo que está sendo restrito.

Como eu interpreto seu comentário, a abordagem é dizer que um campo privado (em união ou estrutura, não importa) implica em uma invariante arbitrária desconhecida para o usuário, portanto, quaisquer operações que alterem esse campo (diretamente ou por meio de ponteiros selvagens, não t importa) resultar em UB porque eles podem quebrar potencialmente aquele invariante não especificado.

Isso significa que, se uma união tiver um único campo privado, seu implementador (mas não o compilador) pode assumir que nenhum terceiro gravará um valor inesperado nessa união.
Essa é uma "cláusula de documentação de união padrão" para o usuário de alguma forma:
- (Padrão) Se um sindicato tiver um campo privado, você não pode escrever lixo nele.
- Caso contrário, você pode escrever lixo em uma união, a menos que seus documentos proíbam explicitamente.

Se algum sindicato deseja proibir valores inesperados enquanto ainda fornece pub acesso aos seus campos esperados (por exemplo, quando esses campos não têm suas próprias invariáveis), então ele ainda pode fazer isso por meio de documentação, é por isso que o "a menos" em a segunda cláusula é necessária.

@RalfJung
Isso descreve sua posição com precisão?

Como cenários como este são tratados?

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

Como eu interpreto seu comentário, a abordagem é dizer que um campo privado (em união ou estrutura, não importa) implica em uma invariante arbitrária desconhecida para o usuário, portanto, quaisquer operações que alterem esse campo (diretamente ou por meio de ponteiros selvagens, não t importa) resultar em UB porque eles podem quebrar potencialmente aquele invariante não especificado.

Não, não foi isso que eu quis dizer.

Existem vários invariantes. Não sei de quantos precisaremos, mas serão pelo menos dois (e não tenho bons nomes para eles):

  • O "invariante de nível de layout" (ou "invariante sintático") de um tipo é completamente definido pela forma sintática do tipo. São coisas como " &mut T não é NULL e está alinhado", " bool é 0 ou 1 ", " ! não pode existir". Neste nível, *mut T é o mesmo que usize - ambos permitem qualquer valor (ou talvez qualquer valor inicializado , mas essa distinção é para outra discussão). Teremos, eventualmente, um documento explicando esses invariantes para todos os tipos, por recursão estrutural: O invariante no nível de layout de uma estrutura é que todos os seus campos têm seus invariantes mantidos, etc. A visibilidade não desempenha um papel aqui.
Violating the layout-level invariant is instantaneous UB. This is a statement we can make because we have defined this invariant in very simple terms, and we make it part of the definition of the language itself. We can then exploit this UB (and we already do), e.g. to perform enum layout optimizations.
  • O "invariante de nível de tipo personalizado" (ou "invariante semântico") de um tipo é escolhido por quem implementa o tipo. O compilador não pode conhecer esta invariante, pois não temos uma linguagem para expressá-la, e o mesmo vale para a definição da linguagem. Não podemos violar este invariante UB, como nem podemos dizer o que é esse invariante! O fato de ser possível ter invariantes personalizados é uma característica de qualquer sistema de tipos útil: Abstração. Escrevi mais sobre isso em uma postagem anterior no blog .

    A conexão entre o invariante semântico personalizado e o UB é que declaramos que o código não seguro pode depender de seus invariantes semânticos sendo preservados por código estrangeiro . Isso torna incorreto simplesmente colocar qualquer coisa aleatória em um campo de tamanho de Vec . Note que eu disse incorreta (Eu às vezes uso o termo doentio) - mas não um comportamento indefinido! Outro exemplo para demonstrar essa diferença (na verdade, o mesmo exemplo) é a discussão sobre regras de aliasing para &mut ZST . Criar um não nulo &mut ZST oscilante e bem alinhado nunca é UB imediato, mas ainda é incorreto / não sólido porque pode-se escrever código inseguro que depende de que isso não aconteça

Seria bom alinhar esses dois conceitos, mas não acho que seja prático. Em primeiro lugar, para alguns tipos (ponteiros de função, traços din), a definição do invariante semântico personalizado, na verdade, usa a definição de UB na linguagem. Essa definição seria circular se quiséssemos dizer que é UB violar o invariante semântico personalizado. Em segundo lugar, eu preferiria que a definição de nossa linguagem, e se um determinado traço de execução exibisse UB, fosse uma propriedade decidível. Os invariantes semânticos e personalizados frequentemente não são decidíveis.


Não tenho certeza de como exatamente a parte do contrato Wraps sobre a ausência de valores inesperados está relacionada à privacidade do campo.

Essencialmente, quando um tipo escolhe seu invariante personalizado, ele deve se certificar de que tudo o que o código seguro pode fazer preserva o invariante . Afinal, a promessa é que apenas usar a API segura desse tipo nunca pode levar ao UB. Isso se aplica a estruturas e sindicatos. Uma das coisas que o código seguro pode fazer é acessar campos públicos, que é de onde vem essa conexão.

Por exemplo, um campo público de uma estrutura não pode ter uma invariante personalizada que seja diferente da invariante personalizada do tipo de campo : Afinal, qualquer usuário seguro poderia escrever dados arbitrários nesse campo, ou ler do campo e esperar "bom" dados. Uma estrutura onde todos os campos são públicos pode ser construída com segurança, colocando mais restrições no campo.

Uma união com um campo público ... bem, isso é um tanto interessante. Ler campos de união não é seguro de qualquer maneira, então nada muda lá. Gravar campos de união é seguro, portanto, uma união com um campo público deve ser capaz de lidar com dados arbitrários que satisfaçam o invariante personalizado do tipo do campo sendo colocado no campo. Duvido que isso seja muito útil ...

Portanto, para recapitular, ao escolher uma invariante customizada, é sua responsabilidade garantir que o código seguro estrangeiro não possa quebrar esta invariante (e você tem ferramentas como campos privados para ajudá-lo a conseguir isso). É responsabilidade do código estrangeiro não seguro não violar sua invariante quando esse código faz algo que o código seguro não poderia fazer.


Isso significa que internamente, dentro do módulo do Wrap, a implementação do WrapO módulo pode, por exemplo, gravar temporariamente "valores inesperados" nele, se não vazá-los para os usuários, e o compilador ficará bem com eles.

Corrigir. (a segurança do pânico é uma preocupação aqui, mas você provavelmente sabe). É como, em Vec , posso fazer com segurança

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

e não há UB.


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

Em termos de invariante de layout sintático, my_private_ffi_function pode fazer qualquer coisa (assumindo a chamada de função ABI e correspondências de assinatura). Em termos de invariante semântico personalizado, isso não é visível no código - quem escreveu este módulo tinha um invariante em mente, eles deveriam documentá-lo próximo à sua definição de união e então certificar-se de que a função FFI retornasse um valor que satisfaça o invariante .

Eu finalmente escrevi aquela postagem no blog sobre se e quando &mut T deve ser inicializado e os dois tipos de invariantes que mencionei acima.

Resta alguma coisa a rastrear aqui que ainda não foi abordada em https://github.com/rust-lang/rust/issues/55149 ou devemos encerrar?

E0658 ainda aponta aqui:

erro [E0658]: as uniões com campos não Copy são instáveis ​​(consulte o problema # 32836)

Isso atualmente funciona terrivelmente com atômicos, uma vez que eles não implementam Copy . Alguém conhece uma solução alternativa?

Quando https://github.com/rust-lang/rust/issues/55149 for implementado, você poderá usar ManuallyDrop<AtomicFoo> em um sindicato. Até então, a única solução é usar Nightly (ou não usar union e encontrar alguma alternativa).

Com isso implementado, você nem precisa de ManuallyDrop ; afinal, o rustc sabe que Atomic* não implementa Drop .

Atribuindo-me para mudar o problema de rastreamento para o novo.

Esta página foi útil?
0 / 5 - 0 avaliações