Ruby Box - Ruby 进程内的类和模块分离
Ruby Box 旨在提供 Ruby 进程内的独立空间,以隔离应用程序代码、库和 monkey patches。
已知问题
-
当 ruby 使用
RUBY_BOX=1启动时会显示实验性警告(指定-W:no-experimental选项隐藏它)。 -
在
RUBY_BOX=1下安装原生扩展可能会失败,因为 extconf.rb 中出现堆栈深度过大的问题。 -
在
RUBY_BOX=1下,require 'active_support/core_ext'可能会失败。 -
在 box 中定义的类/方法可能无法被 Ruby 编写的内置方法引用。
待办事项
-
将加载的 box 添加到 iseq 中,以检查另一个 box 是否尝试运行该 iseq(仅在 VM_CHECK_MODE? 时添加一个字段)。
-
为 box 分配自己的 TOPLEVEL_BINDING。
-
修复在 box 中调用
warn的问题,使其能够引用 box 中的$VERBOSE和Warning.warn。 -
使内部数据容器类
Ruby::Box::Entry不可见。 -
增加关于
$LOAD_PATH和$LOADED_FEATURES的测试用例。
如何使用
启用 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
-
用户 box
存在根 box,它是 Ruby 进程中的唯一一个 box。Ruby 的引导过程在根 box 中运行,所有内置类/模块都在根 box 中定义。(参见“内置类和模块”。)
用户 box 用于运行用户编写的程序和从用户程序加载的库。用户的 Cmain 程序(由 ruby 命令行参数指定)在“main” box 中执行,该 box 是在 Ruby 引导过程结束时自动创建的用户 box,从根 box 复制而来。
调用 Ruby::Box.new 时,会创建一个“可选” box(一个用户 box,非 main box),并从根 box 复制。所有用户 box 都是平坦的,从根 box 复制而来。
Ruby Box 类和实例
Ruby::Box 是一个类,继承自 Module。Ruby::Box 实例是一种 Module。
在 box 中定义的类和模块
在新定义于 box box 中的类和模块,可以通过 box 访问。例如,如果一个类 A 定义在 box 中,那么从 box 外部可以将其访问为 box::A。
在 box 中,A 可以直接引用为 A(或 ::A)。
在 box 中重新打开的内置类和模块
在 box 中,内置类/模块可见,并且可以重新打开。这些类/模块可以使用 class 或 module 子句重新打开,并且可以修改类/模块定义。
修改后的定义仅在 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 box 中的 monkey patches 在 box 中也是不可见的。
内置类和模块
在 box 上下文中,“内置”类和模块是指
-
无需任何
require调用即可在用户脚本中访问 -
在任何用户程序开始运行之前定义
-
包括由 prelude.rb 加载的类/模块(例如,包括 RubyGems 的
Gem)。
此后,“内置类和模块”将简称为“内置类”。
通过 box 对象引用的内置类
在 box 中,内置类可以从其他 box 中引用。例如,box::String 是一个有效的引用,并且 String 和 box::String 是相同的(String == box::String,String.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。
-
Ruby::Box.current返回当前 box。 -
Ruby::Box.enabled?返回 true/false,表示是否指定了RUBY_BOX=1。 -
Ruby::Box.root返回根 box。 -
Ruby::Box.main返回 main box。 -
Ruby::Box#eval在接收方 box 中评估 Ruby 代码(String),类似于加载文件。
实现细节
ISeq 内联方法/常量缓存
如上文“Ruby Box 作用域”所述,一个 “.rb” 文件在一个 box 中运行。因此,方法/常量解析将一致地在 box 中进行。
这意味着即使有 boxes,ISeq 内联缓存也能正常工作。否则,这是一个 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 的行为因此改变。
但是在使用 boxes 时,Hash#map 在根 box 中运行。Ruby 用户只能在用户 box 中定义 Hash#each,因此用户在这种情况下无法更改 Hash#map 的行为。要实现这一点,用户应该同时覆盖 Hash#map 和 Hash#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 可以在启动主脚本评估后通过调用包含类似 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。
-
根
-
Ruby 进程引导的 box,然后是 CoW 的来源。
-
启动 main box 后,此 box 中没有代码运行。
-
内置
-
与“main”同时从根 box 复制的 box。
-
在“根” box 中定义的方法和 proc 在此 box 中运行。
-
在此 box 中加载请求的类和模块。
此设计实现了 box CoW 的一致来源。
分离 cc_tbl、callable_m_tbl 和 cvc_tbl 以减少类扩展 CoW
rb_classext_t 的字段包含多个缓存(类似)数据:cc_tbl(调用缓存表)、callable_m_tbl(解析的补充方法表)和 cvc_tbl(类变量缓存表)。
当 rb_classext_t 的内容发生更改时(包括 cc_tbl、callable_m_tbl 和 cvc_tbl),会触发类扩展 CoW。但这三个表仅通过调用方法或引用类变量就会发生更改。因此,目前类扩展 CoW 的触发次数比最初的预期要多。
如果我们能将这三个表移出 rb_classext_t,那么复制的 rb_classext_t 的数量将远少于当前实现。