El macro std::ptr::addr_of! cumple un papel crítico en el Rust inseguro al permitir la creación de punteros sin procesar a campos sin el paso intermedio de crear una referencia. Al tratar con estructuras #[repr(packed)], los campos pueden residir en desplazamientos de memoria desalineados, violando los requisitos de alineación inherentes a los tipos de referencia. Intentar crear una referencia a esos datos desalineados mediante el operador & constituye un comportamiento indefinido inmediato, independientemente de si la referencia se utiliza posteriormente. El macro addr_of! elude esto al materializar directamente un puntero sin procesar a partir de la dirección del campo, evitando las invariantes de alineación y validez impuestas por las referencias. Esta distinción es vital para interacciones seguras de FFI y manipulación de memoria de bajo nivel donde los diseños de datos empaquetados son comunes.
Mientras desarrollaban un analizador de alto rendimiento para un protocolo binario legado, el equipo de ingeniería encontró una estructura #[repr(packed)] donde un campo u32 estaba intencionalmente colocado en un desplazamiento de 1 byte para coincidir con un mapa de registro de hardware externo. La implementación inicial intentó tomar prestado este campo usando &packet.status_register para pasarlo a una función de validación, sin darse cuenta de que esto creó una referencia desalineada y desencadenó un comportamiento indefinido inmediato.
La primera solución considerada implicaba eliminar el atributo packed e insertar manualmente bytes de relleno para forzar la alineación. Este enfoque garantizaba seguridad al permitir la creación de referencias naturales, pero rompía la compatibilidad binaria con la especificación de hardware y desperdiciaba ancho de banda de memoria al transferir grandes arreglos de estas estructuras.
La segunda propuesta sugería utilizar aritmética de punteros con unsafe { &*(base_ptr.add(1) as *const u32) } para calcular manualmente la dirección del campo. Si bien esto evitaba la sintaxis de acceso directo al campo, aún materializaba una referencia a través del operador de desreferencia &*, lo que constituye comportamiento indefinido si el puntero resultante no está correctamente alineado, sin ofrecer ninguna mejora de seguridad sobre el préstamo ingenuo original y potencialmente engañando a futuros mantenedores.
El equipo finalmente eligió la tercera solución, utilizando std::ptr::addr_of! para derivar un puntero sin procesar al campo desalineado sin crear una referencia intermedia. Este puntero se pasó luego a std::ptr::read_unaligned para copiar de manera segura el valor en una variable local debidamente alineada. Esta estrategia preservó el diseño de memoria requerido mientras cumplía estrictamente con el modelo de memoria de Rust, resultando en un código que pasó rigurosas pruebas con Miri y funcionó correctamente en múltiples arquitecturas objetivo, incluyendo ARM y x86_64.
¿Por qué crear una referencia a datos desalineados constituye un comportamiento indefinido incluso si la referencia se convierte inmediatamente en un puntero sin procesar?
En Rust, el acto de crear una referencia—como &packed.field—no es meramente un cálculo de puntero, sino una afirmación para el compilador de que la memoria objetivo satisface todas las invariantes de ese tipo de referencia, incluidos la alineación y validez para lecturas. El backend de LLVM y el optimizador de Rust suponen que estas invariantes se mantienen inmediatamente al momento de crear la referencia, permitiendo optimizaciones agresivas como la reordenación de cargas y almacenes o cargas especulativas. Incluso si la referencia se convierte instantáneamente a *const T, el optimizador puede ya haber emitido instrucciones suponiendo un acceso alineado, o puede marcar el valor de la referencia como dereferenceable en los metadatos de LLVM, llevando a una mala compilación en arquitecturas con estrictos requisitos de alineación. Por lo tanto, el comportamiento indefinido ocurre en el momento de creación de la referencia, no en el punto de desreferencia, haciendo que la mera existencia de una referencia desalineada sea tóxica para la corrección del programa.
¿Cómo se diferencia addr_of! de usar as *const _ en una referencia existente, y por qué es necesario el macro?
Al escribir &packed.field as *const T, el compilador de Rust primero crea una referencia (desencadenando verificaciones de alineación y potencial UB) y solo luego convierte esa referencia válida en un puntero sin procesar. Por el contrario, std::ptr::addr_of! opera directamente sobre la expresión de lugar (el campo), generando un puntero sin procesar sin nunca construir una referencia intermedia. Esto es crucial porque el compilador trata el interior de addr_of! como un constructo especial que elude las verificaciones de validez de referencia, mientras que la palabra clave as realiza una conversión de valor a valor que requiere que el valor de origen (la referencia) sea válido. Usar el macro asegura que la derivación del puntero en sí no pueda introducir comportamiento indefinido debido a violaciones de alineación, proporcionando el único camino seguro para obtener direcciones de datos potencialmente desalineados.
¿Qué consideraciones adicionales se aplican al usar addr_of_mut! para obtener punteros a campos dentro de una estructura que contiene UnsafeCell?
Cuando una estructura #[repr(packed)] contiene un UnsafeCell<T>, obtener un puntero mutable al interior requiere un manejo cuidadoso de las reglas de aliasing de Rust. El UnsafeCell proporciona mutabilidad interior, pero crear una referencia mutable (&mut) a un campo desalineado de UnsafeCell aún viola los requisitos de alineación y es un comportamiento indefinido. Los candidatos a menudo asumen que UnsafeCell de alguna manera exime al puntero de las reglas de alineación, pero solo exime de la garantía de aliasing de referencia exclusiva (noalias), no de la alineación. Usar addr_of_mut! produce un *mut T que aún debe respetar la alineación del tipo subyacente cuando se desreferencia o se pasa a UnsafeCell::raw_get, necesitando el uso de read_unaligned o write_unaligned para el acceso real a los datos.