Rust: Problema de rastreamento para APIs de PIN (RFC 2349)

Criado em 18 mar. 2018  ·  211Comentários  ·  Fonte: rust-lang/rust

Problema de rastreamento para rust-lang / rfcs # 2349

Estabilização de bloqueio:

  • [x] Implementação (PR # 49058)
  • [] Documentação

Perguntas não resolvidas:

  • [] Devemos fornecer garantias mais fortes em relação ao vazamento de !Unpin dados?

Editar : Comentário resumido: https://github.com/rust-lang/rust/issues/49150#issuecomment -409939585 (na parte oculta por padrão)

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

Comentários muito úteis

@rfcbot concern api-refactor

Um pouco de inspiração struct e ontem à noite descobri como poderíamos refatorar essa API para que haja apenas um tipo Pin , que envolve um ponteiro, em vez de ter que criar uma versão fixada de cada ponteiro. Esta não é uma reformulação fundamental da API de forma alguma, mas é melhor extrair o componente "pinos da memória" em uma peça combinável.

Todos 211 comentários

Agora noto que a fixação de pilha não faz parte do RFC, @withoutboats você está planejando liberar uma caixa para isso ou devo apenas copiar o código de exemplo para minha caixa que precisa dele?

@ Nemo157 Você deve copiá-lo e relatar sua experiência!

A questão não resolvida sobre o vazamento de Unpin dados está relacionada a isso. Essa API é incorreta se dissermos que você não pode sobrescrever Unpin dados em um Pin menos que o destruidor seja executado, como @cramertj solicitou. Existem outras APIs de fixação de pilha menos ergonômicas que funcionam para isso. Não está claro qual é a escolha certa aqui - a API ergonômica de fixação de pilha é mais útil ou a garantia extra de vazamento é mais útil?

Uma coisa que observarei é que a fixação de pilha não foi suficiente para coisas como Future::poll dentro da macro await! , porque não nos permitiu pesquisar em um loop. Eu estaria interessado se você se deparasse com esses problemas e como / se você os resolveria.

Meu uso atual é bastante trivial, um executor de thread único executando um único StableFuture sem suporte para spawn . Mudar para uma API como @cramertj sugere que funcionaria bem com isso. Tenho me perguntado como estender isso para permitir a geração de vários StableFuture s, mas pelo menos com meu projeto atual isso não é necessário.

Apenas tentei experimentar a API. Parece que a seguinte definição (sugerida pela RFC) de Future não é mais segura para o objeto?

trait Future {
    type Item;
    type Error;

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

Deixa pra lá. Encontrada uma nota sobre um plano para tornar arbitrary_self_types seguro para objetos.

@withoutboats

Uma coisa que observarei é que a fixação de pilha não foi suficiente para coisas como Future :: poll dentro do await! macro, porque não nos permitia pesquisar em um loop.

Você poderia elaborar sobre isso?

@RalfJung Você precisa de Pin para suportar novos empréstimos como Pin , o que atualmente não acontece.

@cramertj Isso soa como uma restrição da API Pin , não da API de fixação de pilha?

@RalfJung Sim, correto. No entanto, PinBox pode ser emprestado novamente como Pin , enquanto o tipo preso na pilha não pode (um empréstimo porque Pin cria um empréstimo para toda a vida útil do tipo de pilha).

Dado um Pin , posso pegá-lo emprestado como &mut Pin e usar Pin::borrow - essa é uma forma de empréstimo. Suponho que esse não seja o tipo de rebrota de que você está falando?

@RalfJung Não - métodos como Future::poll foram planejados para tomar self: Pin<Self> , em vez de self: &mut Pin<Self> (que não é um tipo self válido, pois não é t Deref<item = Self> - é Deref<Item = Pin<Self>> ).

Pode ser o caso de que possamos fazer isso funcionar com Pin::borrow na verdade. Não tenho certeza.

@cramertj Eu não sugeri chamar poll em x: &mut Pin<Self> ; Pensei em x.borrow().poll() .

@RalfJung Oh, entendo. Sim, usar um método para fazer um novo empréstimo manualmente pode funcionar.

Tentarei e lembrarei de postar um exemplo de algumas das coisas que estou fazendo com Pin s na próxima semana, pelo que posso dizer que o novo empréstimo funciona perfeitamente. Eu tenho uma versão fixada do traço futures::io::AsyncRead junto com adaptadores funcionais como fn read_exact<'a, 'b, R: PinRead + 'a>(read: Pin<'a, R>, buf: &'b [u8]) -> impl StableFuture + 'a + 'b e sou capaz de trabalhar isso em um StableFuture relativamente complexo que está apenas preso no topo nível.

Aqui está o exemplo completo do que estou usando para leitura:

pub trait Read {
    type Error;

    fn poll_read(
        self: Pin<Self>,
        cx: &mut task::Context,
        buf: &mut [u8],
    ) -> Poll<usize, Self::Error>;
}

pub fn read_exact<'a, 'b: 'a, R: Read + 'a>(
    mut this: Pin<'a, R>,
    buf: &'b mut [u8],
) -> impl StableFuture<Item = (), Error = Error<R::Error>>
         + Captures<'a>
         + Captures<'b> {
    async_block_pinned! {
        let mut position = 0;
        while position < buf.len() {
            let amount = await!(poll_fn(|cx| {
                Pin::borrow(&mut this).poll_read(cx, &mut buf[position..])
            }))?;
            position += amount;
            if amount == 0 {
                Err(Error::UnexpectedEof)?;
            }
        }
        Ok(())
    }
}

Isso é um pouco irritante, pois você tem que passar as instâncias por todos os lugares como Pin e usar Pin::borrow sempre que você chamar funções nelas.

#[async]
fn foo<'a, R>(source: Pin<'a, R>) -> Result<!, Error> where R: Read + 'a {
    loop {
        let mut buffer = [0; 8];
        await!(read_exact(Pin::borrow(&mut source), &mut buffer[..]));
        // do something with buffer
    }
}

Eu acabei de pensar que poderia impl<'a, R> Read for Pin<'a R> where R: Read + 'a para contornar tendo que passar valores como Pin<'a, R> todos os lugares, em vez de usar fn foo<R>(source: R) where R: Read + Unpin . Infelizmente, isso falha porque Pin<'a, R>: !Unpin , acho que é seguro adicionar um unsafe impl<'a, T> Unpin for Pin<'a, T> {} pois o pino em si é apenas uma referência e os dados por trás dele ainda estão fixados.

Preocupação: Parece provável que queremos que a maioria dos tipos em libstd implementem Unpin incondicionalmente, mesmo se seus parâmetros de tipo não forem Pin . Os exemplos são Vec , VecDeque , Box , Cell , RefCell , Mutex , RwLock , Rc , Arc . Imagino que a maioria dos engradados não pensará em fixar e, portanto, seus tipos serão Unpin se todos os seus campos forem Unpin . Essa é uma boa escolha, mas leva a interfaces desnecessariamente fracas.

Isso se resolverá sozinho se nos certificarmos de implementar Unpin para todos os tipos de ponteiros libstd (talvez até incluindo ponteiros brutos) e UnsafeCell ? Isso é algo que queremos fazer?

Isso se resolverá sozinho se nos certificarmos de implementar Desafixar para todos os tipos de ponteiro libstd (talvez até incluindo ponteiros brutos) e UnsafeCell? Isso é algo que queremos fazer?

Sim, parece a mesma situação que Send para mim.

Uma nova pergunta acabou de me ocorrer: Quando são Pin e PinBox Send ? No momento, o mecanismo de autotratamento os torna Send sempre que T for Send . Não há razão a priori para fazer isso; assim como os tipos no estado tipográfico compartilhado têm seus próprios traços de marcador para envio (chamado Sync ), poderíamos fazer um traço de marcador dizendo quando Pin<T> é Send , por exemplo, PinSend . Em princípio, é possível escrever tipos que são Send mas não PinSend e vice-versa.

@RalfJung Pin é enviado quando &mut T é enviado. PinBox é enviado quando Box<T> é enviado. Não vejo razão para eles serem diferentes.

Bem, assim como alguns tipos são Send mas não Sync , você poderia ter um tipo baseado em "Uma vez que este método é chamado com Pin<Self> , posso confiar que nunca será movido para outro tópico ". Por exemplo, isso pode dar origem a futuros que podem ser enviados antes de serem iniciados pela primeira vez, mas depois precisam permanecer em um segmento (da mesma forma que podem ser movidos antes de serem iniciados, mas então têm que permanecer fixados). Não tenho certeza se posso apresentar um exemplo convincente, talvez algo sobre um futuro que usa armazenamento local de thread.

Acabei de chegar ao problema vitalício mencionado por @Diggsey . Acredito que Pin<Option<T>> -> Option<Pin<T>> deve ser uma operação segura, mas não parece possível implementá-la mesmo usando as APIs não seguras atuais, muito menos que tipo de API seria necessária para tornar este código seguro:

trait OptionAsPin<T> {
    fn as_pin<'a>(self: Pin<'a, Self>) -> Option<Pin<'a, T>>;
}

impl<T> OptionAsPin<T> for Option<T> {
    fn as_pin<'a>(self: Pin<'a, Self>) -> Option<Pin<'a, T>> {
        match *unsafe { Pin::get_mut(&mut self) } {
            Some(ref mut item) => Some(unsafe { Pin::new_unchecked(item) }),
            None => None,
        }
    }
}

(É possível contornar usando transmutar para forçar as vidas, mas isso me faz sentir muito nojento).

Eu gostaria de adicionar uma pergunta não resolvida: Devemos adicionar de volta um tipo de referência fixada compartilhada? Eu acho que a resposta é sim. Veja esta postagem com discussão para mais detalhes.

Acabei de ler que o futuro 0,2 não é tão final quanto pensei que fosse , então talvez ainda seja possível renomear Pin volta para PinMut e adicionar uma versão compartilhada.

@RalfJung Eu li sua postagem no blog novamente com mais detalhes para entender as mudanças que você propõe.

Acho que você encontrou um caso de uso potencialmente atraente para ter uma variante de Pin imutável, mas não entendo seus comentários sobre Deref e &Pin<T> <=> &&T . Mesmo que Pin<T> possa ser lançado em &T com segurança, isso não os torna equivalentes, porque &T não pode ser lançado em Pin<T> . Não vejo um motivo para tornar a conversão segura insegura (eliminando o Deref impl seguro).

Atualmente, o método map em Pin tem a assinatura

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

Qual é a razão de não ser o seguinte?

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

Da forma como está, não posso transformar Pin de um tipo em Pin de um de seus campos sem encurtar o tempo de vida desnecessariamente.

Outro problema com o método map é que parece impossível transformar um Pin de uma estrutura em dois Pin s, cada um de um campo diferente da estrutura. Qual é a maneira correta de conseguir isso?

Tenho usado esta macro para isso:

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

Em grande parte da discussão em torno de Pin , parece haver uma suposição de que "projetar" um pino em um campo privado deve ser considerado seguro. Eu não acho que isso seja verdade. Por exemplo, o comentário do documento em map atualmente diz:

Você deve garantir que os dados retornados não se movam, contanto que o valor do argumento não se mova (por exemplo, porque é um dos campos desse valor), e também que você não saia do argumento que recebe para a função interior.

A garantia de que "os dados que você retorna não se movem, desde que o valor do argumento não se mova" é a descrição correta do contrato que um chamador de map deve manter. O entre parênteses "(por exemplo, porque é um dos campos desse valor)" parece implicar que, contanto que você apenas retorne uma referência ao seu próprio campo privado, você está seguro. Mas isso não é verdade se você implementar Drop . Um implemento Drop verá &mut self , mesmo quando outro código já viu Pin<Self> . Ele pode continuar usando mem::replace ou mem::swap para sair de seus campos, violando a promessa feita por um uso "correto" anterior de map .

Em outras palavras: usando uma chamada de "projeção de pino correta" para Pin::map (a chamada se parece com unsafe { Pin::map(&mut self, |x| &mut x.p) } ), sem outras invocações de unsafe , pode-se produzir algo incorreto / indefinido comportamento. Aqui está um link do playground que demonstra isso.

Isso não significa que algo esteja errado com a API atual. Isso demonstra que Pin::map deve ser marcado como unsafe , o que já é. Também não acho que haja muito perigo de as pessoas acidentalmente tropeçarem nisso ao implementar futuros ou semelhantes - você realmente tem que sair do seu caminho para sair de seus próprios campos em um Drop impl, e Duvido que haja muitas razões práticas para querer fazer isso.

Mas acho que o comentário do documento map pode querer mencionar isso, e também acho que invalida algumas ideias para extensões que vejo na RFC e nas discussões vizinhas:

  • Não deve haver um macro / derivar que faz "projeção pin" e faz com que pareça seguro. A projeção realmente impõe um contrato ao outro código que o cerca, que o compilador não pode cumprir totalmente. Portanto, deve exigir o uso da palavra-chave unsafe sempre que for feito.
  • A RFC menciona que se Pin fosse transformado em um recurso de linguagem &'a pin T , seria "trivial projetar através de campos". Acredito ter mostrado que isso ainda deve exigir unsafe , mesmo se limitado a campos privados de projeção.

@withoutboats

Mesmo se pinpodem ser lançados para & T com segurança, o que não os torna equivalentes, porque & T não pode ser lançado para Pin.

Na verdade, essa não é uma condição suficiente. Eles são iguais em meu modelo porque em meu post anterior , fizemos a seguinte definição:

Definição 5: PinBox<T>.shr . Um ponteiro ptr e tempo de vida 'satisfazem o estado de tipo compartilhado de PinBox<T> se ptr é um ponteiro somente leitura para outro ponteiro inner_ptr tal que T.shr('a, inner_ptr)

(E, meio que implicitamente, a definição correspondente para Pin<'a, T>.shr .)
Observe como PinBox<T>.shr depende de T.shr e nada mais. Isso torna PinBox<T>.shr exatamente o mesmo invariante de Box<T>.shr , o que implica que &Box<T> = &PinBox<T> . Raciocínio semelhante mostra que &&T = &Pin<T> .

Portanto, isso não é uma consequência da API ou do contrato escrito na RFC. É uma consequência do modelo que possui apenas três estados tipográficos: Pertencente, compartilhado, fixado. Se você quiser argumentar contra &&T = &Pin<T> , você deve argumentar a favor da introdução de um quarto estado tipográfico, "compartilhado fixado".

@MicahChalmer

Em grande parte da discussão em torno do Pin, parece haver uma suposição de que "projetar" um pin em um campo privado deve ser considerado seguro. Eu não acho que isso seja verdade.

Esse é um ponto muito bom! Só para ficar claro, não há problema em tornar o campo p de Shenanigans público, não é? Nesse ponto, qualquer cliente poderia escrever do_something_that_needs_pinning , e a intenção do RFC é tornar isso seguro. (Não sei por que a RFC menciona especificamente campos privados, minha interpretação sempre foi de que deveria funcionar com todos os campos.)

É interessante ver como isso interage com a interpretação de drop em meu modelo . Lá eu escrevi que drop(ptr) tem uma pré-condição de T.pin(ptr) . Com essa interpretação, o código real com falha em seu exemplo seria a implementação drop ! (Agora me pergunto por que ainda não percebi isso ao escrever o post ...) Acho que queremos permitir projeções seguras para os campos eventualmente, e realmente não devemos fornecer drop com um &mut se o tipo tiver fixação. Isso é claramente falso.

Existe alguma maneira de (a) tornar impl Drop inseguro se o tipo for !Unpin , ou (b) alterar sua assinatura para drop(self: Pin<Self>) if T: !Unpin ? Ambos soam extremamente rebuscados, mas por outro lado, ambos resolveriam o ponto de @MicahChalmer , preservando a segurança das projeções de campo. (Se ao menos isso ainda fosse anterior a 1.0 e pudéssemos mudar Drop::drop para apenas tomar Pin<Self> ;) Mas é claro que neste ponto esta não é mais uma solução apenas para biblioteca. A parte triste é que, se estabilizarmos como está, nunca teremos projeções de campo seguras.

@RalfJung Estou mais interessado nas questões práticas (como você provavelmente esperaria: wink :). Acho que são dois:

  • Pin<T: !Unpin> implementar Deref<Target =T> ?
  • Deve haver Pin<T> e PinMut<T> (sendo o primeiro um pin compartilhado)?

Não vejo razão para responder negativamente à primeira pergunta. Eu acho que você ter reaberto a segunda questão; Estou inclinado a voltar para dois tipos diferentes de pinos (mas não completamente convencido). Se alguma vez atualizarmos isso para um tipo de referência de nível de idioma, teremos &pin T e &pin mut T .

Parece que não importa o que façamos, seu modelo provavelmente precisará de um quarto estado de tipo para refletir com precisão as invariáveis ​​da API. Não acho que converter &&T em &Pin<T> deva ser seguro.

Portanto, estou mais interessado nas questões práticas (como você provavelmente esperaria do wink).

Justo. ;) No entanto, acho importante que tenhamos pelo menos um conhecimento básico do que é e não é garantido em torno de Pin quando um código inseguro entra em cena. O Basic Rust teve vários anos de estabilização para descobrir isso, agora estamos repetindo isso por Pin em alguns meses. Embora Pin seja uma adição apenas de biblioteca no que diz respeito à sintaxe, é uma adição significativa no que diz respeito ao modelo. Acho que é prudente documentar o mais exaustivamente possível o que o código inseguro é e não tem permissão para assumir ou fazer cerca de Pin .

Embora essas preocupações sejam teóricas agora, de repente elas se tornarão muito práticas, uma vez que tivermos a primeira insegurança devido ao código inseguro fazer suposições incompatíveis.


Com relação à sua segunda pergunta:

Parece que não importa o que façamos, seu modelo provavelmente precisará de um quarto estado de tipo para refletir com precisão as invariáveis ​​da API. Não acho que converter um && T em um & Pindeve ser seguro.

Isso é o que eu esperava.

Se quisermos ser conservadores, podemos renomear Pin para PinMut mas não adicionar PinShr (provavelmente se chamará Pin mas estou tentando eliminar a ambigüidade aqui ) por enquanto, e declarar que o código não seguro pode assumir que &PinMut<T> realmente aponta para algo fixado. Então teríamos a opção de adicionar Pin como uma referência fixada compartilhada posteriormente.

Uma razão prática para ter PinShr foi levantada por @comex : deve ser possível fornecer um getter que vai de PinShr<'a, Struct> a PinShr<'a, Field> ; se usarmos &PinMut para pinos compartilhados, isso não funciona. Não sei quanto desses getters serão necessários.


Para sua primeira pergunta, parece haver alguns argumentos fortes a favor: (a) os planos atuais para tipos de self arbitrários seguros para objetos e (b) ser capaz de usar com segurança a vasta quantidade de APIs existentes em referências compartilhadas ao manter um PinShr (ou PinMut ).

É uma pena que não parecemos ter uma maneira fácil de fornecer operações que possam funcionar tanto em &mut quanto em PinMut ; afinal, muito código trabalhando em &mut não tem intenção de usar mem::swap . (Essa, a meu ver, seria a principal vantagem de uma solução baseada em !DynSized ou algo comparável: &mut se transformaria em um tipo de referências que podem ou não ser fixadas. poderia ter isso como mais um tipo de biblioteca na API Pin , mas isso é inútil, visto que já temos todos esses &mut métodos por aí.)

Há um argumento leve contra, que são os tipos que querem fazer coisas "interessantes" tanto para &T quanto PinMut<T> . Isso vai ser difícil de fazer, nem tudo é possível e é preciso ter muito cuidado. Mas não acho que isso supere os bons argumentos a favor.

Neste caso (ou seja, com este impl Deref ), PinShr<'a, T> deve vir com um método seguro que o transforma em &'a T (preservando a vida).


E há outra preocupação que acho que temos que resolver: As regras para Pin::map e / ou drop , à luz do que @MicahChalmer observou acima. Temos duas opções:

  • Declare que usar Pin::map com uma projeção para um campo público (sem derefs, nem mesmo implícito) é sempre seguro. Esta é minha interpretação da RFC atual. Isso corresponderia a &mut e & . Entretanto, temos um problema em torno de Drop : agora podemos escrever um código seguro que causa UB, dado um tipo !Unpin bem formado.
  • Não faça nada engraçado em torno de Drop . Então, temos que adicionar avisos altos a Pin::map que mesmo usá-lo para campos públicos pode causar problemas. Mesmo que algum dia tenhamos &pin T , não poderemos usá-lo para acessar um campo no código seguro.

O único argumento possível para a segunda opção que posso ver é que pode ser o único que podemos realmente implementar. ;) Eu acho que é inferior em todos os sentidos possíveis - torna &pin bastante estranho e não ergonômico, mesmo que um dia fique embutido, é uma footgun, atrapalha a composicionalidade.

Pode haver uma maneira de alcançar a primeira opção, mas não é trivial e não tenho idéia de como torná-la compatível com versões anteriores: Poderíamos adicionar um Unpin vinculado a Drop e adicionar um DropPinned que não tem o limite e onde drop leva Pin<Self> . Talvez o limite de Unpin em Drop possa ser aplicado de uma maneira estranha, onde você pode escrever impl Drop for S , mas isso adiciona um limite implícito a S dizendo que tem que ser Unpin . Provavelmente não é realista. : / (Eu acho que este também é um ponto onde a abordagem baseada em !DynSized funciona melhor - ela transforma &mut T em "pode ​​ou não ser fixado", mantendo drop som.)

@RalfJung @MicahChalmer Acho melhor apenas documentar que, se você sair do campo no Drop impl, projetar para um Pin desse campo em outro lugar não é válido.

Na verdade, já é o caso hoje que (usando código inseguro) você poderia sair de um campo do tipo !Unpin , e isso é seguro e bem definido, desde que você nunca projete para um pino desse campo em outro lugar . A única diferença com Drop é que a parte de mudança contém apenas o código seguro. Parece-me que as notas sobre Pin::map necessidade de mudar a nota que não é seguro se você nunca sair desse campo, independentemente do impl Gota.

Deve ser seguro sair dos campos de um tipo !Unpin em alguns casos, porque os geradores muito provavelmente sairão de um de seus campos quando retornarem.

Acho melhor apenas documentar que, se você sair do campo no implemento Drop, projetar para um Pin desse campo em outro lugar não é válido.

Esta é a segunda opção então, aquela que torna &pin para um campo uma operação permanentemente insegura.
Eu acho que esta não é uma pequena mudança. Isso muda fundamentalmente o que significa pub em um campo. Ao usar um tipo de biblioteca, não posso saber o que ele faz em seu drop impl, portanto, fundamentalmente, não tenho como obter uma referência fixa a esse campo.

Por exemplo, eu não teria permissão para ir de Pin<Option<T>> para Option<Pin<T>> menos que Option declarasse explicitamente que nunca haverá Drop fazendo qualquer coisa " engraçado". O compilador não consegue entender essa instrução, portanto, embora Option possa fornecer um método apropriado para fazer isso, fazer o mesmo com match deve permanecer uma operação insegura.

A única diferença com Drop é que a parte de mudança contém apenas código seguro.

Mas essa é uma grande diferença, não é? Podemos estabelecer regras arbitrárias sobre o que o código inseguro pode ou não fazer, mas não para o código seguro.

Deve ser seguro sair dos campos de um tipo! Unpin em alguns casos, porque os geradores muito provavelmente sairão de um de seus campos quando retornarem.

Suponho que, neste caso, o campo será Unpin ? Portanto, poderíamos provavelmente ter uma regra dizendo que Pin::mut para um campo público de uma estrutura estrangeira está bem se esse campo tiver o tipo Unpin . Não tenho certeza se isso é útil, mas provavelmente é melhor do que nada.

Quero reafirmar rapidamente minha confusão sobre &Pin<T> não oferecer mais garantias do que &&T . & , &mut e &pin respectivamente fornecem "acesso compartilhado", "acesso exclusivo" e "acesso exclusivo a um valor que não será movido". Entender &&pin como "acesso compartilhado a um acesso exclusivo a um tipo que não será movido" indica que a memória é compartilhada (a garantia de exclusividade de &pin é cancelada pelo compartilhamento de & ), mas você ainda mantém a propriedade de que o tipo não será movido, não?

Não tenho certeza do que você está perguntando ou dizendo. Você está confuso por que eu acho que "compartilhada fixada" é um modo / estado tipográfico fundamental por si só?

A questão é que "acesso compartilhado" não é algo que eu saiba definir por conta própria. Existem muitas maneiras diferentes de compartilhar e coordenar o compartilhamento, como é testemunhado pelas maneiras muito diferentes em que, por exemplo, Cell , RefCell e Mutex compartilham.

Você não pode simplesmente dizer que está compartilhando algo ("cancelando a garantia de exclusividade" de algo) que possui e esperar que essa afirmação faça sentido. Você tem que dizer como está compartilhando e como garantir que isso não cause estragos. Você pode "compartilhar tornando-o somente leitura" ou "compartilhar apenas dando acesso atômico por meio de carregamentos / armazenamentos sincronizados" ou "compartilhar [em apenas um segmento] tendo este sinalizador emprestado coordenando qual tipo de acesso distribuir" . Um dos pontos-chave no RustBelt foi perceber a importância de deixar cada tipo definir por si mesmo o que acontece quando ele é compartilhado.

Não consigo pensar em uma maneira de fazer a "fixação compartilhada" surgir como a composição ortogonal de compartilhamento e fixação. Talvez haja uma maneira de definir a noção de um "mecanismo de compartilhamento" que poderia então ser aplicado ao invariante possuído ou fixado para dar origem a "compartilhado (normal)" e "compartilhado com pino", mas eu duvido seriamente. Além disso, como vimos que cai por terra para RefCell - se RefCell faz para a invariante fixada compartilhada algo semelhante ao que faz para a invariante compartilhada recentemente, certamente não podemos justificar isso de & &pin RefCell<T> por meio de &RefCell<T> (usando Deref ) por meio de borrow_mut podemos obter uma referência de &mut que diz que não houve fixação.

@RalfJung

Poderíamos adicionar um Unpin vinculado a Drop e adicionar um DropPinned que não tem o limite e onde drop leva Pin.

A definição de Drop realmente o problema aqui? Outra maneira de pensar sobre isso é colocar a culpa em mem::swap e mem::replace . Estas são as operações que permitem mover algo que você não possui. Suponha que um limite T: Unpin fosse adicionado a _teles_?

Para começar, isso consertaria o buraco drop que apontei - meu Shenanigans não seria compilado e não acho que conseguiria violar as promessas de alfinetes sem outro unsafe . Mas permitiria mais do que isso! Se tornou-se seguro obter uma referência &mut para um valor previamente fixado em drop , por que limitá-lo a apenas drop ?

A menos que esteja faltando alguma coisa, acho que isso tornaria seguro pegar emprestada uma referência de &mut de PinMut<T> qualquer hora que você quiser. (Estou usando PinMut para me referir ao que, atualmente, é chamado de Pin , para evitar confusão com a discussão sobre pinos compartilhados.) PinMut<T> poderia implementar DerefMut incondicionalmente, em vez de apenas T: Unpin .

É lamentável que não tenhamos uma maneira fácil de fornecer operações que possam funcionar tanto no & mut quanto no PinMut; afinal, muito código trabalhando em & mut não tem intenção de usar mem::swap .

Um implante de DerefMut em PinMut consertaria isso, certo? O código que se preocupa com a fixação e requer PinMut , pode chamar o código que funciona em &mut com segurança e facilidade. A carga seria colocada em vez de funções genéricas que _do_ querem usar mem::swap essas teriam que ter um limite Unpin adicionado a elas, ou usar unsafe e tome cuidado para não violar as condições de pin.

Adicionar tais limites a swap e replace agora quebraria a compatibilidade com versões anteriores desde a primeira versão estável. Não vejo uma maneira realista de chegar lá a partir daqui. Mas estou perdendo algum outro buraco em pensar que teria sido a coisa certa a fazer, se apenas isso fosse conhecido nos dias anteriores à 1.0?

Do jeito que está, não vejo nenhuma solução melhor do que a que @withoutboats disse - mantenha map inseguro e coloque uma mensagem em seus documentos alertando as pessoas para não saírem de qualquer campo previamente fixado em drop impl.

Podemos estabelecer regras arbitrárias sobre o que o código inseguro pode ou não fazer, mas não para o código seguro.

Usar unsafe sempre impõe regras ao código seguro circundante. A boa notícia aqui é que, até onde sabemos, se uma estrutura fixável tem um método para projetar um pino em um campo privado, apenas seu próprio drop impl pode usar isso para violar o contrato em código seguro. Portanto, ainda é possível adicionar tal projeção e apresentar _usuários_ dessa estrutura com uma API totalmente segura.

A definição de queda é realmente o problema aqui?

Atribuir a culpa é um tanto arbitrário, há coisas diferentes que podem ser mudadas para tapar o buraco de segurança. Mas temos um acordo de que alterar Drop como sugeri corrigirá o problema?

Outra maneira de pensar sobre isso é colocar a culpa em mem :: swap e mem :: replace. Estas são as operações que permitem mover algo que você não possui. Suponha que um limite T: Unpin foi adicionado a eles?

Bem, isso tornaria &mut T geralmente seguro para usar para os tipos !Unpin . Como você observou, não precisaríamos mais de PinMut . PinMut<'a, T> em sua proposta poderia ser definida como &'a mut T , certo?
Esta é essencialmente a proposta ?Move que foi descartada anteriormente devido a problemas de compatibilidade com versões anteriores e complexidade de linguagem.

Usar inseguro sempre impõe regras sobre o código seguro circundante.

Eu não estou certo do que você quer dizer. Além dos limites da privacidade, esse não deve ser o caso; o código inseguro não pode impor nada a seus clientes.

A boa notícia aqui é que, pelo que sabemos, se um struct pinnable tem um método para projetar um pin em um campo privado, apenas seu próprio impl drop pode usar isso para violar o contrato no código seguro. Portanto, ainda é possível adicionar tal projeção e apresentar aos usuários dessa estrutura uma API totalmente segura.

Sim, os tipos podem optar por declarar a projeção segura. Mas, por exemplo, o verificador de empréstimo não entenderá que este é um acesso de campo, então dado um PinMut<Struct> você não pode usar tais métodos para obter PinMut para dois campos diferentes ao mesmo tempo.

Mas nós concordamos que mudar o Drop conforme sugerido resolveria o problema?

Eu concordo, isso resolveria.

Nem precisaríamos mais do PinMut. PinMut <'a, T> em sua proposta poderia ser definido como &' a mut T, certo?

Não, PinMut<'a, T> ainda precisa prometer que o referente nunca mais se moverá. Com &'a mut T você só poderia confiar que ele não se moveria por toda a vida 'a . Isso ainda seria permitido, como é hoje:

`` `` ferrugem
struct X;
impl! Desafixar para X {}
fn takes_a_mut_ref (_: & mut X) {}

fn borrow_and_move_and_borrow_again () {
deixe mut x = X;
leva_a_mut_ref (& mut x);
deixe mut b = Box :: new (x);
leva_a_mut_ref (& mut * b);
}
`` ``

Seria seguro ir de PinMut<'a, T> para &'a mut T mas não vice-versa - PinMut::new_unchecked ainda existiria e ainda seria unsafe .

Esta é essencialmente a proposta ?Move que foi descartada anteriormente devido a problemas de compatibilidade com versões anteriores e complexidade de linguagem.

Pelo que entendi, as ?Move propostas estavam tentando fazer com que não precisassem de PinMut , alterando as regras fundamentais da linguagem para proibir o trecho de código acima (fazendo Unpin ser um traço de Move .) Não estou propondo nada parecido - minha proposta é começar exatamente do jeito que é todas as noites agora, mais:

  • Adicionar um Unpin ligado ao std::mem::swap e std::mem::replace funções
  • Remova o limite de Unpin de DerefMut impl de Pin ( PinMut se essa renomeação acontecer)

Isso é tudo - nenhuma mudança fundamental na linguagem de como os movimentos funcionam, ou qualquer coisa assim. Minha alegação é: sim, esta seria uma alteração decisiva, mas teria menos impacto de interrupção do que alterar Drop (que por sua vez é ainda menos drástico do que as propostas de ?Move ), ao mesmo tempo que retém muitos dos benefícios. Em particular, permitiria a projeção de pinos seguros (pelo menos para campos privados, e acho que até mesmo para públicos? Não tenho certeza) e evitaria situações em que o código paralelo deve ser escrito para funcionar com PinMut e &mut .

Não, PinMut <'a, T> ainda precisa prometer que o referente nunca mais se moverá. Com & 'um mut T você só podia confiar que ele não se moveria por toda a vida' a.

Eu vejo. Faz sentido.

Pelo que entendi, as propostas de? Move estavam tentando fazer com que não precisassem do PinMut, alterando as regras fundamentais da linguagem para proibir o trecho de código acima (tornando o Unpin um traço do Move.)

Entendido.

Minha afirmação é: sim, esta seria uma alteração significativa, mas teria menos impacto de interrupção do que alterar a queda

Que mudança você quer dizer? Adicionando um limite de Unpin ? Você pode estar certo, mas não tenho ideia de quão amplamente usados mem::swap e mem::replace são.

Em particular, permitiria a projeção segura de pinos (pelo menos para campos privados, e acho que até mesmo para públicos? Não tenho certeza)

Não vejo como o privado versus o público pode fazer a diferença aqui. Como um campo público permitiria menos do que um campo privado?

Mas sim, isso parece se manter no geral. Future ainda levaria PinMut porque tem que contar com coisas que nunca se movem, mas teria uma gama mais ampla de métodos disponíveis para uso.

No entanto , o aspecto da compatibilidade é um grande problema. Eu não acho que isso seja realista, iria quebrar todo o código genérico que chama mem::swap / mem::replace . Além disso, agora o código inseguro está livre para implementar esses métodos usando ptr::read / ptr::write ; isso pode levar à quebra silenciosa do código não seguro existente. Isso é proibido, eu acho.

Já que estamos no tópico de apresentar Unpin vinculado a mem::swap e mem::replace (e não se preocupar com quebra). Se assumirmos que a rota do "compilador embutido" é seguida. Seria possível também introduzir o mesmo limite em mem::forget para garantir que os destruidores sejam executados para variáveis ​​fixadas na pilha fazendo thread::scoped som e evitar "pré-cocô nas calças" em certos casos?

Observe que mem::forget em PinBox ainda é permitido. A nova proposta de garantia relacionada à queda NÃO diz que "as coisas não vazam". Diz que "as coisas não são desalocadas sem que drop seja chamado primeiro". Essa é uma declaração muito diferente. Esta garantia não ajuda thread::scoped .

Para adicionar contexto, mover dados para fora de uma estrutura que implementa Future é algo que faço normalmente. Freqüentemente, é necessário realizar um trabalho de limpeza se um futuro nunca foi concluído (descartado antes da conclusão da pesquisa).

Portanto, eu certamente teria atingido esta mina terrestre ao portar o código existente para futuros 0.3, mesmo com a documentação adicionada a map .

@carllerche a função map diz claramente que você não deve usar isso para mover nada. Não podemos e não queremos proteger contra pessoas que saem deliberadamente de Pin<T> , mas você tem que sair do seu caminho (usando código não seguro) para fazer isso. Eu não chamaria isso de mina terrestre.

Então, a qual mina você está se referindo?

@RalfJung Tenho tentado descobrir os limites do mapeamento de referências fixadas por mim mesmo e acho que isso vai ser um tiro certeiro se não for resolvido logo. Acho que a primeira solução é preferível, apesar da complexidade; não ser capaz de projetar com segurança em campos fixados torna virtualmente impossível para os consumidores realmente usar APIs que dependem de fixação sem escrever código inseguro.

Se isso não puder ser feito, acho que, na prática, a maioria das APIs utilizáveis ​​que usam a fixação terá que usar o PinShare. Isso pode não ser uma grande desvantagem, eu acho, mas ainda não estou certo sobre a relação com o Unpin nesse caso. Especificamente: digamos que eu pegue uma referência compartilhada de pinos e obtenho uma referência a um campo no tipo (por um determinado período de vida). Posso realmente confiar que ele não se moverá quando essa vida acabar? Provavelmente posso, se o campo for !Unpin então talvez não haja problema, desde que Pin não possa projetar - estou mais preocupado com enums. A menos que você esteja dizendo que mesmo a fixação compartilhada não pode ser segura sem a correção de queda - nesse caso, acho que consertar a queda para funcionar com a fixação essencialmente tem que acontecer; caso contrário, torna-se um recurso de biblioteca de nicho que realmente não pode ser usado com segurança e não (IMO) merece nenhum lugar de destaque na linguagem central, mesmo que seja muito útil para Futuros.

Devo também mencionar que a única API prática que tenho para coleções intrusivas até agora (ainda preciso resolver os problemas) precisa de garantias ainda mais fortes do que isso; precisa ser capaz de garantir que a queda não seja acionada enquanto houver algum empréstimo na coleção. Posso fazer isso usando uma técnica estilo GhostCell, mas parece muito estranho e requer que o usuário faça o gerenciamento manual da memória (já que temos que vazar se a memória de apoio de algo na coleção for descartada sem receber o token). Portanto, estou um pouco preocupado que a queda automática em si pareça difícil de funcionar com tipos que usam a fixação de maneiras interessantes.

Por curiosidade: quais são os argumentos contra adicionar um Unpin vinculado a Drop ? Você citou a compatibilidade reversa, com a alternativa de que você precisaria de alguma forma vincular automaticamente o que estava sendo eliminado, mas elimine as restrições de nível de sistema de tipo já estranhas que não existem para outras características - por que este é tão diferente? Certamente não é tão elegante quanto fazer a queda levar Pin<T> mas não podemos realmente fazer essa mudança neste momento. É a questão que nós realmente não sabemos o que fazer se você fizer queda de chamadas em um tipo que só tem um Unpin implementação, quando o tipo em si é !Unpin ? Eu acho que lançar uma exceção na implementação de drop nesse caso pode ser a abordagem correta, uma vez que qualquer um que depende de drop rodando para tipos genéricos já precisa lidar com o caso de pânico. Isso significaria que seria muito difícil usar tipos !Unpin na prática sem um grupo de pessoas atualizando seu código para usar o novo traço Drop (então o ecossistema seria forçado a mover tudo para a nova versão), mas acho que ficaria bem com isso, pois ainda preservaria a integridade e não quebraria o código que não usava !Unpin todo. Além disso, o "seu código entra em pânico se a biblioteca não for atualizada" realmente incentivaria as pessoas a seguir em frente!

Na verdade, aqui está meu projeto proposto:

Estenda a característica Soltar com um segundo método que usa Pin, como você propôs. Faça uma implementação padrão especializada where T: Unpin call drop (presumo que isso passaria pelas regras de especialização atuais? Mas mesmo se não fosse, Drop pode sempre ser especial; mais, Drop funções são geralmente geradas automaticamente). Agora você tem exatamente o mesmo comportamento que propus acima, sem problemas de compatibilidade com versões anteriores e nenhuma tentativa de auto-derivar um limite desajeitadamente.

Ele tem o problema de que as bibliotecas de terceiros terão que ser atualizadas para serem praticamente úteis com os tipos !Unpin , mas como eu disse, isso é indiscutivelmente uma coisa boa. Em qualquer caso, não tenho ideia de quantas implementações drop realmente alteram seus campos de maneiras que exigem &mut maioria das que posso pensar que fazem algo interessante ou usam unsafe ou usar mutabilidade interior, mas provavelmente não estou pensando em alguns casos de uso comuns.

O principal aspecto dessa abordagem é que, para ser adotada, ela teria que ser realizada antes de Pin ser estabilizado. Este é mais um motivo pelo qual realmente espero que Pin não seja apressado. Não acho que exploramos suficientemente as consequências do design.

(Eu vejo uma questão mais potencial: ManuallyDrop e sindicatos na média geral de que alguém provavelmente poderia ter escrito um processo de destruição ao longo de um tipo genérico que se supor que o drop implementação dentro dele não poderia entrar em pânico , simplesmente porque ele nunca foi capaz de executar (ele também não seria permitido para chamar quaisquer outras &mut funções no T genérico, exceto para aqueles implementados em traços inseguros que prometeu não entrar em pânico). Desde Pin ing um tipo já deve garantir que sua implementação de eliminação seja executada antes que sua memória seja destruída [de acordo com a nova semântica], eu acredito que o tipo !Unpin dentro de um ManuallyDrop usado para esse propósito no código existente não pode ser considerado fixado em primeiro lugar, então o código existente ainda deve estar correto se fizer essa suposição; certamente, você não deve ser capaz de projetar com segurança um pino em um ManuallyDrop , uma vez que ele só pode ser seguro se você garantir que seu destruidor seja chamado antes do lançamento. Só não sei como alguém comunicaria esse caso ao computador Isso pode tirar vantagem do "tapa-olho", já que parece que tem um propósito semelhante?]. Não tenho certeza se isso voa semanticamente, mas provavelmente funciona para os propósitos da implementação Drop para o código existente.

Quanto ao tapa-olho, porém ... Ainda não estou certo da definição formal exata que ele tem, mas se a ideia geral é que nenhuma função genérica interessante é chamada em T, talvez alguém pudesse explorar isso mais? Isso significa que se a implementação drop_pinned para o tipo !Unpin fosse implementada, o recipiente que respeita o tapa-olho se comportaria corretamente. Parece-me possível que se pudesse compilar falha de tempo para !Unpin tipos que implementam Drop , não implementam drop_pinned e não olham para seus parâmetros genéricos, em da mesma forma que fazemos quando você usa recipientes sem tapa-olhos com tempos de vida autorreferenciais.

As existenciais representam um sério risco de compatibilidade com versões anteriores com qualquer estratégia de tempo de compilação; É por isso que acho que uma solução que falha em tempo de execução é mais realista. isso não teria falha de tempo de execução e não teria falsos positivos.

Edit: Na verdade, risque tudo acima: só precisamos realmente nos preocupar com os campos acessíveis nos destruidores, não com o acesso &mut geral, correto? Porque estamos preocupados apenas com as projeções de campo implícitas de &mut self .

Posso estar apenas reinventando a proposta !DynSized , mas essencialmente: os contêineres genéricos já devem ser !Unpin se eles exporem quaisquer métodos que se preocupam com o estado de tipo pin (eu percebo que isso soa suspeitamente como a parametricidade incorreta argumento, mas me ouça!). Existenciais como Box<Trait> e &mut Trait não refletem necessariamente o status de Unpin , mas (pelo menos olhando para as APIs atuais?) Não acho Pin<Box<T>> e Pin<&mut T> necessariamente têm que ser coercíveis para Pin<T> onde T: !Unpin ; não implementar isso significaria que as referências fixadas a esses tipos não fornecerão acesso fixado com segurança ao seu interior (observe que há algum precedente para isso com o relacionamento de & mut e &: & mut & T não é coercível para & mut T, apenas & T, e A caixa <& mut T> não é coercível para Box<T> , apenas & mut T; em geral, quando diferentes estados tipográficos colidem, eles não precisam ser propagados automaticamente). Eu reconheço que geralmente &mut T , Box<T> e T são considerados totalmente intercambiáveis ​​de uma perspectiva de estado tipográfico, e este argumento não se estende a existenciais inline hipotéticos, mas talvez isso seja a substância da proposta DynSize (não permitir trocas seguras ou mem::replace s para valores de objeto de traço, eu acho? Na verdade, já não é permitido para eles ... mas estou assumindo que há alguma razão pela qual isso pode mudar no futuro). Isso torna uma solução de tempo de compilação muito simples: para qualquer estrutura que (transitivamente) possui diretamente (no & mut, &, Box , ou ponteiros brutos - nenhum dos quais propagaria de forma transitiva Pin acesso , exceto & para pin compartilhado, mas & não pode ser movido de qualquer maneira - ou se você decidiu ir com a solução "não pode sair dos objetos de traço", você também poderia verificar campos transitivamente ao longo de &mut e Box eu acho) e ter acesso (em um sentido de visibilidade) a um tipo !Unpin conhecido (incluindo ele mesmo), teria que implemente o segundo tipo de drop . Isso me parece totalmente normal e não é uma arma de compatibilidade com versões anteriores, já que nenhum tipo estável é !Unpin pessoas podem ter que reimplementar destruidores após atualizar uma biblioteca, mas isso é tudo, certo? Além disso, os detalhes da implementação interna permaneceriam internos; somente se um campo !Unpin fosse exposto haveria algum problema. Finalmente, todos os tipos de contêineres genéricos (não apenas Vec e material de biblioteca padrão, mas essencialmente todo o ecossistema crates.io) continuariam a funcionar bem. Qual é a coisa catastrófica que estou perdendo nessa solução?

(Na verdade, parece-me que mesmo se você não pudesse impor isso no tempo de definição de drop , você deveria pelo menos ser capaz de fazer isso no momento da instanciação do tipo, como dropck faz, já que você só precisa se preocupar com os tipos totalmente instanciados).

Relendo a proposta Dynsized : observo que um argumento contra ela exigia que os tipos imóveis sempre fossem DynSized mesmo antes de serem fixados. Afirmei acima que realmente só precisamos nos preocupar com isso no caso de objetos de traço; pode ser possível (embora difícil de justificar) impor que coagir um tipo !Unpin a uma característica exigisse limitá-lo explicitamente com + ?DynSized (ou qualquer outra coisa; isso poderia ser feito automaticamente, suponho). Embora possa haver muitos casos em que o tamanho deva ser conhecido para os tipos !Unpin (na verdade, eu tenho esse caso de uso!), Ou eles devam ser capazes de ser trocados antes de serem fixados, espero que haja muito poucos desses casos para os interiores de objetos de características feitos de tipos imóveis (os únicos casos que temos agora são para coisas como a conversão Box -> Rc que queremos proibir explicitamente, certo? Na verdade, nem mesmo está claro para mim se size_of_val ser exposto é realmente um problema aqui ou não, já que ainda não sei se você será capaz de transformar Pin<'a, Box<Trait> em Pin<'a, Trait> , mas se você não puder, podemos apenas confiar em Sized claro). Alguém realmente quer ser capaz de vinculá-los com !Unpin em qualquer caso, mas como eu disse, acho que as pessoas querem evitar adicionar mais características negativas do que já precisamos (pessoalmente, espero que !Unpin os objetos de características serão suficientemente raros e especializados para que objetos de características delimitadoras com ?Unpin vez de Unpin sejam totalmente razoáveis ​​e não infectem muito o sistema de tipos; a maioria dos usos para !Unpin Posso pensar que Pin<self> . Normalmente, você também gostaria de usá-los com PinBox ou PinTypedArena ou qualquer outro ponto em que os limites de ?Unpin parecem bastante naturais).

Tenho um novo design, que acho que não é tão horrível quanto o antigo: https://github.com/pythonesque/pintrusive/blob/master/src/main.rs. Em vez de tentar fazer com que as projeções do pino funcionem em todos os lugares, este design pergunta como podemos interoperar com o código Rust "legado" que não sabe nada sobre a fixação, desde o código Rust "moderno" que sempre quer oferecer suporte à fixação. A resposta óbvia parece ser o uso do PinDrop trait @RalfJung proposto, mas apenas para tipos que possuem uma

Os tipos optam explicitamente por campos de projeção (correspondendo a derivar o traço PinFields ), que é análogo ao código escrito em Rust "moderno"; no entanto, não faz requisitos adicionais sobre o código em Rust "legado", em vez disso, opta por suportar apenas a projeção de profundidade 1 para qualquer derivação de PinFields . Ele também não tenta mover Pin por meio de referências, o que acredito ser uma boa idéia não fazer de qualquer maneira. Ele suporta estruturas e enums, incluindo qualquer análise de disjunção que o Rust deve ser capaz de fornecer, gerando uma estrutura com campos e variantes idênticos, mas com os tipos substituídos por Pin 'd referências aos tipos (deve ser trivial para estender isso para Pin e PinMut quando essa alteração for feita). Obviamente, isso não é ideal (embora esperemos que o otimizador possa se livrar da maior parte dele), mas tem a vantagem de funcionar com borrowck e NLL e funcionar facilmente com enums (em oposição aos acessadores gerados).

O argumento de segurança funciona garantindo que o Drop não seja implementado para a estrutura ou garantindo que se o Drop for implementado para ela, é uma implementação trivial que apenas chama PinDrop (uma versão do Drop que leva Pin) Acredito que isso elimine todos os problemas de solidez com a projeção de campos fixados, com um ponto de interrogação: minha principal preocupação é encontrar um bom argumento para o porquê de campos disjuntos no mesmo contêiner (ou seja, campos na profundidade 1) que podem invalidar os do outro pinos em seus destruidores já seriam inválidos sem as projeções dos pinos. Acho que posso justificar isso se pudermos mostrar que você também poderia fazer a mesma coisa se eles fossem mantidos em PinBoxes separadas, o que implica que o local onde eles moram faz parte de seu contrato de segurança; isso significa que seus destruidores não são seguros isoladamente e construí-los fora do módulo exigiria um código inseguro. Em outras palavras, sua correção depende da implementação do tipo de contêiner, o que significa que não há problema em pedir mais de seu destruidor do que pediríamos por um código seguro arbitrário.

@pythonesque Eu realmente não segui o que você escreveu acima sobre DynSize , mas acho que está tudo desatualizado agora mesmo? Então, eu só vou comentar sua última postagem.

Para resumir, você está dizendo que projetar para um campo de uma estrutura / enum (incluindo pub campos) não é seguro em geral , mas um tipo pode optar por projeções de campo seguras por não implementar Drop . Se o tipo quiser um destruidor, ele deve PinDrop vez de Drop :

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

Já temos o typecheck procurando Drop para rejeitar a mudança de um campo, então não parece irrealista também verificar se Drop rejeitar a projeção por meio de &pin . Obviamente, a verificação de "mover para fora do campo" ainda seria rejeitada se houver PinDrop , enquanto a projeção seria permitida nesse caso.

O compilador aplicaria a PinDrop as mesmas restrições que aplica a Drop , além de garantir que um tipo não implemente Drop e PinDrop . Ao gerar cola drop, ele chama qualquer tipo de Drop que o tipo implementou.

Isso resume a proposta? A parte que não entendi é seu último parágrafo. Você poderia dar um exemplo demonstrando o que o preocupa?


Agora, é claro, estou me perguntando quais são as obrigações de prova aqui. Acho que a maneira mais fácil de ver isso é dizer que PinDrop é na verdade o principal e único tipo de destruidor que existe formalmente, e impl Drop for T é, na verdade, açúcar sintático para impl PinDrop que chama o método não seguro PinMut::get_mut e depois chama Drop::drop . No entanto , este é um código inseguro gerado automaticamente, porque a fixação é uma "extensão local" (ou seja, é compatível com os códigos não seguros existentes), esse código inseguro é sempre seguro se o tipo fornecido não se importar com a fixação.

Falando de maneira um pouco mais formal, há uma "invariante de fixação padrão" que os tipos têm se o autor não se preocupa com a fixação. Os tipos que usam aquele invariante de fixação padrão são automaticamente Unpin . Escrever impl Drop para um tipo que tem um invariante personalizado afirma que o tipo usa o "invariante de fixação padrão". Isso é um pouco suspeito, pois parece um pouco como se houvesse uma obrigação de prova aqui, mas não há unsafe para avisar sobre isso - e de fato isso não é perfeito, mas a compatibilidade com versões anteriores é importante, então o que você pode Faz. Também não é uma catástrofe, porque pode-se argumentar que o que isso realmente significa é que muda a obrigação de prova que incorre em código inseguro em outro lugar, na medida em que esse código inseguro deve funcionar com o "invariante de fixação padrão". Se não houver código inseguro em outro lugar neste módulo, está tudo bem.

Eu poderia até imaginar que poderíamos adicionar um lint contra impl Drop for T menos que T: Unpin , talvez restrito ao caso em que o módulo que define o tipo contenha um código inseguro. Esse seria um lugar para educar as pessoas sobre o problema e incentivá-las a unsafe impl Unpin (formalmente afirmando que usam a invariante de fixação padrão) ou impl PinDrop .

@RalfJung

Isso resume a proposta?

Sim, mais ou menos (acho que sua proposta é significativamente mais ambiciosa do que a minha, uma vez que parece propor a execução automática de projeções se o Drop não for implementado, o que eu acho que é provavelmente um problema de compatibilidade com versões futuras para bibliotecas; mas talvez haja alguma maneira de contornar este).

A parte que não entendi é seu último parágrafo. Você poderia dar um exemplo demonstrando o que o preocupa?

Em termos gerais: estou preocupado com dois campos que vivem diretamente na mesma estrutura, um dos quais muda o outro quando seu Drop é chamado (possivelmente contando com coisas como drop order para segurança) de uma forma que viola os invariantes de fixação do outro campo, mas preserva sua integridade estrutural (permitindo assim que você testemunhe violações do invariante de fixação). Obviamente, isso não pode usar um código completamente seguro (ou então seria rejeitado por dropck, entre outras coisas), então minha preocupação é apenas sobre algum caso hipotético em que os destruidores nos tipos dos campos eram seguros para serem executados quando os campos são fixados separadamente estruturas, mas não é seguro quando eles são fixados na mesma estrutura. Minha esperança é que não existam tais casos, a menos que haja uma invariante compartilhada que inclua a estrutura em que os campos estão contidos; se houver tal invariante, sabemos que ele deve estar ciente de que seus componentes não estão respeitando o invariante personalizado adequadamente e, portanto, podemos culpar o código em algum lugar do módulo.

Acho que a maneira mais fácil de ver isso é dizer que PinDrop é na verdade o principal e único tipo de destruidor que existe formalmente, e impl Drop for T é na verdade um açúcar sintático para impl PinDrop que chama o método inseguro PinMut :: get_mut e depois chama Drop :: drop.

Acordado. Infelizmente, essa avenida não estava aberta para a solução atual, pois ela tenta fazer coisas em uma biblioteca.

Falando de maneira um pouco mais formal, há uma "invariante de fixação padrão" que os tipos têm se o autor não se preocupa com a fixação. Tipos que usam aquele invariante de fixação padrão são automaticamente Desafixados. Escrever impl Drop para um tipo que tem uma invariante customizada afirma que o tipo usa a "invariante de fixação padrão". Isso é um pouco duvidoso, pois parece um pouco como se houvesse uma obrigação de prova aqui, mas não é inseguro avisar sobre isso - e de fato isso não é perfeito, mas a compatibilidade com versões anteriores é importante, então o que você pode fazer.

Sim, é mais ou menos isso que tenho em mente ... Só estou preocupado por não ter uma boa intuição do que significaria realmente formalizar essa garantia de "código inseguro no módulo". Eu gosto da sua ideia de um lint (eu gosto de qualquer coisa que faça mais pessoas usar o PinDrop, na verdade!), Mas acho que unsafe impl Unpin provavelmente estaria errado com muita frequência para ser uma boa sugestão, em menos para tipos com campos públicos genéricos (mas, novamente, para estruturas que não têm tais campos, seria realmente verdadeiro com bastante frequência ... é amplamente verdadeiro para a biblioteca padrão, por exemplo).

Escrevi um exemplo de como #[derive(PinnedFields)] poderia funcionar: https://github.com/withoutboats/derive_pinned

Já vi pessoas afirmarem que derivações como essa "não são sólidas", mas que isso não é verdade. Você precisaria usar um código inseguro para fazer algo que entraria em conflito com o código gerado por esta derivação - isto é, isso torna outro código inseguro inseguro (e eu acho que esse código - movendo em torno de ?Unpin dados - é algo que você pode / deve sempre evitar).

EDIT: Ok, na verdade, leia as últimas postagens sobre destruidores. Processará.

@withoutboats Sim, acho que você já viu isso, mas o problema era que o código inseguro em um tipo !Unpin correto poderia ser invalidado por um destruidor seguro em um tipo que derivou PinFields (então não havia código inseguro no módulo para o tipo que derivou PinFields exceto para o material gerado automaticamente). Essa é a parte problemática. No entanto, dê uma olhada no design que vinculei (ignorando a escolha estilística de criar uma estrutura separada em vez de derivar acessores individuais - fui apenas eu tentando fazê-lo suportar o maior número possível de casos de uso com o verificador de empréstimo). Fiquei preocupado por um tempo, mas agora tenho quase certeza de que #[derive(PinFields)] ainda pode funcionar, basta tomar cuidado para garantir que Drop não seja implementado diretamente.

Também quero trazer à tona outro ponto em que estive pensando (que ainda não vi diretamente resolvido em nenhum lugar?): Acho que algo que tornaria Pin muito mais utilizável e se integraria melhor ao código existente, seria descer firmemente ao lado de essencialmente todos os tipos de ponteiro sendo Unpin . Isto é, fazendo &mut T , &T , *const T , *mut T , Box<T> , e assim por diante, todos considerados Unpin para qualquer T . Embora possa parecer suspeito permitir que algo como, digamos, Box<T> seja Unpin , faz sentido quando você considera que não é possível obter Pin no interior de o Box de um Pin<Box<T>> . Acho que permitir que apenas !Unpin infecte coisas que estão "inline" é uma abordagem muito razoável - não tenho um único caso de uso para permitir que a fixação se torne viral nas referências, e isso torna a semântica de qualquer tipo eventual de &pin muito agradável (eu trabalhei em uma tabela de como ele interagiria com os outros tipos de ponteiro nesse cenário e, essencialmente, se você ignorar os movimentos, &mut pin agiria da mesma forma que &mut , &pin atuam da mesma forma que & , e box pin atuam da mesma forma que box em relação ao relacionamento com outros indicadores). Também não é operacionalmente importante, tanto quanto posso dizer: em geral, mover um valor do tipo A contendo um ponteiro para um valor do tipo B não move o valor apontado de digite B , a menos que o valor do tipo B esteja embutido no valor do tipo A mas se for esse o caso, então A é automaticamente !Unpin uma vez que contém B sem direção indireta do ponteiro. Talvez o mais importante, significaria que uma grande porcentagem dos tipos que atualmente precisam de uma implementação insegura manual de !Unpin não precisariam de uma, já que a maioria das coleções mantém apenas T atrás de uma direção indireta . Isso permitiria que o Drop atual continuasse funcionando e permitiria que esses tipos implementassem PinDrop sem alterar sua semântica (já que se um tipo for Unpin ele pode tratar Pin 'd argumento como &mut qualquer maneira).

Posso estar perdendo algum motivo pelo qual esse tipo de fixação transitiva seria uma boa ideia, mas até agora não encontrei nenhum. A única coisa operacional que me preocupa é se é realmente possível implementar esse comportamento com características automáticas - acho que provavelmente seria, mas talvez em alguns casos onde as pessoas estão usando PhantomData<T> mas na verdade têm um ponteiro para T , seria bom para eles alterá-lo para PhantomData<Box<T>> . Normalmente pensamos neles como sendo exatamente iguais semanticamente, mas com a fixação isso não é totalmente verdade.

@pythonesque A terminologia de "uma nova linguagem" e tal é uma espécie de confundir a situação para mim. Minha impressão sobre o que você faz é:

  • Por padrão, #[derive(PinFields)] gera um não op Drop impl. Isso garante que você nunca acesse o campo fixado no destruidor.
  • Um atributo opcional altera este Drop impl para chamar PinDrop::pin_drop , e você deve implementar PinDrop .

Isto está certo?


Também acredito que tudo isso só importaria se estendêssemos as garantias de Pin para dar suporte a coleções intrusivas. Isso é consistente com seu entendimento de @RalfJung e @pythonesque?


Tudo isso é bastante frustrante, porque o que parece claro é que Drop deveria apenas assumir Pin ! Fazer uma mudança mais radical (possivelmente histórica) parece atraente, mas não vejo uma maneira que não seja extremamente perturbadora.

Aqui está um exemplo do tipo de insegurança que o acesso a campos fixados + drop pode causar: https://play.rust-lang.org/?gist=8e17d664a5285e941fe1565ce0eca1ea&version=nightly&mode=debug

O tipo Foo está passando um buffer interno para algum tipo de API externa que requer que o buffer permaneça onde está até que seja explicitamente desvinculado. Tanto quanto eu sei, isso é válido sob as restrições que Pin<Foo> é criado, você tem a garantia de que ele não será movido até que Drop tenha sido chamado (com a condição de que, em vez disso, ele possa vazar e Drop nunca seja chamado, mas nesse caso você tem a garantia de que nunca será movido).

O tipo Bar então quebra isso movendo Foo durante sua implementação de Drop .

Estou usando uma estrutura muito semelhante a Foo para oferecer suporte a um periférico de rádio que se comunica via DMA, posso ter um StableStream com um buffer interno que é gravado pelo rádio.

@withoutboats

Isto está certo?

Sim, exceto que na verdade ele não gera um no-op Drop impl (porque os tipos que não implementam Drop no Rust funcionam melhor em geral). Em vez disso, ele tenta afirmar que Drop não foi implementado usando alguns recursos de biblioteca duvidosa (funciona no estável, mas quebra na especialização - acho que há uma variante que deveria funcionar sob especialização, mas não funciona agora porque de problemas com constantes associadas). Se fosse feito um recurso de linguagem, seria muito fácil aplicá-lo.

Também acredito que tudo isso só importaria se estendêssemos as garantias do Pin para apoiar coleções intrusivas. Isso é consistente com o seu entendimento de @ralfj e @pythonesque?

Não, não é o caso, infelizmente. O contra-exemplo vinculado acima não tem nada a ver com as garantias extras para coleções intrusivas. Um tipo Pin 'd já deve ser capaz de assumir que não se moverá antes de ser usado novamente, mesmo sem essa garantia, uma vez que se um método é chamado duas vezes em um valor atrás de uma referência fixada, o valor tem nenhuma maneira de saber se ele mudou entre as duas chamadas. As garantias extras que são necessárias para torná-lo útil para coleções intrusivas acrescentam uma coisa adicional sobre ter que chamar drop antes que a memória seja liberada, mas mesmo sem essa garantia drop ainda pode ser chamado em algo que está atualmente fixado (atrás de um PinBox, por exemplo). Se o que é solto incluir campos embutidos, e permitirmos projetar o pino do que é solto nesses campos, o destruidor do tipo externo ainda pode mover o campo interno, fixá-lo novamente (colocando o valor do campo em um PinBox , por exemplo) e, em seguida, chamar métodos nele que estão esperando referências de quando ele foi fixado antes para ainda serem válidos. Eu realmente não vejo nenhuma maneira de contornar isso; contanto que você possa implementar drop , você pode ter esse problema para qualquer campo !Unpin . É por isso que acho que a solução menos ruim é não permitir que as pessoas implementem drop se quiserem que a fixação funcione corretamente.

Tudo isso é bastante frustrante, porque o que parece claro é que Drop deveria se auto-levar por Pin! Fazer uma mudança mais radical (possivelmente histórica) parece atraente, mas não vejo uma maneira que não seja extremamente perturbadora.

Sim, é realmente irritante ... Fiquei muito irritado com isso por cerca de uma semana. Mas, como Ralf apontou, teríamos que esperar mais três anos pelo 1.0 antes que alguém descobrisse isso ... e sempre haverá algo mais.

Se o que é descartado inclui campos embutidos, e permitimos projetar o pino do que é solto para esses campos, o destruidor do tipo externo ainda pode mover o campo interno e, em seguida, chamar métodos nele que estão esperando referências de quando foi marcado para ainda ser válido.

A parte enfatizada parece muito importante para mim; na verdade, parece ser o ponto crucial da questão.

Inicialmente imaginei este código, que usa nosso único método que realmente se preocupa com endereços internos:

struct TwoFutures<F>(F, F);

impl Drop for TwoFutures {
     fn drop(&mut self) {
          mem::swap(&mut self.0, &mut self.1);
          unsafe { Pin::new_unchecked(&mut self.0).poll() }
     }
}

Mas envolve um código inseguro para voltar ao Pin ! Esse código deve apenas ser considerado incorreto.

Podemos descartar a capacidade dos métodos que usam &self e &mut self de confiar na validade dos endereços internos para a integridade? Como é isso?

@withoutboats Mesmo se apenas os métodos que levam Pin<Self> dependem da validade dos endereços internos para a integridade, você ainda pode ter o problema. O destruidor do tipo externo pode mem::replace o campo do tipo interno (usando sua própria &mut self referência), então PinBox::new o valor e, em seguida, chamar um método fixado nele . Não é necessário inseguro. A única razão pela qual não é um problema sem ser capaz de projetar campos fixados é que é considerado aceitável para um tipo que realmente implementa métodos estranhos !Unpin usando unsafe (ou compartilha uma invariante com um tipo que faz ) para ter obrigações de prova em sua implementação drop mesmo se ele usar apenas código seguro lá. Mas você não pode pedir isso de tipos que simplesmente contêm um tipo que é !Unpin e não têm código inseguro próprio.

@pythonesque

Acho que sua proposta é significativamente mais ambiciosa do que a minha, uma vez que parece propor a execução automática de projeções se o Drop não for implementado, o que provavelmente é um problema de compatibilidade de versões futuras para bibliotecas; mas talvez haja alguma maneira de contornar isso

Bem, acho que queremos fazer isso eventualmente. Como isso é um problema de compatibilidade?

Estou preocupado com dois campos que vivem diretamente na mesma estrutura, um dos quais muda o outro quando seu Drop é chamado (possivelmente contando com coisas como ordem de drop para segurança) de uma forma que viola as invariantes de fixação do outro campo, mas preserva sua integridade estrutural (permitindo assim que você testemunhe violações da invariante de fixação).

Mas isso já seria ilegal. Você não deve violar as invariantes dos outros.

mas eu acho que o implante inseguro Desafixar provavelmente estaria errado com muita frequência para ser uma boa coisa a ser sugerida, pelo menos para tipos com campos públicos genéricos

Acho que na verdade estará correto na maioria das vezes - espero que a maioria das pessoas não forneça nenhum acessador para Pin<Self> e, nesse caso, provavelmente estão usando a invariante de fixação padrão e, portanto, pode ser unsafe impl Unpin para eles.

Acho que algo que tornaria o Pin muito mais utilizável e se integraria melhor ao código existente seria reduzir com firmeza o fato de essencialmente todos os tipos de ponteiro serem Desafixados. Ou seja, fazer & mut T, & T, * const T, * mut T, Boxe assim por diante, todos podem ser considerados Desafixar para qualquer T. Embora possa parecer suspeito permitir algo como, digamos, Caixapara ser Desafixado, faz sentido quando você considera que não pode colocar um alfinete no interior da caixa fora de um alfinete>.

Concordo que isso provavelmente acontecerá (veja também meu comentário em https://github.com/rust-lang/rust/pull/49621#issuecomment-378286959). Atualmente, acho que devemos esperar um pouco até que nos sintamos mais confiantes em toda a coisa de fixação, mas na verdade não vejo muito sentido em impor a fixação além de direcionamentos indiretos.
Não tenho certeza de como me sinto em fazer isso com ponteiros brutos, porém, geralmente somos extremamente conservadores sobre as características do automóvel para eles, já que são usados ​​de todas as formas malucas.


@withoutboats

Escrevi um exemplo de como # [derive (PinnedFields)] poderia funcionar: https://github.com/withoutboats/derive_pinned

@pythonesque já disse isso, mas só para deixar claro: essa biblioteca não é sólida. Usando-o com o problema de queda do @MicahChalmer , posso quebrar qualquer tipo de fixação que realmente dependa da fixação. Isso é independente da garantia adicional de chamada de drop. Deixe-me saber se um exemplo ainda for necessário. ;)

@RalfJung

Essa biblioteca não é sólida.

Para esclarecer, a derivação é apenas unsafe , e não pode ser usada em combinação com um manual Drop impl. Ao usar a derivação, você promete não fazer nenhuma das coisas ruins listadas em uma implementação de Drop .

https://github.com/rust-lang/rust/pull/50497 muda a API de fixação, o mais importante é que renomeia Pin para PinMut para abrir espaço para adicionar um Pin compartilhado


Para esclarecer, o derivado é apenas inseguro e não pode ser usado em combinação com um implemento de queda manual.

Concordo, tornar inseguro dessa forma funcionaria. Embora o teste pareça que atualmente não está marcado como inseguro?

@RalfJung Certo, não acho que seja no momento. Outra opção que acabei de pensar seria fazer com que a versão "segura" criasse uma implementação de Drop para o tipo, bloqueando outros impls manuais de Drop . Pode haver um sinalizador unsafe para desativar isso. WDYT?

@cramertj sim, isso também deve funcionar. No entanto, teria efeitos colaterais, como dropck mais restritivo e não ser capaz de sair dos campos.

@pythonesque e eu

Concluímos que provavelmente o comportamento "correto" teria sido Drop se auto-definir. No entanto, fazer a transição para isso é assustador. Embora seja possível com uma mudança de edição de alguma forma, creio, isso seria extremamente perturbador.

Uma mudança compatível com versões anteriores é apenas torná-la de alguma forma incoerente para tipos que podem ser "projetados por pino" para implementar Drop . Em seu repositório, @pythonesque implementou isso gerando impls a partir de um conjunto complicado de características.

Pode-se imaginar um formulário integrado um pouco mais simples:

  • Um traço de marcador, vamos chamá-lo de PinProjection , controla se um tipo pode ou não ser projetado por pino. É incoerente (por meio da mágica embutida do compilador) implementar tanto PinProjection quanto Drop .
  • Outra característica, PinDrop , estende PinProjection para fornecer um destruidor alternativo para Drop .

Derivados para gerar métodos de projeção de pinos como os @pythonesque e eu mostramos também geram impls de PinProjection para o tipo.

@withoutboats

Concluímos que provavelmente o comportamento "correto" teria sido Queda se auto-avaliar. No entanto, fazer a transição para isso é assustador. Embora seja possível com uma mudança de edição de alguma forma, creio, isso seria extremamente perturbador.

Apenas me perguntando: SE quiséssemos fazer isso um dia, o que você está propondo é compatível com o futuro?

Por exemplo, digamos que decidimos ...

  • make Drop magicamente aceitar formulários fixados e não fixados, OU
  • reescrever as assinaturas de Drop para eles como parte da atualização automática para Rust 2021 :)

(Não estou propondo nada disso, mas parece difícil responder à pergunta sem exemplos concretos.)

@tmandry Essa é essencialmente a ideia.

O grande problema é uma implementação Drop genérica que move o parâmetro de tipo:

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

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

O tipo de atualização automática de que você está falando apenas quebraria esse implemento do Drop, que só é válido se adicionarmos um limite de T: Unpin .

Não conheço nenhum caso de uso válido para mover ?Unpin dados em um destruidor, então é apenas este caso, em que tecnicamente poderia, mas não deveria, isso é realmente um problema.

@withoutboats

Outra característica, PinDrop, estende PinProjection para fornecer um destruidor alternativo para Drop.

Por que PinDrop estende PinProjection ? Isso parece desnecessário.

Também pode ser um pouco mais inteligente dizendo que PinProjection e Drop são incompatíveis a menos que o tipo seja Unpin (ou encontrando alguma outra maneira de remover todas essas novas distinções / restrições para Unpin tipos).

@RalfJung , gostaríamos que PinDrop e Drop fossem mutuamente exclusivos de alguma forma. Em última análise, existem algumas maneiras de implementar isso com diferentes compensações; Acho que precisamos de um RFC para isso de qualquer maneira, porque não é uma mudança pequena.

@withoutboats acordado. Achei que a exclusividade poderia surgir de um tratamento especial de PinDrop , mas obviamente existem várias maneiras de fazer isso. Acho que seria muito útil se os tipos Unpin não se importassem; junto com a criação de todos os tipos de ponteiro Unpin incondicionalmente, isso provavelmente ajudará a reduzir o número de casos em que as pessoas precisam saber sobre isso.

@withoutboats Também vale a pena notar que as instâncias principais em que consigo pensar em que as pessoas querem, digamos, mover dados genéricos em um Vec em um destruidor (eu escolhi Vec porque as pessoas do IIRC gostariam de implementar métodos em Vec que realmente se preocupam com Pin , o que significa que não poderia implementar incondicionalmente Unpin ), é na verdade um &mut Vec<T> ou Vec<&mut T> ou algo assim. Eu trago isso principalmente porque esses são casos que provavelmente seriam enganados pelo lint sugerido por @RalfJung e, com sorte, as pessoas serão capazes de mover para PinDrop facilmente nesses casos, em vez de unsafe impl Unpin (teoricamente, eles sempre podem fazer o primeiro, é claro, delimitando T com Unpin no tipo, mas isso será uma alteração decisiva para os clientes da biblioteca).

Além disso, é importante notar que, embora isso adicione algumas características a mais do que gostaríamos, as características são aquelas que praticamente, nunca, nunca aparecerão na assinatura de tipo de ninguém. Em particular PinProjection é um limite completamente sem sentido, a menos que você esteja escrevendo uma macro #[derive(PinFields)] porque o compilador sempre (eu acredito) será capaz de determinar se PinProjection é válido, se puder descobrir quais são os campos no tipo, e a única coisa para a qual é útil é para projetar campos. Da mesma forma, PinDrop basicamente nunca deve precisar ser um limite para nada, pela mesma razão que Drop quase nunca é usado como um limite. Mesmo os objetos de características não devem ser afetados (a menos que algum dia tenhamos "campos associados", eu acho, mas com um novo recurso como esse poderíamos exigir que as características com campos associados fossem implementáveis ​​apenas em PinProjection tipos, o que resolveria perfeitamente Esse problema).

@RalfJung

Bem, acho que queremos fazer isso eventualmente. Como isso é um problema de compatibilidade?

Suponho que não seja mais do que adicionar um tipo que simplifica Send ou Sync , a diferença é que nenhum deles afeta a linguagem central na mesma medida que isso. Fazer isso de forma totalmente automática parece análogo a como Rust costumava tratar Copy (os tipos sempre eram Copy se contivessem apenas Copy tipos e não implementassem Drop ) que foi eventualmente alterado para torná-lo explícito porque, presumivelmente, as pessoas não gostaram do fato de que adicionar uma característica ( Drop ) poderia quebrar o código genérico sem eles perceberem (uma vez que todo o ponto de coerência é aquele adicional implementações de traços não devem quebrar as caixas downstream) Isso parece quase idêntico, apenas com PinProjection vez de Copy . Na verdade eu gostei do comportamento antigo, só acho que seria difícil justificar PinProjection trabalhando dessa maneira quando Copy não o faz.

Mas isso já seria ilegal. Você não deve violar as invariantes dos outros.

Sim, quanto mais penso sobre isso, menos plausível parece que pode ser um problema.

Acho que vai ser correto na maioria das vezes

Bem, sim, mas apenas porque a maioria dos tipos não implementa nenhum método que requeira Pin ou expõe seus campos genéricos como públicos. Embora seja improvável que o último mude, o anterior não é - espero que pelo menos alguns dos tipos stdlib que são atualmente !Unpin adicionem métodos de projeção de pinos, ponto em que a implementação de unsafe não não será mais válido. Portanto, parece uma coisa muito frágil para mim. Além disso, estou preocupado em aumentar a quantidade de código inseguro "clichê" que as pessoas precisam escrever; Acho que Send e Sync limites são unsafe impl d corretamente com a mesma frequência com que são implementados incorretamente, e Unpin seria ainda mais insidioso porque o geralmente a versão correta não tem limites em T . Portanto, parece amplamente preferível direcionar as pessoas para PinDrop (eu entendo por que você está desconfiado de fazer isso para tipos de ponteiro brutos. Só estou preocupado que o código já- unsafe irá é ainda mais provável fazer unsafe impl sem pensar, mas quanto mais penso sobre isso, mais parece que o padrão para ponteiros brutos está provavelmente correto e sinalizá-lo com seu lint seria útil).

Acho que seria muito útil se os tipos de Desafixar não precisassem se importar.

Eu meio que concordo, mas como eu disse, as pessoas não usarão PinProjection como um limite real, não tenho certeza de quanto isso importaria na prática. Já existe uma implementação de DerefMut para PinMut onde T: Unpin então você não obteria nenhum benefício com isso hoje. Uma regra implementada no compilador (para projeções) presumivelmente transformaria PinMut::new em uma espécie de &pin amanhã para Unpin tipos, mas isso não tem nada a ver com as projeções de campo, na verdade. E como derivar PinProjection não requer PinProjection para seus campos, você não precisaria dele apenas para satisfazer os limites de derive em outro tipo. Então, realmente, qual é o ganho? A única coisa que ele realmente deixaria você fazer é implementar Drop e derivar PinProjections ao mesmo tempo, mas sempre queremos que as pessoas implementem PinDrop se possível de qualquer maneira para que ser um OMI líquido negativo.

Eu escolhi o Vec porque o pessoal do IIRC gostaria de implementar métodos no Vec que realmente se preocupassem com o Pin, o que significa que ele não poderia implementar incondicionalmente o Unpin

Hm, acho que não gosto disso. Se Box é incondicionalmente Unpin , acho que Vec deve ser o mesmo. Os dois tipos geralmente são equivalentes em termos de propriedade.

Também devemos ter cuidado com a garantia de drop ; Vec::drain por exemplo, pode fazer com que as coisas vazem em caso de pânico.

Isso parece quase idêntico, apenas com PinProjection em vez de Copy

Oh, agora eu entendo sua pergunta. Na verdade, eu não estava falando sobre a derivação automática de PinProjections pois minha proposta não tinha uma característica como essa - mas, de fato, uma consequência da minha proposta seria que adicionar Drop é uma alteração significativa se você têm campos públicos.

A única coisa que ele realmente deixaria você fazer é implementar Drop e derivar PinProjections ao mesmo tempo, mas sempre queremos que as pessoas implementem PinDrop se possível de qualquer maneira, de modo que seria um IMO líquido negativo.

Esse era realmente o ponto. Quanto menos as pessoas precisarem se preocupar com todas essas coisas de fixação, melhor.

Pergunta de esclarecimento: em sua proposta e na de @withoutboats , PinDrop um item de idioma e tratado pelo compilador como um substituto para Drop , ou é apenas o traço usado por derive(PinProjections) para implementar Drop ?

Também temos que ter cuidado com a garantia de queda; Vec :: drenar, por exemplo, pode causar vazamento em caso de pânico.

Bem, isso é verdade, mas na verdade não desaloca nenhuma memória, correto? Para ser claro, eu realmente prefiro se Vec fez incondicionalmente implementar Unpin uma vez que tornaria mais fácil para as pessoas a mudar apenas para PinDrop , mas não foi definitivamente falar em algumas das discussões de fixação sobre como permitir a fixação de elementos de apoio. Depois de começar a falar sobre os campos que estão sendo fixados, fica claro que nem sempre você pode simplesmente transformar Vec em Box<[T]> no lugar e fixá-lo, então ele pode realmente ter algum valor em esse ponto (embora obviamente você também possa adicionar um tipo PinVec vez de um tipo Vec , como foi feito para Box ).

Esse era realmente o ponto. Quanto menos as pessoas precisarem se preocupar com todas essas coisas de fixação, melhor.

Hm, da minha perspectiva, isso seria verdade inicialmente, mas a longo prazo, gostaríamos de migrar o máximo de pessoas possível para um padrão de PinDrop , especialmente se o tipo realmente incomodasse #[derive(PinProjections)] (que, novamente, nem mesmo seria necessário para qualquer propósito se o tipo fosse Unpin --provavelmente só aconteceria em coisas como código gerado). Então (talvez depois de &pin já estar no idioma por um tempo) você poderia ter uma mudança de época que mudou Drop para DeprecatedDrop ou algo assim. Para os tipos Unpin apenas alterar a assinatura de tipo de &mut para &mut pin resolveria praticamente tudo, então talvez isso não seja realmente necessário.

Pergunta de esclarecimento: Na sua proposta e na de @withoutboats , o PinDrop é um item de idioma e tratado pelo compilador como um substituto para o Drop ou é apenas o traço usado pelo derivar (PinProjections) para implementar o Drop?

O antigo.

Toda essa discussão sobre Drop parece um descuido drástico em nome do RFC original. Seria valioso abrir um novo RFC para discutir isso? É um pouco difícil entender quais são as preocupações, quais preocupações são mais perniciosas do que outras, exatamente quanto mais máquinas precisaremos adicionar à linguagem do que pensávamos originalmente, quanta falha devemos esperar (e se pode ser atenuado pela edição) no pior e no melhor cenário, como podemos fazer a transição para qualquer coisa que suceda ao Drop e se poderíamos aproveitar tudo isso e ainda cumprir nossas metas para 2018 por outros meios (como https: //github.com/rust-lang/rfcs/pull/2418 sugere ocultar o Pin de todas as APIs públicas para não bloquear a estabilização).

@bstrie Acho que há apenas uma preocupação principal, mas é bem substancial. A RFC foi aprovada com o entendimento de que um eventual tipo &pin mut poderia, em algum momento, tornar esse tipo de código seguro:

struct Foo<T> { foo : T }

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

É importante que esse tipo de código seja seguro porque, na prática, as referências fixadas não são realmente combináveis ​​de outra forma em código seguro, então, por exemplo, a implementação de qualquer um dos combinadores de futuros exigiria o uso de unsafe . Na época, pensava-se que o uso de unsafe poderia ser suspenso na maioria dos casos e, portanto, não seria um grande problema.

Infelizmente, se isso é seguro depende da implementação de Foo Drop para Foo . Portanto, se quisermos oferecer suporte a projeções de campo seguras (seja por meio de um recurso de linguagem, um #[derive] ou qualquer outro mecanismo), temos que ser capazes de garantir que tais Drop implementações ruins possam ' não acontecer.

A alternativa que parece funcionar melhor permite que o código acima funcione sem o uso de unsafe (com apenas a adição de #[derive(PinProjections)] no topo da estrutura) e não quebra nenhum código existente . Ele pode ser adicionado de forma compatível com versões anteriores, mesmo se Pin já estiver estabilizado e pode até ser adicionado como uma biblioteca pura (com um impacto ergonômico severo). É compatível com macros #[derive] para gerar acessadores e um eventual tipo de referência nativa &pin . Embora exija a adição de pelo menos um novo traço (e possivelmente dois, dependendo de como a versão fixada de Drop é implementada), eles nunca precisam aparecer em qualquer lugar nas cláusulas where ou objetos de traço, e a nova versão de drop só precisa ser implementada para tipos que atualmente têm uma implementação Drop e desejam optar por projeções de pinos.

Só porque uma solução parece não ter muitas desvantagens, não significa que não seja uma mudança substancial, então acho que quase certamente criaremos um RFC para esta proposta ( @withoutboats e discutimos fazer isso na chamada) . Seria bom atacá-lo isoladamente, já que é totalmente compatível com versões anteriores. Pessoalmente, porém, acho que faz mais sentido avançar com essa mudança em vez de se livrar da fixação em outro lugar.

A preocupação da maioria das pessoas com PinMut em APIs públicas é precisamente que isso exigirá unsafe ou passar por Unpin limites em todos os lugares, e esta proposta resolve esse problema. As alternativas discutidas no ferrugem-lang / RFCs # 2418 parecem muito mais controversa, tanto na mecânica reais da maneira como ele quer evitar lidar com pinagem (que envolve a proliferação de várias outras características que irão aparecer em APIs públicas e documentação) e na complexidade geral da solução. Na verdade, mesmo se a fixação fosse completamente resolvida, acho que há uma série de questões que as pessoas acham que não foram resolvidas de forma adequada com essa RFC, então acho que há pelo menos uma chance decente de que uma RFC adicionando projeções de alfinetes seguras possa acabar sendo aceito antes disso.

É verdade que a fixação está em seus primeiros dias (e eu sei que reclamei que parecia que estava sendo estabilizada muito rapidamente), mas acredito que a falta de projeção de campo segura é a última coisa importante que desencoraja as pessoas de usá-la . Para fins de integridade, aqui estão todas as questões que vi as pessoas levantarem com a fixação, suas soluções propostas e se a solução é compatível com versões anteriores do tipo Pin (em uma ordem muito aproximada e tendenciosa de como controverso, percebo que o problema está no momento):

  • Este (podemos fazer com que os campos de projeção sejam feitos em código seguro?). A ser resolvido pela próxima RFC (onde as estratégias de implementação possíveis serão especificadas, bem como outras alternativas que consideramos e porque elas tiveram que ser descartadas). Todas as variações da resolução proposta são compatíveis com versões anteriores.
  • Deve fixar um valor do tipo !Unpin com um destruidor manual implicar em uma garantia adicional (que a memória de apoio do valor não seja invalidada até que o destruidor seja chamado), também conhecido como "mudanças para coleções intrusivas"?

    Ainda não resolvido, principalmente porque poderia quebrar a API de fixação de pilha proposta; se uma API stack pinning que preserva esta garantia pode ser feita para funcionar com async / await, a maioria das partes interessadas parece estar disposta a aceitar isso (alguém do IIRC já tentou resolver isso usando geradores, mas rustc ICE).

    Não compatível com as antigas garantias; exigir um código inseguro para impor a garantia por enquanto é compatível com os dois resultados possíveis, mas apenas se o código inseguro não puder contar com sua aplicação para correção. Portanto, isso certamente requer resolução de uma forma ou de outra antes que a fixação seja estabilizada.

    Felizmente, os usuários de fixação de Future s podem ignorar isso além do formato da API de fixação de pilha. A API de fixação de pilha baseada em fechamento é compatível com qualquer resolução, então Future usuários que não usam async / await podem usar a API baseada em fechamento hoje sem esperar que isso seja decidido. A decisão afeta mais as pessoas que desejam usar a fixação para outros fins (como coleções intrusivas).

  • Os ponteiros brutos devem ser incondicionalmente Unpin ? Ainda não resolvido (acho que fui o único que propôs isso e ainda estou quase 50-50 nisso). Não seria compatível com versões anteriores; Estou marcando isso como controverso principalmente por esse motivo.
  • Os tipos de biblioteca padrão como Vec devem ser feitos incondicionalmente Unpin , ou devem ter Pin acessores de campo adicionados? Ainda não resolvido e pode precisar de resolução caso a caso. Para qualquer tipo de wrapper seguro, adicionar os acessadores ou tornar o tipo incondicionalmente Unpin é compatível com versões anteriores.
  • PinMut::deref ser removido? A resposta basicamente parece ser "não", uma vez que as vantagens de mantê-lo parecem superar as desvantagens e parece haver soluções alternativas para os casos que originalmente fizeram as pessoas desejá-lo (especificamente, Pin<RefCell<T>> ). Alterá-lo seria incompatível com versões anteriores.
  • Como devemos fornecer acessores de campo no curto prazo (ignorando os problemas de queda)? Ainda não resolvido: duas opções apresentadas até agora são https://github.com/withoutboats/derive_pinned e https://github.com/pythonesque/pintrusive. A resolução é totalmente compatível com versões anteriores, uma vez que, após as alterações de eliminação, isso pode ser feito corretamente em uma macro puramente no código da biblioteca.
  • Como devemos fornecer acessores de campo em longo prazo (ou seja, deve haver tipos personalizados de &mut pin e &pin Rust? Como o novo empréstimo deve funcionar?). Ainda não resolvido, mas compatível com todas as outras alterações propostas (até onde sei) e pode obviamente ser punido indefinidamente.
  • Os tipos de referência seguros devem ser incondicionalmente Unpin ? Parece estar resolvido (sim, deveria). A resolução é totalmente compatível com versões anteriores.
  • Devemos ter um tipo Pin compartilhado além de um único Pin one, a fim de tornar os acessadores de campo viáveis ​​(não funciona com &Pin porque isso é uma referência a uma referência)? A resolução estava mudando Pin para PinMut e adicionando um tipo Pin para o caso compartilhado. Isso não era compatível com versões anteriores, mas a alteração de última hora ( Pin para PinMut ) já foi feita e tenho quase certeza de que já foi efetivamente aceita.

Acho que isso cobre tudo. Como você pode ver, todos eles (além dos indicadores brutos e da decisão final, a última das quais parece estar amplamente resolvida neste ponto) têm alguns caminhos compatíveis com versões anteriores, mesmo se a fixação estivesse estabilizada hoje; mais importante, o fato de podermos resolver projeções de campo significa que usar um tipo fixado em sua API não está destinado a ser uma decisão da qual você se arrependerá mais tarde.

Além das questões acima, surgiram outras propostas que visam repensar a fixação de uma forma muito mais fundamental. As duas que eu acho que entendo melhor são a proposta @comex de fazer !Unpin types !DynSized , e a proposta steven099 (em internos; desculpe, não sei o nome do Github) para tem um novo tipo de invólucro Pinned dimensionado que torna os internos imóveis (como um invólucro ZST).

A opção !DynSized é um recurso bastante conservador (no sentido de que Rust já tem um traço semelhante disponível) que tem a vantagem de já ser necessário para lidar com tipos opacos. Nesse sentido, pode ser ainda menos invasivo do que as alterações propostas para Drop . Também tem uma grande vantagem: resolve automaticamente o problema com Drop porque !Unpin tipos seriam !DynSized e, portanto, ninguém seria capaz de sair deles. Isso faria &mut T e &T funcionarem automaticamente como PinMut e Pin onde quer que T fosse !DynSized , então você não não precisa de uma proliferação de versões fixas de tipos e métodos que funcionam em &mut T muitas vezes podem ser feitos para funcionar normalmente (se eles foram alterados para não exigir um limite de DynSized quando não precisavam )

A principal desvantagem (além das preocupações usuais em torno de ?Trait ) parece ser que um tipo !Unpin nunca poderia ser movido, o que é bastante diferente da situação com tipos fixados atualmente. Isso significa que compor dois tipos fixados sem usar uma referência realmente não seria possível (pelo que posso dizer, de qualquer maneira) e não tenho certeza de como ou se há alguma proposta de resolução para isso; IIRC foi planejado para funcionar junto com uma proposta &move , mas não tenho certeza de qual é a semântica pretendida disso. Eu também não entendo (pela proposta) como você poderia ter projeções de campo seguras com ele, uma vez que depende de tipos opacos; parece que você teria que usar muito código inseguro em geral para usá-lo.

O tipo Pinned<T> dimensionado é um tanto semelhante em espírito, mas deseja contornar o problema acima permitindo que você envolva um tipo em um ZST que o torna imóvel (efetivamente atuando como não dimensionado). Rust não tem nada comparável no momento: PhantomData não inclui realmente uma instância do tipo, e os outros tipos de tamanho dinâmico geram indicadores de gordura e ainda permitem movimentos (usando APIs que dependem de size_of_val ; isso é o que ?DynSized pretendia consertar, portanto, esta proposta provavelmente pega carona nessa característica novamente). Não me parece que esta proposta realmente corrige o problema de Drop se você permitir projeções seguras e também parece incompatível com Deref , então para mim as vantagens sobre Pin não são tão claros.

se uma API de fixação de pilha que preserva esta garantia pode ser feita para funcionar com assíncrono / aguardar, a maioria das partes interessadas parece estar disposta a aceitar isso (alguém do IIRC já tentou resolver isso usando geradores, mas rustc ICE)

Para referência, isso provavelmente está se referindo a https://github.com/rust-lang/rust/issues/49537, que é desta tentativa de uma API de fixação de pilha baseada em gerador aninhado. Eu não tenho certeza se as vidas lá iriam funcionar mesmo se o ICE fosse resolvido.

Ainda não resolvido, principalmente porque poderia quebrar a API de fixação de pilha proposta; se uma API stack pinning que preserva esta garantia pode ser feita para funcionar com async / await, a maioria das partes interessadas parece estar disposta a aceitar isso (alguém do IIRC já tentou resolver isso usando geradores, mas rustc ICE).

Eu vejo a API de fixação de pilha baseada em fechamento como uma solução para isso, com uma avenida futura (uma vez que &pin é uma linguagem primitiva) para algo mais ergonômico e verificado pelo compilador. Existe também esta solução baseada em macro proposta por @cramertj.

Os ponteiros brutos devem ser liberados incondicionalmente? [...] Não seria compatível com versões anteriores; Estou marcando isso como controverso principalmente por esse motivo.

Por que você acha que adicionar impl Unpin for Vec<T> é compatível com versões anteriores, mas fazer o mesmo com ponteiros brutos não é?

A principal desvantagem (além das preocupações usuais em torno do? Trait) parece ser que um tipo! Unpin nunca poderia ser movido, o que é bastante diferente da situação com os tipos fixados atualmente. Isso significa que compor dois tipos fixados sem usar uma referência realmente não seria possível (pelo que posso dizer, de qualquer maneira) e não tenho certeza de como ou se há alguma proposta de resolução para isso; IIRC foi planejado para funcionar junto com uma proposta de movimento, mas não tenho certeza de qual é a semântica pretendida disso.

Bem, na proposta de variante de @ steven099 (que eu preferia), a maioria dos usuários usaria (por enquanto) um Pinned<T> , que contém T por valor, mas é !Sized ( e talvez !DynSized ; o design exato para a hierarquia de traços está aberto para bicicletas). Na verdade, isso acaba parecendo muito semelhante à proposta existente, exceto com &'a mut Pinned<T> representando Pin<'a, T> . Mas é mais combinável com o código atual e futuro que é genérico em &mut T (para T: ?Sized ), e é mais compatível com versões anteriores com um design futuro para verdadeiros tipos nativos imóveis que não precisariam use Pinned .

Para entrar em mais detalhes, Pinned poderia ter a seguinte aparência:

extern { type MakeMeUnsized; }

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

Você geralmente não construiria um Pinned<T> diretamente. Em vez disso, você seria capaz de lançar de Foo<T> para Foo<Pinned<T>> , onde Foo é qualquer ponteiro inteligente que garante que não moverá seu conteúdo:

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

(Também pode haver uma API de fixação de pilha, provavelmente baseada em uma macro, mas é um pouco mais complicada de implementar.)

Neste exemplo, FakeGenerator representa um tipo de gerador gerado pelo compilador:

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

Uma vez que um FakeGenerator é fixado, o código do usuário não deve mais ser capaz de acessá-lo diretamente por valor ( foo: FakeGenerator ) ou mesmo por referência ( foo: &mut FakeGenerator ), já que o último faria permitir o uso de swap ou replace . Em vez disso, o código do usuário trabalha diretamente com, por exemplo, &mut Pinned<FakeGenerator> . Novamente, isso é muito semelhante às regras para PinMut<'a, FakeGenerator> da proposta existente. Mas como um exemplo de melhor composição, o impl gerado pelo compilador pode usar o traço Iterator , em vez de precisar de um novo que use Pin<Self> :

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

Por outro lado, para a construção inicial, podemos distribuir FakeGenerator valores diretamente e permitir que eles sejam movidos, desde que garantamos que apenas os estados de não auto-empréstimo estão acessíveis antes de serem fixados:

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

Portanto, se quisermos compor dois FakeGenerator s por valor, a construção é direta:

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

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

Então, podemos fixar o objeto TwoGenerators como um todo:

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

Mas, como você mencionou, precisamos de projeções de campo: uma maneira de ir de &mut Pinned<TwoGenerators> para &mut Pinned<FakeGenerator> (acessando a ou b ). Aqui também, isso acaba parecendo muito semelhante ao design existente de Pin . Por enquanto, usaríamos uma macro para gerar acessadores:

// Some helper methods:
impl<T> Pinned<T> {
    // Only call this if you can promise not to call swap/replace/etc.:
    unsafe fn unpin_mut(&mut self) -> &mut T {
        &mut self.val
    }
    // Only call this if you promise not to call swap/replace/etc. on the
    // *input*, after the borrow is over.
    unsafe fn with_mut(ptr: &mut T) -> &mut Pinned<T> {
        &mut *(ptr as *mut T as *mut Pinned<T>)
    }
}

// These accessors would be macro-generated:
impl Pinned<TwoGenerators> {
    fn a(&mut self) -> &mut Pinned<FakeGenerator> {
        unsafe { Pinned::with_mut(&mut self.unpin_mut().a) }
    }
    fn b(&mut self) -> &mut Pinned<FakeGenerator> {
        unsafe { Pinned::with_mut(&mut self.unpin_mut().b) }
    }
}

No entanto, assim como o design existente, a macro precisaria impedir o usuário de implementar Drop para TwoGenerators . Por outro lado, o ideal é que o compilador nos permita impl Drop for Pinned<TwoGenerators> . Atualmente, ele rejeita isso com um erro de que "Implementações de Drop não podem ser especializadas", mas isso poderia ser alterado. IMO, isso seria um pouco melhor do que PinDrop , já que não precisaríamos de um novo traço.

Agora, como uma extensão futura, em princípio, o compilador poderia suportar o uso de sintaxe de campo nativo para ir de Pinned<Struct> para Pinned<Field> , semelhante à sugestão sob o design existente de que o compilador poderia adicionar um &pin T nativo

No entanto, meu "fim de jogo" ideal não é isso, mas algo mais dramático (e mais complexo de implementar) onde o compilador um dia suportaria tipos imóveis "nativos", incluindo vidas existenciais. Algo como:

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

Esses tipos se comportariam como Pinned<T> , sendo !Sized etc. [1] No entanto, ao contrário de Pinned , eles não exigiriam um estado móvel inicial. Em vez disso, eles trabalhariam com base na "eliminação de cópia garantida", em que o compilador permitiria o uso de tipos imóveis em valores de retorno de função, literais de estrutura e vários outros lugares, mas garantiria que, nos bastidores, eles seriam construídos no lugar. Portanto, não apenas poderíamos escrever:

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

(o que você já pode meio que fazer ) ... também poderíamos escrever coisas como:

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

ou

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

Obviamente, o texto acima é apenas um esboço muito aproximado de como o recurso seria; a sintaxe que usei tem problemas e a sintaxe seria a menor das muitas complexidades envolvidas no projeto do recurso. (Dito isso, eu pensei sobre isso o suficiente para ter certeza de que as complexidades podem ser resolvidas - que não é apenas uma ideia incoerente que não pode funcionar.)

Estou apenas mencionando isso como parte da motivação, de volta ao presente, para usar um design !DynSized -ish em vez do design Pin . Acho que &'a Pinned<T> já é melhor que Pin<'a, T> porque evita uma explosão combinatória de tipos de ponteiros. Mas herda alguns dos mesmos problemas:

  1. Fundamentalmente, não suporta tipos que não tenham um estado móvel inicial e
  2. É barulhento, confuso (a diferença entre referências fixas e não fixadas ao mesmo tipo é difícil de explicar) e faz com que os tipos imóveis pareçam de segunda classe. Quero que os tipos imóveis e autorreferenciais se sintam de primeira classe - tanto porque são inerentemente úteis, quanto para fazer Rust parecer melhor para pessoas que vêm de outras línguas, onde criar valores autorreferenciais é fácil e comum.

No futuro, estou imaginando que os tipos imóveis nativos resolveriam os dois problemas, e a maioria dos códigos nunca precisaria usar a palavra "pin". (Embora Pinned ainda possa ter alguns casos de uso, para casos em que a elisão de cópia não é boa o suficiente e você realmente precisa de um estado móvel inicial.) Em contraste, Pin assenta o conceito de um estado separado "ainda não fixado" no design de cada característica que o usa. E um &pin embutido o transformaria na linguagem.

De qualquer forma, aqui está um link de playground para o design de Pinned acima.

[1] ... embora possamos querer uma nova característica ReallySized (com um nome menos bobo), para tipos que são estaticamente dimensionados, mas podem ou não ser móveis. O fato é que haverá alguma rotatividade aqui de qualquer maneira, porque com suporte para rvalues ​​não dimensionados, muitas funções que atualmente recebem argumentos Sized poderiam funcionar tão bem com argumentos não dimensionados, mas móveis. Estaremos mudando os limites das funções existentes e recomendações sobre quais limites usar para funções futuras. Eu afirmo que pode até valer a pena ter uma edição futura alterando o limite padrão (implícito) para os genéricos de função, embora isso tenha algumas desvantagens.

[editar: exemplo de código fixo]

@RalfJung

Eu vejo a API de fixação de pilha baseada em fechamento como uma solução para isso, com uma avenida futura (uma vez e pin é uma linguagem primitiva) para algo mais ergonômico e verificado pelo compilador. Existe também esta solução baseada em macro proposta por @cramertj.

O baseado em encerramento não funciona corretamente com async / await, acredito, porque você não pode render de dentro de um encerramento. O baseado em macro é interessante, no entanto; se for realmente seguro, é muito engenhoso. Não achei que pudesse funcionar no início, porque um pânico durante uma das quedas no escopo poderia levar ao vazamento dos outros, mas aparentemente isso foi corrigido com o MIR?

Eu também não tinha certeza sobre a interação entre a garantia "destruidores rodam antes que a memória seja desalocada" e a capacidade de projetar campos fixos; se a queda de nível superior entrasse em pânico, pensei que os campos fixados projetados no valor não teriam suas quedas executadas. No entanto, no playground de Rust, realmente parece que os campos do tipo têm suas quedas funcionando de qualquer maneira, mesmo após o pânico de tipo de nível superior, o que é bastante emocionante! Essa garantia está realmente documentada em algum lugar? Parece necessário se o stack pinning funcionar com projeções de pin (que, ou algo mais pesado, como PinDrop sempre abortando em pânico, o que parece indesejável, pois introduziria uma diferença de funcionalidade entre Drop e PinDrop).

Por que você acha que adicionar um implante Unpin para Vecé compatível com versões anteriores, mas fazer o mesmo para ponteiros brutos não é?

Entendo seu ponto: alguém pode estar contando com a implementação automática !Unpin de um campo Vec<T> e implementando seus próprios acessores que presumiram que a fixação era transitiva para algum !Unpin T . Embora seja verdade que PinMut<Vec<T>> não fornece nenhuma maneira segura de obter PinMut<T> , o código inseguro ainda pode explorar PinMut::deref para obter indicadores brutos e fazer suposições sobre os ponteiros estão estáveis. Portanto, acho que esta é outra situação em que é compatível com versões anteriores apenas se o código inseguro não depender de !Unpin sendo transitivo por meio de Vec (ou qualquer outro). No entanto, esse tipo de forte confiança no raciocínio negativo parece suspeito para mim de qualquer maneira; se você quiser ter certeza de que está !Unpin onde T não é, você pode sempre adicionar um PhantomData<T> (acho que este argumento também se aplica a ponteiros brutos). Uma declaração geral como "é UB assumir que os tipos na biblioteca padrão são! Desafixar em código inseguro, independentemente de seus parâmetros de tipo, a menos que o tipo explicitamente desative ou sua documentação declare que isso é confiável" provavelmente seria suficiente.

O baseado em macro é interessante, no entanto; se for realmente seguro, é muito engenhoso. Não achei que pudesse funcionar no início, porque um pânico durante uma das quedas no escopo poderia levar ao vazamento dos outros, mas aparentemente isso foi corrigido com o MIR?

Seria um bug no MIR se o pânico durante a queda levasse a pular a queda das variáveis ​​locais restantes. Isso deve apenas mudar de "queda normal" para "queda desenrolada". Outro pânico irá abortar o programa.

@RalfJung

Seria um bug no MIR se o pânico durante a queda levasse a pular a queda das variáveis ​​locais restantes. Isso deve apenas mudar de "queda normal" para "queda desenrolada". Outro pânico irá abortar o programa.

Os outros campos da estrutura que estão sendo eliminados são considerados variáveis ​​locais neste contexto? Isso não está claro para mim em qualquer documentação voltada para o usuário (na verdade, isso é verdade para toda a garantia da qual você está falando - eu só descobri que era realmente considerado um bug que precisava ser corrigido no rastreador de problemas).

@comex

O problema de Pinned (o que você está propondo) é que, se quiséssemos implementá-lo (uma vez que o Rust tivesse todos os recursos no lugar), não teríamos que fazer muito mais do que isso para torná-lo compatível com versões anteriores com o código existente:

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

(Acho que ter uma implementação de deref de Pin para Pinned também foi proposta). É instrutivo observar os lugares onde parece que isso não funcionaria. Por exemplo:

impl Drop for Pinned<TwoGenerators>

não funciona (pelo menos, não diretamente) com PinMut . Mesmo se assumirmos que isso substitui o Drop pelo próprio TwoGenerators quando o tipo Pinned é construído (não tenho certeza de como isso funcionaria?), Rust ainda não sabe como chamar a versão Pinned do construtor para quaisquer campos que foram projetados, uma vez que os campos seriam mantidos apenas por valor. Se a versão fixada de um destruidor fosse sempre usada se existisse, isso é efetivamente idêntico a PinDrop , apenas com uma sintaxe estranha, e não vejo como é melhor.

No entanto, ficamos tentados a considerar a escolha de um destruidor analisando recursivamente se um valor estava enraizado em Pinned . Observe que, se quisermos permitir objetos de traço por valor, não podemos necessariamente confiar em sermos capazes de decidir se vamos usar o drop Pinned<T> ou o drop T em tempo de compilação; Suponho que você esteja pensando que poderia haver uma entrada separada na vtable para a versão Pinned nesses casos? Essa ideia é meio intrigante para mim. Definitivamente, requer suporte de compilador substancial (muito mais do que o PinDrop proposto), mas pode ser no geral melhor em alguns aspectos.

No entanto, relendo o tópico, lembro-me de que há outros problemas: a implementação deref para tipos fixados realmente não funciona bem para Pinned<T> (suspeito que isso seja porque a implementação de deref on PinMut está logicamente errado de alguma forma, e é por isso que continua causando problemas, mas é realmente difícil justificar perdê-lo devido à sua conveniência - a menos que você crie um monte de tipos incondicionalmente Unpin qualquer maneira). Especificamente, acho o exemplo RefCell bastante preocupante, pois na presença de Pinned::deref isso significa que não seríamos nem mesmo capazes de forçar a fixação dinamicamente com o tipo existente (não sei se a especialização bastasse). Isso sugere ainda que se mantivermos a implementação deref , teremos que terminar duplicando a superfície da API quase tanto com Pinned quanto fazemos com Pin ; e se não o mantivermos, Pinned<T> se tornará incrivelmente difícil de usar. Também não parece que Box<Pinned<T>> funcionaria a menos que adicionássemos ?Move separadamente de ?DynSized (como apontado no tópico).

Tudo isso pode ser uma boa ideia, mas no contexto do Rust atual a coisa toda parece meio desagradável para mim, especialmente quando você considera que basicamente nenhum método no Rust atual teria limites de ?Move (significando a falta de deref realmente machucaria, a menos que o tipo fosse Unpin , caso em que Pinned não oferece vantagens). Eu suspeito que ele está modelando o que realmente está acontecendo mais de perto, tornando a fixação de uma propriedade proprietária, o que torna mais difícil tomar decisões ad hoc como PinMut::deref e torna a interface muito mais agradável (subjetivamente, pelo menos) , mas adiciona um monte de maquinário de linguagem para fazer isso e não parece que você pense que algum deles seja particularmente útil (em comparação com os tipos imóveis nativos que você está propondo). Eu também não sei se o que obtivemos que não conseguimos obter completamente com compatibilidade reversa (principalmente, chamando uma implementação de drop diferente dependendo do status do pino) é realmente útil, mesmo se puder ser feito (talvez você possa salvar um ramificação no destruidor às vezes se você souber que o tipo foi fixado?). Portanto, não tenho certeza se vale a pena alterar a proposta PinMut neste momento. Mas talvez eu esteja perdendo algum caso de uso concreto realmente convincente.

O problema de Pinned (o que você está propondo) é que, se quiséssemos implementá-lo (uma vez que o Rust tivesse todos os recursos no lugar), não teríamos que fazer muito mais do que isso para torná-lo compatível com versões anteriores com o código existente:

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

Primeiro de tudo, Pinned si exigiria suporte mínimo ou nenhum do compilador, se é isso que você quer dizer com "recursos". Se você quer dizer design de biblioteca, como DynSized e características relacionadas, isso é válido, mas ...

O que você sugeriu não seria compatível com versões anteriores, já que, por exemplo, você pode tentar implementar o mesmo traço para Pin<'a, T> e &'a T , o que de repente se tornaria conflitante.

De maneira mais geral, há uma diferença significativa nos designs de API. Com Pin , os traços que se espera que sejam implementados por tipos imóveis devem ter seus métodos assumindo PinMut<Self> , funções genéricas que desejam fazer referências a tipos imóveis devem ser semelhantes a fn foo<T>(p: PinMut<T>) , etc. Por outro lado, o design Pinned evita a necessidade de novas características na maioria dos casos, já que você pode implantar características por Pinned<MyStruct> . Portanto:

  1. Tipos imóveis seriam incompatíveis com características existentes cujos métodos levam &self ou &mut self : por exemplo, geradores não poderiam implantar Iterator . Portanto, precisaríamos de um monte de novas características que sejam equivalentes às existentes, mas que usem PinMut<Self> . Se mudássemos o curso e fizéssemos de PinMut<T> um apelido para &mut Pinned<T> , poderíamos voltar e desaprovar todas essas características duplicadas, mas isso seria muito bobo. Melhor não precisar das duplicatas em primeiro lugar.

  2. Por outro lado, características recém-projetadas ou específicas do gerador provavelmente tomariam PinMut<Self> como a única opção, ao custo de adicionar ruído para tipos que desejam implementar as características, mas não são imóveis e não precisam para ser fixado. (Especificamente, os chamadores teriam que chamar PinMut::new para ir de &mut self para PinMut<Self> , assumindo Self: Unpin .) Mesmo se Pin<T> se tornasse um alias para &mut Pinned<T> , não haveria como se livrar desse ruído. E os futuros tipos imóveis nativos que estou imaginando estariam na mesma situação que os tipos móveis, precisando desnecessariamente ser embrulhados em Pinned quando eles são sempre considerados fixados.

Vou responder ao resto de sua postagem em uma segunda postagem.

Sobre Drop

Estou um pouco confuso com o que você está dizendo sobre Drop , mas na medida em que você está tentando torná-lo compatível com PinMut , não vou pensar nisso porque não acho que seja uma boa abordagem.

Acho que a melhor abordagem é que, se você tiver uma estrutura como TwoGenerators , terá duas opções:

  1. Nenhum manual Drop impl para TwoGenerators ou Pinned<TwoGenerators> ;
  2. Você implanta Drop por Pinned<TwoGenerators> ; enquanto isso, a mesma macro que fornece acessores irá gerar um Drop impl para TwoGenerators que simplesmente se auto-converte em &mut Pinned<TwoGenerators> e o descarta. (Isso é seguro: o invariante necessário para lançar &mut T para &mut Pinned<T> é que você não moverá T após o término do empréstimo, e no caso de Drop , você tem o último empréstimo que será criado para esse valor.)

A única razão para ter duas opções é que, conforme mencionado anteriormente, você pode não querer que sua estrutura implemente Drop porque as estruturas que não as implementam são tratadas de forma mais flexível pelo verificador de empréstimo.

Não vejo por que você deseja ter um destruidor separado real para o estado fixado versus não fixado, portanto, não há necessidade de pregar peças com vtables para diferenciá-los.

Em relação a RefCell

Não acho que Pinned::deref deva existir. Os acessores de campo gerados por macro seguros devem ser suficientes; Não vejo como isso é "incrivelmente difícil de usar". É um pouco menos agradável do que poder usar a sintaxe de campo nativo, mas algum dia isso será corrigido por estruturas imóveis nativas. De qualquer forma, se for difícil de usar, o mesmo problema se aplica a Pin .

Isso evita o problema com RefCell .

especialmente quando você considera que basicamente nenhum método no Rust atual teria limites de ?Move (o que significa que a falta de um deref realmente prejudicaria [..])

Pelo contrário, tudo o que tem um limite ?Sized é implicitamente ?Move .

Isso faz sentido porque, em geral, é impossível para o código com um limite ?Sized assumir a mobilidade. A única exceção é o código inseguro que chama size_of_val e então memcpy's tantos bytes, é por isso que precisamos do hack onde size_of_val entraria em pânico para tipos imóveis (e seria obsoleto em favor de uma nova função com um limite adequado).

Estou um pouco confuso com o que você está dizendo sobre o Drop, mas na medida em que você está tentando torná-lo compatível com as versões anteriores do PinMut, não vou pensar nisso porque não acho que seja uma boa abordagem .

Eu estava dizendo que se algo não é compatível com PinMut , deve haver um bom motivo para isso. No entanto, o que você está propondo é funcionalmente idêntico a PinDrop em todos os detalhes, exceto que você deseja implementá-lo em Pinned<T> (que não funciona no Rust atual). Pessoalmente, acho que a especialização de Drop estabelece um precedente realmente duvidoso e quase certamente indesejável por motivos que nada têm a ver com fixação, então não considero isso nenhum tipo de vantagem intrínseca. Em qualquer caso, acho que PinDrop pode ser principalmente separado do resto de sua proposta.

De qualquer forma, se for difícil de usar, o mesmo problema se aplica ao Pin.

Claro, e se estivéssemos dispostos a nos livrar de PinMut::deref , também seria uma boa composição com tipos como RefCell ; a diferença é que ainda podemos remendar uma solução com PinMut enquanto apoiamos deref , que parece não funcionar com Pinned . Se fôssemos nos livrar da implementação de deref , acho que seria muito mais provável que concordássemos que Pinned fornece uma vantagem significativa.

Mas realmente não tenho certeza se concordo que não ter deref é apenas um pequeno problema na prática: por exemplo, significa que em seu estado atual você não pode fazer nada com &Pinned<Vec<T>> onde T: !Unpin , e o mesmo se aplica a basicamente todos os tipos de biblioteca existentes. Este é um problema decorrente da maneira Unpin funciona, não do tipo específico de referência que você tem. O ecossistema teria que decidir coletivamente fazer coisas ao longo das linhas de impl Deref for Pinned<Vec<T>> { type Target = Pinned<[T]>; } ou algo assim, o que eu acho que concordo seria preferível a impl PinDeref<Vec<T>> se pudesse ser feito para funcionar, mas isso está em um mundo sem deref . No mundo com deref , quase todas as bibliotecas podem se safar sem nenhum acessador relacionado a pinos e ainda têm um suporte decente para !Unpin tipos.

Pelo contrário, tudo o que tem um? Limite de tamanho está implicitamente? Mova.

Sim, é um bom ponto. Infelizmente, um monte de código Rust não funciona com tipos com um limite !Sized , porque não é o padrão, mas pelo menos parte dele funciona. Não acho que seja uma vantagem tão convincente porque a maioria das coisas que posso fazer em valores não dimensionados são chamar métodos & ou &mut neles (por exemplo, para fatias ou objetos de traço), nenhum dos o que eu poderia fazer em sua proposta (exceto para Unpin tipos), já que você não quer Pinned::deref . Talvez os casos comuns pudessem ser tratados fazendo com que #[derive] implementações também gerassem instâncias distintas para Pinned<T> ou algo assim?

Drop

Pessoalmente, acho que a especialização em Drop estabelece um precedente realmente duvidoso e quase certamente indesejável por motivos que não têm nada a ver com a fixação, então não considero isso nenhum tipo de vantagem intrínseca. Em qualquer caso, acho que o PinDrop pode ser separado do resto da sua proposta.

Concordo que isso é separável, mas não acho que seja duvidoso. Pelo menos ... o que você disse está certo, é uma forma de especialização. Não é literalmente especializar alguns implantes de cobertor pai de Drop , mas a cola gerada pelo compilador está fazendo o equivalente à especialização chamando Drop apenas se estiver implementada. Uma implementação de 'userland' ficaria assim (ignorando o fato de que você não pode chamar manualmente drop ):

trait DropIfImplemented {
    fn maybe_drop(&mut self);
}
impl<T: ?Sized> DropIfImplemented for T {
    default fn maybe_drop(&mut self) {}
}
impl<T: ?Sized + Drop> DropIfImplemented for T {
    fn maybe_drop(&mut self) { self.drop() }
}

Então, eu imagino que a razão pela qual você não pode escrever impls Drop 'especializados' é a mesma razão pela qual a especialização em si não é válida: incoerência entre trans (que apaga parâmetros vitalícios) e typeck (que não faz). Em outras palavras, se pudéssemos escrever, digamos, impl Drop for Foo<'static> , ele na verdade seria chamado para qualquer Foo<'a> , não apenas Foo<'static> , porque codegen assume que os dois tipos são idênticos.

A boa notícia é que, como você provavelmente sabe, tem havido tentativas de encontrar uma maneira de limitar os impls especializados de forma que eles não possam criar esse tipo de incoerência. E espera-se que a especialização venha a ser lançada com algum desses limites. Depois que isso acontecer, não vejo motivo para não podermos aplicar as mesmas regras a Drop impls - e, para tornar a linguagem o mais consistente possível, devemos.

Agora, não queremos bloquear a fixação de especialização. No entanto, eu alego que permitir impl Drop for Pinned<MyStruct> - ou mais geralmente, permitir impl<params> Drop for Pinned<MyStruct<params>> sob as mesmas condições que o compilador permite atualmente impl<params> Drop for MyStruct<params> - é garantido como um subconjunto de qual especialização será permitir, então se fizermos um caso especial para isso hoje, ele acabará desaparecendo em uma regra mais geral.

Mas, novamente, isso é separável; se as pessoas não gostarem disso, poderíamos ter uma característica separada.

Unpin

Mas eu realmente não tenho certeza se concordo que não ter deref é apenas um pequeno problema na prática: por exemplo, significa que em seu estado atual você não pode fazer nada com &Pinned<Vec<T>> onde T: !Unpin , e o mesmo se aplica a basicamente todos os tipos de biblioteca existentes. Este é um problema decorrente da maneira Unpin funciona, não do tipo específico de referência que você tem.

Er ... ok, deixe-me corrigir minha declaração. Pinned::deref deve existir, mas deve estar limitado a Unpin - embora eu vá chamá-lo de Move .

A única razão pela qual Deref impl para PinMut causa problemas com RefCell é que (ao contrário de DerefMut impl) ele não está limitado a Unpin . E a razão para não ter o limite é o desejo de permitir que os usuários obtenham &MyImmovableType , permitindo que tipos imóveis implementem características com métodos que levam &self , para serem passados ​​para funções genéricas que levam &T , etc. Isso é fundamentalmente impossível para &mut self , mas funciona principalmente com &self porque você não pode sair dele com mem::swap ou mem::replace - isto é, exceto quando você usa RefCell . Mas, prossegue o raciocínio, a compatibilidade com referências existentes é valiosa o suficiente para que deva ser suportada, mesmo que a limitação de referências imutáveis ​​pareça arbitrária, mesmo que cause erros.

Com Pinned , podemos suportar referências imutáveis ​​e mutáveis: você apenas implanta suas características em Pinned<MyStruct> vez de diretamente em MyStruct . A desvantagem é que ele não é compatível com características ou funções que usam &T mas separadamente têm um limite de Self: Sized ; mas esses são relativamente raros e frequentemente não intencionais.

Curiosamente, Pinned si realmente não exige que Unpin exista. Afinal, por que alguém criaria realmente um &Pinned<Vec<T>> ? Com PinMut , várias características tomariam PinMut<Self> , então mesmo impls dessas características para tipos móveis teriam que receber PinMut . Com Pinned , como eu disse, as características continuariam a receber &self ou &mut self , e você as implementaria por Pinned<MyStruct> . Se você quiser implantar o mesmo traço para Vec<T> , Pinned não precisa entrar em ação.

No entanto, uma fonte potencial seria a macro de acessador de campo. Se você tem

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

então o design mais simples sempre geraria acessores usando Pinned :

impl Pinned<SomePinnable> {
    fn gen(&self) -> &Pinned<FakeGenerator> { … }
    fn gen_mut(&mut self) -> &mut Pinned<FakeGenerator> { … }
    fn also_a_vec(&self) -> &Pinned<Vec<Foo>> { … }
    fn also_a_vec_mut(&mut self) -> &mut Pinned<Vec<Foo>> { … }
}

… E depende de você lidar com os Pinned se não quiser. Na verdade, acho que isso é bom, porque Unpin / Move realmente deveria existir, veja abaixo. Mas, se não existisse, uma alternativa seria ter uma maneira de aceitar em uma base por campo para receber uma referência direta em vez de Pinned um. Ou seja, você teria

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

Não seria aconselhável ter acessores Pinned e não Pinned , mas qualquer um por si só deve funcionar.

… Mas sim, precisamos ter um traço Move , mas não por Pinned . Por exemplo, seria parte do limite para a nova versão de size_of_val (correção: não seria, mas seria esperado que o código inseguro o verificasse antes de tentar memcpy tipos arbitrários com base no resultado de size_of_val ); no futuro com rvalues ​​não dimensionados, será o limite (relaxado de Sized ) para ptr::read e mem::swap e mem::replace ; se algum dia conseguirmos &move , seria o limite para permitir que você, bem, saia de um; e algo semelhante se aplica à eliminação de cópia garantida.

Portanto, enquanto tivermos a característica, não há razão para não ter Pinned::deref (e deref_mut ) com um limite de T: Move .

[editar: como pythonesque me lembrou, isso tem uma semântica diferente de Unpin , então deixa pra lá.]

(E então tipos como Vec e Box irão querer implantar manualmente Move para que seja aplicado independentemente de o tipo de elemento ser Move .)

Er ... ok, deixe-me corrigir minha declaração. Pinned :: deref deve existir, mas deve ser limitado em Unpin - embora eu vá chamá-lo de Mover.

Ok, espere. É ?Move ou Move ? O primeiro significaria que os tipos !Unpin nem podem ser construídos em muitos casos; o último me faz pensar como, exatamente, estamos nos referindo a tipos como Pinned<T> (já que ?DynSized não é realmente o limite certo para eles). Eu certamente espero que eles não sejam a mesma coisa - caso contrário, tratar Move como Unpin mais uma vez faz exatamente o que estamos tentando evitar e torna o tipo imóvel no momento em que é construído.

A desvantagem é que não é compatível com características ou funções que aceitam & T, mas separadamente têm um limite Self: Sized; mas esses são relativamente raros e frequentemente não intencionais.

Existe uma desvantagem prática muito mais significativa, que é que poucos desses traços ou funções realmente funcionam com & fixadoshoje. Eles poderiam ser feitos para funcionar com ele, mas isso exigiria um grande número de implementações de características extras e (como eu disse) provavelmente uma revisão significativa das implementações de #[derive] . Este também é um custo que teria que ser pago por coisas novas - você teria que implementar tudo por &Pinned<Self> também se quisesse que funcionasse em !Unpin tipos. Esta é uma situação (muito) melhor para características que levam &mut self que com PinMut , mas pior para &self , que é (eu suspeito) muito mais comum. Por isso digo que acho que esta é a solução mais correta (no sentido de que se não tivéssemos muitas bibliotecas Rust existentes, a versão Pinned seria melhor), mas possivelmente não a mais utilizável.

Com Fixado, como eu disse, os traços continuariam a assumir & self ou & mut self, e você os implantaria para Fixado

Reimplementar todas as características na superfície da API de Vec , apenas com Pinned desta vez, não parece muito bom para mim (especialmente porque algumas das características nem funcionam com isso). Tenho certeza de implementar seletivamente Deref caso a caso (permitindo que &Pinned<Vec<T>> vá para &[Pinned<T>] , por exemplo) ou apenas permitindo Vec inteiro Unpin (e não permitindo a projeção do pino), é muito mais lógico. Em qualquer caso, ambos são mais trabalhosos do que não fazer absolutamente nada, e teria que ser replicado em um monte de tipos e características existentes para que coisas imóveis funcionassem com eles.

Eu poderia ser convencido do contrário - eu realmente gosto da solução Pinned mais quanto mais penso sobre ela - mas eu simplesmente não sei onde todas essas novas implementações de características em Pinned<T> estão realmente vai vir de; parece-me mais provável que as pessoas simplesmente não se importem em implementá-los.

Curiosamente, o Pinned em si realmente não exige que o Unpin exista. Afinal, por que alguém realmente criaria um & marcado>?

Existem boas razões para querer fazer isso (como: seu tipo fixado contém Vec ). Coleções intrusivas encontrarão esse tipo de cenário com muita frequência. Acho que qualquer proposta baseada na ideia de que as pessoas nunca vão querer Pinned referências a contêineres existentes, ou que você tem que optar por fazer Unpin funcionar, provavelmente não funcionará bem. Não ser capaz de basicamente optar pelo ecossistema regular de Rust adicionando um limite de Unpin seria incrivelmente perturbador (na verdade, quase todos os casos de uso que tenho para tipos imóveis se tornariam significativamente mais difíceis).

Com PinMut, várias características levariam PinMut, então mesmo impls dessas características para tipos móveis teriam que receber um PinMut.

Certo! A grande vantagem da versão Pinned é que você não precisa de traços distintos para referências mutáveis ​​fixadas. No entanto, é pior ou neutro do que PinMut com deref para quase todos os outros cenários.

Não seria correto ter acessores fixados e não fixados, mas qualquer um por si só deve funcionar.

Acessores manuais que requerem código inseguro para implementação parecem uma má ideia para mim; Não vejo como tal proposta permitiria a geração de acessores segura (como você impede alguém de fornecer acessores não fixados sem obrigá-los a escrever unsafe para afirmar que não o farão?). No entanto, como você pode notar, usar Move (assumindo que realmente significa Unpin ) funcionará bem.

Portanto, enquanto tivermos o traço, não há razão para não ter Pinned :: deref (e deref_mut) com um limite T: Move.

Certo. Estou falando especificamente sobre os tipos !Unpin aqui. Unpin tipos PinMut , então eles não são tão relevantes da minha perspectiva. No entanto, Unpin (ou Move ) limites de genéricos não são agradáveis ​​e idealmente você deve ser capaz de evitá-los sempre que possível. Novamente, como eu disse antes: Unpin e tudo o que está implícito em !Sized não são os mesmos e não acho que você possa tratá-los como sendo iguais.

… Mas sim, precisamos ter uma característica Move, não apenas para Fixado. Por exemplo, seria parte do limite para a nova versão de size_of_val; no futuro com rvalues ​​não dimensionados, será o limite (relaxado de Sized) para ptr :: read e mem :: swap e mem :: replace; se alguma vez conseguirmos e nos movermos, será o limite para permitir que você, bem, saia de um; e algo semelhante se aplica à eliminação de cópia garantida.

Acho que isso está mais uma vez combinando !Unpin (que diz que um tipo pode ter um invariante de fixação não padrão) e !DynSized -like !Move ; eles não podem realmente ser os mesmos sem causar o comportamento de congelamento indesejado.

Opa, você está completamente certo. Unpin não pode ser igual a Move .

Então eu acho que voltei a acreditar que Unpin e, portanto, Pinned::deref não deveriam existir, e em vez disso, devemos evitar quaisquer situações (como com a macro geradora de acessadores) onde você obtenha um tipo como &Pinned<MovableType> . Mas talvez haja um argumento de que deveria existir como uma característica separada.

Reimplementar todas as características na superfície da API de Vec , apenas com Pinned desta vez, não parece muito bom para mim (especialmente porque algumas das características nem mesmo funcionam com isso). Tenho quase certeza de implementar seletivamente Deref caso a caso (permitindo que &Pinned<Vec<T>> vá para &[Pinned<T>] , por exemplo) ou apenas deixando Vec inteiro Unpin (e não permitindo a projeção do pino), é muito mais lógico.

Sim, definitivamente não tive a intenção de propor a reimplementação de toda a superfície de API de Vec ou algo parecido.

Concordo que seria uma boa ideia não permitir a "projeção do pino" em Vec , uma vez que &Pinned<Vec<T>> parece uma invariante estranha - você deve ter permissão para mover Vec sem invalidando o pin do conteúdo.

Como alternativa, que tal permitir a transmutação de Vec<T> para Vec<Pinned<T>> , que teria a maior parte da API Vec mas omitiria os métodos que podem causar a realocação? Ou seja, altere a definição de Vec do atual struct Vec<T> para struct Vec<T: ?Sized + ActuallySized> , para algum nome menos bobo, onde basicamente Sized torna-se um apelido para ActuallySized + Move ; em seguida, adicione um limite Sized aos métodos que podem causar a realocação e um método para fazer a transmutação.

Também seria necessário alterar o limite no tipo de elemento do tipo de fatia interna de Sized para ActuallySized . Pensar nisso me lembra da estranheza de alterar Sized para significar algo diferente de Sized , mas por outro lado, ainda é verdade que a grande maioria dos limites de Sized no código existente requer inerentemente Move . Precisar saber size_of::<T>() para indexar em uma fatia é uma exceção ...

Certo! A grande vantagem da versão Pinned é que você não precisaria de traços distintos para referências fixadas mutáveis. No entanto, é pior ou neutro do que PinMut com deref para quase todos os outros cenários.

Também tem a vantagem de evitar conflito com RefCell , reconhecidamente ao custo de entrar em conflito com outras coisas (limites de Sized ).

Acessores manuais que requerem código inseguro para implementação parecem uma má ideia para mim; Não vejo como tal proposta permitiria a geração de acessadores ser segura (como você impede alguém de fornecer acessores não fixados sem torná-los inseguros para escrever para afirmar que não o farão?).

Porque os acessores estão implementados em Pinned<MyStruct> , não em MyStruct diretamente. Se você tiver &mut MyStruct , sempre poderá acessar manualmente um campo para obter &mut MyField , mas ainda estará em um estado móvel. Se você tem &mut Pinned<MyStruct> , você não pode obter &mut MyStruct (assumindo que MyStruct seja !Unpin ou Unpin não exista), então você tem que usar um acessador para chegar aos campos. O acessador leva &mut Pinned<MyStruct> (ou seja, leva &mut self e está implícito em Pinned<MyStruct> ) e dá a você &mut Pinned<MyField> ou &mut MyField , dependendo de qual opção você escolheu. Mas você só pode ter um tipo de acessador ou outro, pois a invariante crítica é que você não deve ser capaz de obter um &mut Pinned<MyField> , escrever nele, liberar o empréstimo e, em seguida, obter um &mut MyField (e mova-o).

Existem boas razões para querer fazer isso (como: seu tipo fixado tem um Vec nele). Coleções intrusivas encontrarão esse tipo de cenário com muita frequência. Acho que qualquer proposta baseada na ideia de que as pessoas nunca vão querer Pinned referências a contêineres existentes, ou que você tem que optar por fazer Unpin funcionar, provavelmente não funcionará bem. Não ser capaz de basicamente optar pelo ecossistema regular de Rust adicionando um limite de Unpin seria incrivelmente perturbador (na verdade, quase todos os casos de uso que tenho para tipos imóveis se tornariam significativamente mais difíceis).

Não entendo totalmente o que você quer dizer aqui, mas pode ser porque você estava reagindo ao meu próprio erro wrt Unpin versus Move :)

Agora que estou corrigido ... Se Unpin existe, então Vec deve implementá-lo. Mas supondo que não exista, quais são exatamente os cenários aos quais você está se referindo?

Para uma estrutura que tem Vec como um de seus campos, expliquei acima como você obteria uma referência não fixada para o campo (ao custo de não ser capaz de obter uma referência fixada a ele, que está bem).

Acho que isso seria problemático se você quiser uma estrutura genérica com um campo que pode conter Vec , ou pode conter um tipo imóvel, dependendo do parâmetro de tipo. No entanto, pode haver uma maneira diferente de resolver isso sem a necessidade de um traço Unpin que todos devem pensar se devem ser implementados.

@comex

Como alternativa, que tal permitir a transmutação de Vecpara Vec>, que teria a maior parte da API Vec, mas omitiria os métodos que podem causar a realocação?

Porque...

Ou seja, altere a definição de Vec da estrutura atual Vecpara estruturar Vec, para algum nome menos bobo, onde basicamente Sized se torna um alias para ActuallySized + Move; em seguida, adicione um limite Sized aos métodos que podem causar a realocação e um método para fazer a transmutação.

... isso soa muito, muito complicado e não faz realmente o que você quer (que é obter um Vec<T> normal de &mut Pinned<Vec<T>> ou qualquer outra coisa). É uma espécie de legal que permite fixar um vector após o fato, de modo a obter uma boa analógico para o Box<Pinned<T>> , mas isso é uma preocupação ortogonal; é apenas mais uma ilustração do fato de que fixar ser uma propriedade do tipo possuído provavelmente está correto. Eu acho que tudo sobre Unpin é quase completamente não relacionado à questão de como o tipo de referência é construído.

Agora que estou corrigido ... Se o Unpin existir, então Vec deve implementá-lo. Mas supondo que não exista, quais são exatamente os cenários aos quais você está se referindo?

Vou responder a isso porque acho que vai ilustrar meu ponto: eu não posso nem transformar um campo i32 em um PinMut sem Unpin . Em algum ponto, se você quiser fazer algo com sua estrutura, geralmente terá que mover algo dentro dela (a menos que seja completamente imutável). Necessitar que as pessoas implementem explicitamente os acessadores de campo em Pinned<MyType> parece realmente irritante, especialmente se o campo em questão é sempre seguro para mover. Esta abordagem também parece muito confusa de usar com um tipo Pinned embutido, pois as projeções legais de alguma forma variam por campo de uma forma que não depende do tipo de campo, algo que já foi rejeitado no Rust quando mut campos foram removidos (e IMO, se nós vamos adicionar tal anotação unsafe é uma escolha melhor, já que campos inseguros são uma grande arma na prática). Como os tipos fixos embutidos são praticamente a única maneira de tornar os enums fixos agradáveis ​​de usar, eu me preocupo que eles sejam capazes de ser um tanto consistentes com o resto da linguagem.

Mas mais importante...

Acho que isso seria problemático se você quiser uma estrutura genérica com um campo que pode conter um Vec ou pode conter um tipo imóvel, dependendo do parâmetro de tipo

Esse é o caso de uso matador para Unpin e (para mim) o fato de que realmente parece funcionar é bastante fantástico e valida todo o modelo de fixação (a ponto de eu acho que mesmo se estivéssemos começando antes do Rust 1.0, provavelmente desejaríamos manter Unpin como está). Parâmetros genéricos que vivem em linha na estrutura também são praticamente o único momento em que você deve limitar por Unpin se os planos atuais (para fazer quase todos os tipos de referência seguros implementarem Unpin incondicionalmente) ir através.

Mas o que realmente não estou entendendo é: por que você deseja remover Unpin ? Eliminá-lo não lhe compra essencialmente nada; todas as coisas boas que você ganha com Pinned sobre PinMut (ou vice-versa) não estão relacionadas à sua presença. Adicioná-lo facilita a compatibilidade com o resto do ecossistema Rust. FWIW, Unpin e a implementação incondicional de deref em Pin estão relacionados entre si, exceto que sem Unpin todos os tipos se sentem iguais dor que !Unpin tipos fazem (significando que provavelmente tornaria a implementação deref incondicional mais útil). Eu não posso evitar, mas sinto que estou perdendo algo.

Os outros campos da estrutura que estão sendo eliminados são considerados variáveis ​​locais neste contexto? Isso não está claro para mim em nenhuma documentação voltada para o usuário

Muito justo, abri https://github.com/rust-lang/rust/issues/50765 para rastrear isso.


@pythonesque

Especificamente, acho o exemplo RefCell bastante problemático, pois na presença de Pinned :: deref isso significa que nem mesmo seríamos capazes de impor a fixação dinamicamente com o tipo existente (não sei se a especialização seria o suficiente). Isso sugere ainda que, se mantivermos a implementação de deref, teremos que duplicar a superfície da API quase tanto com Pinned quanto com Pin; e se não o mantivermos, fixadotorna-se incrivelmente difícil de usar.

A solução para RefCell é fornecer métodos extras borrow_pin e borrow_pin_mut (pegando Pin<RefCell<T>> ) e manter o controle do estado fixo do interior de RefCell em tempo de execução. Isso deve funcionar para PinMut e Pinned . Então, seu argumento aqui é que Pinned não ajuda? Também não deve piorar as coisas para RefCell .

Mas então você escreve

a diferença é que ainda podemos remendar uma solução com PinMut enquanto suportamos deref, que parece não funcionar com Pinned

e não sei a que você está se referindo, por que isso não funcionaria com Pinned ?

@comex

Não acho que Pinned :: deref deva existir. Os acessores de campo gerados por macro seguros devem ser suficientes; Não vejo como isso é "incrivelmente difícil de usar".

Não vejo a conexão entre deref e os acessadores de campo. Mas também não vejo como deref se torna mais problemático com Pinned<T> , então acho que vou esperar por uma resposta à minha pergunta acima primeiro.

Assim como @pythonesque , acho que rastrear o estado fixado no tipo por trás da referência ("no tipo de propriedade") é fundamentalmente mais correto. No entanto, tenho dúvidas de que ele possa realmente ser transformado em uma API geral mais ergonômica, em particular devido à restrição de trabalhar com o ecossistema Rust existente.

Se vamos deliberadamente seguir a abordagem que pensamos ser menos "fundamentalmente correta", devemos pelo menos nos dar bastante tempo para experimentação antes de estabilizá-la, para que possamos estar tão confiantes quanto razoavelmente possível de que não somos vai acabar se arrependendo.

@pythonesque , uau, muito obrigado pelo extenso resumo! Fico feliz em saber que há um RFC em andamento. :)

É fácil chamar uma função que leva uma referência mutável a um valor fixado, desde que essa função não use algo como mem::swap ou mem::replace . Por causa disso, parece mais natural ter essas funções usando o limite Unpin do que ter todo derefe mutável de um Pin para um Unpin não seguro.

Se uma função fosse posteriormente atualizada para usar swap não seria mais seguro chamá-la em uma referência mutável para um valor fixado. Quando swap e replace possuem este limite, a função atualizada também deve fazer isso torna mais óbvio que esta não é uma alteração compatível com versões anteriores.

Então, alguns pensamentos que tive:

  1. Fundamentalmente, Drop fornece o mesmo privilégio de Unpin - você pode obter &mut para algo que estava anteriormente em PinMut . E Drop é seguro, o que significa que Unpin deve ser seguro (isso é mem::forget e o apocalipse de vazamento de novo).
  2. Isso é bom, porque significa que coisas como as atuais APIs baseadas em futuros, que não lidam com geradores de liberação, são 100% seguras de implementar, mesmo que recebam self: PinMut<Self> (sem unsafe impl Unpin ).
  3. A API soará se Unpin for seguro? A resposta é sim: contanto que os geradores não implementem Unpin e não seja seguro fixar o projeto a um tipo !Unpin , tudo está seguro.
  4. Mas isso significa que as projeções dos pinos não são seguras! Não é ideal.
  5. As projeções de pinos são seguras se o Self não implementar Unpin ou Drop [edit: true or false?] Podemos automatizar essa verificação?

Tenho algumas idéias para uma alternativa mais suportada por linguagem para esta API de biblioteca, que envolve descartar Unpin inteiramente e, em vez disso, inverter a polaridade - um traço Pin que você opta por obter essas garantias, em vez de optando por sair . Mas isso exigiria suporte de linguagem significativo, enquanto a implementação atual é inteiramente baseada em biblioteca. Farei outra postagem depois de pensar mais sobre isso.


outra nota porque sempre me esqueço:

A segurança da projeção do pino depende apenas do tipo Self, não do tipo de campo, pois o tipo de campo deve garantir a segurança de sua API pública, que é incondicional. Portanto, não é uma verificação recursiva - se Self nunca mover nada de seu Pin , a projeção de pinos para um campo de qualquer tipo é segura.

@withoutboats FWIW isso corresponde exatamente às conclusões que @cramertj e eu chegamos em nossa rodada anterior de discussões. E eu acredito que podemos automatizar a verificação de exclusão mútua, inicialmente por meio de alguns atributos criados para fins específicos, emitidos pela derivação.

@withoutboats

Fundamentalmente, Drop fornece o mesmo privilégio que Unpin - você pode obter um & mut para algo que estava anteriormente em um PinMut. E o Drop é seguro, o que significa que Desafixar deve ser seguro (isto é mem :: esqueça e vaze o pico de novo).

Não vejo a conexão com o vazamento de focos, mas concordo em contrário. A única razão pela qual estou (estava?) Um pouco hesitante é que, contanto que seja apenas Drop , isso parecia mais um caso secundário para mim com o qual muitas pessoas não precisam se preocupar. Não tenho certeza se isso é uma vantagem ou não. E de qualquer forma, um Unpin seguro não só aumenta a consistência aqui e "resolve" o problema de Drop ao não torná-lo um caso especial (em vez disso, podemos pensar em cada impl Drop como vindo com um impl Unpin implícito); pelo que você diz, também torna mais fácil usar o lado Future das coisas. Portanto, parece uma vitória geral.

@pythonesque, a menos que esteja faltando alguma coisa, o seguro Unpin também não causa nenhum problema novo para coleções intrusivas, certo? Se eles trabalharam apesar do seguro Drop , eles ainda devem funcionar.


@withoutboats

As projeções de pinos são seguras se Self não implementar Desafixar ou Remover [editar: verdadeiro ou falso?] Podemos automatizar essa verificação?

Inicialmente, você também mencionou o tipo de campo aqui e eu estava prestes a perguntar por que você acha que isso é relevante. Agora vejo você editar a postagem. :) Concordo que se trata apenas do tipo Self . A seguir, praticamente repito seu argumento em meus termos.

Essencialmente, a questão é: Como Self escolhe seu invariante de fixação? Por padrão, assumimos (mesmo se houver código inseguro por perto!) Que o invariante de fixação é exatamente o mesmo que o invariante de propriedade, ou seja, o invariante é independente da localização e este tipo não faz fixação. Contanto que eu não possa fazer nada com PinMut<T> além de transformá-lo em &mut T , essa é uma suposição segura.

Para habilitar projeções de campo, o invariante de fixação deve ser "todos os meus campos são fixados em seu invariante de tipo respectivo". Esse invariante justifica facilmente as projeções de fixação, independentemente dos tipos de campo (ou seja, eles podem escolher qualquer invariante de fixação que quiserem). Claro que este invariante é incompatível com a transformação de PinMut<T> em &mut T , então é melhor nos certificarmos de que tais tipos não sejam Drop ou Unpin .

Não vejo a conexão com o vazamento de focos, mas concordo em contrário.

apenas uma analogia - Desafixar é largar como mem :: esquecer é para ciclos Rc. mem :: esquecer foi originalmente marcado como inseguro, mas não havia justificativa para isso. (E o mesmo argumento de que os ciclos Rc são um caso extremo foi feito contra a marcação de mem :: esquecer seguro.)

Copiando (espiritualmente) do Discord, eu realmente gostaria de ver evidências de que não acabamos de inverter o problema: tornando o Unpin seguro para implementar, tornando os acessores de pinos estruturais inseguros de implementar (isso também seria verdadeiro com qualquer traço inseguro que foi introduzido, certo? Você ainda teria que escrever código inseguro). Isso me irrita porque na grande maioria das vezes eles são completamente seguros - basicamente, contanto que não haja um implante Unpin explícito para o tipo, assim como estamos sempre seguros se não houver implante Drop para o tipo. Com o plano atual, estamos exigindo algo muito mais forte - deve haver um! Desafixar impl explícito para o tipo - o que será verdadeiro para muito menos tipos (isso é tudo que podemos fazer em uma biblioteca).

Infelizmente, não tenho ideia de como ou se o compilador pode verificar se há um Unpin impl manual para um tipo específico ou não, ao contrário de "has any impl", e não tenho certeza se ele tem interações ruins com a especialização. Se tivermos uma maneira definida de realizar essa verificação, de modo que o criador de um tipo não precise escrever nenhum código inseguro para obter a fixação estrutural, eu ficaria muito mais feliz com impl Unpin sendo seguro, eu acho ... isso é algo que parece viável?

Tenho uma pergunta simples que estou tentando entender agora. Em genéricos, a liberação será um limite implícito como dimensionado, a menos que sua API para todos os parâmetros genéricos?

isso deve estar correto para que vecpara continuar a estar seguro.

irá desafixar; será um limite implícito como tamanho

Não.

isso deve estar correto para que o vec continue a ser seguro.

Porque você acha isso?

Coisa irritante que @MajorBreakfasts atingiu hoje: PinMut::get_mut dá a você um &mut com a vida útil do PinMut , mas não há maneira segura de fazer a mesma coisa, já que DerefMut dá a você um empréstimo com o tempo de vida da referência mutável para PinMut . Talvez devêssemos fazer get_mut o seguro e adicionar get_mut_unchecked ?

Você quer dizer assim?

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

Sim, devemos ter isso.

@RalfJung Exatamente.

O método da caixa de futuros em que eu gostaria de usar isso se parece com este:

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

Tive que usar um bloco inseguro, embora F tenha um limite de Unpin e seja totalmente seguro. Atualmente, simplesmente não há uma maneira segura de produzir uma referência que retenha a vida útil de PinMut .

Sim, isso é apenas uma omissão na API.

Você quer preparar um PR ou devo?

Eu posso fazer isso. Queremos chamá-lo de get_mut e get_mut_unchecked ?

Vou adicionar um map seguro e renomear o atual para map_unchecked também. Principalmente por consistência. Então, todas as funções inseguras de PinMut terminam em _unchecked e têm um equivalente seguro

Queremos chamá-lo de get_mut e get_mut_unchecked?

Parece razoável.

Vou adicionar um map seguro

Cuidado aí. Como você quer que seja? O motivo pelo qual o mapa não é seguro é que não sabemos como fazer um seguro.

Cuidado aí. Como você quer que seja?

Você está certo. Ele precisa exigir um limite de Unpin no valor de retorno.

Acabei de ter uma ideia: e quanto a PinArc ? Poderíamos usar tal coisa no BiLock impl (https://github.com/rust-lang-nursery/futures-rs/pull/1044) na caixa de futuros.

@MajorBreakfasts PinArc (e PinRc , etc) depende de Pin (a versão não Mut ). Eu estaria pronto para adicioná-lo, mas não tinha certeza se havia consenso sobre exatamente quais garantias ele ofereceria.

Não tenho mais certeza se adicionar PinArc adiciona algo ^^ ' Arc já não permite "retirar o conteúdo emprestado"

@MajorBreakfast Você pode sair de Arc usando Arc::try_unwrap . Seria uma alteração incompatível com versões anteriores remover isso ou introduzir um limite T: Unpin .

Oi!
Tenho pensado um pouco em como fazer com que os acessores de pinos funcionem com segurança. Pelo que entendi, o problema com acessores de pin ocorre _somente_ quando você tem a combinação T: !Unpin + Drop . Para esse fim, vi alguma discussão tentando evitar a possibilidade de tal tipo T existir - por exemplo, tornando as características !Unpin e Drop mutuamente exclusivas de várias maneiras, mas há não era uma maneira clara de fazer isso sem quebrar a compatibilidade com versões anteriores. Olhando para este problema mais de perto, podemos obter acessores Pin de Segurança sem impedir um tipo de ser !Unpin e Drop , é apenas que tal tipo não poderia ser colocado em um Pin<T> . Impedir que _que_ aconteça é algo que acredito ser muito mais factível, e mais no espírito de como o traço Unpin funciona de qualquer maneira.

Atualmente, tenho uma "solução" para evitar que um tipo entre com segurança em Pin<T> que é !Unpin e Drop . Ele meio que requer recursos que ainda não temos na ferrugem, o que é um problema, mas espero que seja um começo. Enfim, aqui está o código ...

/// This is an empty trait, it is used solely in the bounds of `Pin`
unsafe trait Pinnable {}

/// Add the extra trait bounds here, and add it to the various impls of Pin as well
struct Pin<T: Pinnable> {
    ...
}

/// Then we impl Pinnable for all the types we want to be able to put into pins
unsafe impl<T: Unpin> Pinnable for T {}
unsafe impl<T: !Unpin + !Drop> Pinnable for T {}

Os tipos Unpin e os tipos !Unpin mas também !Drop não têm problemas (que eu saiba) com acessores de pinos. Isso não quebra nenhum código existente que usa Drop , apenas limita o que pode ser colocado em uma estrutura Pin . No caso improvável * em que alguém precisa que um tipo seja !Unpin e Drop (e capaz de ser realmente colocado dentro de um Pin ), ele seria capaz de unsafe impl Pinnable para seu tipo.

* Não tenho experiência com Pin , mas espero que a maioria dos casos em que alguém precisa de impl Drop para um tipo !Unpin não sejam causados ​​por coisa que precisa ser eliminada sendo inerentemente !Unpin , mas sim causada por uma estrutura com Drop campos e !Unpin campos. Nesse caso, os campos Drop podem ser separados em sua própria estrutura que não contém os campos !Unpin . Posso entrar em mais detalhes sobre isso, se necessário, porque acho que a explicação foi duvidosa, mas não era para ser o corpo principal do comentário, então vou deixá-lo resumido por enquanto.

Pelo que entendi, o problema com acessores de pinos ocorre apenas quando você tem a combinação T:! Desafixar + Soltar.

É mais um "ou" do que um "e".

Os acessores de pin implicam em uma interpretação "estrutural" da fixação: quando T é fixada, todos os seus campos também o são. impl Unpin e Drop , por outro lado, são seguros porque presumem que o tipo não se preocupa com a fixação - T ser fixado ou não não faz diferença; em particular, os campos de T não são fixados em nenhum dos casos.

No entanto, você entendeu isso mais tarde quando disse que !Unpin + !Drop tipos são aqueles cujos acessores são seguros para adicionar. (O tipo ainda precisa ter cuidado para não fazer nada de errado em seu código inseguro , mas Unpin ad Drop são as duas maneiras seguras de quebrar a fixação de acessadores.)

Olhando para este problema mais de perto, podemos obter acessores de pino seguros sem impedir que um tipo seja! Solte e solte, só que esse tipo não poderia ser colocado em um pino.

Eu não sigo. Por que você acha que isso é o suficiente? E se for, por que estaríamos interessados ​​em acessores fixados se um tipo não pode ser fixado ...?

É mais um "ou" do que um "e".

Vamos apenas falar sobre isso por agora, porque toda a minha ideia foi baseada no oposto disso, então se eu não entendi isso, o resto da ideia não ajuda.

No meu entendimento (atual), os acessores de pinos com um tipo que é Unpin são completamente válidos, independentemente de cair, já que Pin<T> é efetivamente &mut T qualquer maneira. Além disso, os acessores de pino em um tipo que é !Drop são completamente válidos, mesmo para um tipo !Unpin , pois drop é a única função que poderia obter &mut self no tipo !Unpin uma vez que ele entra em Pin , nada seria capaz de mover as coisas.

Se eu cometi um erro nisso, por favor me avise. Mas se não, então Unpin + Drop tipos ou !Unpin + !Drop tipos estariam completamente bem com acessores de pin, e os únicos tipos de problema seriam aqueles que são !Unpin + Drop .

@ashfordneil

Além disso, os acessores de pino em um tipo que é! Soltar são completamente válidos, mesmo para um tipo! Desafixar, pois soltar é a única função que poderia se & mutar no tipo! Desafixar uma vez que ele entra em um Pino, então nada seria capaz de mova as coisas.

Se eu escrever struct Foo(InnerNotUnpin); impl Unpin for Foo {} então não é saudável ir de PinMut<Foo> para PinMut<InnerNotUnpin> . Para que a projeção seja sólida, você tem que (a) proibir drop impls e (b) garantir que a estrutura seja Unpin somente quando o campo for Unpin .

(a) deveria apenas estar proibindo de lançar impls em coisas que são !Unpin certas?
(b) deve ser tratado pelo fato de que a implementação de Unpin não é segura - certeza de que é possível atirar no próprio pé ao alegar de forma insegura que um tipo pode ser desafixado quando na verdade não pode - mas é tão possível rotular algo como Sync quando não deveria e acaba com um comportamento doentio dessa forma

(a) deveria apenas estar proibindo derrubar impls em coisas que são! Desafixar certo?

Sim, mas isso é impossível por motivos de compatibilidade com versões anteriores.

E isso vai ao meu ponto - proibir drop impls em coisas que são! Desafixar é impossível por razões de compatibilidade com versões anteriores. Mas não precisamos impedir a ocorrência de impls em coisas que estão! O marcador! Unpin em um tipo não tem significado e não faz promessas até que o tipo esteja dentro de um Pin, então o implemento de queda em um tipo! Unpin é totalmente válido _desde que esse tipo não esteja dentro de um Pin_. O que estou propondo é que mudemos o Pin de Pin<T> para Pin<T: Pinnable> onde o traço Pinnable atua como um guardião para prevenir tipos que são Drop + !Unpin (ou mais geralmente, os tipos para os quais os acessadores de pinos são problemáticos) sejam colocados dentro de Pin .

Nesse ponto, você está tornando o traço Unpin totalmente inútil, não é? Além disso, você ainda não consegue expressar os limites necessários com o Rust atual. Se pudéssemos dizer " Unpin e Drop são incompatíveis", não haveria um problema. Parece que você precisa de algo semelhante com Pinnable . Não vejo como isso ajuda.

O traço Unpin - por si só - já é “inútil”. O traço _somente_ significa algo, uma vez que o tipo foi fixado, o tipo pode ser movido antes de ir para o Pin.

@RalfJung e eu acabamos de nos reunir para falar sobre essas APIs e concordamos que estamos prontos para estabilizá-las sem grandes mudanças na API.

Resolução para questões pendentes

Minha crença sobre as APIs de pin é que:

  1. Os conceitos básicos da API de PinMut e Unpin constituem a melhor solução possível que não envolve suporte de linguagem integrado.
  2. Essas APIs não impedem o futuro suporte a idiomas integrados se decidirmos que é necessário.

Existem algumas questões pendentes em torno de suas limitações e compensações, porém, pelas quais desejo registrar nossa decisão.

O problema de Drop

Um problema que não descobrimos até depois de fundirmos o RFC foi o problema do traço Drop . O método Drop::drop tem uma API que assume &mut self . Isso é essencialmente "liberar" Self , violando as garantias que os tipos fixados deveriam fornecer.

Felizmente, isso não afeta a solidez dos tipos principais de "gerador imóvel" que estamos tentando projetar com o pino: eles não implementam um destruidor, então sua implementação !Unpin realmente significa isso .

No entanto, o que isso significa é que para os tipos que você mesmo define, é necessariamente seguro "liberá-los", tudo o que você precisa fazer é implementar Drop . O que isso significa é que Unpin sendo um unsafe trait não estava nos dando nenhuma segurança adicional: implementar Drop é tão bom quanto implementar Unpin na medida em que a solidez for em causa.

Por esta razão, tornamos o traço Unpin um traço seguro para implementação.

Projeção de pino

A principal limitação da API atual é que não é possível determinar automaticamente que é seguro realizar uma projeção de pino quando o campo não implementa Unpin . Uma projeção de pino é:

Dado um tipo T com um campo a com o tipo U , posso "projetar" com segurança de PinMut<'a, T> para PinMut<'a, U> acessando o campo a .

Ou, no código:

struct Foo {
    bar: Bar,
}

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

A partir de agora, a menos que o tipo de campo implemente Unpin , não temos que rustc para _provar_ que isso é seguro. Ou seja, a implementação desse tipo de método de projeção atualmente requer código inseguro. Felizmente, você pode provar que é seguro. Se o tipo de campo não pode implementar Unpin , isso só é seguro se todos eles forem válidos:

  1. O tipo Self não implementa Unpin , ou então apenas implementa Unpin condicionalmente quando o tipo de campo o faz.
  2. O tipo Self não implementa Drop , caso contrário, o destruidor para o tipo Self nunca move nada para fora desse campo.

Com extensões de linguagem, poderíamos fazer com que o compilador verifique se algumas dessas condições são válidas (por exemplo, Self não implementa Drop e apenas implementa Unpin condicionalmente).

Por causa do problema de Drop , um traço estável, esse problema existia antes de qualquer decisão que pudéssemos tomar sobre esse assunto e não há como sem mudanças de idioma para tornar a projeção de pinos automaticamente segura. Algum dia, podemos fazer essas alterações, mas não vamos bloquear a estabilização dessas APIs nisso.

Especificando a garantia do "pin"

Mais tarde no processo de RFC, @cramertj percebeu que, com uma pequena extensão além do que precisamos para geradores imóveis, a fixação poderia ser usada para encapsular com segurança as garantias de listas intrusivas. Ralf escreveu uma descrição desta extensão aqui .

Essencialmente, a garantia de fixação é esta:

Se você tiver um PinMut<T> e T não implementar Unpin , T não será movido e o suporte de memória T não será ser invalidado até que o destruidor execute T .

Isso não é exatamente o mesmo que "liberdade de vazamento" - T ainda pode vazar, por exemplo, por ficar preso atrás de um ciclo de Rc, mas mesmo se vazar, isso significa que a memória nunca será invalidada (até que o programa termina), porque o destruidor nunca será executado.

Essa garantia impede certas APIs para fixação de pilha, portanto, não estávamos certos a princípio sobre como estender a fixação para incluir isso. No entanto, em última análise, devemos optar por essa garantia por vários motivos:

  1. Outras APIs de fixação de pilha, ainda mais ergonômicas, foram descobertas, como esta macro , eliminando o lado negativo.
  2. A vantagem é muito grande! Esses tipos de listas intrusivas podem ter um grande impacto em bibliotecas como a josephine, que integra o Rust com linguagens de coleta de lixo.
  3. Ralf nunca teve certeza de que algumas dessas APIs eram realmente sólidas para começar (especialmente aquelas baseadas em "empréstimo permanente").

Reorganização API

No entanto, quero fazer uma alteração final proposta para a API, que é mover as coisas. Revendo as APIs existentes, Ralf e eu percebemos que não havia um bom lugar para descrever as garantias e invariantes de alto nível relacionadas a Drop e assim por diante. Portanto, eu proponho

  1. Criamos um novo módulo std::pin . Os documentos do módulo fornecerão uma visão geral de alto nível da fixação, as garantias que ela oferece e as invariáveis ​​que os usuários de suas partes inseguras devem manter.
  2. Movemos PinMut e PinBox de std::mem e std::boxed para o módulo pin . Deixamos Unpin no módulo std::marker , com as outras características do marcador.

Também precisaremos escrever essa documentação mais extensa antes de terminar de estabilizar esses tipos.

Adicionando implementações de Unpin

Ralf e eu também discutimos a adição de mais implementações de Unpin à biblioteca padrão. Ao implementar Unpin , sempre há uma compensação: se Unpin for implementado incondicionalmente em relação ao tipo de um campo, você não pode fixar o projeto a esse campo. Por esse motivo, não temos sido muito agressivos quanto à implementação de Unpin até agora.

No entanto, acreditamos que, como regra:

  1. A projeção de alfinetes através de um ponteiro nunca deve ser tratada como segura (veja o quadro de notas abaixo).
  2. A projeção do pino por meio de mutabilidade interna nunca deve ser tratada como segura.

Em geral, os tipos que fazem uma dessas coisas fazem algo (como realocar a loja de apoio como em Vec ) que torna a projeção do pino insustentável. Por essa razão, pensamos que seria apropriado adicionar uma implementação incondicional de Unpin a todos os tipos em std que contenham um parâmetro genérico atrás de um ponteiro ou dentro de um UnsafeCell; isso inclui std::collections por exemplo.

Não investiguei muito de perto, mas acredito que seria suficiente para a maioria desses tipos se apenas adicionarmos estes impls:

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

Porém, há um problema: tecnicamente, acreditamos que a projeção do pino através de Box poderia ser tecnicamente segura: isto é, ir de PinMut<Box<T>> para PinMut<T> pode ser feito com segurança. Isso ocorre porque Box é um ponteiro totalmente pertencente que nunca se realoca.

É possível que desejemos fornecer um buraco para que Unpin só seja implementado condicionalmente para Box<T> quando T: Unpin . No entanto, minha opinião pessoal é que deveria ser possível pensar em pinning como sendo um bloco de memória que foi fixado no lugar e, portanto, toda projeção por meio de ponteiros indiretos deve ser perigosa.

Poderíamos atrasar essa decisão sem bloquear a estabilização e adicionar esses impls ao longo do tempo.

Conclusão

Eu realmente adoraria ouvir a opinião de outras pessoas que investiram muito nas APIs de pinos até agora, e se esse plano de estabilização parece razoável ou não. Vou fazer um resumo mais curto com uma proposta de fcp em um post subsequente, para o resto da equipe de idiomas.

@rfcbot fcp merge

Estou propondo estabilizar as APIs de pinos existentes após uma pequena reorganização. Você pode ler um resumo mais longo no meu post anterior . O TL; DR da minha opinião é que a API atual é:

  • Som
  • Melhor sem alterações de idioma
  • Forward compatível com as alterações de idioma que o tornariam melhor

Resolvemos todas as questões principais sobre as garantias precisas e invariantes de fixação nos últimos meses, as decisões que estamos tomando atualmente (que eu acho que são as decisões que devemos nos estabilizar) estão documentadas no post mais longo.

Estou propondo algumas mudanças antes de realmente estabilizar as coisas:

  1. Reorganize PinMut e PinBox em um novo módulo std::pin , que conterá documentação de alto nível sobre as invariáveis ​​e garantias de fixação em geral.
  2. Adicione implementações incondicionais de Unpin para os tipos em std que contêm parâmetros genéricos por trás de um ponteiro indireto ou dentro de um UnsafeCell; a justificativa de por que essas implementações são apropriadas está no resumo mais longo.

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

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

Nenhuma preocupação listada atualmente.

Assim que a maioria dos revisores aprovar (e nenhum objeção), isso entrará em seu período final de 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.

@rfcbot fcp cancel

Estou marcando lang também por causa das principais decisões invariantes que tivemos que tomar, então estou cancelando e reiniciando o FCP

Proposta de @withoutboats cancelada.

@rfcbot fcp merge Veja a mensagem de mesclagem anterior e o longo resumo, desculpe!

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

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

Preocupações:

Assim que a maioria dos revisores aprovar (e nenhum objeção), isso entrará em seu período final de 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.

Estou feliz em ver um pouco de confiança de que isso é bom. No entanto, é difícil entender tudo o que mudou desde o RFC original; o texto RFC poderia ser atualizado para refletir o design que está sendo proposto para estabilização? (Em geral, esse é um problema que muitas RFCs apresentam, em que mudam tanto durante o período de implementação que tentar entendê-lo lendo a RFC é inútil. Devemos encontrar uma maneira de fazer melhor lá.)

RFCs não são especificações. Ler o RFC não substitui a documentação real.

@bstrie, a mudança significativa do RFC ( Unpin é seguro) é abordada em meu post de resumo. A longo prazo, as pessoas devem obter uma compreensão da API de fixação lendo os documentos em std::pin (um bloqueador de estabilização), não desenterrando o RFC original.

Se a intenção é que a documentação bloqueie a estabilização, mas a documentação ainda não foi escrita, então este FCP não é prematuro? Esperar que os comentadores em potencial reconstruam a documentação provisória eles mesmos juntando fontes díspares é IMO uma barreira um pouco mais alta de entrada do que se gostaria.

@bstrie é nosso procedimento padrão propor FCP antes da documentação. Eu escrevi um comentário resumido muito extenso sobre a situação, que deve fornecer informações suficientes para qualquer um que não acompanhou o problema de rastreamento, bem como um comentário mais curto para pessoas que não querem tanto contexto

Para elaborar um pouco mais, o FCP é a pergunta “queremos estabilizar isso?” Se a resposta for “não”, então muito trabalho com documentos foi perdido. A conclusão do FCP não significa que o recurso se torna instantaneamente estável; significa que foi tomada a decisão de estabilizá-lo e, portanto, agora é a hora de fazer o trabalho necessário para fazê-lo; que inclui compilador e trabalho de documentação.

Em 2 de agosto de 2018, às 12h56, boats [email protected] escreveu:

@bstrie é nosso procedimento padrão propor FCP antes da documentação. Eu escrevi um comentário resumido muito extenso sobre o estado do jogo, que deve fornecer informações suficientes para qualquer pessoa que não acompanhou o problema de rastreamento

-
Você está recebendo isso porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub ou ignore a conversa.

@withoutboats

A projeção de alfinetes através de um ponteiro nunca deve ser tratada como segura (veja o quadro de notas abaixo).
A projeção do pino por meio de mutabilidade interna nunca deve ser tratada como segura.

Você pode esclarecer um pouco esse ponto? Você está se referindo especificamente aos tipos na biblioteca padrão ? Semelhante a Box , existem tipos como Mutex que podem projetar a fixação, uma vez que são os proprietários exclusivos de seu conteúdo (embora possam ser fora de banda). Pelo menos fora da biblioteca padrão, eu suporia que desejaríamos a capacidade de ter primitivas de simultaneidade que projetassem PinRef<InPlaceMux<T>> a PinMut<T> em uma chamada para .lock() , o que parece como um caso de "projeção pela mutabilidade interior".

@cramertj Projetar em Mutex parece perigoso; pode-se usar a conversão segura para &Mutex e então Mutex::lock para obter &mut e, em seguida, mover as coisas para fora.

EDIT: Oh, você quer dizer um Mutex sempre fixado. Por exemplo, PinMutex . Sim, isso funcionaria. A mutabilidade interior é adequada se você nunca distribuir &mut . Mas parece improvável atualmente que tenhamos tais estruturas de dados no libstd.

Mas sim, para precisão total, a declaração de @withoutboats deve ser emendada para dizer que "a projeção do pino através de mutabilidade interna é segura apenas se o tipo sempre fixa, ou seja, nunca distribui &mut ."

@RalfJung Exatamente, sim.

Mas parece improvável atualmente que tenhamos tais estruturas de dados no libstd.

Certo, eu queria ter certeza de que os requisitos declarados sobre a não projeção por meio de ponteiros ou mutabilidade interior eram especificamente em torno de libstd, não de regras gerais.

@cramertj definitivamente não é uma regra rígida e rápida (não é UB violá-la, por exemplo), mas eu diria que é uma boa diretriz para pessoas que não têm certeza

Estou decepcionado com a decisão de estabilizar isso. Não estou muito preocupado com o que acontecerá em curto prazo, já que sem o suporte do compilador, qualquer projeto em potencial parecerá um pouco hackeado. No entanto, a longo prazo, se Rust ganhar tipos nativos imóveis, eles não se sentirão verdadeiramente de primeira classe, a menos que possam ser usados ​​com métodos de característica aceitando &self e &mut self . Como consequência, a fixação precisa ser uma propriedade do tipo referente, não do tipo de referência. Isso ainda pode acontecer, é claro, em um projeto futuro para tipos nativos imóveis. Mas isso resultaria em Pin<T> se tornando redundante - não apenas, digamos, um apelido obsoleto para algum &pin T nativo, mas totalmente desnecessário. Por sua vez, características como Future (e potencialmente muitas outras) que aceitam Pin<Self> precisarão ser renovadas ou descontinuadas, o que exigiria um período de transição complicado.

Confuso, não impossível. Mas, correndo o risco de ser pessimista demais, estou preocupado que o desejo de evitar tal transição influencie as decisões futuras no sentido de adotar &pin vez disso como a base para tipos nativos imóveis - que, na minha opinião, irão embora eles permanentemente de segunda classe. E ainda acho que entre os projetos de curto prazo, a abordagem &Pinned<T> teria sido viável, evitando essas preocupações de compatibilidade com versões futuras e tendo outros benefícios (por exemplo, evitando completamente o problema de mutabilidade interior).

Ah bem. Do jeito que está, ainda estou ansioso para usar Pin , mas eu teria preferido se continuasse em crates.io em vez de entrar na biblioteca padrão.

Concordo que é muito cedo para estabilizar isso. Atualmente é um pouco estranho em algumas situações, por exemplo, ao acessar campos do tipo !Unpin dentro de um método chamado PinMut<Self> . Acho que as alterações em

@Thomasdezeeuw Que mudanças você acha que melhorariam a situação? As mudanças que propus não parecem estar relacionadas a esse inconveniente.

Não há como escrever uma solução genérica - isto é, adicionar um método a std - mas esta variação da macro de futuros é segura:

macro_rules! get_mut {
    ($f:tt: $t:ty) => (
        fn $f<'a>(
            self: &'a mut std::mem::PinMut<Self>
        ) -> &'a mut $t
            where $t: Unpin
        {
            unsafe {
                 &mut std::mem::PinMut::get_mut_unchecked(self.reborrow()).$f
            }
        }
    )
}

struct Foo {
    bar: Bar,
}

impl Foo {
     get_mut!(bar: Bar);
}

@withoutboats Isso é seguro porque fornece acesso apenas a Unpin dados, certo?

@RalfJung exatamente! a linha da cláusula where o torna seguro

@withoutboats é uma boa macro, mas só funciona para tipos Unpin .

Mas não seria possível adicionar um método / macro que pegasse PinMut<Self> e retornasse PinMut<Self.field> , de preferência em código seguro. Posso estar errado, mas meu pensamento é que se o chamador garante que Self está fixado, então Self.field , certo? Isso também deve funcionar para tipos que não implementam Unpin .

@Thomasdezeeuw Este problema é discutido em meu resumo na seção "Projeção do pino"; não é seguro projetar para !Unpin campos, a menos que você também garanta que Self não fará certas outras coisas, para as quais não podemos gerar um cheque. Só podemos garantir automaticamente a capacidade de projetar para campos que implementam Unpin (porque isso é sempre seguro e podemos verificar isso com uma cláusula where ).

@semoutboats li o resumo, mas talvez não tenha entendido. Mas se um tipo T for !Unpin então PinMut<T> não implementará DerefMut for T , portanto, mover o valor não seria possível sem um código inseguro. Você ainda tem o problema com o traço Drop , mas ele é maior do que apenas obter acesso ao campo de um tipo. Portanto, não vejo o que torna o mapeamento de PinMut<T> a PinMut<T.field> inseguro, mesmo que T.field seja !Unpin .

@Thomasdezeeuw Em futuros-rs, temos situações em que temos um tipo dentro de PinMut , mas um de seus campos não é considerado fixado

A desvantagem da macro por @withoutboats é que ela só pode dar acesso a um campo por vez porque contém uma referência mutável para self . Os acessos ao campo Struct não têm essa limitação.

Acredito que decidir estabilizar a API de fixação é a decisão certa. No entanto, gostaria de sugerir a implementação do módulo core::pin proposto primeiro.

Você ainda tem o problema com o traço de queda, mas é maior do que apenas obter acesso ao campo de um tipo.

Não, não é. Drop está bem , contanto que você não faça mapeamento de campo . É por isso que o mapeamento de campo não é seguro.

Você ainda tem o problema com o traço de queda, mas é maior do que apenas obter acesso ao campo de um tipo. Portanto, não vejo o que torna o mapeamento do PinMutpara PinMutinseguro, mesmo se T.field for! Desafixar.

Para ser claro, não podemos gerar automaticamente uma prova de que T: !Unpin também. Mas mesmo que não seja, aqui está um exemplo de como o Drop impl pode ser usado:

struct Foo {
    field: UnpinFuture,
}

impl Foo {
     fn field(self: PinMut<Self>) -> PinMut<UnpinFuture> { ... }

     fn poll_field(self: PinMut<Self>, ctx: &mut Context) {
         self.field().poll(ctx);
     }
}

impl Drop {
    fn drop(&mut self) {
        // ...
        let moved_field = mem::replace(&mut self.field, UnpinFuture::new());

        // polling after move! violated the guarantee!
        PinBox::new(moved_field).as_pin().poll();
    }
}

Como @RalfJung diz, este é exatamente o problema com Drop - se você não fizer nenhuma projeção de pino em !Unpin campos, tudo o que você fizer em Drop está bem.

A desvantagem da macro de @withoutboats é que ela só pode dar acesso a um campo por vez porque contém uma referência mutável para si mesmo. Os acessos ao campo Struct não têm essa limitação.

Você pode escrever o método que dá acesso a vários campos, mas você tem que fazer isso para cada combinação de seu interesse.

Por curiosidade, existem outros casos de uso concretos além de futuros, ou pelo menos alguma outra coisa motivando o desejo de estabilizá-los agora (em oposição a mais tarde com mais experiência)?

Parece não haver mais discussão aqui sobre os pontos levantados por @comex. Eles me deixaram muito curioso, porque não me lembro da ideia de fazer de pin uma propriedade do tipo em vez da referência. Isso já foi discutido em outro lugar? Não é fácil acompanhar tudo "de fora", faço o meu melhor ;-)

Por sua vez, características como Futuro (e potencialmente muitas outras) que aceitam Pinprecisará ser reformulado ou descontinuado, o que exigiria um período de transição complicado.

Hmmm. Será que alguma mágica relacionada à edição nos permite embalar e desembrulhar esses Pin s silenciosamente e fazer as alterações totalmente compatíveis com versões anteriores? Um tanto grosseiro, mas não excessivamente, e manteria a linguagem e as APIs limpas durante essa transição.

Talvez a ideia precisasse ser mais desenvolvida para responder a essa pergunta (lembro-me disso ter sido mencionado antes no tópico, mas perdi a noção de onde.)

@yasammez Pude pegar a discussão sobre fixação orientada a valor por aqui

Só para confirmar, o caminho para a estabilização não inclui um plano de projeções de campo seguras? Parece-me que isso vai introduzir uma grande quantidade de código inseguro em todas as bibliotecas futuras. Na minha experiência, impls Future manuais não são incomuns, e sei que todos os meus exigem pesquisa de campos.

Eu não acho que importe que muito desse código inseguro seja provado como seguro trivialmente. A mera existência do código inseguro dilui a capacidade de auditar códigos inseguros reais .

@tikue a curto prazo, você pode:

  1. use uma macro como a macro unsafe_pinned que a biblioteca de futuros desenvolveu, limitando-se a uma única insegura para cada enquete interna, que você deve verificar em relação às restrições descritas em meu longo resumo (você também não pode usar uma macro e basta escrever o acessador manualmente, também é apenas um único unsafe ).
  2. exija que seus campos implementem Unpin , tornando tais projeções trivialmente seguras e não exigindo nenhum código inseguro, ao custo de alocar em heap quaisquer !Unpin futuros.

@semoutboats Eu entendo as opções, mas acho difícil de paladar. Exigir Unpin restringirá a compatibilidade com uma série de futuros, como qualquer coisa que se auto-empreste, por exemplo, qualquer fn assíncrono usando canais. E a poluição insegura é uma preocupação real.

Na prática, tentei migrar uma biblioteca para o futuro 0.3 e descobri que é uma luta por esses motivos.

@tikue Isso é compatível com o fornecimento de cheques de forma automatizada. Ainda não projetamos ou implementamos totalmente isso, mas nada aqui impede que aconteça.

No entanto, eu realmente não entendo essa preocupação:

E a poluição insegura é uma preocupação real.

O "problema da insegurança" é freqüentemente deixado neste nível, eu acho - apenas uma proibição geral de unsafe .

Para a maioria dos futuros aninhados, deve ser muito fácil determinar se você pode usar a macro unsafe_pinned! com segurança. A lista de verificação geralmente é assim:

  1. Eu não implementei Unpin manualmente para o meu futuro, em vez disso, confio apenas no auto trait impl.
  2. Não implementei Drop manualmente para o meu futuro, porque não tenho um destruidor personalizado.
  3. O campo que desejo projetar é privado.

Nesse caso: você é bom! unsafe_pinned é seguro para uso.

Se você aplicou manualmente Unpin ou Drop , você tem que realmente pensar sobre isso, mas mesmo nesse caso, não é necessariamente um problema tão difícil:

  1. Se eu implementei Unpin , ele está restrito no futuro, eu abstraio de ser Unpin .
  2. Se eu implementar Drop , nunca movo o campo futuro durante meu destruidor.

@tikue para referência, eu escrevi uma essência da proposta básica para uma solução baseada em atributos . Isso requer suporte do compilador, mas não extensões significativas para a linguagem - apenas um novo traço e um novo atributo embutido.

Há também uma solução mais "intrusiva" na qual adicionamos tipos de referência &pin apropriados, que teriam a mesma forma em termos de restrições, mas teriam mais impacto na linguagem como um todo.

E quanto a PinMut::replace ?

impl<'a, T> PinMut<'a, T> {
  pub fn replace(&mut self, x: T) { unsafe {
    ptr::drop_in_place(self.inner as *mut T);
    ptr::write(self.inner as *mut T, x);
  } }
}

Mesmo se não quisermos adicionar tal método, devemos ter uma resposta para a questão de saber se isso é seguro - para PinMut , e também para PinBox que poderia ter algo semelhante.

Eu acho que é certamente seguro para PinBox , porque estou tendo o cuidado de chamar o destruidor "no lugar" e então isso é apenas reutilizar a memória. No entanto, estou um pouco preocupado com PinMut sobre o valor ser descartado antes, ou seja, antes que sua vida útil termine. Não parece claro para mim que isso sempre ficará bem, mas também não consigo encontrar um contra-exemplo. @cramertj @withoutboats alguma opinião?

(Eu adicionaria isso a @rfcbot se pudesse ...)

@rfcbot preocupação substituir

@RalfJung Eu posso estar errado aqui, mas aquele código não poderia cair duas vezes se T entrar em pânico?

@RalfJung Já temos PinMut::set .

@cramertj D'oh. Sim, ok, eu poderia ter sido um pouco mais meticuloso na verificação disso.

Desculpe pelo barulho, minha preocupação foi resolvida.

@rfcbot resolve replace

Este é o código real que estou escrevendo agora para futuros 0.1 => 0.3 compatibilidade com tokio_timer::Deadline . Ele precisa fazer um PinMut<Future01CompatExt<Delay>> , onde Delay é um campo de Deadline .

        let mut compat;
        let mut delay = unsafe {
            let me = PinMut::get_mut_unchecked(self);
            compat = Future01CompatExt::compat(&mut me.delay);
            PinMut::new_unchecked(&mut compat)
        };

Além do uso não trivial de inseguro, também demorei cerca de 5 iterações neste código para encontrar uma versão que compilasse. Escrever e pensar sobre o que está acontecendo aqui não é ergonômico.

Acho que há muito espaço entre uma proibição geral de inseguros e este código. Por exemplo, map_unchecked é muito restritivo para ajudar com este código porque requer o retorno de uma referência, enquanto este código requer o retorno de Future01CompatExt<&mut Delay> .

Edit: Outra fonte de unergonomia é que você não pode pedir emprestado mutuamente em guardas de fósforo, então PinMutnão pode funcionar com um código como este:

`` `ferrugem
corresponde a self.poll_listener (cx)? {
// costumava ser if self.open_connections> 0
Poll :: Ready (None) if self.open_connections ()> 0 => {
^^^^ emprestado mutably no guarda padrão
return Poll :: Pending;
}
Enquete :: Pronto (Nenhum) => {
return Poll :: Ready (None);
}
}
`` ``

Seu comentário sobre map_unchecked está certo, talvez haja uma assinatura mais genérica que também pode permitir o retorno de temporários em torno de ponteiros.

Existem diretrizes para quando é seguro obter uma referência &mut de um campo de PinMut<Type> ? Acho que desde que Type nunca assuma que o campo está fixado, está tudo bem? Tem que ser privado?

Sim, ou o campo implementa Unpin ou você nunca constrói um PinMut do campo.

variância @rfcbot

Estou escrevendo uma quantidade razoável de código baseado em PinMut e uma coisa que tem acontecido muito é que eu tenho uma operação que não depende de fixação, mas se auto-realiza por PInMut em vez de &mut como uma forma de dizer "Prometo que não vou sair desta memória." No entanto, isso força todos os usuários a terem um valor fixado, o que é desnecessário. Estive pensando sobre isso e não consegui encontrar uma boa API para ele, mas seria maravilhoso se houvesse uma maneira de separar "Preciso que meu argumento seja fixado" de "Posso trabalhar com um argumento".

Acho que "variância" é o termo errado aqui; o que você deseja são construtores de tipo associados para abstrair sobre diferentes tipos de referência: D

@RalfJung Sim, é verdade. No entanto, eu me conformaria com um tipo como poderia vir de um PinMut ou de um & mut, mas só pode ser usado como um PinMut, embora eu saiba que isso não se adapta muito bem. Acho que isso é mais realizável do que arbitrary_self_types genérico: smile: Talvez seja melhor apenas encerrar com "estamos bastante confiantes de que estamos preparados para o futuro para funcionar com assinaturas como esta":

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

@rfcbot concern api-refactor

Um pouco de inspiração struct e ontem à noite descobri como poderíamos refatorar essa API para que haja apenas um tipo Pin , que envolve um ponteiro, em vez de ter que criar uma versão fixada de cada ponteiro. Esta não é uma reformulação fundamental da API de forma alguma, mas é melhor extrair o componente "pinos da memória" em uma peça combinável.

Quando eu estava trabalhando na pilha de rede para nt-rs , disseram-me que referências fixadas ajudariam na "dança da propriedade" que estou resolvendo atualmente com fold como visto aqui (tx.send move tx para um Send<<type of tx>> futuro, tornando difícil fazer um loop nos dados de entrada para enviar pacotes.). De que forma a fixação ajudaria nesse tipo de coisa?

@Redrield send() leva &mut self em futuros 0.3.

@withoutboats Estou ansioso para experimentar esta nova API no futuro-rs!

Como a nova API usa identificadores diferentes, acho que deveria ser possível ter as duas APIs disponíveis simultaneamente. Acho que seria melhor ter um curto período de transição durante o qual ambas as APIs estão disponíveis, o que nos permite experimentar, preparar um PR e portar a API futura em libcore para o novo estilo.

@MajorBreakfasts , parece que poderíamos potencialmente type PinMut<T> = Pin<&mut T>; e oferecer vários dos mesmos métodos, o que reduziria a magnitude e o imediatismo da quebra.

@withoutboats

Portanto, com a nova API:

  1. Pin<&T> e Pin<&mut T> têm o invariante de "fixação padrão", onde o valor por trás deles:
    1.a. não é mais um &T / &mut T válido, a menos que Unpin
    1.b. não será usado como Pin de outro endereço de memória.
    1.c. será eliminado antes de ser invalidada a memória.
  2. Pin<Smaht<T>> não tem nenhuma garantia "especial", exceto que retorna Pin<&mut T> válidos e Pin<&T> quando solicitado (que é apenas a garantia de segurança padrão, dado que as APIs são seguras )
    2.a. Queremos um DerefPure garantia, onde Pin<Smaht<T>> é obrigado a devolver o mesmo valor a menos que seja transformado? Alguém quer isso?

Correção sobre o invariante.

O invariante para Pin<Smaht<T>> é:

  1. Chamar Deref::deref(&self.inner) dará um Pin<&T::Target> válido (observe que &self.inner não precisa ser um &Smaht<T> seguro).
  2. Se Smaht<T>: DerefMut , chamar DerefMut::deref_mut(&mut self.inner) dará um Pin<&mut T::Target> válido (note que &mut self.inner não precisa ser um seguro &mut Smaht<T> ).
  3. Chamar o destruidor de self.inner para destruí-lo está OK, desde que seja seguro para destruição.
  4. self.inner não precisa ser um seguro Smaht<T> - não precisa ser compatível com outras funções.

Houve algum feedback postado no post do @withoutboats :

  • (múltiplo) Own é um nome estranho, veja abaixo as possíveis maneiras de resolver isso.

  • ryani pergunta por que as implementações que restringem Deref<Target = T> têm aquele T genérico extra? Você não pode simplesmente usar P::Target ?

  • jnicklas pergunta qual é o propósito do traço Own , que tal adicionar pinned métodos inerentes por convenção? Isso significa que os usuários da API não precisam importar a característica, no entanto, você perde a capacidade de ser genérico em vez de construtores fixáveis. Este é um grande negócio? A motivação para o traço parece insuficientemente motivada: _ [eles] têm a mesma forma, afinal .'_

  • RustMeUp (eu) pergunta, no traço Own , qual é o propósito do método own ? Por que o traço não pode simplesmente deixar pinned para ser implementado pelos tipos que desejam aderir? Isso permite que a característica seja segura e permite que ela seja chamada de Pinned que é menos estranho do que Own . A razão para o próprio método parece motivada de forma insuficiente.

Vou acrescentar outra pergunta:

Por que nem Pin<P> si nem Pin<P>::new_unchecked restritos a P: Deref ?

#[derive(Copy, Clone)]
pub struct Pin<P> {
    pointer: P,
}

impl<P: Deref> Pin<P> { // only change
    pub unsafe fn new_unchecked(pointer: P) -> Pin<P> {
        Pin { pointer }
    }
}

ou

#[derive(Copy, Clone)]
pub struct Pin<P: Deref> { // changed
    pointer: P,
}

impl<P: Deref> Pin<P> { // changed
    pub unsafe fn new_unchecked(pointer: P) -> Pin<P> {
        Pin { pointer }
    }
}

ambos evitariam Pin<P> onde P: !Deref instâncias que são inúteis, uma vez que não há nada que você possa fazer com elas, a menos que métodos extras sejam adicionados. Eu acho que...

Aqui está uma essência com o meu e os comentários de ryani endereçados.

ryani pergunta por que as implementações que restringem Dereftem aquele T genérico extra? Você não pode simplesmente usar P :: Target?

Não há nenhuma diferença entre esses dois impls além de como eles foram escritos.

Por que nem pin

próprio nem pin

:: new_unchecked restrito a P: Deref?

Eu não tenho opinião; historicamente, a biblioteca std não se limitou a structs, mas não tenho certeza se essa política é bem motivada ou um acidente histórico.


Planejando implementar isso após o lançamento de # 53227, mas não implementará o traço Own porque é controverso. Por enquanto, apenas um construtor inerente pinned em Box , Rc e Arc .

Seguindo um pouco o que @ arielb1 escreveu, eu vejo Pin aqui como realmente agindo em "construtores do tipo ponteiro" - coisas como Box ou &'a mut que têm "tipo" * -> * . É claro que não temos sintaxe para isso, mas essa é uma maneira pela qual posso entender isso em termos de invariantes. Ainda temos 4 (segurança (invariáveis ​​por tipo, como em minha postagem do blog : Pertencente, Compartilhado, Pinado-Pertencente, Pinado-Compartilhado.

Eu acho que uma formalização adequada requer esta visão, porque a ideia é que Pin<Ptr><T> pega T invariantes, transforma-os (usando os invariantes fixados em todos os lugares) e, em seguida, aplica o Ptr construtor para esse tipo resultante. (Isso requer mudar a maneira como definimos Owned , mas isso é algo que planejei a longo prazo de qualquer maneira.) Em termos de formalismo, realmente preferiríamos escrever Ptr<Pin<T>> , mas tentamos isso e não funciona muito bem com Rust.

A promessa, então, que um construtor de tipo de ponteiro faz ao "optar por" Pin é que aplicar Deref / DerefMut será seguro: Quando a entrada for realmente Ptr aplicado à variante Pinned-Owned / Shared do tipo, a saída irá satisfazer a variante Pinnd-Owned / Shared desse tipo.

Esses são realmente os dois únicos "métodos inseguros" aqui (no sentido de que devem ser cuidadosos com as invariantes):

impl<P, T> Pin<P> where
    P: Deref<Target = T>,
{
    pub fn as_ref(this: &Pin<P>) -> Pin<&T> {
        Pin { pointer: &*this.pointer }
    }
}

impl<P, T> Pin<P> where
    P: DerefMut<Target = T>,
{
    pub fn as_mut(this: &mut Pin<P>) -> Pin<&mut T> {
        Pin { pointer: &mut *this.pointer }
}

Tudo o mais que é seguro usar pode ser implementado em cima disso com um código seguro, explorando que Pin<&T> -> &T é uma conversão segura e para Unpin tipos o mesmo vale para Pin<&mut T> -> &mut T .

Eu preferiria se pudéssemos mudar as implementações de Pin Deref e DerefMut para Pin para algo como

impl<'a, T: Unpin> Pin<&'a mut T> {
    pub fn unpin_mut(this: Pin<&'a mut T>) -> &'a mut T {
        this.pointer
    }
}

impl<'a, T> Pin<&'a T> {
    // You cannot move out of a shared reference, so "unpinning" this
    // is always safe.
    pub fn unpin_shr(this: Pin<&'a T>) -> &'a T {
        this.pointer
    }
}

// The rest is now safe code that could be written by users as well
impl<P, T> Deref for Pin<P> where
    P: Deref<Target = T>,
{
    type Target = T;
    fn deref(&self) -> &T {
        Pin::unpin_shr(Pin::as_ref(self))
    }
}

impl<P, T> DerefMut for Pin<P> where
    P: DerefMut<Target = T>,
    T: Unpin,
{
    fn deref_mut(&mut self) -> &mut T {
        Pin::unpin_mut(Pin::as_mut(self))
    }
}

Isso deixaria claro que esses dois não fazem nada de novo , eles apenas compõem as_ref / as_mut com uma conversão segura de Pin<&[mut] T> para &[mut] T - - o que claramente deixa as_ref / as_mut como a única coisa que outros construtores de tipo de ponteiro precisam se preocupar.


O PinMut atual tem um método borrow pegando &mut self para um empréstimo mais fácil (já que isso leva self , obtemos empréstimo automático). Este método faz exatamente o mesmo que Pin::as_mut . Suponho que gostaríamos de algo assim aqui também?

Acabei de notar que as fatias têm get_unchecked_mut , mas o pino tem get_mut_unchecked . Parece que devemos nos ater a algo consistente?

Alguém poderia adicionar isso às preocupações do rfcbot?

@rfcbot concern get_mut_unchecked_mut_mut

Eu acabei de perceber que esquecemos de uma situação em que o rustc copia coisas por aí - provavelmente não é grande coisa, por enquanto. Estou falando de estruturas compactadas. Se uma estrutura compactada tem um campo que precisa ser eliminado, o rustc emitirá um código para copiar os dados desse campo para algum lugar alinhado e, em seguida, chamar drop nisso. Isso é para ter certeza de que &mut passado para drop está realmente alinhado.

Para nós, significa que uma estrutura repr(packed) não deve ser "estrutural" ou "recursiva". pinning - seus campos não podem ser considerados fixados, mesmo que a própria estrutura seja. Em particular, não é seguro usar a macro pin-accessor em tal estrutura. Isso deve ser adicionado à sua documentação.

Parece uma espingarda gigante que deve ser colocada sobre todos os documentos de fixação.

@alercah Não acho que seja muito mais parecido com uma footgun do que a capacidade existente de implantar Unpin ou Drop - certamente ambos são vistos mais comumente ao lado de valores fixados do que #[repr(packed)] .

Isso é justo. Minha preocupação é que alguém possa pensar que uma projeção é segura para um tipo que eles não escreveram e não perceber que packed torna inseguro fazê-lo, porque é decididamente não óbvio. É verdade que quem está fazendo a projeção é responsável por estar ciente disso, mas acho que precisa ser documentado adicionalmente em qualquer lugar onde tal projeção possa ocorrer.

Hm, isso também significa que todas as estruturas compactadas podem ser Unpin independentemente dos tipos de campo, já que a fixação não é recursiva?

@alercah eu não estou super familiarizado com a forma tipos embalados são implementadas, mas acredito que é seguro pin a um tipo embalado, não apenas ao pino através de um tipo embalado. Portanto, o único caso em que você não controla o tipo compactado é se você projeta para um campo público do tipo de outra pessoa, que pode ser igualmente inseguro por causa de Drop ou qualquer outra coisa. Em geral, parece desaconselhável fixar o projeto nos campos de outra pessoa, a menos que eles forneçam uma projeção de pino indicando que é seguro.

Hm, isso também significa que todas as estruturas compactadas podem ser Desafixadas, independentemente dos tipos de campo, uma vez que a fixação não é recursiva?

Um caso de uso que eu poderia imaginar é uma lista encadeada intrusiva, onde você só se preocupa com o endereço de toda a estrutura, mas ela tem um monte de dados mal alinhados que você deseja compactar sem se preocupar com o endereço desses dados.

Tive que aprender a Pin API devido ao meu trabalho com Futures e tenho uma pergunta.

  • Box implementa Unpin incondicionalmente.

  • Box implementa DerefMut incondicionalmente.

  • O método Pin::get_mut sempre funciona em &mut Box<T> (porque Box implementa Unpin incondicionalmente).

Em conjunto, isso permite mover um valor fixado com um código totalmente seguro .

Isso é intencional? Qual é o motivo pelo qual isso é seguro fazer?

Parece que você tem Pin<&mut Box<...>> , que fixa Box , não o material dentro de Box . É sempre seguro mover Box , porque Box nunca armazena referências à sua localização na pilha.

O que você provavelmente quer é Pin<Box<...>> . Link do parque infantil .

EDIT: não é mais preciso

@Pauan Só porque Box está fixado, não significa que a coisa _interior_ está fixada. Qualquer código dependendo disso estaria incorreto.

O que você está procurando provavelmente é PinBox , o que não permite o comportamento que você mencionou e permite que você obtenha um PinMut<Foo> .

@tikue Ainda é possível sair de Pin<Box<...>> , o que eu acho que eles estavam tentando evitar.

@tmandry Corrija-me se eu estiver errado, mas Pin<Box<...>> fixa a coisa dentro de Box , não a própria caixa. No exemplo original de @Pauan , eles tinham Pin<&mut Box<...>> , que apenas fixou Box . Veja meu link de playground mostrando como Pin<Box<...>> evita obter uma referência mutável para a coisa na caixa.

Observe que PinBox foi removido recentemente e Pin<Box<T>> agora tem a mesma semântica de PinBox .

@tmandry PinBox<T> foi removido e substituído por Pin<Box<T>> no Nightly (o link do documento que você forneceu é para o Stable). Aqui está o link noturno correto.

Oh, as regras devem ter mudado desde a última vez que usei isso. Desculpe pela confusão.

@tmandry Sim, as mudanças eram muito recentes. Como as coisas ainda estão em fluxo, é difícil acompanhar todas as mudanças.

O comentário de @tikue está correto. Você precisa se lembrar que os pinos apenas fixam um nível de indireção abaixo.

@tikue @tmandry @withoutboats Obrigado pelas respostas! Foi muito útil.

Então, quais são os estados das duas preocupações agora? ( api-refactor & get_mut_unchecked_mut_mut ) Como alguém que está esperando ansiosamente pelo recurso da série async / await, eu me pergunto qual versão rustc as APIs Pin alvo? Existe uma estimativa?

@ crlf0710 veja a proposta de estabilização .

@withoutboats Parece feito? Vamos fechar?

Ok, peço desculpas se este não é o lugar para postar isso, mas estive pensando sobre o problema de Drop + !Unpin e tive a seguinte ideia:

  1. Idealmente, se Drop::drop fosse fn(self: Pin<&mut Self>) não haveria problema. Vamos chamá-lo de Drop PinDrop . Não podemos simplesmente substituir Drop por PinDrop devido a problemas de retrocompatibilidade.
  1. uma vez que o único problema com Drop::drop(&mut self) é para o caso Drop + !Unpin , poderíamos derivar um impl padrão de PinDrop para Drop + Unpin (desde então Pin<&mut T> : DerefMut<Target = T> ) e fazer PinDrop ser o traço automagicamente usado por rustc (graças a Pin::new_unchecked(&mut self) , já que drop é o único caso de stack pinning quando pensamos sobre isso).

Aqui está um esboço de PoC da ideia: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=9aae40afe732babeafef9dab3d7525a8

De qualquer forma, isso deve permanecer em beta e não ficar estável ainda, mesmo que seja uma exceção. Se houver um momento em que o comportamento de Drop pode depender de Unpin sem quebrar a compatibilidade, então esse momento é agora.

@danielhenrymantilla Não vejo como isso resolve o problema de compatibilidade com impls drop genéricos existentes, como impl<T> Drop for Vec<T> .

Você está certo, requer outra coisa:
um limite T: Unpin implícito em todos os genéricos, com um opt-out usando ?Unpin da mesma maneira que Sized

Isso deve fazer com que se torne

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

um limite T: Unpin implícito em todos os genéricos, com um opt-out usando ?Unpin da mesma maneira que Sized

Isso tem um efeito enorme no design geral da API e foi amplamente discutido como parte das propostas ?Move . Por exemplo, isso significaria que muitas, muitas bibliotecas existentes precisariam ser atualizadas para trabalhar com fixação. A conclusão foi que optar por uma solução apenas de biblioteca, como a que temos agora, é melhor porque não requer nada disso.

Sim, um custo enorme a curto prazo, já que todas as bibliotecas existentes precisariam ser atualizadas para serem !Unpin compatíveis, mas no longo prazo acabaríamos com um Drop "mais seguro". Não parecia tão ruim no começo, já que pelo menos não estamos quebrando nada.

Mas é uma preocupação justa (eu não sabia que isso havia sido levantado anteriormente; obrigado por apontar isso, @RalfJung ), e acho que as desvantagens práticas de curto prazo drop(Pin<&mut Self>) .

Houve alguma discussão sobre a implementação de Pin Hash para Pin types hashing em endereços?

Pin provavelmente deve ter uma implementação de Hash que simplesmente delega para o ponteiro contido, não há precedência para hash baseado em endereços (e não vejo nenhuma razão para que fixar um valor deva mudar como ele é hash).

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