class Ruby::Box

Ruby Box - Ruby 的进程内类和模块隔离

Ruby Box 旨在提供 Ruby 进程中的隔离空间,以隔离应用程序代码、库和 monkey patches。

已知问题

待办事项

如何使用

启用 Ruby Box

首先,应在 ruby 进程启动时设置环境变量:RUBY_BOX=1。唯一有效的值是 1 以启用 Ruby Box。其他值(或未设置 RUBY_BOX)表示禁用 Ruby Box。并且在 Ruby 程序启动后设置该值无效。

使用 Ruby Box

Ruby::Box 类是 Ruby Box 的入口点。

box = Ruby::Box.new
box.require('something') # or require_relative, load

已加载的文件(.rb 或 .so/.dll/.bundle)将在 box(此处为 box)中加载。从 something 加载的文件将递归地在 box 中加载。

# something.rb

X = 1

class Something
  def self.x = X
  def x = ::X
end

在 box 中定义的类/模块、其方法和常量可以通过 box 对象访问。

X = 2
p X                 # 2
p ::X               # 2
p box::Something.x  # 1
p box::X            # 1

在 box 中定义的实例方法也使用 box 中的定义运行。

s = box::Something.new

p s.x  # 1

规格

Ruby Box 类型

有两种 box 类型

有一个根 box,它是 Ruby 进程中的唯一一个 box。Ruby 引导程序在根 box 中运行,所有内置类/模块都定义在根 box 中。(参见“内置类和模块”。)

用户 box 用于运行用户编写的程序和从用户程序加载的库。用户的 main 程序(由 ruby 命令行参数指定)在“main” box 中执行,这是一个在 Ruby 引导程序结束时自动创建的用户 box,从根 box 复制而来。

当调用 Ruby::Box.new 时,会创建一个“可选” box(一个用户、非 main box),从根 box 复制而来。所有用户 box 都是平坦的,从根 box 复制而来。

Ruby Box 类和实例

Ruby::Box 是一个类,作为 Module 的子类。Ruby::Box 实例是一种 Module

在 box 中定义的类和模块

在 box box 中新定义的类和模块可以通过 box 访问。例如,如果一个类 A 定义在 box 中,则在 box 外部可以将其访问为 box::A

在 box box 中,A 可以被引用为 A(和 ::A)。

在 box 中重新打开的内置类和模块

在 box 中,内置类/模块是可见的,并且可以被重新打开。可以使用 classmodule 子句重新打开这些类/模块,并且可以更改类/模块的定义。

更改后的定义仅在 box 中可见。在其他 box 中,内置类/模块及其实例在没有更改定义的情况下工作。

# 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

main box 和 box 是不同的 box,因此 main 中的 monkey patches 在 box 中也不可见。

内置类和模块

在 box 上下文中,“内置”类和模块是类和模块

此后,“内置类和模块”将仅称为“内置类”。

通过 box 对象引用的内置类

box box 中的内置类可以从其他 box 引用。例如,box::String 是一个有效的引用,而 Stringbox::String 是相同的(String == box::StringString.object_id == box::String.object_id)。

box::String 类的引用在当前 box 中只返回一个 String,因此它在 box 中的定义是 String,而不是在 box 中。

# foo.rb
class String
  def self.foo = "foo"
end

# main.rb
box = Ruby::Box.new
box.require('foo')

box::String.foo  # NoMethodError

类实例变量、类变量、常量

内置类在不同 box 之间可以拥有不同的类实例变量、类变量和常量集合。

# 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

全局变量

在 box 中,对全局变量的更改也仅限于 box 内部。在 box 中对全局变量的更改仅在该 box 中可见/适用。

# 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

顶层常量

通常,顶层常量定义为 Object 的常量。在 box 中,顶层常量是 box 中 Object 的常量。并且 box 对象 box 的常量与 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

顶层方法

顶层方法是 Object 的私有实例方法,在每个 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

无法将 box 中的顶层方法暴露给其他人。(参见下面的“讨论”部分的“将顶层方法暴露为 box 对象的方”)。

Ruby Box 作用域

Ruby Box 在文件作用域内工作。一个 .rb 文件在一个 box 中运行。

一旦一个文件在一个 box box 中加载,在该文件中定义/创建的所有方法/proc 都将在 box 中运行。

实用方法

提供了几种方法来尝试/测试 Ruby Box。

实现细节

ISeq 内联方法/常量缓存

如上面“Ruby Box 作用域”所述,一个“.rb”文件在一个 box 中运行。因此,方法/常量的解析将在一个 box 中一致地完成。

这意味着 ISeq 内联缓存即使与 box 一起也能正常工作。否则,就是一个 bug。

方法调用全局缓存(gccct)

rb_funcall() C 函数引用全局 cc 缓存表(gccct),缓存键是使用当前 box 计算的。

因此,当 Ruby Box 启用时,rb_funcall() 调用会有性能损失。

当前 box 和加载 box

当前 box 是执行代码所在的 box。Ruby::Box.current 返回当前 box 对象。

加载 box 是一个内部管理的 box,用于确定加载新需要的/加载的文件的 box。例如,当调用 box.require("foo") 时,box 是加载 box。

讨论

更多用 Ruby 编写的内置方法

如果 Ruby Box 默认启用,内置方法可以用 Ruby 编写,因为它们不能被用户的 monkey patches 覆盖。内置 Ruby 方法可以进行 JIT 编译,这可能会带来性能提升。

内置方法调用的方法 monkey patching

内置方法有时会调用其他内置方法。例如,Hash#map 调用 Hash#each 来检索要映射的条目。没有 Ruby Box,Ruby 用户可以覆盖 Hash#each 并期望 Hash#map 的行为因此改变。

但是有了 box,Hash#map 在根 box 中运行。Ruby 用户只能在用户 box 中定义 Hash#each,因此用户无法在此情况下更改 Hash#map 的行为。要实现这一点,用户应该同时重写 Hash#mapHash#each(或只重写 Hash#map)。

这是一个破坏性变更。

用户可以使用 Ruby::Box.root.eval(...) 定义方法,但这显然不是理想的 API。

为内置方法使用的全局变量赋值

类似于 monkey patching 方法,在 box 中分配的全局变量与根 box 分离。在根 box 中引用全局变量的方法找不到重新分配的值。

$LOAD_PATH$LOADED_FEATURES 的上下文

全局变量 $LOAD_PATH$LOADED_FEATURES 控制 require 方法的行为。因此,这些变量由加载 box 决定,而不是当前 box。

这可能会与用户的期望发生冲突。我们应该找到解决方案。

将顶层方法暴露为 box 对象的方

目前,box 中的顶层方法无法从 box 外部访问。但可能有调用其他 box 的顶层方法的用例。

拆分根 box 和内置 box

目前,唯一的“根” box 是类扩展 CoW 的来源。此外,“根” box 可以在启动 main 脚本求值后通过调用包含类似 require "openssl" 行的方法来加载其他文件。

这意味着用户 box 可以根据创建时间的不同而拥有不同的定义集。

[root]
 |
 |----[main]
 |
 |(require "openssl" called in root)
 |
 |----[box1] having OpenSSL
 |
 |(remove_const called for OpenSSL in root)
 |
 |----[box2] without OpenSSL

这可能导致用户 box 之间出现意外的行为差异。这不应该成为问题,因为引用 OpenSSL 的用户脚本应该自己调用 require "openssl"。但在最坏的情况下,一个脚本(没有 require "openssl")在 box1 中运行正常,但在 box2 中运行失败。这种情况对用户来说看起来像是“随机故障”。

防止这种情况的一种可能选项是拥有“根”和“内置” box。

此设计实现了 box CoW 的一致来源。

分离 cc_tblcallable_m_tblcvc_tbl 以减少 classext CoW

rb_classext_t 的字段包含各种缓存(类似)数据,cc_tbl(callcache 表)、callable_m_tbl(解析的补充方法表)和 cvc_tbl(类变量缓存表)。

classext CoW 在 rb_classext_t 的内容更改时触发,包括 cc_tblcallable_m_tblcvc_tbl。但是这三个表仅仅通过调用方法或引用类变量就会更改。因此,目前,classext CoW 的触发次数远远超过最初的预期。

如果我们能将这三个表移出 rb_classext_t,那么复制的 rb_classext_t 的数量将远少于当前实现。