Geschichte der Frage:
Vor RFC 1758 hatte Rust keinen Mechanismus für Nullkosten-Neutypen in FFI. Entwickler verließen sich auf #[repr(C)], das ein deterministisches Layout aufzwang, aber möglicherweise unnötige Auffüllung einführte, oder #[repr(Rust)], das aggressive Compileroptimierungen wie Feldneuanordnung und Nischenexploitation erlaubte. Dies schuf ein grundlegendes Dilemma: Typensicherheit durch Wrapper-Strukturen durchzusetzen versus ABI-Stabilität für ausländische Funktionsaufrufe zu garantieren. #[repr(transparent)] wurde speziell eingeführt, um diese Spannung zu lösen, indem versprochen wurde, dass eine Struktur, die genau ein nicht nullgroßes Feld enthält, ein identisches Speicherlayout, eine identische Ausrichtung und eine identische Aufrufkonvention wie dieses zugrunde liegende Feld besitzt.
Das Problem:
Wenn ein #[repr(Rust)]-Neutyp per Referenz oder Wert an eine ausländische Funktion übergeben wird, die den rohen inneren Typ erwartet (z.B. einen u32-Handle), kann der Compiler die Felder des Wrappers neu anordnen oder Nischenoptimierungen anwenden. Da #[repr(Rust)] keine Stabilitätsgarantien bietet, kann der Wrapper eine andere Größe, Gültigkeit des Bitmusters oder Auffüllung als der innere Typ haben. Dies führt dazu, dass der ausländische C-Code möglicherweise nicht ausgerichteten Speicher liest, ungültige Bitmuster als gültige Zeiger interpretiert oder auf Mülldaten zugreift, was zu sofortigem undefiniertem Verhalten und katastrophaler Speicherbeschädigung an der Grenze führt.
Die Lösung:
#[repr(transparent)] weist den Compiler an, sicherzustellen, dass der Wrapper und sein einzelnes nicht nullgroßes Feld identische Größe, Ausrichtung und ABI teilen, wodurch der Wrapper effektiv zu einer Abstraktion nur zur Compile-Zeit wird. Der Compiler prüft statisch, dass genau ein Feld eine nicht nullgroße Größe hat (erlaubt zusätzliche PhantomData- oder Einheitstypfelder). Dies ermöglicht es, den Wrapper sicher zum inneren Typ zu überführen oder direkt über FFI-Grenzen zu übergeben, ohne Umwandlungsüberkopf, wie unten demonstriert:
#[repr(transparent)] pub struct SocketFd(i32); extern "C" { fn close_socket(fd: i32); } pub fn close(sock: SocketFd) { // Sicher: SocketFd hat identische ABI wie i32 unsafe { close_socket(sock.0); } }
Ein Entwickler integriert eine Rust-Anwendung mit einer Linux-Kernel-Netlink-Socket-API, die über rohe ganzzahlige Dateideskriptoren kommuniziert. Um eine versehentliche Vermischung von Socket-Typen zu verhindern, definieren sie struct NetlinkSocket(i32) als Neutyp. Zunächst mit #[repr(Rust)] markiert, übergeben sie Referenzen auf NetlinkSocket an einen extern "C"-Callback, der einen Zeiger auf i32 erwartet. Während der lokalen Entwicklung scheint dies korrekt zu funktionieren, aber in Release-Bauten, die LTO (Link-Time Optimization) verwenden, wendet der Compiler aggressive Nischenoptimierungen auf NetlinkSocket an, was die Speicherrepräsentation grundlegend ändert. Der C-Kernel-Modul erhält daraufhin einen beschädigten Zeigerwert, der eine kritische Kernel-Panik auslöst.
Drei unterschiedliche Lösungen wurden evaluiert. Zuerst wurde #[repr(C)] in Betracht gezogen, um ein stabiles, deterministisches Layout durchzusetzen. Während dies die Speichersicherheit gewährte, deaktivierte es vorteilhafte Nischenoptimierungen und führte möglicherweise zu Auffüllbytes, die die Strukturgröße unnötig aufblähten und die API-Oberfläche für rein Rust-interne Nutzung komplizierten.
Zweitens wurde versucht, das innere Feld (socket.0) an jedem FFI-Aufrufort manuell dereferenzieren. Dieser Ansatz vermied Layout-Annahmen, stellte sich jedoch als äußerst fehleranfällig und umständlich heraus und brach effektiv die Abstraktionsschranke, wodurch rohe, untypisierte Ganzzahlen unkontrolliert im gesamten Codebestand verbreitet wurden.
Drittens wurde #[repr(transparent)] auf NetlinkSocket angewendet. Diese Garantie stellte die ABI-Äquivalenz zu i32 sicher und bewahrte gleichzeitig die Typdifferenz in Rust, sodass die Struktur nahtlos an C übergeben werden konnte, ohne manuelles Entpacken oder Umwandlungslogik.
Das Ingenieurteam nahm schließlich #[repr(transparent)] an, was die Kernel-Paniken vollständig eliminierte und gleichzeitig eine Nullkostenabstraktion bewahrte. Der Wrapper dient nun als strenger Compile-Zeit-Schutz innerhalb von Rust, während er völlig unsichtbar und mit der C-ABI kompatibel bleibt.
Warum verbietet #[repr(transparent)] ausdrücklich, dass das einzige nicht nullgroße Feld ein nullgroßer Typ ist, und wie verhindert diese Einschränkung undefiniertes Verhalten in FFI beim Passieren durch Wert?
#[repr(transparent)] garantiert, dass der Wrapper ABI-identisch zu seinem inneren Typ ist. Ein Zero-Sized Type (ZST) hat die Größe Null und die Ausrichtung 1. Wenn dem Wrapper erlaubt wäre, ausschließlich einen ZST zu umhüllen, wäre die resultierende Struktur selbst nullgroß; jedoch hat C keine nullgroßen Typen, und seine Aufrufkonventionen erwarten typischerweise mindestens ein Byte an Daten für „Durch-Wert“-Semantik. Ein ZST durch Wert über FFI zu übergeben, stellt undefiniertes Verhalten dar, weil C nullgroße Werte nicht darstellen oder richtig handhaben kann. Diese Einschränkung stellt sicher, dass der Wrapper immer die gleiche nicht nullgroße Größe und Ausrichtung wie das zugrunde liegende Feld beibehält, wodurch ein wohldefiniertes ABI bewahrt wird, das mit den Erwartungen von C kompatibel ist.
Kann #[repr(transparent)] auf Enums angewendet werden, und welche Einschränkungen regeln die Sichtbarkeit des Diskriminanten über FFI-Grenzen hinweg?
Ja, #[repr(transparent)] kann auf Enums angewendet werden, die genau eine Variante enthalten, die selbst genau ein nicht nullgroßes Feld enthalten muss. Das Enum muss auch eine explizite primitive Darstellung angeben (z.B. #[repr(u8)]), um den Diskriminanten-Typ zu definieren. Allerdings garantiert #[repr(transparent)], dass das endgültige Layout identisch mit dem nicht nullgroßen Feld ist, wodurch der Diskriminant aus dem ABI effektiv ausgeblendet wird. Folglich ist es sicher, ein solches Enum an C als den zugrunde liegenden Feldtyp zu übergeben, aber der Versuch, auf einen Diskriminantenwert aus C zuzugreifen oder diesen zu interpretieren, führt zu undefiniertem Verhalten. Kandidaten missverstehen häufig, dass der Diskriminant physisch aus dem Layout abwesend ist, nicht nur versteckt oder unzugänglich.
Wie beeinflusst die Anwesenheit von PhantomData<T> als zusätzliches Feld in einer #[repr(transparent)]-Struktur die Varianz und die Drop-Überprüfung, ohne die ABI zu beeinträchtigen?
PhantomData<T> ist ausdrücklich als sekundäres Feld innerhalb von #[repr(transparent)]-Strukturen erlaubt, da es nullgroß mit Ausrichtung 1 ist. Während es die Größe, Ausrichtung oder ABI des Wrappers nicht ändert (da #[repr(transparent)] nur das einzelne nicht nullgroße Feld für das Layout berücksichtigt), informiert es den Compiler entscheidend über die strukturelle Beziehung zum Typparameter T. Dies beeinflusst die Varianz: Zum Beispiel wird eine Struktur Wrapper<T>(*const T, PhantomData<fn(T)>) wegen des PhantomData-Markers kontravariant über T sein. Darüber hinaus ermöglicht es die Drop Check-Analyse (dropck), zu erkennen, dass die Struktur konzeptionell Daten vom Typ T besitzen kann, wodurch Unsoundness verhindert wird, wenn T keine nicht-'static-Lebensdauern hat. Kandidaten glauben oft fälschlicherweise, dass PhantomData das Speicherlayout beeinflusst oder seine wesentliche Rolle bei der Aufrechterhaltung der Lebensdauer- und Eigentumsinvarianten für generische FFI-Wrapper ignoriert.