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

TODOs

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

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,

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.

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.

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.