Historia de la pregunta:
Antes de RFC 1758, Rust carecía de un mecanismo para nuevos tipos de costo cero en FFI. Los desarrolladores dependían de #[repr(C)], que impone un diseño determinista pero puede introducir un relleno innecesario, o de #[repr(Rust)], que permite optimizaciones agresivas del compilador como reordenamiento de campos y aprovechamiento de nichos. Esto creó un dilema fundamental: hacer cumplir la seguridad de tipos a través de structs envolventes frente a garantizar la estabilidad de ABI para llamadas a funciones extranjeras. #[repr(transparent)] se introdujo específicamente para resolver esta tensión prometiendo que un struct que contiene exactamente un campo de tamaño no nulo posee un diseño de memoria, alineación y convención de llamada idénticos a los de ese campo subyacente.
El problema:
Cuando un nuevo tipo #[repr(Rust)] se pasa por referencia o valor a una función extranjera que espera el tipo interno bruto (por ejemplo, un manejador de u32), el compilador sigue siendo libre de reordenar los campos del envoltorio o aplicar optimizaciones de nicho. Debido a que #[repr(Rust)] no ofrece garantías de estabilidad, el envoltorio podría adquirir un tamaño, validez de patrón de bits o relleno diferente al del tipo interno. Esto provoca que el código C extranjero potencialmente lea memoria desalineada, interprete patrones de bits inválidos como punteros válidos o acceda a datos basura, resultando en comportamiento indefinido inmediato y corrupción de memoria catastrófica en la frontera.
La solución:
#[repr(transparent)] instruye al compilador a hacer cumplir que el envoltorio y su único campo no nulo compartan un tamaño, alineación y ABI idénticos, convirtiendo efectivamente el envoltorio en una abstracción solo en tiempo de compilación. El compilador verifica estáticamente que exactamente un campo tiene un tamaño no nulo (permitiendo campos adicionales de PhantomData o tipo unidad). Esto permite que el envoltorio se transmuta de manera segura al tipo interno o se pase directamente a través de los límites de FFI sin sobrecarga de conversión, como se demuestra a continuación:
#[repr(transparent)] pub struct SocketFd(i32); extern "C" { fn close_socket(fd: i32); } pub fn close(sock: SocketFd) { // Seguro: SocketFd tiene un ABI idéntico a i32 unsafe { close_socket(sock.0); } }
Un desarrollador integra una aplicación Rust con una API de socket netlink del kernel de Linux, que se comunica a través de descriptores de archivo de enteros en bruto. Para evitar la mezcla accidental de tipos de socket, definen struct NetlinkSocket(i32) como un nuevo tipo. Inicialmente marcado con #[repr(Rust)], pasan referencias a NetlinkSocket a un callback extern "C" que espera un puntero a i32. Durante el desarrollo local, esto parece funcionar correctamente, pero en las compilaciones de lanzamiento que utilizan LTO (Optimización en Tiempo de Enlace), el compilador aplica optimizaciones agresivas de nicho a NetlinkSocket, alterando fundamentalmente su representación de memoria. El módulo de kernel C posterior recibe un valor de puntero corrupto, provocando un pánico crítico en el kernel.
Se evaluaron tres soluciones distintas. Primero, se consideró #[repr(C)] para hacer cumplir un diseño estable y determinista. Si bien esto aseguraba la seguridad de memoria, desactivaba optimizaciones de nicho beneficiosas y potencialmente introducía bytes de relleno, agrandando innecesariamente el tamaño del struct y complicando la superficie del API para un uso puramente interno de Rust.
En segundo lugar, se intentó desreferenciar manualmente el campo interno (socket.0) en cada lugar de llamada a FFI. Este enfoque evitó suposiciones de diseño pero resultó ser muy propenso a errores y verboso, rompiendo efectivamente la barrera de abstracción y permitiendo que enteros en bruto y no tipados se propagaran sin control a través de la base de código.
En tercer lugar, se aplicó #[repr(transparent)] a NetlinkSocket. Esta garantía aseguró la equivalencia de ABI con i32 mientras preservaba la distinción de tipos dentro de Rust, permitiendo que el struct se pasara sin problemas a C sin lógica de desempaquetado o conversión manual.
El equipo de ingeniería finalmente adoptó #[repr(transparent)], lo que eliminó por completo los pánicos del kernel mientras mantenía una abstracción de costo cero. El envoltorio ahora sirve como una guardia rigurosa en tiempo de compilación dentro de Rust mientras permanece completamente invisible y compatible con el ABI de C.
¿Por qué #[repr(transparent)] prohíbe explícitamente que el único campo no nulo sea un tipo de tamaño cero, y cómo previene esta restricción el comportamiento indefinido en FFI al pasar por valor?
#[repr(transparent)] garantiza que el envoltorio es idéntico a su tipo interno en ABI. Un Tipo de Tamaño Cero (ZST) posee tamaño cero y alineación 1. Si se permitiera que el envoltorio encapsulara exclusivamente un ZST, el struct resultante sería también de tamaño cero; sin embargo, C carece de tipos de tamaño cero y sus convenciones de llamada generalmente esperan al menos un byte de datos para la semántica de "pasar por valor". Pasar un ZST por valor a través de FFI constituye comportamiento indefinido porque C no puede representar o manejar adecuadamente valores de tamaño cero. Esta restricción asegura que el envoltorio siempre mantenga el mismo tamaño y alineación no nulos que su campo subyacente, preservando un ABI bien definido compatible con las expectativas de C.
¿Se puede aplicar #[repr(transparent)] a enums, y qué restricciones rigen la visibilidad del discriminante a través de los límites de FFI?
Sí, #[repr(transparent)] se puede aplicar a enums que contengan exactamente una variante, la cual debe contener exactamente un campo de tamaño no nulo. El enum también debe especificar una representación primitiva explícita (por ejemplo, #[repr(u8)]) para definir el tipo del discriminante. Sin embargo, #[repr(transparent)] garantiza que el diseño final sea idéntico al campo no nulo, eliminando efectivamente el discriminante del ABI. En consecuencia, pasar tal enum a C como el tipo de campo subyacente es seguro, pero intentar acceder o interpretar un valor de discriminante desde C resulta en comportamiento indefinido. Los candidatos a menudo malinterpretan que el discriminante está físicamente ausente del diseño, no meramente oculto o inaccesible.
¿Cómo influye la presencia de PhantomData<T> como un campo adicional en un struct #[repr(transparent)] en la variación y verificación de eliminación sin afectar el ABI?
PhantomData<T> se permite explícitamente como un campo secundario dentro de los structs #[repr(transparent)] porque es de tamaño cero con alineación 1. Si bien no altera el tamaño, la alineación o el ABI del envoltorio (ya que #[repr(transparent)] considera solo el único campo no nulo para el diseño), informa al compilador sobre la relación estructural con el parámetro de tipo T. Esto afecta la variación: por ejemplo, un struct Wrapper<T>(*const T, PhantomData<fn(T)>) será contravariante sobre T debido al marcador PhantomData. Además, permite que el análisis de Drop Check (dropck) reconozca que el struct puede poseer conceptualmente datos de tipo T, previniendo inseguridades cuando T tiene vidas no 'estáticas. Los candidatos a menudo creen erróneamente que PhantomData afecta el diseño de memoria o ignoran su papel esencial en el mantenimiento de invariantes de vida y propiedad para los wrappers de FFI genéricos.