class Ruby::Box
Ruby Box - Trennung von Klassen und Modulen innerhalb eines Ruby-Prozesses
Ruby Box wurde entwickelt, um getrennte Bereiche in einem Ruby-Prozess bereitzustellen, um Anwendungscodes, Bibliotheken und Monkey-Patches zu isolieren.
Bekannte Probleme
-
Beim Starten von Ruby mit
RUBY_BOX=1wird eine experimentelle Warnung angezeigt (verwenden Sie die Option-W:no-experimental, um sie auszublenden). -
Die Installation nativer Erweiterungen schlägt möglicherweise unter
RUBY_BOX=1fehl, da die Stacktiefe in extconf.rb zu groß ist. -
require 'active_support/core_ext'kann unterRUBY_BOX=1fehlschlagen. -
In einer Box definierte Methoden können möglicherweise nicht von integrierten Methoden, die in Ruby geschrieben sind, 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 nur ein Feld hinzu, wenn VM_CHECK_MODE?).
-
Weisen Sie den Boxen ihr eigenes TOPLEVEL_BINDING zu.
-
Beheben Sie das Aufrufen von
warnin Boxen, um$VERBOSEundWarning.warnin der Box zu referenzieren. -
Machen Sie eine interne Datencontainerklasse
Ruby::Box::Entryunsichtbar. -
Weitere Testfälle für
$LOAD_PATHund$LOADED_FEATURES.
Verwendung
Aktivieren von Ruby Box
Zuerst muss beim Booten des Ruby-Prozesses eine Umgebungsvariable 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. Das Setzen des Wertes nach dem Start des Ruby-Programms funktioniert nicht.
Verwendung von Ruby Box
Die Klasse Ruby::Box ist der Einstiegspunkt für Ruby Box.
box = Ruby::Box.new box.require('something') # or require_relative, load
Die benötigte Datei (entweder .rb oder .so/.dll/.bundle) wird in der Box (hier box) geladen. Die benötigten/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 zugegriffen 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. Der Ruby-Bootstrap wird in der Root-Box ausgeführt, und alle integrierten Klassen/Module sind in der Root-Box definiert. (Siehe „Integrierte Klassen und Module“.)
Benutzer-Boxen dienen zur Ausführung von benutzergeschriebenen Programmen und Bibliotheken, die von Benutzerprogrammen geladen werden. Das Hauptprogramm des Benutzers (angegeben durch das ruby-Kommandozeilenargument) wird in der „Haupt“-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-Box, nicht die Haupt-Box) erstellt, die von der Root-Box kopiert wird. Alle Benutzer-Boxen sind flach, kopiert von der Root-Box.
Ruby Box-Klasse und -Instanzen
Ruby::Box ist eine Klasse, als Unterklasse von Module. Ruby::Box-Instanzen sind eine Art von Module.
Klassen und Module, die in Boxen definiert sind
Die Klassen und Module, die neu in einer Box box definiert sind, 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.
In der Box box kann A als A (und ::A) referenziert werden.
Integrierte Klassen und Module, die in Boxen wiedereröffnet werden
In Boxen sind integrierte 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 integrierte Klassen/Module und deren Instanzen ohne geänderte 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 Haupt-Box und box sind unterschiedliche Boxen, daher sind Monkey-Patches in der Haupt-Box auch in box unsichtbar.
Integrierte Klassen und Module
Im Box-Kontext sind „integrierte“ Klassen und Module Klassen und Module
-
Zugänglich ohne jegliche
require-Aufrufe in Benutzerskripten. -
Definiert, bevor irgendein Benutzerprogramm mit der Ausführung beginnt.
-
Einschließlich Klassen/Module, die von prelude.rb geladen wurden (einschließlich RubyGems
Gem, zum Beispiel).
Ab hier werden „integrierte Klassen und Module“ einfach als „integrierte Klassen“ bezeichnet.
Integrierte Klassen, die über Box-Objekte referenziert werden
Integrierte 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 die 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
Klasseninstanzvariablen, Klassenvariablen, Konstanten
Integrierte Klassen können unterschiedliche Sätze von Klasseninstanzvariablen, Klassenvariablen und Konstanten zwischen Boxen 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/wirksam.
# 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 streng 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 freizugeben. (Siehe „Top-Level-Methoden als Methode des Box-Objekts freigeben“ im Abschnitt „Diskussionen“ unten).
Ruby Box-Geltungsbereiche
Ruby Box arbeitet im Dateigeltungsbereich. 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 für das Ausprobieren/Testen von Ruby Box zur Verfügung.
-
Ruby::Box.currentgibt die aktuelle Box zurück. -
Ruby::Box.enabled?gibttrue/falsezurück, um darzustellen, obRUBY_BOX=1angegeben wurde oder nicht. -
Ruby::Box.rootgibt die Root-Box zurück. -
Ruby::Box.maingibt die Haupt-Box zurück. -
Ruby::Box#evalwertet einen Ruby-Code (String) in der Empfänger-Box aus, ähnlich wie ein Aufruf vonloadmit einer Datei.
Implementierungsdetails
ISeq Inline-Methoden-/Konstanten-Cache
Wie oben in „Ruby Box-Geltungsbereiche“ beschrieben, wird eine „.rb“-Datei in einer Box ausgeführt. 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.
Methodenaufruf-Global-Cache (gccct)
Die C-Funktion rb_funcall() verweist 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 Ladebox
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 Ladebox ist eine intern verwaltete Box, um die Box zu bestimmen, in der neu angeforderte/geladene Dateien geladen werden. Wenn beispielsweise box.require("foo") aufgerufen wird, ist box die Ladebox.
Diskussionen
Mehr integrierte Methoden, die in Ruby geschrieben sind
Wenn Ruby Box standardmäßig aktiviert ist, können integrierte Methoden in Ruby geschrieben werden, da sie nicht durch Benutzer-Monkey-Patches überschrieben werden können. Integrierte Ruby-Methoden können JIT-kompiliert werden, was zu Leistungsvorteilen führen könnte.
Monkey-Patching von Methoden, die von integrierten Methoden aufgerufen werden
Integrierte Methoden rufen manchmal andere integrierte Methoden auf. Zum Beispiel ruft Hash#map Hash#each auf, um die zu mappenden Einträge abzurufen. Ohne Ruby Box können Ruby-Benutzer Hash#each überschreiben und Verhaltensänderungen von Hash#map als Ergebnis erwarten.
Aber mit Boxen wird Hash#map in der Root-Box ausgeführt. Ruby-Benutzer können Hash#each nur in Benutzer-Boxen definieren, sodass Benutzer das Verhalten von Hash#map in diesem Fall nicht ändern können. 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 mithilfe von Ruby::Box.root.eval(...) definieren, aber dies ist eindeutig keine ideale API.
Zuweisen von Werten zu globalen Variablen, die von integrierten Methoden verwendet werden
Ähnlich wie beim Monkey-Patching von Methoden sind globale Variablen, denen in einer Box ein Wert zugewiesen wird, von der Root-Box getrennt. Methoden, die in der Root-Box definiert sind und eine globale Variable referenzieren, können die neu zugewiesene Variable 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 Ladebox und nicht von der aktuellen Box bestimmt.
Dies könnte potenziell zu Konflikten mit den Erwartungen des Benutzers führen. Wir sollten eine Lösung finden.
Top-Level-Methoden als Methode des Box-Objekts freigeben
Derzeit sind Top-Level-Methoden in Boxen von außerhalb der Box nicht zugänglich. Es kann jedoch Anwendungsfälle geben, in denen die Top-Level-Methoden anderer Boxen aufgerufen werden sollen.
Root- und integrierte Box aufteilen
Derzeit ist die einzelne „Root“-Box die Quelle für Klassen-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 kann zu unerwarteten Verhaltensunterschieden zwischen Benutzer-Boxen führen. Dies sollte KEIN Problem sein, da Benutzer-Skripte, die OpenSSL referenzieren, require "openssl" selbst aufrufen sollten. Aber im schlimmsten Fall läuft ein Skript (ohne require "openssl") in box1 gut, aber nicht in box2. Diese Situation erscheint Benutzern wie ein „zufälliger Fehler“.
Eine mögliche Option, um diese Situation zu verhindern, ist die Verwendung von „Root“- und „integrierten“ Boxen.
-
root
-
Die Box für den Ruby-Prozess-Bootstrap, dann die Quelle für CoW.
-
Nachdem die Haupt-Box gestartet wurde, wird in dieser Box kein Code mehr ausgeführt.
-
integriert
-
Die Box, die zur gleichen Zeit wie „Haupt“ von der Root-Box kopiert wird.
-
In dieser Box werden Methoden und Procs ausgeführt, die in der „Root“-Box definiert sind.
-
Klassen und Module, die benötigt werden, werden in dieser Box geladen.
Dieses Design realisiert eine konsistente Quelle für Box-CoW.
Trennung von cc_tbl und callable_m_tbl, 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 komplementierter Methoden) und cvc_tbl (Klassenvariablen-Cache-Tabelle).
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 einfaches Aufrufen von Methoden oder Referenzieren von Klassenvariablen geändert. Daher wird 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.
Öffentliche Klassenmethoden
Source
static VALUE
rb_box_s_current(VALUE recv)
{
const rb_box_t *box;
if (!rb_box_available())
return Qnil;
box = rb_vm_current_box(GET_EC());
VM_ASSERT(box && box->box_object);
return box->box_object;
}
Gibt die aktuelle Box zurück. Gibt nil zurück, wenn Ruby Box nicht aktiviert ist.
Source
static VALUE
rb_box_s_getenabled(VALUE recv)
{
return RBOOL(rb_box_available());
}
Gibt true zurück, wenn Ruby::Box aktiviert ist.
Source
static VALUE
box_initialize(VALUE box_value)
{
rb_box_t *box;
rb_classext_t *object_classext;
VALUE entry;
ID id_box_entry;
CONST_ID(id_box_entry, "__box_entry__");
if (!rb_box_available()) {
rb_raise(rb_eRuntimeError, "Ruby Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box.");
}
entry = rb_class_new_instance_pass_kw(0, NULL, rb_cBoxEntry);
box = get_box_struct_internal(entry);
box->box_object = box_value;
box->box_id = box_generate_id();
rb_define_singleton_method(box->load_path, "resolve_feature_path", rb_resolve_feature_path, 1);
// Set the Ruby::Box object unique/consistent from any boxes to have just single
// constant table from any view of every (including main) box.
// If a code in the box adds a constant, the constant will be visible even from root/main.
RCLASS_SET_PRIME_CLASSEXT_WRITABLE(box_value, true);
// Get a clean constant table of Object even by writable one
// because ns was just created, so it has not touched any constants yet.
object_classext = RCLASS_EXT_WRITABLE_IN_BOX(rb_cObject, box);
RCLASS_SET_CONST_TBL(box_value, RCLASSEXT_CONST_TBL(object_classext), true);
rb_ivar_set(box_value, id_box_entry, entry);
return box_value;
}
Gibt ein neues Ruby::Box-Objekt zurück.
Öffentliche Instanzmethoden
Source
static VALUE
rb_box_eval(VALUE box_value, VALUE str)
{
const rb_iseq_t *iseq;
const rb_box_t *box;
StringValue(str);
iseq = rb_iseq_compile_iseq(str, rb_str_new_cstr("eval"));
VM_ASSERT(iseq);
box = (const rb_box_t *)rb_get_box_t(box_value);
return rb_iseq_eval(iseq, box);
}
Source
static VALUE
rb_box_inspect(VALUE obj)
{
rb_box_t *box;
VALUE r;
if (obj == Qfalse) {
r = rb_str_new_cstr("#<Ruby::Box:root>");
return r;
}
box = rb_get_box_t(obj);
r = rb_str_new_cstr("#<Ruby::Box:");
rb_str_concat(r, rb_funcall(LONG2NUM(box->box_id), rb_intern("to_s"), 0));
if (BOX_ROOT_P(box)) {
rb_str_cat_cstr(r, ",root");
}
if (BOX_USER_P(box)) {
rb_str_cat_cstr(r, ",user");
}
if (BOX_MAIN_P(box)) {
rb_str_cat_cstr(r, ",main");
}
else if (BOX_OPTIONAL_P(box)) {
rb_str_cat_cstr(r, ",optional");
}
rb_str_cat_cstr(r, ">");
return r;
}
Source
static VALUE
rb_box_load(int argc, VALUE *argv, VALUE box)
{
VALUE fname, wrap;
rb_scan_args(argc, argv, "11", &fname, &wrap);
rb_vm_frame_flag_set_box_require(GET_EC());
VALUE args = rb_ary_new_from_args(2, fname, wrap);
return rb_load_entrypoint(args);
}
Source
static VALUE
rb_box_load_path(VALUE box)
{
VM_ASSERT(BOX_OBJ_P(box));
return rb_get_box_t(box)->load_path;
}
Gibt den box-lokalen Suchpfad zurück.
Source
static VALUE
rb_box_require(VALUE box, VALUE fname)
{
rb_vm_frame_flag_set_box_require(GET_EC());
return rb_require_string(fname);
}
Source
static VALUE
rb_box_require_relative(VALUE box, VALUE fname)
{
rb_vm_frame_flag_set_box_require(GET_EC());
return rb_require_relative_entrypoint(fname);
}