Ruby Box - Rubys In-Prozess-Trennung von Klassen und Modulen
Ruby Box wurde entwickelt, um getrennte Räume in einem Ruby-Prozess bereitzustellen, um Anwendungscodes, Bibliotheken und Monkey-Patches zu isolieren.
Bekannte Probleme
-
Eine experimentelle Warnung wird angezeigt, wenn Ruby mit
RUBY_BOX=1gestartet wird (spezifizieren Sie die Option-W:no-experimental, um sie zu verbergen). -
Das Installieren nativer Erweiterungen kann unter
RUBY_BOX=1aufgrund einer zu tiefen Stapelaufrufe in extconf.rb fehlschlagen. -
require 'active_support/core_ext'kann unterRUBY_BOX=1fehlschlagen. -
In einer Box definierte Methoden können möglicherweise nicht von in Ruby geschriebenen eingebauten Methoden referenziert werden.
TODOs
-
Fügen Sie die geladene Box zu iseq hinzu, um zu prüfen, ob eine andere Box versucht, die iseq auszuführen (fügen Sie ein Feld nur hinzu, wenn VM_CHECK_MODE?)
-
Weisen Sie in Boxes eigene TOPLEVEL_BINDING zu.
-
Beheben Sie den Aufruf von
warnin Boxes, um auf$VERBOSEundWarning.warnin der Box zu verweisen. -
Machen Sie eine interne Datencontainer-Klasse
Ruby::Box::Entryunsichtbar. -
Weitere Testfälle für
$LOAD_PATHund$LOADED_FEATURES.
Anwendung
Ruby Box aktivieren
Zuerst muss eine Umgebungsvariable beim Start des Ruby-Prozesses gesetzt werden: RUBY_BOX=1. Der einzige gültige Wert ist 1, um Ruby Box zu aktivieren. Andere Werte (oder nicht gesetzte RUBY_BOX) bedeuten die Deaktivierung von Ruby Box. Und das Setzen des Wertes nach dem Start des Ruby-Programms funktioniert nicht.
Ruby Box verwenden
Die Klasse Ruby::Box ist der Einstiegspunkt von Ruby Box.
box = Ruby::Box.new box.require('something') # or require_relative, load
Die erforderliche Datei (entweder .rb oder .so/.dll/.bundle) wird in der Box (hier box) geladen. Die erforderten/geladenen Dateien von something werden rekursiv in der Box geladen.
# something.rb X = 1 class Something def self.x = X def x = ::X end
Klassen/Module, deren Methoden und Konstanten, die in der Box definiert sind, können über das box-Objekt abgerufen werden.
X = 2 p X # 2 p ::X # 2 p box::Something.x # 1 p box::X # 1
Instanzmethoden, die in der Box definiert sind, werden ebenfalls mit den Definitionen in der Box ausgeführt.
s = box::Something.new p s.x # 1
Spezifikationen
Ruby Box-Typen
Es gibt zwei Box-Typen
-
Root-Box
-
Benutzer-Boxen
Es gibt die Root-Box, eine einzige Box in einem Ruby-Prozess. Ruby-Bootstrap läuft in der Root-Box, und alle eingebauten Klassen/Module sind in der Root-Box definiert. (Siehe „Eingebaute Klassen und Module“.)
Benutzer-Boxen dienen dazu, vom Benutzer geschriebene Programme und von Benutzerprogrammen geladene Bibliotheken auszuführen. Das Hauptprogramm des Benutzers (spezifiziert durch das Kommandozeilenargument ruby) wird in der „main“-Box ausgeführt, die eine Benutzer-Box ist, die automatisch am Ende des Ruby-Bootstraps erstellt und von der Root-Box kopiert wird.
Wenn Ruby::Box.new aufgerufen wird, wird eine „optionale“ Box (eine Benutzer-, Nicht-Main-Box) erstellt, die von der Root-Box kopiert wird. Alle Benutzer-Boxen sind flach, von der Root-Box kopiert.
Ruby Box-Klasse und Instanzen
Ruby::Box ist eine Klasse, als Unterklasse von Module. Ruby::Box-Instanzen sind eine Art Module.
In Boxen definierte Klassen und Module
Die Klassen und Module, die neu in einer Box box definiert werden, sind über box zugänglich. Wenn beispielsweise eine Klasse A in box definiert ist, ist sie von außerhalb der Box als box::A zugänglich.
Innerhalb der Box box kann auf A als A (und ::A) verwiesen werden.
Wiedereröffnete eingebaute Klassen und Module in Boxen
In Boxen sind eingebaute Klassen/Module sichtbar und können wiedereröffnet werden. Diese Klassen/Module können mit class- oder module-Klauseln wiedereröffnet werden, und Klassen-/Moduldefinitionen können geändert werden.
Die geänderten Definitionen sind nur in der Box sichtbar. In anderen Boxen funktionieren eingebaute Klassen/Module und deren Instanzen ohne die geänderten Definitionen.
# in foo.rb class String BLANK_PATTERN = /\A\s*\z/ def blank? self =~ BLANK_PATTERN end end module Foo def self.foo = "foo" def self.foo_is_blank? foo.blank? end end Foo.foo.blank? #=> false "foo".blank? #=> false # in main.rb box = Ruby::Box.new box.require('foo') box::Foo.foo_is_blank? #=> false (#blank? called in box) "foo".blank? # NoMethodError String::BLANK_PATTERN # NameError
Die main-Box und box sind verschiedene Boxen, daher sind Monkey-Patches in main auch in box unsichtbar.
Eingebaute Klassen und Module
Im Box-Kontext sind „eingebaute“ Klassen und Module solche,
-
die ohne jegliche
require-Aufrufe in Benutzer-Skripten zugänglich sind. -
die definiert sind, bevor irgendein Benutzerprogramm zu laufen beginnt.
-
die Klassen/Module einschließen, die von prelude.rb geladen wurden (einschließlich RubyGems
Gem, zum Beispiel).
Von nun an werden „eingebaute Klassen und Module“ nur noch als „eingebaute Klassen“ bezeichnet.
Eingebaute Klassen, auf die über Box-Objekte verwiesen wird
Eingebaute Klassen in einer Box box können von anderen Boxen referenziert werden. Zum Beispiel ist box::String eine gültige Referenz, und String und box::String sind identisch (String == box::String, String.object_id == box::String.object_id).
box::String-ähnliche Referenzen geben nur einen String in der aktuellen Box zurück, daher ist seine Definition String in der Box, nicht in box.
# foo.rb class String def self.foo = "foo" end # main.rb box = Ruby::Box.new box.require('foo') box::String.foo # NoMethodError
Klassen-Instanzvariablen, Klassenvariablen, Konstanten
Eingebaute Klassen können zwischen Boxen unterschiedliche Sätze von Klassen-Instanzvariablen, Klassenvariablen und Konstanten haben.
# foo.rb class Array @v = "foo" @@v = "_foo_" V = "FOO" end Array.instance_variable_get(:@v) #=> "foo" Array.class_variable_get(:@@v) #=> "_foo_" Array.const_get(:V) #=> "FOO" # main.rb box = Ruby::Box.new box.require('foo') Array.instance_variable_get(:@v) #=> nil Array.class_variable_get(:@@v) # NameError Array.const_get(:V) # NameError
Globale Variablen
In Boxen sind Änderungen an globalen Variablen ebenfalls in den Boxen isoliert. Änderungen an globalen Variablen in einer Box sind nur innerhalb der Box sichtbar/anwendbar.
# foo.rb $foo = "foo" $VERBOSE = nil puts "This appears: '#{$foo}'" # main.rb p $foo #=> nil p $VERBOSE #=> false box = Ruby::Box.new box.require('foo') # "This appears: 'foo'" p $foo #=> nil p $VERBOSE #=> false
Top-Level-Konstanten
Normalerweise werden Top-Level-Konstanten als Konstanten von Object definiert. In Boxen sind Top-Level-Konstanten Konstanten von Object in der Box. Und die Konstanten des Box-Objekts box sind strikt gleich den Konstanten von Object.
# foo.rb FOO = 100 FOO #=> 100 Object::FOO #=> 100 # main.rb box = Ruby::Box.new box.require('foo') box::FOO #=> 100 FOO # NameError Object::FOO # NameError
Top-Level-Methoden
Top-Level-Methoden sind private Instanzmethoden von Object in jeder Box.
# foo.rb def yay = "foo" class Foo def self.say = yay end Foo.say #=> "foo" yay #=> "foo" # main.rb box = Ruby::Box.new box.require('foo') box::Foo.say #=> "foo" yay # NoMethodError
Es gibt keine Möglichkeit, Top-Level-Methoden in Boxen für andere zugänglich zu machen. (Siehe „Top-Level-Methoden als Methode des Box-Objekts freigeben“ im Abschnitt „Diskussionen“ unten.)
Ruby Box-Geltungsbereiche
Ruby Box arbeitet im Datei-Geltungsbereich. Eine .rb-Datei wird in einer einzigen Box ausgeführt.
Sobald eine Datei in einer Box box geladen wurde, werden alle in der Datei definierten/erstellten Methoden/Procs in box ausgeführt.
Hilfsmethoden
Mehrere Methoden stehen zum Ausprobieren/Testen von Ruby Box zur Verfügung.
-
Ruby::Box.currentgibt die aktuelle Box zurück. -
Ruby::Box.enabled?gibt true/false zurück, um darzustellen, obRUBY_BOX=1spezifiziert wurde oder nicht. -
Ruby::Box.rootgibt die Root-Box zurück. -
Ruby::Box.maingibt die main-Box zurück. -
Ruby::Box#evalwertet einen Ruby-Code (String) im Empfänger-Box aus, ähnlich wie load mit einer Datei.
Implementierungsdetails
ISeq Inline-Methoden-/Konstanten-Cache
Wie oben unter „Ruby Box-Geltungsbereiche“ beschrieben, läuft eine „.rb“-Datei in einer Box. Daher wird die Methoden-/Konstantenauflösung konsistent in einer Box durchgeführt.
Das bedeutet, dass ISeq-Inline-Caches auch mit Boxen gut funktionieren. Andernfalls ist es ein Fehler.
Globaler Cache für Methodenaufrufe (gccct)
Die C-Funktion rb_funcall() bezieht sich auf die globale CC-Cache-Tabelle (gccct), und der Cache-Schlüssel wird mit der aktuellen Box berechnet.
Daher haben rb_funcall()-Aufrufe eine Leistungseinbuße, wenn Ruby Box aktiviert ist.
Aktuelle Box und ladende Box
Die aktuelle Box ist die Box, in der sich der auszuführende Code befindet. Ruby::Box.current gibt das aktuelle Box-Objekt zurück.
Die ladende Box ist eine intern verwaltete Box, um die Box zu bestimmen, in der neu erforderte/geladene Dateien geladen werden sollen. Zum Beispiel ist box die ladende Box, wenn box.require("foo") aufgerufen wird.
Diskussionen
Mehr eingebaute Methoden, die in Ruby geschrieben sind
Wenn Ruby Box standardmäßig aktiviert wäre, könnten eingebaute Methoden in Ruby geschrieben werden, da sie nicht von Benutzer-Monkey-Patches überschrieben werden können. Eingebaute Ruby-Methoden könnten JIT-kompiliert werden, was Leistungsvorteile bringen könnte.
Monkey-Patching von Methoden, die von eingebauten Methoden aufgerufen werden
Eingebaute Methoden rufen manchmal andere eingebaute Methoden auf. Zum Beispiel ruft Hash#map Hash#each auf, um die abzubildenden Einträge abzurufen. Ohne Ruby Box können Ruby-Benutzer Hash#each überschreiben und eine Verhaltensänderung von Hash#map als Ergebnis erwarten.
Aber mit Boxen läuft Hash#map in der Root-Box. Ruby-Benutzer können Hash#each nur in Benutzer-Boxen definieren, daher können Benutzer das Verhalten von Hash#map in diesem Fall nicht ändern. Um dies zu erreichen, sollten Benutzer sowohl Hash#map als auch Hash#each (oder nur Hash#map) überschreiben.
Dies ist eine Breaking Change.
Benutzer können Methoden mit Ruby::Box.root.eval(...) definieren, aber das ist offensichtlich keine ideale API.
Zuweisen von Werten zu globalen Variablen, die von eingebauten Methoden verwendet werden
Ähnlich wie beim Monkey-Patching von Methoden sind globale Variablen, die in einer Box zugewiesen werden, von der Root-Box getrennt. Methoden, die in der Root-Box definiert sind und auf eine globale Variable verweisen, können die neu zugewiesene nicht finden.
Kontext von $LOAD_PATH und $LOADED_FEATURES
Globale Variablen $LOAD_PATH und $LOADED_FEATURES steuern das Verhalten der require-Methode. Daher werden diese Variablen von der ladenden Box und nicht von der aktuellen Box bestimmt.
Dies könnte potenziell mit den Erwartungen des Benutzers kollidieren. Wir sollten eine Lösung finden.
Top-Level-Methoden als Methode des Box-Objekts freigeben
Derzeit sind Top-Level-Methoden in Boxen nicht von außerhalb der Box zugänglich. Aber es könnte einen Anwendungsfall geben, um Top-Level-Methoden anderer Boxen aufzurufen.
Root- und Builtin-Box aufteilen
Derzeit ist die einzelne „Root“-Box die Quelle für Classext CoW. Und auch die „Root“-Box kann nach dem Start der Hauptskriptauswertung zusätzliche Dateien laden, indem sie Methoden aufruft, die Zeilen wie require "openssl" enthalten.
Das bedeutet, dass Benutzer-Boxen je nach Erstellungszeitpunkt unterschiedliche Definitionssätze haben können.
[root] | |----[main] | |(require "openssl" called in root) | |----[box1] having OpenSSL | |(remove_const called for OpenSSL in root) | |----[box2] without OpenSSL
Dies könnte zu unerwarteten Verhaltensunterschieden zwischen Benutzer-Boxen führen. Dies sollte KEIN Problem sein, da Benutzer-Skripte, die auf OpenSSL verweisen, require "openssl" selbst aufrufen sollten. Aber im schlimmsten Fall läuft ein Skript (ohne require "openssl") in box1 gut, aber in box2 nicht. Diese Situation erscheint Benutzern wie ein „zufälliger Fehler“.
Eine mögliche Option, um diese Situation zu verhindern, ist die Existenz von „Root“- und „Builtin“-Boxen.
-
root
-
Die Box für den Ruby-Prozess-Bootstrap, dann die Quelle für CoW.
-
Nach dem Start der Hauptbox wird in dieser Box kein Code ausgeführt.
-
builtin
-
Die Box, die zur gleichen Zeit wie „main“ von der Root-Box kopiert wird.
-
In der „Root“-Box definierte Methoden und Procs werden in dieser Box ausgeführt.
-
Erforderte Klassen und Module werden in dieser Box geladen.
Dieses Design realisiert eine konsistente Quelle für Box CoW.
Separate cc_tbl, callable_m_tbl und cvc_tbl für weniger Classext CoW
Die Felder von rb_classext_t enthalten verschiedene Cache-ähnliche Daten: cc_tbl (Call-Cache-Tabelle), callable_m_tbl (Tabelle aufgelöster ergänzter Methoden) und cvc_tbl (Klassenvariablen-Cache-Tabelle).
Der Classext CoW wird ausgelöst, wenn der Inhalt von rb_classext_t geändert wird, einschließlich cc_tbl, callable_m_tbl und cvc_tbl. Aber diese drei Tabellen werden durch bloßes Aufrufen von Methoden oder Referenzieren von Klassenvariablen geändert. Daher wird der Classext CoW derzeit viel öfter ausgelöst als ursprünglich erwartet.
Wenn wir diese drei Tabellen außerhalb von rb_classext_t verschieben können, wird die Anzahl der kopierten rb_classext_t viel geringer sein als bei der aktuellen Implementierung.