Il macro std::ptr::addr_of! svolge un ruolo critico in Rust non sicuro consentendo la creazione di puntatori raw a campi senza il passaggio intermedio di creare un riferimento. Quando si lavora con le struct #[repr(packed)], i campi possono risiedere a offset di memoria non allineati, violando i requisiti di allineamento intrinseci ai tipi di riferimento. Tentare di creare un riferimento tramite l'operatore & su tali dati non allineati costituisce immediatamente un comportamento indefinito, indipendentemente dal fatto che il riferimento venga utilizzato successivamente. Il macro addr_of! aggira questo problema materializzando direttamente un puntatore raw dall'indirizzo del campo, eludendo gli invarianti di allineamento e validità imposti dai riferimenti. Questa distinzione è vitale per interazioni FFI sicure e manipolazioni di memoria a basso livello dove le strutture di dati impacchettate sono prevalenti.
Durante lo sviluppo di un parser ad alte prestazioni per un protocollo binario legacy, il team di ingegneria ha incontrato una struct #[repr(packed)] in cui un campo u32 era intenzionalmente posizionato a un offset di 1 byte per allinearsi a una mappa di registri hardware esterna. L'implementazione iniziale ha tentato di prendere in prestito questo campo usando &packet.status_register per passarlo a una funzione di validazione, inconsapevole del fatto che questo creava un riferimento non allineato e scatenava un immediato comportamento indefinito.
La prima soluzione considerata prevedeva la rimozione dell'attributo packed e l'inserimento manuale di byte di padding per forzare l'allineamento. Questo approccio garantiva la sicurezza consentendo la creazione di riferimenti naturali, ma rompeva la compatibilità binaria con la specifica hardware e sprecava larghezza di banda di memoria durante il trasferimento di grandi array di queste strutture.
Il secondo approccio proposto utilizzava l'aritmetica dei puntatori con unsafe { &*(base_ptr.add(1) as *const u32) } per calcolare manualmente l'indirizzo del campo. Sebbene questo evitasse la sintassi di accesso diretto al campo, materializzava comunque un riferimento attraverso l'operatore di dereferenziazione &*, il che costituisce comportamento indefinito se il puntatore risultante non è correttamente allineato, non offrendo alcun miglioramento di sicurezza rispetto al prestito naive originale e potenzialmente fuorviando i futuri manutentori.
Il team alla fine ha selezionato la terza soluzione, utilizzando std::ptr::addr_of! per derivare un puntatore raw al campo non allineato senza creare un riferimento intermedio. Questo puntatore è stato quindi passato a std::ptr::read_unaligned per copiare in modo sicuro il valore in una variabile locale correttamente allineata. Questa strategia ha preservato il layout di memoria richiesto seguendo rigorosamente il modello di memoria di Rust, risultando in codice che ha superato rigorosi test con Miri e ha funzionato correttamente su più architetture di destinazione, tra cui ARM e x86_64.
Perché creare un riferimento a dati non allineati costituisce un comportamento indefinito anche se il riferimento viene immediatamente convertito in un puntatore raw?
In Rust, l'atto di creare un riferimento—come &packed.field—non è semplicemente un calcolo di puntatore ma un'affermazione al compilatore che la memoria di destinazione soddisfa tutti gli invarianti di quel tipo di riferimento, compresi allineamento e validità per le letture. Il backend LLVM e l'ottimizzatore di Rust presumono che questi invarianti siano immediatamente soddisfatti al momento della creazione del riferimento, consentendo ottimizzazioni aggressive come il riordino load-store o caricamenti speculativi. Anche se il riferimento viene subito convertito in *const T, l'ottimizzatore potrebbe aver già emesso istruzioni assumendo accesso allineato, o potrebbe contrassegnare il valore di riferimento come dereferenceable nei metadati di LLVM, portando a una compilazione errata su architetture con requisiti di allineamento rigorosi. Pertanto, il comportamento indefinito si verifica al momento della creazione del riferimento, non al punto di dereferenziazione, rendendo la mera esistenza di un riferimento non allineato tossica per la correttezza del programma.
In cosa addr_of! differisce dall'utilizzo di as *const _ su un riferimento già esistente e perché è necessario il macro?
Quando si scrive &packed.field as *const T, il compilatore Rust prima crea un riferimento (attivando i controlli di allineamento e potenziale UB) e solo dopo converte quel riferimento valido in un puntatore raw. Al contrario, std::ptr::addr_of! opera direttamente sull'espressione del luogo (il campo), generando un puntatore raw senza mai costruire un riferimento intermedio. Questo è cruciale perché il compilatore tratta l'interno di addr_of! come un costrutto speciale che salta i controlli di validità del riferimento, mentre la parola chiave as esegue una conversione valore-valore che richiede che il valore sorgente (il riferimento) sia valido. Utilizzare il macro garantisce che la derivazione del puntatore stessa non possa introdurre comportamento indefinito a causa di violazioni di allineamento, fornendo l'unico percorso sicuro per ottenere gli indirizzi di dati potenzialmente non allineati.
Quali ulteriori considerazioni si applicano quando si utilizza addr_of_mut! per ottenere puntatori a campi all'interno di una struct contenente UnsafeCell?
Quando una struct #[repr(packed)] contiene un UnsafeCell<T>, ottenere un puntatore mutabile all'interno richiede una gestione attenta delle regole di aliasing di Rust. L'UnsafeCell fornisce mutabilità interna, ma creare un riferimento mutabile (&mut) a un campo UnsafeCell non allineato viola comunque i requisiti di allineamento ed è comportamento indefinito. I candidati spesso presumono che l'UnsafeCell esenti in qualche modo il puntatore dalle regole di allineamento, ma esclude solo dalla garanzia di aliasing di riferimento esclusivo (noalias), non dall'allineamento. Utilizzare addr_of_mut! produce un *mut T che deve comunque rispettare l'allineamento del tipo sottostante quando viene dereferenziato o passato a UnsafeCell::raw_get, necessitando l'uso di read_unaligned o write_unaligned per l'accesso ai dati effettivi.