O que é `std::pin::Pin` no Rust?
(vrong.me)std::pin::Pinexpressa, no nível de tipos, a garantia de que o valor apontado por um ponteiro não será movido por meio desse ponteiro, sendo necessário para valores cujo endereço precisa ser estável, como tipos que fazem referência a si mesmos- Em
async/await, variáveis locais e referências que sobrevivem além de um.awaitpodem se tornar campos da máquina de estados gerada pelo compilador, entãoFuture::pollexigePinpara impedir que o future seja movido após começar a ser pollingado Pinimpede, em código seguro, que um valor fixado seja movido, mas não proíbe modificações em geral; seT: Unpinnão for satisfeito, não é possível extrair com segurança um&mut Tde umPin- A maioria dos tipos em Rust é Unpin por padrão, então structs auto-referenciais que não podem ser movidas normalmente precisam incluir um campo
PhantomPinnedpara se tornarem!Unpin - Na prática, usa-se
Box::pinoustd::pin::pin!ao fazerpollde futures diretamente ou ao passá-los para APIs que exigem futures pinados; ao implementarFuturediretamente ou primitivas assíncronas de baixo nível, também é preciso lidar com invariantesunsafe
Por que Pin é necessário
std::pin::Piné um wrapper de ponteiro que representa a garantia de que o valor apontado por esse ponteiro não será movido por meio dele- O problema central surge em tipos auto-referenciais
- A struct de exemplo
SelfReftemdata: i32eptr: *const i32, eptraponta paraself.data - Se uma instância da struct for movida para outra variável ou retornada por uma função, seu endereço de memória pode mudar
- O ponteiro bruto
ptrcontinuará apontando para a posição antiga de memória e se tornará um ponteiro pendente
- A struct de exemplo
- Depois que a autorreferência é estabelecida, é necessário um mecanismo que impeça que esse valor seja movido novamente
O problema em async/await e Future
async/awaite Future são áreas típicas em quePinaparece com frequência- Variáveis locais que sobrevivem além de um ponto
.awaittornam-se campos da máquina de estados gerada pelo compilador - Se uma referência a uma variável local também sobreviver além do mesmo
.await, o future gerado pode se tornar auto-referencial - Depois que o polling começa, o future pode passar a depender de referências que apontam para outros campos internos
- Se o future for movido nesse estado, essas referências serão invalidadas
- Para evitar isso,
Future::pollrecebePinem vez de&mut self
pub trait Future {
type Output;
fn poll(self: Pin, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
O que Pin garante e o que não garante
Pin<Ptr>fixa o valor apontado, não o próprio ponteiroPtr- Por exemplo,
Pin<Box<T>>pode ser movido livremente - Nesse caso, o que se move é o valor
Boxna stack, e não o valorTalocado no heap dentro dele
- Por exemplo,
- O pinning também não impede mutação por si só
- Campos ainda podem ser modificados desde que não sejam movidos para fora do lugar
- Se
T: Unpin,Pin<&mut T>se comporta quase como um&mut Tcomum
impl<'a, T: ?Sized> Pin<&'a mut T> {
pub const fn get_mut(self) -> &'a mut T
where
T: Unpin
{ ... }
}
- Se o tipo não implementa
Unpin, ou seja, é!Unpin, não é possível obter um&mut Tapenas com código seguro - Nesse caso, é preciso usar métodos unsafe como
Pin::get_unchecked_mut, e o código deve garantir que o valor não será movido para fora daquela referência
Unpin e PhantomPinned
- Tipos que implementam
Unpinnão dependem de pinning para sua segurança de memória
// std::marker
pub auto trait Unpin {}
- A maioria dos tipos em Rust pode ser movida sem problema, então é
Unpinpor padrão- Exemplos:
i32,String,Vec
- Exemplos:
Unpiné implementado automaticamente para todos os tipos, a menos que se torne explicitamente!Unpinstd::marker::PhantomPinnedé uma struct marcador que é explicitamente!Unpin- Como auto traits se propagam automaticamente, uma struct que contém um campo
PhantomPinnedtambém se torna!Unpin
- Como auto traits se propagam automaticamente, uma struct que contém um campo
use std::marker::PhantomPinned;
struct SelfRef {
data: i32,
ptr: *const i32,
_phantom: PhantomPinned, // makes the entire struct !Unpin
}
- Essa é a forma padrão de declarar que uma struct definida pelo usuário se torna insegura se for movida depois de ser fixada
- Normalmente, o compilador não consegue detectar automaticamente autorreferências criadas com ponteiros brutos
unsafe - Por isso, o desenvolvedor precisa abrir mão de
Unpinexplicitamente para structs auto-referenciais- Em geral, isso é feito incluindo um campo
PhantomPinned
- Em geral, isso é feito incluindo um campo
- Se um tipo auto-referencial permanecer
Unpinpor engano, código seguro poderá extrair uma referência mutável de umPine mover o valor- Isso quebrará as suposições feitas pelo código
unsafeque criou a autorreferência
- Isso quebrará as suposições feitas pelo código
Como criar um Pin
-
Pinpor si só não fixa um valor -
Criar um
Pinsignifica provar que o pointee permanecerá em uma posição estável na memória durante a vida útil daquele pin -
Pin::new- A forma mais simples de criação é
Pin::new
let mut value = 42; let pinned = Pin::new(&mut value);- Esse construtor só pode ser usado quando
T: Unpin - Tipos
Unpinnão dependem de pinning, então envolvê-los comPiné sempre seguro - Nesse caso, a garantia de pinning é, na prática, um no-op
- A forma mais simples de criação é
-
std::pin::pin!- Quando é preciso fixar um valor localmente sem alocação no heap, pode-se usar a macro
pin!
use std::pin::pin; let future = pin!(async { println!("Hello"); });- Essa macro cria uma variável local e retorna um
Pinque aponta para ela - O compilador garante que essa variável local não será movida durante o restante de sua vida útil, então é possível fixar com segurança um valor
!Unpinna stack - Apesar do nome,
pin!não fixa a própria memória da stack - Ela apenas cria uma referência fixada associada a uma variável local; quando a variável sai de escopo, a garantia de pinning também termina
- Quando é preciso fixar um valor localmente sem alocação no heap, pode-se usar a macro
-
Box::pin- Para tipos
!Unpin, o construtor mais comum éBox::pin
let pinned = Box::pin(SelfRef { ... });- Enquanto
pin!cria umPinligado a uma variável local,Box::pinretorna umPinpertencente a umBox - Como a alocação no heap em si não se move, o pointee tem uma posição estável na memória durante toda a vida do
Box - Mesmo que o próprio
Boxseja movido, o valor que ele possui não se move; apenas o ponteiro dentro doBoxé movido - A alocação no heap permanece no mesmo endereço
- Para tipos
-
Pin::new_unchecked- Quando os construtores seguros não conseguem provar que o valor permanecerá no lugar, é possível criar um
Pindiretamente com código unsafe
let pinned = unsafe { Pin::new_unchecked(ptr) };- Quem chama
Pin::new_uncheckedpromete que o pointee não será movido novamente por nenhum ponteiro durante a vida útil doPinretornado - Se essa promessa for quebrada, pode ocorrer comportamento indefinido em código que depende da garantia de pinning
- Por isso, esse recurso costuma ser usado apenas ao implementar abstrações de baixo nível capazes de manter essa invariante
- Quando os construtores seguros não conseguem provar que o valor permanecerá no lugar, é possível criar um
Quando você realmente precisa se preocupar com isso
- Para a maioria dos desenvolvedores Rust,
PineUnpinfuncionam silenciosamente em segundo plano - Em geral, você só precisa lidar com isso diretamente em dois casos
- Consumo de código async: ao fazer
pollde um future manualmente ou passá-lo para uma API que exige futures pinados, é comum usarBox::pin(future)para fixá-lo no heap oustd::pin::pin!(future)para fixá-lo localmente na stack - Implementação manual de
Future: ao escrever máquinas de estado personalizadas ou primitivas assíncronas de baixo nível, você precisa lidar comPin, e pode precisar dePhantomPinnede código unsafe para preservar as invariantes de pinning
- Consumo de código async: ao fazer
Piné a solução zero-cost do Rust para lidar com tipos sensíveis a endereço- Graças a isso, Rust consegue oferecer
async/awaite outras abstrações auto-referenciais mantendo garantias de segurança de memória sem precisar de garbage collector
1 comentários
Comentários no Lobste.rs
std::pin::Pinparece a Monad do mundo Rust. Depois que você entende, fica impossível não escrever um post de blog sobre issoSeria bom abordar algumas coisas em que eu e outras pessoas tropeçamos ao tentar entender
PinO nome
Unpinnão é muito bom. Nomes mais precisos, embora também ruins, teriam sidoMovableWhenPinnedouPinIsNoOpA dupla negação de
!Unpinno nightly parece estranha, mas, para manter os tipos existentes como o caso padrão em 99% das situações, foi preciso adicionar o auto traitUnpin, do qual um tipo pode escapar. Se você pensar como!MovableWhenPinned, faz mais sentidoA alternativa estável,
PhantomPinned, também não tem um bom nome, porque estar pinned é um estado temporário causado pela existência de uma referência pinned, não uma característica do tipo. Um nome alternativo teria sido algo comoPhantomNotMovableWhenPinnedQuando comecei a traduzir mentalmente dessa forma, ficou muito mais fácil de entender. Claro, ainda é confuso; talvez eu só tenha tido sorte
!Unpinme dava dor de cabeça, mas ficou um pouco mais tranquilo quando comecei a lerUnpincomoSafeToUnpinFiz essa pergunta antes e acho que alguém respondeu de forma cuidadosa, mas não lembro. Pelo que entendi,
Pinsurgiu de async, e o problema era que referências a variáveis locais se tornavam autorreferências dentro de um bloco de dados que representa a máquina de estados de uma função específicaSe o estado async for movido, essas referências a variáveis locais passam a apontar para o local antigo e inválido
Mas isso não acontece apenas porque referências são ponteiros reais com endereços absolutos completos? Fico me perguntando por que a solução foi remover a capacidade de mover, em vez de tornar as referências relativas
Fico na dúvida se a resposta é basicamente “porque milhões de anos-engenheiro foram investidos para que compiladores, CPUs e SOs lidem muito bem com ponteiros, então ponteiros são melhores em vários aspectos, e por isso é melhor usar
Pinpor toda parte”, ou se existe algum motivo rígido pelo qual referências relativas realmente não funcionariam como alternativaSe as referências fossem relativas, esses tipos teriam de ter uma representação em memória diferente dependendo de estarem ou não sendo usados dentro de um estado async, e também seria necessário algum conceito de ponteiro-base a ser passado junto para reconstruir o ponteiro real a partir da referência relativa
Objetos aninhados dentro de uma referência pinned ainda podem ser movidos livremente mesmo que o objeto raiz esteja pinned, então também não dá para dizer que todas essas referências relativas hipotéticas seriam relativas ao mesmo ponteiro-base
No fim, ponteiros absolutos são necessários, e referências relativas não se encaixam bem. Então, como o compilador Rust conhece os tipos aqui, que tal tornar os objetos móveis rastreando todo o grafo de objetos e corrigindo as referências que apontam para objetos movidos para a nova localização? Isso, na prática, seria criar um coletor de lixo com rastreamento
Além disso, o compilador Rust não conhece todos os tipos no grafo de objetos. Referências podem ser passadas por FFI, e bibliotecas externas podem armazená-las. Corrigir referências movidas através de uma fronteira FFI é, na prática, um problema intratável
Por isso é realmente complicado. Também é importante notar que mover objetos em si é uma técnica relativamente nova. Na maioria dos programas C/C++, todos os objetos podem ser vistos como implicitamente pinned. O motivo de pinning ser menos discutido por lá é que os objetos simplesmente não se movem, ou, se se movem, a responsabilidade de não deixar referências pendentes é do programador
Pintambém é necessário para interoperabilidade com outras linguagens nas quais Rust não pode simplesmente mover memória como se ela fosse um bloco opaco de bitsPelo que entendo, um dos problemas da interoperabilidade com C++ é que objetos não são simples blocos de bits que podem ser movidos livremente; no fim, muitos tipos acabam precisando de pinning, e a usabilidade fica desconfortável
Dito isso, isso se baseia em conversas que tive há pelo menos uns 6 meses com pessoas que trabalhavam nisso, então não sei o quanto a situação melhorou desde então
No geral, acho uma boa explicação para ler além da documentação oficial do Rust. A forma de chegar ao problema é um pouco mais suave
Ainda assim, acho que começar com structs autorreferenciais acaba confundindo mais do que ajudando. Em especial, a frase da introdução “portanto, depois que essa autorreferência é criada, precisamos de uma forma de impedir que
SelfRefseja movido” me fez pensar mais no “problema de impedir completamente o movimento” do que no ponto centralO ponto central, que aparece bem mais tarde, é: “
Pinnão impede que um valor seja movido fisicamente. Em vez disso, é uma garantia em nível de tipo de que o valor não será movido por meio daquele ponteiro”Como não dá para impedir o movimento em si, usa-se
Pinpara expor dados autorreferenciais em uma API segura apenas atrás de uma referência exclusiva. Talvez eu já entendaPindemais, mas acho que, se a explicação for um pouco refinada, o leitor vai se perder menosEste artigo veio das minhas anotações sobre pinning, e no começo eu também entendia assim. Achei bonito que um problema como “impedir o movimento” pudesse ser resolvido com uma garantia em nível de tipo
Claro que isso não é o que
Pinrealmente faz, então faz sentido ajustar o texto para deixar essa parte claraValeria mencionar em algum ponto do artigo que
!UnPinsó pode ser expresso no nightly Rust. Esse é o principal motivo dePhantomPinnedexistirChamam de “wrapper de ponteiro”, mas mesmo em Rust quase nunca se lida com ponteiros. Não sei por que eu deveria usar isso
*consté difícil de encontrar na documentação do Rust pelo Google; fico me perguntando se é documentadoTambém preciso saber que “isso se torna um campo da máquina de estados gerada pelo compilador”? Ou algum erro absurdo do compilador está tentando dizer que isso realmente aconteceu?
“O future gerado se torna autorreferencial” também é algo que acontece implicitamente quando se usa futures?
Acho que nunca usei
Future::polldiretamenteDizem que “código seguro não consegue recuperar um
&mut Tnormal”, mas também que “modificações comuns são permitidas”; então como isso funciona?Coisas assim me fizeram parar de me aprofundar em Rust
Dito isso, também é verdade que quase não é preciso usá-los a menos que você desça para baixo nível. Eu só fui conhecer quando precisei chamar uma biblioteca em C
Future::pollé a base do código assíncrono em Rust. Você não o chama diretamente; quem chama é o executor. Rust não tem um executor padrão, então é preciso adicionar algo como Tokio, smol ou pollster, e eles usam métodos comopoll, definidos no traitFuture, para fazer o trabalhoA documentação está em vários lugares, incluindo aqui
Esperar que outras pessoas expliquem apenas aquilo de que você mesmo precisou é um pouco demais
Não entendi bem o que você está perguntando com “então como?”