El atributo #[repr(packed)] proviene de los requisitos de programación de sistemas donde la disposición de la memoria debe coincidir con especificaciones externas—como registros de hardware o protocolos de red—eliminando bytes de relleno entre campos. Si bien Rust normalmente garantiza que las referencias estén alineadas a los requisitos de su tipo apuntado, los structs empaquetados fuerzan a que los campos sigan desplazamientos de bytes secuenciales independientemente de la alineación, lo que podría colocar un u32 en una dirección no divisible por cuatro. Intentar crear una referencia (& o &mut) a un campo desalineado constituye un comportamiento indefinido inmediato, ya que el compilador y LLVM asumen direcciones alineadas para optimizaciones como la vectorización o las operaciones atómicas. Para acceder a los datos de manera segura, se debe evitar crear referencias intermedias por completo, utilizando en su lugar los macros addr_of! y addr_of_mut! para obtener punteros sin procesar directamente, luego empleando ptr::read_unaligned o ptr::write_unaligned para copiar datos sin suposiciones de alineación.
use std::ptr::{addr_of, read_unaligned}; #[repr(packed)] struct Packet { flags: u8, timestamp: u64, // Potencialmente en el desplazamiento 1, desalineado } fn get_timestamp(p: &Packet) -> u64 { // UB: &p.timestamp crearía una referencia desalineada let raw_ptr = addr_of!(p.timestamp); unsafe { read_unaligned(raw_ptr) } }
Mientras desarrollaban un analizador de cero-copias para un protocolo financiero binario (FIX), el equipo necesitaba un struct que coincidiera exactamente con el formato de transmisión: un u8 tipo de mensaje seguido inmediatamente por un timestamp u64 sin relleno. La implementación inicial utilizaba #[repr(packed)] con acceso directo a los campos, causando fallos de segmentación intermitentes en arquitecturas ARM donde el acceso desalineado trampa en el núcleo.
Se evaluaron varias soluciones. Primero, reconstrucción manual byte a byte usando desplazamientos y operaciones OR: esto eliminó problemas de alineación, pero introdujo un costo adicional significativo de CPU por paquete y lógica propensa a errores para manipular bits que complicaba la auditoría. Segundo, usar #[repr(C)] con campos de relleno explícitos para forzar la alineación: esto preservó la seguridad pero rompió la compatibilidad del protocolo al alterar los desplazamientos de bytes de los campos subsecuentes, requiriendo costosas copias de memoria para reorganizar datos antes de la transmisión. Tercero, mantener #[repr(packed)] pero acceder a los campos solo a través de punteros sin procesar con lecturas desalineadas: esto mantuvo la disposición exacta de la memoria mientras evitaba el comportamiento indefinido al no crear referencias alineadas al campo de timestamp.
El equipo eligió el tercer enfoque, implementando un método getter que utilizaba addr_of!(self.timestamp) seguido de ptr::read_unaligned para devolver el valor del timestamp. Esto eliminó los bloqueos en ARM y x86_64 mientras preservaba la arquitectura de cero-copias, reduciendo la latencia en un 40% en comparación con el enfoque de reconstrucción de bytes.
¿Por qué crear una referencia a un campo desalineado constituye un comportamiento indefinido incluso en arquitecturas que admiten acceso desalineado?
Mientras que los procesadores x86_64 toleran cargas desalineadas en hardware, las reglas de comportamiento indefinido de Rust son más estrictas que las capacidades de hardware para permitir optimizaciones agresivas. Cuando el compilador ve &u32, asume que la dirección está alineada a cuatro bytes, permitiéndole emitir instrucciones SIMD, optimizar la verificación de alineación posterior, o reordenar operaciones de memoria. Violentar esta suposición—incluso en hardware indulgente—permite al compilador compilar incorrectamente el código, lo que podría causar bloqueos o corrupción silenciosa de datos en futuras versiones del compilador o diferentes arquitecturas.
¿Cómo difiere semánticamente el macro addr_of! del operador & cuando se aplica a campos de structs empaquetados?
El operador & conceptualiza primero la creación de una referencia, luego la convierte en un puntero sin procesar si se asigna a uno, provocando inmediatamente la verificación de validez de alineación. En contraste, addr_of! es un macro incorporado que calcula la dirección directamente sin crear una referencia intermedia, eludiendo completamente el requisito de alineación. Esta distinción es crucial porque addr_of! devuelve un *const T que puede estar desalineado, mientras que &field sería UB si el campo está desalineado, incluso si se convierte inmediatamente a un puntero.
¿Por qué implementar Drop para un struct empaquetado que contiene campos no-Copy es problemático, y cómo se puede implementar de manera segura una destrucción personalizada?
El método Drop::drop recibe &mut self, que está alineado (el struct en sí mantiene la alineación general), pero eliminar campos individuales requiere llamar a sus destructores con &mut Field. Si un campo tiene una alineación más alta que el inicio del struct y, por lo tanto, está desalineado, crear &mut Field para invocar Drop es comportamiento indefinido. Para eliminar de manera segura tales structs, se deben envolver los campos no-Copy en ManuallyDrop, luego en la implementación personalizada de Drop, usar ptr::read_unaligned o ptr::drop_in_place en punteros sin procesar obtenidos a través de addr_of_mut!, asegurando que el destructor se ejecute sin crear nunca una referencia alineada al campo desalineado.