class Ruby::Box
Ruby Box - Ruby 的进程内类和模块隔离
Ruby Box 旨在提供 Ruby 进程中的隔离空间,以隔离应用程序代码、库和 monkey patches。
已知问题
-
当 ruby 以
RUBY_BOX=1启动时,会显示实验性警告(指定-W:no-experimental选项可隐藏它) -
在
RUBY_BOX=1下安装原生扩展可能会因 extconf.rb 中的堆栈深度过大而失败 -
require 'active_support/core_ext'在RUBY_BOX=1下可能会失败 -
在 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 用于运行用户编写的程序和从用户程序加载的库。用户的 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 中,内置类/模块是可见的,并且可以被重新打开。可以使用 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 中的 monkey patches 在 box 中也不可见。
内置类和模块
在 box 上下文中,“内置”类和模块是类和模块
-
无需任何
require调用即可在用户脚本中访问 -
在任何用户程序开始运行之前定义
-
包括通过 prelude.rb 加载的类/模块(例如,包括 RubyGems
Gem)
此后,“内置类和模块”将仅称为“内置类”。
通过 box 对象引用的内置类
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),就像调用load文件一样
实现细节
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#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 可以在启动 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。
-
根
-
Ruby 进程引导的 box,然后是 CoW 的来源
-
启动 main box 后,没有代码在此 box 中运行
-
builtin
-
与“main”同时从根 box 复制的 box
-
在“根” box 中定义的“方法”和“proc”在此 box 中运行
-
将在此 box 中加载类和模块
此设计实现了 box CoW 的一致来源。
分离 cc_tbl 和 callable_m_tbl、cvc_tbl 以减少 classext CoW
rb_classext_t 的字段包含各种缓存(类似)数据,cc_tbl(callcache 表)、callable_m_tbl(解析的补充方法表)和 cvc_tbl(类变量缓存表)。
classext CoW 在 rb_classext_t 的内容更改时触发,包括 cc_tbl、callable_m_tbl 和 cvc_tbl。但是这三个表仅仅通过调用方法或引用类变量就会更改。因此,目前,classext CoW 的触发次数远远超过最初的预期。
如果我们能将这三个表移出 rb_classext_t,那么复制的 rb_classext_t 的数量将远少于当前实现。
Public Class Methods
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;
}
返回当前 box。如果 Ruby Box 未启用,则返回 nil。
Source
static VALUE
rb_box_s_getenabled(VALUE recv)
{
return RBOOL(rb_box_available());
}
如果 Ruby::Box 已启用,则返回 true。
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;
}
返回一个新的 Ruby::Box 对象。
Public Instance Methods
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;
}
返回 box 本地加载路径。
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);
}