- Ante ist der Entwurf einer Systemsprache, die die Flexibilität von Referenzzählung mit der Sicherheit von Borrow Checking verbinden will, ohne Rust-artige Runtime-Panics oder den Overhead von Swifts Laufzeitprüfung auf exklusiven Zugriff
- Der Kernmechanismus besteht aus shape-stability und temporary uniq conversion: Für Felder referenzgezählter Werte lassen sich sicher mutable Borrows erzeugen, während Werte innerhalb von Unions nur in begrenztem Umfang als
uniqbehandelt werden - Rusts
Rc<RefCell<T>>kann bei falscher Nutzung zur Laufzeit panicken, und Swifts Borrowing-System enthält Laufzeitprüfungen auf exklusiven Zugriff; Ante versucht dagegen, einige Fälle über Compile-Time-Regeln abzudecken - Es handelt sich noch um ein nur teilweise implementiertes Work-in-progress-Design; da Typen rekursiv analysiert werden müssen, um zu bestimmen, ob ein bestimmtes Objekt erreichbar ist, kann das Hinzufügen eines Feldes zu einem Breaking API Change werden
- Dieser Ansatz schwächt die Annahme ab, dass shared mutable borrowing grundsätzlich unmöglich sei, und erweitert zusammen mit Techniken wie Vale, group borrowing und Rust GhostCell den Ausnahmebereich im Design von Speichersicherheit
Die Kombination, auf die Ante abzielt
- Ante ist eine Systemprogrammiersprache, die eine einfachere Variante von Rust mit Speichersicherheit und Thread-Sicherheit anstrebt
- Das Grundmodell sind Single Ownership und Borrow Checking; Werte liegen inline auf dem Stack oder innerhalb der enthaltenden Structs bzw. Arrays
- Wenn Einfachheit Vorrang haben soll, kann man durch das Schlüsselwort
sharedan einem Typ Referenzzählung wählen - Eine
balance-Funktion für einen Red-Black-Tree mitshared type Colorundshared type RbTree tist etwa so kurz wie das Python-Beispiel und kleiner als die C++- und Rust-Beispiele - Die zentrale Frage ist, wie sich referenzgezählte Daten mutable borrowen lassen, ohne Rusts Risiko eines
borrow_mut()-Panics oder Swifts Laufzeitprüfung auf exklusiven Zugriff - Ante befindet sich noch im Work-in-progress-Status: Einiges ist implementiert, einiges theoretisch, und auch das Design verändert sich noch
- Den Fortschritt kann man auf der Ante-Website und im Discord verfolgen
shape-stability und mehrere mutable Referenzen
- Antes shape-stability bezeichnet die Idee, dass „eine Referenz auf ein Ziel mit stable shape immer gültig bleibt, egal welche Änderungen anderswo passieren“
- Dieses Konzept macht es möglich, gleichzeitig mehrere mutable Borrow-Referenzen auf dasselbe Struct zu haben
- Im Beispiel
heal (healer: mut Entity) (target: mut Entity)ist einself_heal-Aufruf möglich, bei dem dieselbeEntityals beide Argumente übergeben wird- Selbst wenn
healerundtargetauf dieselbeEntityzeigen, kann dieser Code dieEntitynicht zerstören, sodass beide Referenzen weiterhin gültig bleiben
- Selbst wenn
- Auch mutable Referenzen auf ein Struct selbst, auf seine Felder und auf Felder dieser Felder können gleichzeitig erlaubt sein
- Selbst wenn
ship: mut Spaceshipundengine_alias: mut Engine = ship.enginegleichzeitig verwendet werden, wird angenommen, dass während der Funktionsausführung wedershipnoch die darin enthalteneenginezerstört werden
- Selbst wenn
- Rust und Swift erlauben keine Form, bei der mehrere
&mut-Referenzen gleichzeitig auf dieselben Daten zeigen
Mutable Borrows von Feldern referenzgezählter Werte
- Wenn in Ante einer Typdefinition
sharedvorangestellt wird, wird dieser Typ automatisch referenzgezählt - Im Beispiel
shared mut type SpaceshiphältlauncheinSpaceship, das einemRcentspricht, und übergibtmut ship.engineanset_fuel - Da
launchdas enthaltende ObjektSpaceshipam Leben hält, kann auch dessen Feldengineals lebendig betrachtet werden - Die allgemeine Regel lautet: Für Felder eines
shared mut-Typs kann immer einemut-Borrow-Referenz erzeugt werden- Allerdings lässt sich nicht für jedes Objekt innerhalb dieses Feldes immer ein mutable Borrow erzeugen; dafür braucht es zusätzliche Regeln
- Die folgenden Beispiele verwenden statt des Syntax-Zuckers
shared mut type Spaceshipdie explizitere SchreibweiseRc Spaceshipshared mut type Spaceshipwird zutype Spaceship, undvar ship: Spaceshipwird zuvar ship: Rc Spaceship
Wo Unions Sicherheitsprobleme erzeugen
- Unions können Inhalte inline speichern und dadurch Pointer-Chasing und Cache-Misses reduzieren, was der Geschwindigkeit zugutekommt
- Wenn Cs
union Enginein einemstruct Spaceshipliegt, befinden sichStringTheoryEngineundImpulseEngineim Speicher desSpaceship - Das steht im Gegensatz zu einem Ansatz wie in Java mit Interfaces und Pointern
- Wenn Cs
- Das Problem ist, dass sichere Unterstützung für Unions in speichersicheren Sprachen schwierig ist
- In einem Beispiel, in dem
EngineentwederStringTheoryEngine(str: String)oderImpulseEngine(fuel: I32)ist, kann es zu einem Segfault kommen, wennshipundother_shipauf dasselbeSpaceshipzeigen- Nach
match uniq ship.enginewird eine Referenz auf das Innere des Strings gehalten - Dann wird mit
other_ship.engine := ImpulseEngine 0x42dieselbe Engine in eine andere Variante geändert - Wenn anschließend das alte
strverändert wird, wird Inneres verwendet, nachdem der Container zerstört wurde
- Nach
- Daher muss Ante verhindern, dass aus einer mutable Borrow-Referenz, die auf eine Union zeigt, eine mutable Borrow-Referenz auf eine ihrer Varianten erzeugt wird
- Das ist das Gegenteil der Struct-Regel
- Hat man eine
mut-Referenz auf ein Struct, kann man einemut-Referenz auf ein Feld erzeugen - Hat man eine
mut-Referenz auf eine Union, kann man keinemut-Referenz auf das Innere einer Variante erzeugen
- Hat man eine
uniq und temporary uniq conversion
uniqsteht für exclusive mutable reference, also eine exklusiv mutable Referenz- Wenn eine Variable ein
uniq Spaceshipenthält, ist sie die einzige nutzbare Referenz auf diesesSpaceship- Das ist ein ähnliches Konzept wie Rusts
&mut Spaceship
- Das ist ein ähnliches Konzept wie Rusts
- Um das Innere von Unions sicher zu behandeln, verwendet Ante temporary uniq conversion
- Die Kernregel lautet: Wenn in einem bestimmten Scope keine anderen potenziell aliasenden Referenzen verwendet werden, kann temporär eine
uniq-Referenz erhalten werden- Im Bereich
match uniq ship.enginewird aufship.enginewie überuniqzugegriffen - Während dieses Bereichs verhindert der Compiler die Nutzung anderer bereits existierender Variablen, die indirekt ein
Spaceshipenthalten könnten
- Im Bereich
- Während Rust die Existenz von
uniqschon deshalb verhindert, weil „andere Referenzen irgendwo vorhanden sein könnten“, erlaubt Anteuniqunter der Bedingung, dass diese Referenzen in diesem Scope nicht verwendet werden - Dabei ist
uniq Spaceshiptatsächlich nicht global die einzige Referenz, sondern die einzige in diesem Scope nutzbare Referenz- Die Nuance ähnelt Cs
restrict-Pointer
- Die Nuance ähnelt Cs
Erlaubte und abgelehnte Zugriffe
- Wenn innerhalb des Bereichs
match uniq ship.engineaufother_ship: Rc Spaceshipzugegriffen wird, sollte ein Compile-Fehler entstehenother_ship.enginekönnte ein Alias vonship.enginesein- Und eine Änderung von
other_ship.enginekönnte während der Nutzung vonship.engineeinen Drop auslösen
- Andere Structs, die wie
HasAShipeinRc Spaceshipals Feld enthalten, werden aus demselben Grund abgelehnt- Auch
other.ship.enginekann indirekt dasselbeSpaceshiperreichen
- Auch
- Umgekehrt kann ein Integer wie
new_fuel: I32verwendet werden- Denn
I32kann keine Referenz auf einSpaceshipenthalten
- Denn
- Wenn
Spaceshipselbst ein Feld wiefollow_ship: Rc Spaceshipenthält, wird es abgelehnt- In diesem Fall wäre auch
uniq Spaceshipüber einen Pfad im eigenen Inneren wieder erreichbar; daher kann man rekursive Typen im Allgemeinen nicht vonmutnachuniqkonvertieren
- In diesem Fall wäre auch
Einschränkungen bei Funktionsaufrufen und Rückgaben
- Auch bei Funktionsaufrufen kann eine
mut -> uniq-Konvertierung stattfinden - Wenn
foo (var ship: Rc Spaceship) (new_res: Resonator)maybe_use_resonator ship new_resaufruft, wirdshipan der Aufrufstelle inuniq Spaceshipkonvertiert- Der Compiler muss nur prüfen, ob andere Argumente eine
Spaceship-Referenz enthalten können - Der
Resonatorim Beispiel enthält keine solche Referenz, daher ist es erlaubt
- Der Compiler muss nur prüfen, ob andere Argumente eine
- Bei Rückgaben kann die konvertierte
uniq-Referenz nicht als normaleuniqzurückgegeben werden- Nach der Rückgabe greift die Compilerprüfung „im Scope werden keine aliasfähigen Variablen verwendet“ nicht mehr
- Stattdessen kann der Rückgabetyp als
local uniq Fooangegeben werden- Intern entsteht beim Konvertieren von
mut refzuuniq reftatsächlich immer eine local uniq - In den meisten Fällen kann sie wie eine normale
uniqverwendet werden, bei Rückgaben ist aber eine explizite Angabe nötig
- Intern entsteht beim Konvertieren von
Designkosten und Alternativen
- Ante kann eine referenzgezählte Referenz wie
Rc Spaceshipohne Runtime-Fehler temporär inuniq Spaceshipumwandeln - Der Nachteil ist, dass der Compiler Typen rekursiv untersuchen muss, um Fragen wie „Kann man von
Engineaus einSpaceshiperreichen?“ zu beantworten - Eine solche Analyse kann fragil sein
- Das Hinzufügen eines Feldes zu einem Struct kann zu einem Breaking API Change werden
- Jake, der Entwickler von Ante, sucht nach besseren Wegen, diese Garantie aufrechtzuerhalten
- Einen Ansatz wie group borrowing und Flix references, bei dem jedem shared mutable Typ ein anonymer eindeutiger Brand-Typ zugeordnet wird
- Einen Ansatz, bei dem beim Ändern eines shared Typs ein Effect wie
Mutates 'ahinzugefügt wird, um die Typanalyse zu eliminieren - Einen Ansatz, bei dem Nutzer zur Laufzeit prüfen, ob zwei Referenzen auf unterschiedliche Objekte zeigen, oder bei dem eine safe API eine unsafe Prüfung kapselt
- Einen Ansatz, bei dem der Compiler Werte verfolgt, die nicht indirekt in einem
Rcgespeichert sind und daher nicht aliasen können
- Auch eine Idee ähnlich Ponys iso permission oder eine temporäre Berechtigung, die zwar ins Innere eines Structs schaut, aber keine nach außen zeigenden Referenzen verwenden lässt, bleibt möglich
- Schwierig ist, diese Flexibilität zu erhalten und zugleich Antes Ziele Benutzbarkeit, Lesbarkeit und Einfachheit zu wahren
Der breitere Trend bei Speichersicherheit
- Shared mutable borrowing galt früher als unmöglich; zugrunde liegt die Sichtweise, dass auch Rust auf dieser Annahme entworfen wurde
- Mehrere Ausnahmen häufen sich an
- Ante kann durch Regeln für local uniqueness
uniq-Borrow-Referenzen aus shared-mutable Daten gewinnen - Vale kann über pure functions unveränderliche Borrow-Referenzen aus shared-mutable Daten gewinnen
- group borrowing kann shared-mutable Borrow-Referenzen erzeugen, auch wenn etwas nicht shape-stable ist
- Rusts GhostCell erlaubt Objektgraphen, die frei aufeinander zeigen können, aber zu einem bestimmten Zeitpunkt nur eine mutable Referenz auf eines dieser Objekte haben
- Ante kann durch Regeln für local uniqueness
- Dieser Trend deutet darauf hin, dass es im Design von Speichersicherheit allgemeinere Prinzipien für den Umgang mit shared mutable borrowing geben könnte
Vergleich mit Rusts Cell
- Rust-Nutzer könnten nach dem Unterschied zwischen dem Einfügen von
Cellin Struct-Felder und Antes Ansatz fragen - Im Ante-Beispiel kann man aus
Rc Spaceshipeinemut String-Referenz aufstatus: Stringerhalten und" (refueling)"direkt anhängen - Mit Rusts
Cell<String>-Ansatz kann man ausRc<Spaceship>kein&mut Stringerhalten- Stattdessen muss man mit
status_ref.replace(String::new())einen temporären Standardwert einsetzen - Den herausgenommenen
Stringverändern - Und ihn am Ende mit
replace(status)wieder zurücksetzen
- Stattdessen muss man mit
- Dieser Ansatz hat einige Nachteile
- Es muss eine Standardinstanz wie
""erzeugt werden - Man kann den letzten
replace-Aufruf vergessen - Jemand könnte
statuslesen, während der Wert ersetzt ist
- Es muss eine Standardinstanz wie
- Ante lässt temporär eine Referenz auf den
status-String zu und erzwingt währenddessen per Compiler, dass kein anderer Code darauf zugreift
1 Kommentare
Lobste.rs-Kommentare
Dass man „geteiltes veränderliches Borrowing“ für unmöglich hielt, war nicht einfach nur ein Opfer, das Rust zur Erreichung seiner Ziele in Kauf nahm, sondern liegt eher am Kern von Rusts eigentlichen Zielen
Denn geteilter veränderlicher Zustand erschwert das lokale Schließen über Code
"References are like jumps" von withoutboats behandelt diesen Punkt gut. Zu verhindern, dass Zustand mit Aliasen versehentlich verändert wird, ist zentral dafür, leichter Systeme zu bauen, die korrekt funktionieren, und Rusts Lifetime-Regeln sind nicht bloß ein Mittel, um Garbage Collection zu vermeiden, sondern laut dieser Sicht eine tiefere Struktur, die Inferenzfähigkeit in einer Sprache sicherstellt, die veränderlichen Zustand und aliasierten Zustand zugleich erlaubt
Sieht ziemlich gut aus
Wenn ich es richtig verstanden habe, ist die Magie beim Übergang von einer geteilten Referenz zu einer veränderlichen Referenz möglich, weil sie auf Typen beschränkt ist, die nicht zwischen Threads geteilt werden, und die Eindeutigkeit von
Rcscheint dadurch garantiert zu werden, dass alle Objekte desselben Typs so behandelt werden, als würden sie mit derselben Lifetime geborgtOb explizite oder natürlichere Syntax besser ist, ist vielleicht Geschmackssache, aber es zeigt, dass der Compiler, wenn er mehr über
Cellweiß, veränderliche Referenzen darauf flexibler zulassen kannUnd man vermeidet auch die verwirrende Terminologie in Rust, bei der
mutnicht Veränderlichkeit, sondern Exklusivität/Eindeutigkeit zu bedeuten scheintuniq-Promotion das Erwerben eines Locks?“ Aber ich verstehe es so, dass der Vergleich nicht mitArc, sondern mitRcgemeint istmutExklusivität/Eindeutigkeit bedeutet?Ich frage mich, ob jemand eine Vermutung hat, welches vereinheitlichende Prinzip am Ende angedeutet wurde
Auch die früheren Diskussionen zu Blogposts auf antelang.org sind lesenswert
Ich verstehe nicht wirklich, wie das funktionieren soll. Es klingt so, als hieße es: „Wenn man einen veränderlichen Pointer auf ein Objekt hat, kann man eine veränderliche Referenz auf einen Slice dieses Objekts erhalten“
Wenn das so ist, scheint zum Beispiel so etwas möglich zu sein wie
mutref someobjext = …,mutref subfield = someobjext.a.b,someobjext.a = somethingelse, und dann könntesubfieldungültig werden oder kaputtgehen, weil sich der Wert ändertDer Artikel hatte viele Erklärungen, Vergleiche mit anderen Sprachen und Codebeispiele, aber ich konnte kaum eine Stelle finden, die die Schritt-für-Schritt-Semantik dieses Verhaltens grundlegend erklärt