Historique de la question :
Avant RFC 1758, Rust manquait d'un mécanisme pour des newtypes à coût nul dans FFI. Les développeurs comptaient sur #[repr(C)], qui impose une disposition déterministe mais peut introduire un rembourrage inutile, ou #[repr(Rust)], qui permet des optimisations agressives du compilateur telles que le réordonnancement des champs et l'exploitation de niches. Cela a créé un dilemme fondamental : appliquer la sécurité des types à travers des structs wrapper contre garantir la stabilité ABI pour les appels de fonctions étrangères. #[repr(transparent)] a été introduit spécifiquement pour résoudre cette tension en promettant qu'une struct contenant exactement un champ non de taille nulle possède une disposition mémoire, un alignement et une convention d'appel identiques à ce champ sous-jacent.
Le problème :
Lorsqu'un newtype #[repr(Rust)] est passé par référence ou valeur à une fonction étrangère s'attendant au type brut interne (par exemple, un handle u32), le compilateur demeure libre de réorganiser les champs du wrapper ou d'appliquer des optimisations de niche. Parce que #[repr(Rust)] n'offre aucune garantie de stabilité, le wrapper pourrait acquérir une taille différente, une validité de motif binaire, ou du rembourrage par rapport au type interne. Cela conduit le code C étranger à potentiellement lire une mémoire mal alignée, à interpréter des motifs de bits invalides comme des pointeurs valides, ou à accéder à des données indésirables, entraînant un comportement indéfini immédiat et une corruption catastrophique de la mémoire à la frontière.
La solution :
#[repr(transparent)] indique au compilateur d'appliquer que le wrapper et son unique champ non de taille nulle partagent une taille, un alignement et une ABI identiques, faisant effectivement du wrapper une abstraction uniquement à temps de compilation. Le compilateur vérifie statiquement qu'exactement un champ a une taille non nulle (permettant des champs supplémentaires PhantomData ou de type unitaire). Cela permet au wrapper d'être transmuté en toute sécurité au type interne ou passé directement à travers les frontières FFI sans coût de conversion, comme démontré ci-dessous :
#[repr(transparent)] pub struct SocketFd(i32); extern "C" { fn close_socket(fd: i32); } pub fn close(sock: SocketFd) { // Sûr : SocketFd a une ABI identique à i32 unsafe { close_socket(sock.0); } }
Un développeur intègre une application Rust avec une API de socket netlink du noyau Linux, qui communique via des descripteurs de fichiers entiers bruts. Pour éviter le mélange accidentel des types de socket, ils définissent struct NetlinkSocket(i32) comme un newtype. Initialement marqué avec #[repr(Rust)], ils passent des références à NetlinkSocket à un callback extern "C" s'attendant à un pointeur vers i32. Pendant le développement local, cela semble fonctionner correctement, mais dans les constructions de release utilisant LTO (Optimisation à temps de liaison), le compilateur applique une optimisation agressive de niche à NetlinkSocket, altérant fondamentalement sa représentation mémoire. Le module noyau C reçoit ensuite une valeur de pointeur corrompue, déclenchant une panique critique du noyau.
Trois solutions distinctes ont été évaluées. D'abord, #[repr(C)] a été envisagé pour garantir une disposition stable et déterministe. Bien que cela garantisse la sécurité mémoire, cela désactive les optimisations de niche bénéfiques et peut introduire des octets de rembourrage, gonflant inutilement la taille de la struct et compliquant la surface API pour un usage uniquement interne à Rust.
Deuxièmement, cela a été tenté de déréférencer manuellement le champ interne (socket.0) à chaque point de l'appel FFI. Cette approche a évité les suppositions de disposition mais s'est révélée très sujette aux erreurs et verbeuse, brisant effectivement la barrière d'abstraction et permettant à des entiers bruts et non typés de se propager sans contrôle dans le code.
Troisièmement, #[repr(transparent)] a été appliqué à NetlinkSocket. Cette garantie a assuré l'équivalence ABI avec i32 tout en préservant la distinction de type dans Rust, permettant à la struct d'être passée sans problème à C sans déballage manuel ou logique de conversion.
L'équipe d'ingénierie a finalement adopté #[repr(transparent)], ce qui a complètement éliminé les panics du noyau tout en maintenant une abstraction à coût nul. Le wrapper sert maintenant de garde stricte à temps de compilation dans Rust tout en restant totalement invisible et compatible avec l'ABI C.
Pourquoi #[repr(transparent)] interdit-il explicitement que le champ unique non de taille nulle soit un type de taille nulle, et comment cette restriction empêche-t-elle le comportement indéfini dans FFI lors du passage par valeur ?
#[repr(transparent)] garantit que le wrapper est identique en ABI à son type interne. Un Type de Taille Nulle (ZST) a une taille nulle et un alignement de 1. Si le wrapper était autorisé à envelopper exclusivement un ZST, la struct résultante serait elle-même de taille nulle ; cependant, C n'a pas de types de taille nulle et ses conventions d'appel s'attendent généralement à au moins un octet de données pour des sémantiques de "passage par valeur". Passer un ZST par valeur à travers FFI constitue un comportement indéfini parce que C ne peut pas représenter ou gérer correctement des valeurs de taille nulle. Cette restriction garantit que le wrapper conserve toujours la même taille non nulle et l'alignement que son champ sous-jacent, préservant une ABI bien définie compatible avec les attentes de C.
# #[repr(transparent)] peut-il être appliqué à des énumérations, et quelles contraintes régissent la visibilité du discriminateur à travers les frontières FFI ?**
Oui, #[repr(transparent)] peut être appliqué à des énumérations contenant exactement une variante, qui elle-même doit contenir exactement un champ non de taille nulle. L'énumération doit également spécifier une représentation primitive explicite (par exemple, #[repr(u8)]) pour définir le type du discriminateur. Cependant, #[repr(transparent)] garantit que la disposition finale est identique au champ non nul, éludant effectivement le discriminateur de l'ABI. Par conséquent, passer une telle énumération à C comme le type de champ sous-jacent est sûr, mais tenter d'accéder ou d'interpréter une valeur de discriminateur depuis C entraîne un comportement indéfini. Les candidats comprennent souvent mal que le discriminateur est physiquement absent de la disposition, et pas seulement caché ou inaccessible.
Comment la présence de PhantomData<T> en tant que champ supplémentaire dans une struct #[repr(transparent)] influence-t-elle la variance et la vérification de suppression sans affecter l'ABI ?
PhantomData<T> est explicitement autorisé comme champ secondaire dans les structs #[repr(transparent)] car il est de taille nulle avec un alignement de 1. Bien qu'il n'altère pas la taille, l'alignement ou l'ABI du wrapper (puisque #[repr(transparent)] ne considère que le champ non nul pour la disposition), il informe crucialement le compilateur de la relation structurelle avec le paramètre de type T. Cela affecte la variance : par exemple, une struct Wrapper<T>(*const T, PhantomData<fn(T)>) sera contravariantes par rapport à T en raison du marqueur PhantomData. De plus, cela permet à l'analyse de Drop Check (dropck) de reconnaître que la struct peut conceptuellement posséder des données de type T, empêchant l'insoundness lorsque T porte des durées non 'static. Les candidats croient souvent à tort que PhantomData affecte la disposition mémoire ou ignorent son rôle essentiel dans le maintien des invariants de durée et de propriété pour des wrappers FFI génériques.