L'attributo #[repr(packed)] ha origine da requisiti di programmazione di sistema in cui il layout della memoria deve corrispondere a specifiche esterne—come registri hardware o protocolli di rete—eliminando i byte di padding tra i campi. Mentre Rust solitamente garantisce che i riferimenti siano allineati con i requisiti del tipo puntato, le strutture packed forzano i campi a posizioni di byte sequenziali indipendentemente dall'allineamento, potenzialmente posizionando un u32 in un indirizzo non divisibile per quattro. Tentare di creare un riferimento (& o &mut) a un campo non allineato costituisce comportamento indefinito immediato, poiché il compilatore e LLVM assumono indirizzi allineati per ottimizzazioni come la vettorializzazione o le operazioni atomiche. Per accedere in sicurezza ai dati, è necessario evitare completamente di creare riferimenti intermedi, utilizzando invece i macro addr_of! e addr_of_mut! per ottenere puntatori grezzi direttamente, quindi impiegando ptr::read_unaligned o ptr::write_unaligned per copiare i dati senza assumere allineamento.
use std::ptr::{addr_of, read_unaligned}; #[repr(packed)] struct Packet { flags: u8, timestamp: u64, // Potenzialmente a offset 1, non allineato } fn get_timestamp(p: &Packet) -> u64 { // UB: &p.timestamp creerebbe un riferimento non allineato let raw_ptr = addr_of!(p.timestamp); unsafe { read_unaligned(raw_ptr) } }
Durante lo sviluppo di un parser zero-copy per un protocollo finanziario binario (FIX), il team richiedeva una struttura che corrispondesse esattamente al formato wire: un tipo di messaggio u8 seguito immediatamente da un timestamp u64 senza padding. L'implementazione iniziale utilizzava #[repr(packed)] con accesso diretto ai campi, causando occasionali segment fault su architetture ARM dove l'accesso non allineato fa trap nella kernel.
Sono state valutate diverse soluzioni. Prima, la ricostruzione manuale byte per byte utilizzando operazioni di shift e OR: questo ha eliminato problemi di allineamento ma ha introdotto un notevole sovraccarico della CPU per pacchetto e una logica di manipolazione dei bit soggetta a errori che ha complicato l'audit. Secondo, utilizzare #[repr(C)] con campi di padding espliciti per forzare l'allineamento: questo ha preservato la sicurezza ma ha rotto la compatibilità del protocollo alterando gli offset byte dei campi successivi, richiedendo copie di memoria costose per riorganizzare i dati prima della trasmissione. Terzo, mantenere #[repr(packed)] ma accedere ai campi solo tramite puntatori grezzi con letture non allineate: questo ha mantenuto l'esatto layout della memoria evitando comportamento indefinito, non creando mai riferimenti allineati al campo timestamp.
Il team ha scelto il terzo approccio, implementando un metodo getter che utilizzava addr_of!(self.timestamp) seguito da ptr::read_unaligned per restituire il valore del timestamp. Questo ha eliminato i crash su ARM e x86_64 preservando l'architettura zero-copy, riducendo la latenza del 40% rispetto all'approccio di ricostruzione byte.
Perché creare un riferimento a un campo non allineato costituisce comportamento indefinito anche su architetture che supportano l'accesso non allineato?
Sebbene i processori x86_64 tollerino carichi non allineati in hardware, le regole di comportamento indefinito di Rust sono più rigorose delle capacità hardware per consentire ottimizzazioni aggressive. Quando il compilatore vede &u32, assume che l'indirizzo sia allineato a quattro byte, permettendogli di emettere istruzioni SIMD, ottimizzare via i controlli di allineamento successivi o riordinare le operazioni di memoria. Violare questa assunzione—anche su hardware permissivo—permette al compilatore di compilare erroneamente il codice, potenzialmente causando crash o silenziosa corruzione dei dati su future versioni del compilatore o architetture diverse.
In cosa il macro addr_of! differisce semanticamente dall'operatore & quando applicato ai campi delle strutture packed?
L'operatore & crea concettualmente prima un riferimento, quindi lo coercisce a un puntatore grezzo se assegnato a uno, attivando immediatamente il controllo di validità dell'allineamento. Al contrario, addr_of! è un macro built-in che calcola direttamente l'indirizzo senza creare un riferimento intermedio, eludendo completamente il requisito di allineamento. Questa distinzione è cruciale poiché addr_of! restituisce un *const T che può essere disallineato, mentre &field sarebbe UB se il campo è non allineato, anche se immediatamente castato a un puntatore.
Perché implementare Drop per una struttura packed contenente campi non-Copy è problematico, e come si può implementare in sicurezza una distruzione personalizzata?
Il metodo Drop::drop riceve &mut self, che è allineato (la struttura stessa mantiene l'allineamento complessivo), ma la distruzione di singoli campi richiede di chiamare i rispettivi distruttori con &mut Field. Se un campo ha un allineamento più elevato rispetto all'inizio della struttura ed è quindi non allineato, creare &mut Field per invocare Drop è comportamento indefinito. Per eliminare in sicurezza tali strutture, è necessario racchiudere i campi non-Copy in ManuallyDrop, quindi nella personalizzata implementazione di Drop, utilizzare ptr::read_unaligned o ptr::drop_in_place su puntatori grezzi ottenuti tramite addr_of_mut!, assicurando che il distruttore venga eseguito senza mai creare un riferimento allineato al campo non allineato.