Was ist `std::pin::Pin` in Rust?
(vrong.me)std::pin::Pindrückt eine Garantie auf Typebene aus, dass der Wert, auf den ein Pointer zeigt, nicht über diesen Pointer verschoben wird; das ist nötig für Werte, deren Adresse stabil bleiben muss, etwa bei Typen mit Selbstreferenzen- Bei
async/awaitkönnen lokale Variablen und Referenzen, die über.awaithinaus weiterleben, zu Feldern der vom Compiler erzeugten State Machine werden; damit ein Future nach dem Polling nicht verschoben wird, verlangtFuture::polldaherPin Pinverhindert, dass ein gepinnter Wert in sicherem Code verschoben wird, verbietet aber keine allgemeinen Änderungen; wennT: Unpinnicht gilt, kann man ausPinnicht sicher ein&mut Therausziehen- Die meisten Rust-Typen sind standardmäßig Unpin, daher müssen selbstreferenzielle Structs, die nicht verschoben werden dürfen, meist durch ein
PhantomPinned-Feld zu!Unpingemacht werden - In der Praxis verwendet man
Box::pinoderstd::pin::pin!, wenn man Futures direktpollt oder sie an APIs übergibt, die ein gepinntes Future verlangen; werFutureoder Low-Level-Async-Primitiven selbst implementiert, muss sich außerdem mitunsafe-Invarianten befassen
Warum Pin nötig ist
std::pin::Pinist ein Pointer-Wrapper, der garantiert, dass der Wert, auf den ein Pointer zeigt, nicht über diesen Pointer verschoben wird- Das Kernproblem entsteht bei selbstreferenziellen Typen
- Das Beispiel-Struct
SelfRefbesitztdata: i32undptr: *const i32, wobeiptraufself.datazeigt - Wenn eine Struct-Instanz in eine andere Variable verschoben oder aus einer Funktion zurückgegeben wird, kann sich ihre Speicheradresse ändern
- Der rohe Pointer
ptrzeigt dann weiter auf die frühere Speicheradresse und wird zu einem dangling pointer
- Das Beispiel-Struct
- Sobald eine Selbstreferenz eingerichtet ist, braucht man einen Mechanismus, der verhindert, dass der betreffende Wert erneut verschoben wird
Probleme bei async/await und Future
async/awaitund Future sind typische Bereiche, in denenPinhäufig auftaucht- Lokale Variablen, die über einen
.await-Punkt hinaus weiterleben, werden zu Feldern der vom Compiler erzeugten State Machine - Wenn eine Referenz auf eine lokale Variable ebenfalls über denselben
.awaithinauslebt, kann das erzeugte Future selbstreferenziell sein - Nachdem das Polling begonnen hat, kann ein Future von Referenzen abhängen, die auf andere interne Felder zeigen
- Wird das Future in diesem Zustand verschoben, werden diese Referenzen ungültig
- Um das zu verhindern, nimmt
Future::pollstatt&mut selfeinPinentgegen
pub trait Future {
type Output;
fn poll(self: Pin, cx: &mut Context Pin {
pub const fn get_mut(self) -> &'a mut T
where
T: Unpin
{ ... }
}
- Wenn ein Typ
Unpinnicht implementiert, also!Unpinist, kann man mit sicherem Code kein&mut Terhalten - In diesem Fall muss man unsafe-Methoden wie
Pin::get_unchecked_mutverwenden, und der Code muss garantieren, dass der Wert nicht aus dieser Referenz heraus verschoben wird
Unpin und PhantomPinned
- Typen, die
Unpinimplementieren, sind für ihre Speichersicherheit nicht auf Pinning angewiesen
// std::marker
pub auto trait Unpin {}
- Die meisten Rust-Typen können problemlos verschoben werden und sind daher standardmäßig
Unpin- Zum Beispiel:
i32,String,Vec
- Zum Beispiel:
Unpinwird automatisch für alle Typen implementiert, solange sie nicht ausdrücklich zu!Unpingemacht werdenstd::marker::PhantomPinnedist ein Marker-Struct, das ausdrücklich!Unpinist- Da sich Auto-Traits automatisch fortpflanzen, wird auch ein Struct mit einem
PhantomPinned-Feld automatisch zu!Unpin
- Da sich Auto-Traits automatisch fortpflanzen, wird auch ein Struct mit einem
use std::marker::PhantomPinned;
struct SelfRef {
data: i32,
ptr: *const i32,
_phantom: PhantomPinned, // makes the entire struct !Unpin
}
- Das ist die Standardmethode, um zu deklarieren, dass ein benutzerdefiniertes Struct nach dem Pinning nicht mehr sicher verschoben werden kann
- Der Compiler kann Selbstreferenzen, die typischerweise mit
unsafeund rohen Pointern gebaut werden, normalerweise nicht automatisch erkennen - Daher muss der Entwickler für selbstreferenzielle Structs explizit auf
Unpinverzichten- Üblicherweise geschieht das durch ein
PhantomPinned-Feld
- Üblicherweise geschieht das durch ein
- Wenn ein selbstreferenzieller Typ versehentlich
Unpinbleibt, kann sicherer Code ausPineine mutable Referenz holen und den Wert verschieben- Damit würden die Annahmen des
unsafe-Codes, der die Selbstreferenz aufgebaut hat, verletzt
- Damit würden die Annahmen des
Wie man Pin erzeugt
-
Pinselbst pinnt keinen Wert -
Ein
Pinzu erzeugen bedeutet, nachzuweisen, dass der betreffende pointee während der Lebensdauer des Pin an einer stabilen Speicheradresse bleibt -
Pin::new- Die einfachste Erzeugung ist
Pin::new
let mut value = 42; let pinned = Pin::new(&mut value);- Dieser Konstruktor kann nur verwendet werden, wenn
T: Unpingilt Unpin-Typen hängen nicht von Pinning ab, daher ist das Einhüllen inPinimmer sicher- In diesem Fall ist die Pinning-Garantie faktisch ein no-op
- Die einfachste Erzeugung ist
-
std::pin::pin!- Wenn ein Wert lokal ohne Heap-Allokation gepinnt werden soll, kann das Makro
pin!verwendet werden
use std::pin::pin; let future = pin!(async { println!("Hello"); });- Dieses Makro legt eine lokale Variable an und gibt ein
Pinzurück, das auf diese Variable zeigt - Der Compiler garantiert, dass diese lokale Variable für ihre restliche Lebensdauer nicht verschoben wird; dadurch lassen sich
!Unpin-Werte sicher auf dem Stack pinnen - Anders als der Name vermuten lässt, pinnt
pin!nicht den Stack-Speicher selbst - Es erzeugt nur eine gepinnte Referenz auf eine lokale Variable; verlässt die Variable den Scope, endet auch die Pinning-Garantie
- Wenn ein Wert lokal ohne Heap-Allokation gepinnt werden soll, kann das Makro
-
Box::pin- Für
!Unpin-Typen istBox::pinder häufigste Konstruktor
let pinned = Box::pin(SelfRef { ... });- Während
pin!ein an eine lokale Variable gebundenesPinerzeugt, liefertBox::pineinPin, das einerBoxgehört - Da sich die Heap-Allokation selbst nicht bewegt, hat der pointee während der Lebensdauer der
Boxeine stabile Speicheradresse - Selbst wenn die
Boxselbst verschoben wird, wird der enthaltene Wert nicht verschoben; nur der Pointer in derBoxbewegt sich - Die Heap-Allokation bleibt an derselben Adresse
- Für
-
Pin::new_unchecked- Wenn ein sicherer Konstruktor nicht beweisen kann, dass ein Wert an Ort und Stelle bleibt, kann
Pindirekt mitunsafeerzeugt werden
let pinned = unsafe { Pin::new_unchecked(ptr) };- Wer
Pin::new_uncheckedaufruft, verspricht, dass der pointee während der Lebensdauer des zurückgegebenenPinüber keinen Pointer mehr verschoben wird - Wird dieses Versprechen gebrochen, kann in Code, der auf die Pinning-Garantie vertraut, undefiniertes Verhalten entstehen
- Deshalb wird diese Funktion in der Regel nur beim Implementieren von Low-Level-Abstraktionen verwendet, die diese Invariante tatsächlich einhalten können
- Wenn ein sicherer Konstruktor nicht beweisen kann, dass ein Wert an Ort und Stelle bleibt, kann
Wann man sich tatsächlich darum kümmern muss
- Für die meisten Rust-Entwickler arbeiten
PinundUnpinunauffällig im Hintergrund - Wirklich relevant wird es meist in zwei Fällen
- Async-Code verwenden: Wenn man ein Future direkt
pollt oder an eine API übergibt, die ein gepinntes Future verlangt, pinnt man es mitBox::pin(future)auf dem Heap oder mitstd::pin::pin!(future)lokal auf dem Stack Futureselbst implementieren: Wer eigene State Machines oder Low-Level-Async-Primitiven schreibt, muss mitPinarbeiten und braucht möglicherweisePhantomPinnedsowieunsafe-Code, um die Pinning-Invarianten einzuhalten
- Async-Code verwenden: Wenn man ein Future direkt
Pinist Rusts zero-cost-Lösung für adresssensitive Typen- Damit kann Rust
async/awaitund andere selbstreferenzielle Abstraktionen nutzen und zugleich Speichersicherheit ohne Garbage Collector garantieren
1 Kommentare
Meinungen auf Lobste.rs
std::pin::Pinist so etwas wie die Monad der Rust-Welt. Hat man es einmal verstanden, kann man kaum anders, als einen Blogpost darüber zu schreibenEs wäre gut, ein paar Dinge anzusprechen, über die ich und andere beim Versuch,
Pinzu verstehen, gestolpert sindDer Name
Unpinist nicht besonders gut. Genauere, aber ebenfalls unschöne Namen wärenMovableWhenPinnedoderPinIsNoOpgewesenDie doppelte Verneinung
!Unpinin nightly wirkt seltsam, aber um bestehende Typen als 99%-Standardfall beizubehalten, musste ein Auto-TraitUnpinhinzugefügt werden, aus dem ein Typ aussteigen kann. Wenn man es als!MovableWhenPinnedliest, ergibt es mehr SinnAuch die stabile Alternative
PhantomPinnedist nicht gut benannt, denn der pinned-Zustand ist ein temporärer Zustand, der durch eine pinned Reference entsteht, und keine Eigenschaft des Typs. Ein alternativer Name wäre etwaPhantomNotMovableWhenPinnedgewesenAls ich begann, das im Kopf so zu übersetzen, wurde es viel verständlicher. Natürlich ist es immer noch verwirrend; vielleicht hatte ich einfach Glück
!UnpinKopfschmerzen bereitet, aber seit ichUnpinalsSafeToUnpinlese, ist es etwas angenehmer gewordenIch habe diese Frage früher schon einmal gestellt, und ich glaube, jemand hat sie sehr durchdacht beantwortet, aber ich erinnere mich nicht mehr. So wie ich
Pinverstanden habe, entstand es aus async: Das Problem war, dass Referenzen auf lokale Variablen innerhalb eines Datenblocks, der die Zustandsmaschine einer bestimmten Funktion darstellt, selbstreferenziell werdenWenn der async-Zustand verschoben wird, zeigen diese Referenzen auf lokale Variablen danach auf die alte, falsche Position
Aber ist das nicht nur deshalb so, weil Referenzen echte Pointer mit vollständigen absoluten Adressen sind? Ich frage mich, warum die Lösung darin bestand, die Beweglichkeit zu entfernen, statt Referenzen relativ zu machen
Ich frage mich, ob die Antwort im Wesentlichen lautet: „Weil Millionen Engineering-Jahre in Compiler, CPUs und Betriebssysteme geflossen sind, damit sie sehr gut mit Pointern umgehen können, sind Pointer in vielerlei Hinsicht besser, und deshalb ist es besser, überall
Pinzu verwenden“ – oder ob es harte Gründe gibt, warum relative Referenzen als Alternative tatsächlich nicht funktionierenWenn Referenzen relativ wären, müssten diese Typen je nachdem, ob sie in einem async-Zustand verwendet werden oder nicht, eine andere Speicherrepräsentation haben, und man bräuchte außerdem ein Konzept eines Basispointers, der mitgeführt werden muss, um aus der relativen Referenz wieder einen echten Pointer zu rekonstruieren
Verschachtelte Objekte innerhalb einer pinned Reference können weiterhin frei verschoben werden, selbst wenn das Root-Objekt pinned ist; daher kann man auch nicht sagen, dass all diese hypothetischen relativen Referenzen relativ zum selben Basispointer sind
Am Ende braucht man absolute Pointer, und relative Referenzen passen nicht gut. Was wäre dann damit, dass der Rust-Compiler ja alle Typen hier kennt und daher den gesamten Objektgraphen verfolgt, um Referenzen auf verschobene Objekte auf deren neue Position zu aktualisieren und die Objekte so beweglich zu machen? Dann hätte man im Grunde einen Tracing-Garbage-Collector gebaut
Außerdem kennt der Rust-Compiler nicht alle Typen im Objektgraphen. Referenzen können über FFI weitergegeben werden, und eine externe Bibliothek kann diese Referenz speichern. Bewegte Referenzen über FFI-Grenzen hinweg zu reparieren, ist praktisch ein kaum handhabbares Problem
Deshalb ist das wirklich knifflig. Wichtig ist auch, dass das Verschieben von Objekten selbst eine vergleichsweise neue Technik ist. In den meisten C/C++-Programmen kann man davon ausgehen, dass alle Objekte implizit pinned sind. Dass pinning dort weniger diskutiert wird, liegt daran, dass Objekte einfach nicht verschoben werden oder dass es bei Verschiebungen in der Verantwortung der Programmierer liegt, keine dangling References zurückzulassen
Pinist auch für die Interoperabilität mit anderen Sprachen nötig, in denen Rust Speicher nicht einfach wie einen opaken Haufen Bits beliebig verschieben kannNach meinem Verständnis ist eines der Probleme bei der C++-Interoperabilität, dass Objekte keine einfachen Bitblöcke sind, die sich frei verschieben lassen; am Ende brauchen ziemlich viele Typen pinning, was die Benutzung umständlich macht
Allerdings basiert das auf Gesprächen mit Leuten, die vor mindestens etwa sechs Monaten daran gearbeitet haben; ich weiß nicht, wie sehr sich die Lage seitdem verbessert hat
Insgesamt ist das, zusätzlich zur offiziellen Rust-Dokumentation, eine gut lesbare Erklärung. Der Einstieg in das Problem ist etwas sanfter
Allerdings finde ich, dass der Einstieg über selbstreferenzielle Structs eher mehr verwirrt, als wenn man ihn wegließe. Besonders der Satz in der Einleitung „Daher brauchen wir, nachdem eine solche Selbstreferenz erzeugt wurde, eine Möglichkeit, das Verschieben von
SelfRefzu verhindern“ ließ mich eher an das Problem denken, „Bewegung vollständig zu verhindern“, statt an den eigentlichen KernDer eigentliche Kern steht viel später: „
Pinverhindert nicht, dass ein Wert physisch verschoben wird. Stattdessen ist es eine Garantie auf Typebene, dass der Wert nicht über diesen Pointer verschoben wird“Da man das Verschieben selbst nicht verhindern kann, verwendet man
Pin, um selbstreferenzielle Daten in sicheren APIs nur hinter exklusiven Referenzen offenzulegen. Vielleicht verstehe ichPinschon zu gut, aber mit etwas Feinschliff an der Erklärung würden Leser vermutlich weniger herumirrenEr stammt aus meinen Notizen zu pinning, und anfangs habe ich es selbst so verstanden. Dass man ein Problem wie „Verschieben verhindern“ mit einer Garantie auf Typebene lösen kann, fand ich schön
Natürlich ist das nicht das, was
Pintatsächlich tut, also ist es richtig, den Text so zu ändern, dass das deutlich wirdIrgendwo in diesem Artikel sollte erwähnt werden, dass
!UnPinnur in nightly Rust ausdrückbar ist. Das ist der Hauptgrund, warumPhantomPinnedexistiertEs heißt „Pointer-Wrapper“, aber selbst in Rust hat man kaum je mit Pointern zu tun. Ich weiß nicht, warum man das verwenden sollte
Zu
*constfindet man über Google nur schwer Rust-Dokumentation; ich frage mich, ob es dokumentiert istMuss man auch wissen, dass etwas „zu einem Feld der vom Compiler erzeugten Zustandsmaschine wird“? Oder will ein absurder Compilerfehler einem sagen, dass genau das passiert ist?
Passiert „das erzeugte Future wird selbstreferenziell“ auch implizit, wenn man Futures verwendet?
Future::pollhabe ich, glaube ich, nie direkt benutztEinerseits heißt es: „Sicherer Code kann kein normales
&mut Twiederherstellen“, andererseits „gewöhnliche Änderungen sind erlaubt“ – wie soll das dann funktionieren?Wegen solcher Dinge habe ich aufgehört, mich weiter in Rust zu vertiefen
Es stimmt aber, dass man sie kaum braucht, wenn man nicht auf Low-Level-Ebene arbeitet. Ich selbst bin erst darauf gestoßen, als ich eine C-Bibliothek aufrufen wollte
Future::pollist die Grundlage von asynchronem Rust-Code. Man ruft es nicht direkt auf; der Executor ruft es auf. Rust hat keinen Standard-Executor, daher muss man etwas wie Tokio, smol oder pollster hinzufügen, und diese verwenden Methoden wiepoll, die imFuture-Trait definiert sind, um Arbeit zu erledigenDokumentation gibt es an mehreren Stellen, unter anderem hier
Zu erwarten, dass andere ausschließlich das erklären, was man selbst gebraucht hat, ist ein bisschen viel verlangt
Ich bin mir nicht sicher, was mit „wie soll das dann funktionieren?“ genau gefragt ist