La stabilizzazione di async/await in Rust 1.39, insieme al tipo Pin introdotto nella versione 1.33, ha reso possibile strutture auto-referenziali sicure cruciali per macchine a stati asincrone. Queste strutture contengono spesso puntatori interni che fanno riferimento a dati di proprietà della struttura stessa, come buffer e visualizzazioni attive in questi buffer. Quando si implementano futuri manuali o strutture dati intrusive complesse, i programmatori devono accedere ai singoli campi tramite Pin<&mut Self>, creando la necessità di meccanismi di proiezione sicuri che preservino le garanzie di posizione in memoria.
Quando una struttura è pinna tramite Pin, il compilatore garantisce che il suo indirizzo di memoria rimanga costante per tutta la durata del pin, a condizione che il tipo non implementi Unpin. Se la struttura contiene puntatori auto-referenziali, come un puntatore grezzo in un vettore interno, spostare la struttura invaliderebbe questi puntatori, creando riferimenti pericolosi. Un approccio di proiezione ingenuo che semplicemente dereferenzia Pin<&mut Self> a &mut Self espone i campi a codice Rust sicuro, che potrebbe legalmente invocare mem::swap o mem::replace su questi campi, muovendoli fuori dalle loro posizioni di memoria pinne e violando il fondamentale contratto di pinning.
La proiezione sicura richiede una conversione non sicura che preservi l'invariante di pinning: se la struttura genitore è !Unpin, la proiezione del campo deve restituire Pin<&mut Field> piuttosto che &mut Field per prevenire movimenti. L'implementazione deve garantire che il campo sia strutturalmente pinato, il che significa che il suo stato di pinning è legato allo stato di pinning della struttura genitore, tipicamente raggiunto attraverso aritmetica dei puntatori o Pin::map_unchecked_mut. Per i campi che implementano Unpin, la proiezione può restituire in sicurezza &mut Field perché questi tipi possono muoversi anche se annidati all'interno di dati pinati, sebbene ci si debba assicurare che tali movimenti non invalidino altri campi auto-referenziali.
use std::pin::Pin; use std::marker::PhantomPinned; struct Buffer { data: [u8; 1024], cursor: *const u8, _pin: PhantomPinned, } impl Buffer { // Proiezione sicura al campo dati (Unpin) fn data_mut(self: Pin<&mut Self>) -> &mut [u8; 1024] { unsafe { &mut self.get_unchecked_mut().data } } // Proiezione al campo cursore fn cursor(self: Pin<&mut Self>) -> *const u8 { unsafe { self.get_unchecked_mut().cursor } } }
Contesto
Stavamo costruendo un parser ad alte prestazioni e zero-copy per un protocollo finanziario in cui i messaggi potevano fare riferimento a sotto-intervalli di un buffer interno riutilizzabile. Lo stato del parser doveva essere mantenuto attraverso operazioni I/O asincrone, il che significava che la struttura doveva essere Pin per consentire puntatori auto-referenziali nel buffer.
Descrizione del problema
La struttura Parser conteneva un Vec<u8> buffer e una fetta &[u8] che puntava a quel buffer rappresentando il messaggio corrente. Quando si implementava Stream per questo parser, il metodo poll_next riceve Pin<&mut Self>. Dovevamo modificare il buffer (per leggere più dati) mantenendo la validità del riferimento della fetta, il che richiedeva una proiezione attenta dei campi.
Soluzioni considerate
Soluzione A: Indirizzamento basato su indice
Invece di memorizzare una fetta &[u8], abbiamo memorizzato indici (usize, usize) nel vettore. Pro: Completamente sicuro, nessuna complessità di Pin, facile da implementare. Contro: Sovraccarico di controllo dei limiti a runtime, API meno ergonomica che richiede un affettamento manuale ad ogni accesso, potenziale per bug di dissincronizzazione degli indici.
Soluzione B: Proiezione Pin non sicura con puntatori grezzi
Abbiamo memorizzato il messaggio come puntatore grezzo *const u8 e lunghezza, implementando metodi di proiezione manuali utilizzando Pin::map_unchecked_mut per accedere al buffer mantenendo il campo puntatore pinato. Pro: Astrazione senza costo, mantiene l'auto-referenzialità, consente aritmetica diretta sui puntatori. Contro: Richiede blocchi di codice unsafe, rischio di comportamento indefinito se vengono violati gli invarianti di Pin (ad esempio, implementando Unpin in modo errato).
Soluzione C: Utilizzo del crate pin-project
Sfruttando i macro procedurali per generare automaticamente codice di proiezione sicuro. Pro: Ergonomico, invarianti di sicurezza ben testati, riduce il boilerplate. Contro: Dipendenza aggiuntiva, codice generato dai macro può essere più difficile da debuggare, costo leggero a tempo di compilazione.
Soluzione scelta e risultato
Abbiamo scelto la Soluzione B per evitare dipendenze esterne nel nostro contesto di sistemi embedded e mantenere il controllo esplicito sul layout della memoria. Abbiamo attentamente garantito che la struttura non implementasse Unpin aggiungendo PhantomPinned e scrivendo test esaustivi con Miri per convalidare gli invarianti di pinning. Il risultato è stato un parser che ha raggiunto una semantica zero-copy senza alcuna allocazione per messaggio, sostenendo un throughput di 10Gbps senza saturazione della CPU.
Perché è insano implementare Unpin per una struttura contenente puntatori auto-referenziali?
Unpin segnala specificamente che un tipo è sicuro da muovere anche quando racchiuso in Pin, consentendo al codice sicuro di ottenere &mut T da Pin<&mut T> tramite metodi come Pin::into_inner. Per una struttura auto-referenziale, spostare la struttura modifica l'indirizzo di memoria dei suoi contenuti, invalidando eventuali puntatori interni che fanno riferimento a tali contenuti. Implementare Unpin consentirebbe al codice sicuro di muovere la struttura, pur pinata, violando la garanzia di sicurezza che Pin fornisce ai runtime asincroni e portando a vulnerabilità di uso dopo la liberazione. Pertanto, tali strutture devono utilizzare PhantomPinned per rinunciare esplicitamente a Unpin e prevenire implementazioni automatiche accidentali.
In che modo la proiezione differisce per varianti di enum rispetto a campi di struct?
Molti candidati presumono che i meccanismi di proiezione siano identici per enum e struct, ma le enum presentano sfide uniche poiché il discriminante determina quale variante è attiva. Proiettare Pin<&mut Enum> a una variante specifica richiede di garantire che la variante rimanga pinata mentre si previene la modifica del discriminante, poiché passare da una variante all'altra sposterebbe i dati sottostanti. Rust non ha un supporto incorporato stabile per la proiezione delle varianti poiché il discriminante e i dati della variante condividono considerazioni sul layout della memoria; una proiezione sicura richiede codice non sicuro che affermi la variante attiva e garantisca che non si verifichi uno scambio di varianti mentre l'enumerazione rimane pinata.
Qual è il ruolo di PhantomPinned nel prevenire le implementazioni automatiche dei trait?
I principianti spesso trascurano che Rust implementa automaticamente Unpin per la maggior parte dei tipi a meno che non contengano esplicitamente campi !Unpin, il che renderebbe il tipo contenitore !Unpin per impostazione predefinita. PhantomPinned è un tipo marcatore di dimensioni zero definito esplicitamente come !Unpin, servendo come un vincolo di implementazione negativo quando incluso in una struttura. Senza questo marcatore, anche se gli sviluppatori scrivono codice di proiezione non sicuro assumendo che la struttura sia immobile, il compilatore potrebbe implementare automaticamente Unpin, consentendo al codice sicuro di estrarre e muovere la struttura tramite Pin::into_inner_unchecked, rompendo così gli invarianti non sicuri e invocando un comportamento indefinito.