Le macro std::ptr::addr_of! joue un rôle crucial dans l'unsafe Rust en permettant la création de pointeurs bruts vers des champs sans l'étape intermédiaire de création d'une référence. Lorsqu'on traite des structures #[repr(packed)], les champs peuvent se trouver à des décalages de mémoire mal alignés, violant les exigences d'alignement inhérentes aux types de référence. Tenter de créer une référence via l'opérateur & à de telles données mal alignées constitue un comportement indéfini immédiat, peu importe si la référence est ensuite utilisée. Le macro addr_of! contourne cela en matérialisant directement un pointeur brut à partir de l'adresse du champ, contournant ainsi les invariants d'alignement et de validité imposés par les références. Cette distinction est vitale pour des interactions FFI saines et la manipulation de mémoire à bas niveau où des mises en page de données compactes sont fréquentes.
Lors du développement d'un parseur haute performance pour un protocole binaire hérité, l'équipe d'ingénierie a rencontré une structure #[repr(packed)] où un champ u32 était intentionnellement placé à un décalage de 1 octet pour correspondre à une carte d'enregistrements de matériel externe. L'implémentation initiale a tenté d'emprunter ce champ en utilisant &packet.status_register pour le passer à une fonction de validation, sans réaliser que cela créait une référence mal alignée et déclenchait un comportement indéfini immédiat.
La première solution envisagée consistait à retirer l'attribut packed et à insérer manuellement des octets de remplissage pour forcer l'alignement. Cette approche garantissait la sécurité en permettant la création de références naturelles, mais rompait la compatibilité binaire avec la spécification matérielle et gaspillait la bande passante mémoire lors du transfert de grands tableaux de ces structures.
La deuxième approche proposait d'utiliser l'arithmétique des pointeurs avec unsafe { &*(base_ptr.add(1) as *const u32) } pour calculer manuellement l'adresse du champ. Bien que cela ait évité la syntaxe d'accès direct au champ, cela matérialisait toujours une référence par le biais de l'opérateur de déréférencement &*, ce qui constitue un comportement indéfini si le pointeur résultant n'est pas correctement aligné, n'offrant aucune amélioration de sécurité par rapport à l'emprunt naïf d'origine et pouvant induire en erreur de futurs mainteneurs.
L'équipe a finalement sélectionné la troisième solution, utilisant std::ptr::addr_of! pour dériver un pointeur brut vers le champ mal aligné sans créer d'intermédiaire de référence. Ce pointeur a ensuite été passé à std::ptr::read_unaligned pour copier en toute sécurité la valeur dans une variable locale correctement alignée. Cette stratégie a préservé la mise en page mémoire requise tout en respectant strictement le modèle de mémoire de Rust, aboutissant à un code qui a passé des tests rigoureux avec Miri et fonctionnait correctement sur plusieurs architectures cibles, y compris ARM et x86_64.
Pourquoi créer une référence à des données mal alignées constitue-t-il un comportement indéfini même si la référence est immédiatement convertie en pointeur brut ?
Dans Rust, l'acte de créer une référence—comme &packed.field—n'est pas simplement un calcul de pointeur mais une assertion au compilateur que la mémoire cible satisfait tous les invariants de ce type de référence, y compris l'alignement et la validité pour les lectures. Le backend LLVM et l'optimiseur de Rust supposent que ces invariants sont respectés immédiatement lors de la création de la référence, permettant des optimisations agressives telles que le réarrangement de chargements et de stockages ou des chargements spéculatifs. Même si la référence est immédiatement convertie en *const T, l'optimiseur peut déjà avoir émis des instructions supposant un accès aligné, ou peut marquer la valeur de référence comme déréférenciable dans les métadonnées de LLVM, conduisant à une mauvaise compilation sur des architectures avec des exigences d'alignement strictes. Par conséquent, le comportement indéfini se produit au moment de la création de la référence, pas au moment du déréférencement, rendant la simple existence d'une référence mal alignée toxique pour la correction du programme.
En quoi addr_of! diffère-t-il de l'utilisation de as *const _ sur une référence existante, et pourquoi le macro est-il nécessaire ?
Lorsqu'on écrit &packed.field as *const T, le compilateur Rust crée d'abord une référence (déclenchant des vérifications d'alignement et un potentiel UB) puis convertit cette référence valide en un pointeur brut. À l'inverse, std::ptr::addr_of! fonctionne directement sur l'expression de lieu (le champ), générant un pointeur brut sans jamais construire une référence intermédiaire. Cela est crucial car le compilateur traite l'intérieur de addr_of! comme une construction spéciale qui contourne les vérifications de validité de référence, tandis que le mot-clé as effectue une conversion de valeur à valeur qui nécessite que la valeur source (la référence) soit valide. L'utilisation du macro garantit que la dérivation du pointeur elle-même ne peut pas introduire de comportement indéfini en raison de violations d'alignement, offrant le seul chemin sûr pour obtenir des adresses de données potentiellement mal alignées.
Quelles considérations supplémentaires s'appliquent lors de l'utilisation de addr_of_mut! pour obtenir des pointeurs vers des champs au sein d'une structure contenant UnsafeCell ?
Lorsqu'une structure #[repr(packed)] contient un UnsafeCell<T>, obtenir un pointeur mutable vers l'intérieur nécessite une manipulation soigneuse des règles d'aliasing de Rust. Le UnsafeCell fournit de la mutabilité intérieure, mais créer une référence mutable (&mut) vers un champ UnsafeCell mal aligné viole toujours les exigences d'alignement et constitue un comportement indéfini. Les candidats supposent souvent que UnsafeCell exempt d'une certaine manière le pointeur des règles d'alignement, mais il n'exempte que de la garantie d'aliasing de référence exclusive (noalias), et non de l'alignement. Utiliser addr_of_mut! produit un *mut T qui doit malgré tout respecter l'alignement du type sous-jacent lors de son éventuel déréférencement ou passation à UnsafeCell::raw_get, nécessitant l'utilisation de read_unaligned ou write_unaligned pour l'accès effectif aux données.