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

TODOs

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

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

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.

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.

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.