Geschiedenis van de vraag:
Voorafgaand aan RFC 1758 had Rust geen mechanisme voor kostenloze newtypes in FFI. Ontwikkelaars vertrouwden op #[repr(C)], wat een deterministisch schema oplegt, maar mogelijk onnodige padding introduceert, of #[repr(Rust)], wat agressieve compileroptimalisaties zoals veldherordening en niche-exploitaties toestaat. Dit creëerde een fundamenteel dilemma: typeveiligheid handhaven via wrapper structs versus ABI-stabiliteit garanderen voor oproepen van externe functies. #[repr(transparent)] werd specifiek geïntroduceerd om deze spanning op te lossen door te beloven dat een struct die precies één niet-vrijstaande veld bevat een identiek geheugenschema, uitlijning en aanroepconventie heeft als dat onderliggende veld.
Het probleem:
Wanneer een #[repr(Rust)] newtype bij verwijzing of waarde aan een externe functie wordt doorgegeven die het ruwe interne type verwacht (bijv. een u32 handle), kan de compiler vrij zijn om de velden van de wrapper te herschikken of niche-optimalisaties toe te passen. Aangezien #[repr(Rust)] geen stabiliteitsgaranties biedt, kan de wrapper een andere grootte, bitpatroonvaliditeit of padding krijgen dan het interne type. Dit leidt ertoe dat de externe C-code potentieel niet-uitgelijnde geheugen leest, ongeldige bitpatronen als geldige pointers interpreteert, of ongeldige gegevens benadert, wat resulteert in onmiddellijke ongedefinieerde werking en catastrofale geheugenbeschadiging aan de grens.
De oplossing:
#[repr(transparent)] instrueert de compiler om af te dwingen dat de wrapper en zijn enkele niet-vrijstaande veld identieke grootte, uitlijning en ABI delen, waardoor de wrapper effectief een compilatietijd-abstrahering wordt. De compiler verifieert statisch dat precies één veld een niet-vrijstaand formaat heeft (met de mogelijkheid van extra PhantomData of eenheidstypevelden). Dit stelt de wrapper in staat om veilig naar het interne type te worden omgezet of direct over FFI-grenzen te worden doorgegeven zonder conversiekosten, zoals hieronder gedemonstreerd:
#[repr(transparent)] pub struct SocketFd(i32); extern "C" { fn close_socket(fd: i32); } pub fn close(sock: SocketFd) { // Veilig: SocketFd heeft identieke ABI als i32 unsafe { close_socket(sock.0); } }
Een ontwikkelaar integreert een Rust-toepassing met een Linux-kernel netlink socket API, die communiceert via ruwe integer bestandsdescriptors. Om per ongeluk mengen van socket-typen te voorkomen, definiëren ze struct NetlinkSocket(i32) als een newtype. Aanvankelijk gemarkeerd met #[repr(Rust)], geven ze verwijzingen naar NetlinkSocket door aan een extern "C" callback die een pointer naar i32 verwacht. Tijdens lokale ontwikkeling lijkt dit correct te functioneren, maar in release-builds die LTO (Link-Time Optimization) gebruiken, past de compiler agressieve niche-optimalisatie toe op NetlinkSocket, wat de geheugenrepresentatie fundamenteel verandert. De C kernelmodule ontvangt vervolgens een beschadigde pointerwaarde, wat leidt tot een kritieke kernelpanic.
Drie verschillende oplossingen werden geëvalueerd. Ten eerste werd #[repr(C)] overwogen om een stabiel, deterministisch schema af te dwingen. Hoewel dit geheugenveiligheid garandeerde, deactiveerde het nuttige niche-optimalisaties en introduceerde mogelijk paddingbytes, wat de structgrootte onnodig vergrootte en de API-oppervlakte voor puur Rust-intern gebruik bemoeilijkte.
Ten tweede werd getracht om handmatig het interne veld (socket.0) op elke FFI aanroep locatie te dereferenceren. Deze aanpak vermijdde lay-outveronderstellingen, maar bleek zeer foutgevoelig en omslachtig, waardoor de abstractiebarrière effectief werd doorbroken en ongetypte gehele getallen ongecontroleerd door de codebase konden voortplanten.
Ten derde werd #[repr(transparent)] toegepast op NetlinkSocket. Deze garantie zorgde voor ABI-gelijkheid met i32 terwijl de typeonderscheiding binnen Rust behouden bleef, waardoor de struct naadloos naar C kon worden doorgegeven zonder handmatige ontvouwing of conversielogica.
Het engineeringteam heeft uiteindelijk #[repr(transparent)] aangenomen, wat de kernelpanics volledig elimineerde terwijl een kostenloze abstractie werd gehandhaafd. De wrapper fungeert nu als een strikte compilatietijdbewaker binnen Rust, terwijl deze volledig onzichtbaar en compatibel blijft met de C ABI.
Waarom verbiedt #[repr(transparent)] expliciet dat het enkele niet-vrijstaande veld een zero-sized type is, en hoe voorkomt deze beperking ongedefinieerde werking in FFI bij doorgeven per waarde?
#[repr(transparent)] garandeert dat de wrapper ABI-identiek is aan zijn interne type. Een Zero-Sized Type (ZST) heeft een afmeting van nul en een uitlijning van 1. Als de wrapper exclusief een ZST zou mogen verpakken, zou de resulterende struct zelf nul-grootte hebben; echter, C heeft geen zero-sized types en zijn aanroepconventies verwachten doorgaans ten minste één byte gegevens voor "doorgeven per waarde" semantiek. Het doorgeven van een ZST per waarde over FFI vormt ongedefinieerde werking omdat C nul-grootte waarden niet kan vertegenwoordigen of correct kan verwerken. Deze beperking garandeert dat de wrapper altijd dezelfde niet-vrijstaande grootte en uitlijning behoudt als zijn onderliggende veld, waardoor een goed gedefinieerde ABI wordt behouden die aansluit bij de verwachtingen van C.
Kan #[repr(transparent)] worden toegepast op enums, en welke beperkingen regeren de zichtbaarheid van de discriminant over FFI-grenzen?
Ja, #[repr(transparent)] kan worden toegepast op enums die precies één variant bevatten, die zelf precies één niet-vrijstaand veld moet bevatten. De enum moet ook een expliciete primitieve weergave specificeren (bijv. #[repr(u8)]) om het type van de discriminant te definiëren. #[repr(transparent)] garandeert echter dat de uiteindelijke lay-out identiek is aan het niet-vrijstaande veld, waardoor de discriminant effectief uit de ABI wordt geëlimineerd. Bijgevolg is het veilig om een dergelijke enum naar C door te geven als het onderliggende veldtype, maar het proberen om een discriminantwaarde vanuit C te benaderen of te interpreteren resulteert in ongedefinieerde werking. Kandidaten begrijpen vaak niet dat de discriminant fysiek afwezig is uit de lay-out, niet slechts verborgen of ontoegankelijk.
Hoe beïnvloedt de aanwezigheid van PhantomData<T> als een extra veld in een #[repr(transparent)] struct variatie en drop-checking zonder de ABI te beïnvloeden?
PhantomData<T> is expliciet toegestaan als een secundair veld binnen #[repr(transparent)] structs omdat het nul-grootte heeft met een uitlijning van 1. Hoewel het de grootte, uitlijning of ABI van de wrapper niet verandert (aangezien #[repr(transparent)] alleen het enkele niet-vrijstaande veld voor lay-out beschouwt), informeert het cruciaal de compiler over de structurele relatie met het typeparameter T. Dit beïnvloedt de variatie: bijvoorbeeld, een struct Wrapper<T>(*const T, PhantomData<fn(T)>) zal tegen variabel zijn over T vanwege de PhantomData-marker. Bovendien stelt het de Drop Check (dropck) analyse in staat om te erkennen dat de struct conceptueel gegevens van het type T kan bezitten, waardoor onveiligheid wordt voorkomen wanneer T niet-'static levenscyclus heeft. Kandidaten geloven vaak ten onrechte dat PhantomData van invloed is op de geheugenlay-out of negeren de essentiële rol ervan in het handhaven van levenscyclus- en eigendomsbeperkingen voor generieke FFI-wrappers.