Class: Ruby::Box
| Relationships & Source Files | |
| Namespace Children | |
|
Modules:
| |
|
Classes:
| |
| Super Chains via Extension / Inclusion / Inheritance | |
|
Class Chain:
self,
::Module
|
|
|
Instance Chain:
self,
::Module
|
|
| Inherits: | Module |
| Defined in: | box.c, box.c |
Overview
::Ruby Box - Ruby's in-process separation of Classes and Modules
::Ruby Box is designed to provide separated spaces in a ::Ruby process, to isolate applications and libraries.
Known issues
- Experimental warning is shown when ruby starts with
RUBY_BOX=1(specify-W:no-experimentaloption to hide it) bundle installmay failrequire 'active_support'may fail- A wrong current namespace detection happens sometimes in the root namespace
TODOs
- Add the loaded namespace on iseq to check if another namespace tries running the iseq (add a field only when VM_CHECK_MODE?)
- Delete per-box extension files (.so) lazily or process exit
- Collect
rb_classext_tentries for a box when::GCcollects the box - Assign its own TOPLEVEL_BINDING in boxes
- Fix calling
warnin boxes to refer$VERBOSEandWarning.warnin the box - Make an internal data container
Entryinvisible - More test cases about
$LOAD_PATHand$LOADED_FEATURES - Return classpath and nesting without the namespace prefix in the namespace itself {#21316}, {#21318}
How to use
Enabling Ruby Box
First, an environment variable should be set at the ruby process bootup: RUBY_BOX=1.
The only valid value is 1 to enable namespace. Other values (or unset RUBY_BOX) means disabling namespace. And setting the value after ::Ruby program starts doesn't work.
Using Ruby Box
Box class is the entrypoint of ::Ruby Box.
box = Ruby::Box.new
box.require('something') # or require_relative, load
The required file (either .rb or .so/.dll/.bundle) is loaded in the box (box here). The required/loaded files from something will be loaded in the box recursively.
# something.rb
X = 1
class Something
def self.x = X
def x = ::X
end
Classes/modules, those methods and constants defined in the box can be accessed via box object.
X = 2
p X # 2
p ::X # 2
p box::Something.x # 1
p box::X # 1
Instance methods defined in the box also run with definitions in the box.
s = box::Something.new
p s.x # 1
Specifications
Ruby Box types
There are two box types:
- Root box
- User boxes
There is the root box, just a single box in a Ruby process. Ruby bootstrap runs in the root box, and all builtin classes/modules are defined in the root box. (See "Builtin classes and modules".)
User boxes are to run user-written programs and libraries loaded from user programs. The user's main program (specified by the ruby command line argument) is executed in the "main" box, which is a user namespace automatically created at the end of Ruby's bootstrap, copied from the root box.
When .new is called, an "optional" box (a user, non-main box) is created, copied from the root box. All user boxes are flat, copied from the root box.
Ruby Box class and instances
Box is a class, as a subclass of ::Module. Box instances are a kind of ::Module.
Classes and modules defined in boxes
The classes and modules, newly defined in a box box, are accessible via box. For example, if a class A is defined in box, it is accessible as box::A from outside of the box.
In the box box, A can be referred to as A (and ::A).
Built-in classes and modules reopened in boxes
In boxes, builtin classes/modules are visible and can be reopened. Those classes/modules can be reopened using class or module clauses, and class/module definitions can be changed.
The changed definitions are visible only in the box. In other boxes, builtin classes/modules and those instances work without changed definitions.
# 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
The main box and box are different boxes, so monkey patches in main are also invisible in box.
Builtin classes and modules
In the box context, "builtin" classes and modules are classes and modules:
- Accessible without any #require calls in user scripts
- Defined before any user program start running
- Including classes/modules loaded by
prelude.rb(including RubyGemsGem, for example)
Hereafter, "builtin classes and modules" will be referred to as just "builtin classes".
Builtin classes referred via box objects
Builtin classes in a box box can be referred from other boxes. For example, box::String is a valid reference, and ::String and box::String are identical (String == box::String, String.object_id == box::String.object_id).
box::String-like reference returns just a ::String in the current box, so its definition is ::String in the box, not in box.
# foo.rb
class String
def self.foo = "foo"
end
# main.rb
box = Ruby::Box.new
box.require('foo')
box::String.foo # NoMethodError
Class instance variables, class variables, constants
Builtin classes can have different sets of class instance variables, class variables and constants between boxes.
# 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
Global variables
In boxes, changes on global variables are also isolated in the boxes. Changes on global variables in a box are visible/applied only in the 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
Top level constants
Usually, top level constants are defined as constants of ::Object. In boxes, top level constants are constants of ::Object in the box. And the box object box's constants are strictly equal to constants of ::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 methods
Top level methods are private instance methods of ::Object, in each 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
There is no way to expose top level methods in boxes to others. (See "Expose top level methods as a method of the box object" in "Discussions" section below)
Ruby Box scopes
Ruby Box works in file scope. One .rb file runs in a single box.
Once a file is loaded in a box box, all methods/procs defined/created in the file run in box.
Implementation details
ISeq inline method/constant cache
As described above in "Ruby Box scopes", an ".rb" file runs in a box. So method/constant resolution will be done in a box consistently.
That means ISeq inline caches work well even with boxes. Otherwise, it's a bug.
Method call global cache (gccct)
rb_funcall() C function refers to the global cc cache table (gccct), and the cache key is calculated with the current box.
So, rb_funcall() calls have a performance penalty when Ruby Box is enabled.
Current box and loading box
The current box is the box that the executing code is in. .current returns the current box object.
The loading box is an internally managed box to determine the box to load newly required/loaded files. For example, box is the loading box when box.require("foo") is called.
Discussions
More builtin methods written in Ruby
If Ruby Box is enabled by default, builtin methods can be written in Ruby because it can't be overridden by users' monkey patches. Builtin Ruby methods can be JIT-ed, and it could bring performance reward.
Monkey patching methods called by builtin methods
Builtin methods sometimes call other builtin methods. For example, Hash#map calls Hash#each to retrieve entries to be mapped. Without Ruby Box, Ruby users can overwrite Hash#each and expect the behavior change of Hash#map as a result.
But with boxes, Hash#map runs in the root box. Ruby users can define Hash#each only in user boxes, so users cannot change Hash#map's behavior in this case. To achieve it, users should override bothHash#map and Hash#each (or only Hash#map).
It is a breaking change.
Users can define methods using Ruby::Box.root.eval(...), but it's clearly not ideal API.
Context of $LOAD_PATH and $LOADED_FEATURES
Global variables $LOAD_PATH and $LOADED_FEATURES control #require method behaviors. So those variables are determined by the loading box instead of the current box.
This could potentially conflict with the user's expectations. We should find the solution.
Expose top level methods as a method of the box object
Currently, top level methods in boxes are not accessible from outside of the box. But there might be a use case to call other box's top level methods.
Split root and builtin box
Currently, the single "root" box is the source of classext CoW. And also, the "root" box can load additional files after starting main script evaluation by calling methods which contain lines like require "openssl".
That means, user boxes can have different sets of definitions according to when it is created.
[root]
|
|----[main]
|
|(require "openssl" called in root)
|
|----[box1] having OpenSSL
|
|(remove_const called for OpenSSL in root)
|
|----[box2] without OpenSSL
This could cause unexpected behavior differences between user boxes. It should NOT be a problem because user scripts which refer to OpenSSL should call require "openssl" by themselves.
But in the worst case, a script (without require "openssl") runs well in box1, but doesn't run in box2. This situation looks like a "random failure" to users.
An option possible to prevent this situation is to have "root" and "builtin" boxes.
- root
- The box for the Ruby process bootstrap, then the source of CoW
- After starting the main box, no code runs in this box
- builtin
- The box copied from the root box at the same time with "main"
- Methods and procs defined in the "root" box run in this box
- Classes and modules required will be loaded in this box
This design realizes a consistent source of box CoW.
Separate cc_tbl and callable_m_tbl, cvc_tbl for less classext CoW
The fields of rb_classext_t contains several cache(-like) data, cc_tbl(callcache table), callable_m_tbl(table of resolved complemented methods) and cvc_tbl(class variable cache table).
The classext CoW is triggered when the contents of rb_classext_t are changed, including cc_tbl, callable_m_tbl, and cvc_tbl. But those three tables are changed by just calling methods or referring class variables. So, currently, classext CoW is triggered much more times than the original expectation.
If we can move those three tables outside of rb_classext_t, the number of copied rb_classext_t will be much less than the current implementation.
Class Attribute Summary
-
Namespace.enabled? ⇒ Boolean
readonly
Returns
trueifBoxis enabled.
Class Method Summary
-
Namespace.current ⇒ Box, ...
Returns the current box.
-
Namespace.new ⇒ Box
constructor
Returns a new
Boxobject. - .main Internal use only
- .root Internal use only
::Module - Inherited
| .constants | In the first form, returns an array of the names of all constants accessible from the point of call. |
| .nesting | Returns the list of |
| .new | Creates a new anonymous module. |
| .used_modules | Returns an array of all modules used in the current scope. |
| .used_refinements | Returns an array of all modules used in the current scope. |
Instance Attribute Summary
- #main? ⇒ Boolean readonly Internal use only
- #root? ⇒ Boolean readonly Internal use only
::Module - Inherited
| #singleton_class? | Returns |
Instance Method Summary
- #eval(str)
- #inspect
-
#load(*args)
And it requires calling dlclose before deleting it.
-
#load_path ⇒ Array
Returns box local load path.
- #require(fname)
- #require_relative(fname)
::Module - Inherited
| #< | Returns true if mod is a subclass of other. |
| #<= | Returns true if mod is a subclass of other or is the same as other. |
| #<=> | Returns: |
| #== | Alias for Object#eql?. |
| #=== | Case Equality—Returns |
| #> | Returns true if mod is an ancestor of other. |
| #>= | Returns true if mod is an ancestor of other, or the two modules are the same. |
| #alias_method | Makes new_name a new copy of the method old_name. |
| #ancestors | Returns a list of modules included/prepended in mod (including mod itself). |
| #attr | The first form is equivalent to |
| #attr_accessor | Defines a named attribute for this module, where the name is symbol. |
| #attr_reader | Creates instance variables and corresponding methods that return the value of each instance variable. |
| #attr_writer | Creates an accessor method to allow assignment to the attribute symbol |
| #autoload | Registers filename to be loaded (using Kernel.require) the first time that const (which may be a |
| #autoload? | Returns filename to be loaded if name is registered as |
| #class_eval | Evaluates the string or block in the context of mod, except that when a block is given, constant/class variable lookup is not affected. |
| #class_exec | Evaluates the given block in the context of the class/module. |
| #class_variable_defined? | Returns |
| #class_variable_get | Returns the value of the given class variable (or throws a |
| #class_variable_set | Sets the class variable named by symbol to the given object. |
| #class_variables | Returns an array of the names of class variables in mod. |
| #const_defined? | Says whether mod or its ancestors have a constant with the given name: |
| #const_get | Checks for a constant with the given name in mod. |
| #const_missing | Invoked when a reference is made to an undefined constant in mod. |
| #const_set | Sets the named constant to the given object, returning that object. |
| #const_source_location | Returns the |
| #constants | Returns an array of the names of the constants accessible in mod. |
| #define_method | Defines an instance method in the receiver. |
| #deprecate_constant | Makes a list of existing constants deprecated. |
| #freeze | Prevents further modifications to mod. |
| #include | Invokes |
| #include? | Returns |
| #included_modules | Returns the list of modules included or prepended in mod or one of mod’s ancestors. |
| #inspect | Alias for Module#to_s. |
| #instance_method | Returns an |
| #instance_methods | Returns an array containing the names of the public and protected instance methods in the receiver. |
| #method_defined? | Returns |
| #module_eval | Alias for Module#class_eval. |
| #module_exec | Alias for Module#class_exec. |
| #name | Returns the name of the module mod. |
| #prepend | Invokes |
| #private_class_method | Makes existing class methods private. |
| #private_constant | Makes a list of existing constants private. |
| #private_instance_methods | Returns a list of the private instance methods defined in mod. |
| #private_method_defined? | Returns |
| #protected_instance_methods | Returns a list of the protected instance methods defined in mod. |
| #protected_method_defined? | Returns |
| #public_class_method | Makes a list of existing class methods public. |
| #public_constant | Makes a list of existing constants public. |
| #public_instance_method | Similar to instance_method, searches public method only. |
| #public_instance_methods | Returns a list of the public instance methods defined in mod. |
| #public_method_defined? | Returns |
| #refinements | Returns an array of |
| #remove_class_variable | Removes the named class variable from the receiver, returning that variable’s value. |
| #remove_method | Removes the method identified by symbol from the current class. |
| #set_temporary_name | Sets the temporary name of the module. |
| #to_s | Returns a string representing this module or class. |
| #undef_method | Prevents the current class from responding to calls to the named method. |
| #undefined_instance_methods | Returns a list of the undefined instance methods defined in mod. |
| #append_features | When this module is included in another, |
| #const_added | Invoked as a callback whenever a constant is assigned on the receiver. |
| #extend_object | Extends the specified object by adding this module’s constants and methods (which are added as singleton methods). |
| #extended | The equivalent of |
| #included | Callback invoked whenever the receiver is included in another module or class. |
| #method_added | Invoked as a callback whenever an instance method is added to the receiver. |
| #method_removed | Invoked as a callback whenever an instance method is removed from the receiver. |
| #method_undefined | Invoked as a callback whenever an instance method is undefined from the receiver. |
| #module_function | Creates module functions for the named methods. |
| #prepend_features | When this module is prepended in another, |
| #prepended | The equivalent of |
| #private | With no arguments, sets the default visibility for subsequently defined methods to private. |
| #protected | Sets the visibility of a section or of a list of method names as protected. |
| #public | With no arguments, sets the default visibility for subsequently defined methods to public. |
| #refine | Refine mod in the receiver. |
| #remove_const | Removes the definition of the given constant, returning that constant’s previous value. |
| #ruby2_keywords | For the given method names, marks the method as passing keywords through a normal argument splat. |
| #using | Import class refinements from module into the current class or module definition. |
| #initialize_clone, | |
| #with_jit | Internal helper for built-in initializations to define methods only when JIT is enabled. |
Constructor Details
Namespace.new ⇒ Box
Returns a new Box object.
# File 'box.c', line 353
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;
}
Class Attribute Details
Namespace.enabled? ⇒ Boolean (readonly)
Returns true if Box is enabled.
# File 'box.c', line 394
static VALUE
rb_box_s_getenabled(VALUE recv)
{
return RBOOL(rb_box_available());
}
Class Method Details
Namespace.current ⇒ Box, ...
Returns the current box. Returns nil if ::Ruby Box is not enabled.
# File 'box.c', line 407
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;
}
.main
# File 'box.c', line 924
static VALUE
rb_box_s_main(VALUE recv)
{
return main_box->box_object;
}
.root
# File 'box.c', line 917
static VALUE
rb_box_s_root(VALUE recv)
{
return root_box->box_object;
}
Instance Attribute Details
#main? ⇒ Boolean (readonly)
# File 'box.c', line 939
static VALUE
rb_box_main_p(VALUE box_value)
{
const rb_box_t *box = (const rb_box_t *)rb_get_box_t(box_value);
return RBOOL(BOX_MAIN_P(box));
}
#root? ⇒ Boolean (readonly)
# File 'box.c', line 931
static VALUE
rb_box_root_p(VALUE box_value)
{
const rb_box_t *box = (const rb_box_t *)rb_get_box_t(box_value);
return RBOOL(BOX_ROOT_P(box));
}
Instance Method Details
#eval(str)
[ GitHub ]# File 'box.c', line 807
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);
}
#inspect
[ GitHub ]# File 'box.c', line 856
static VALUE
rb_box_inspect(VALUE obj)
{
rb_box_t *box;
VALUE r;
if (obj == Qfalse) {
r = rb_str_new_cstr("#<Namespace:root>");
return r;
}
box = rb_get_box_t(obj);
r = rb_str_new_cstr("#<Namespace:");
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;
}
#load(*args)
And it requires calling dlclose before deleting it.
# File 'box.c', line 732
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);
}
#load_path ⇒ Array
Returns box local load path.
# File 'box.c', line 426
static VALUE
rb_box_load_path(VALUE box)
{
VM_ASSERT(BOX_OBJ_P(box));
return rb_get_box_t(box)->load_path;
}
#require(fname)
[ GitHub ]# File 'box.c', line 744
static VALUE
rb_box_require(VALUE box, VALUE fname)
{
rb_vm_frame_flag_set_box_require(GET_EC());
return rb_require_string(fname);
}
#require_relative(fname)
[ GitHub ]# File 'box.c', line 752
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);
}