summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSatoshi Tagomori <s-tagomori@sakura.ad.jp>2026-05-08 22:37:22 +0900
committerSatoshi Tagomori <tagomoris@gmail.com>2026-05-10 09:38:45 +0900
commit276f0d9b3efbabe46e74510e5e3585924b37772c (patch)
tree2a7e19f7a1709d02078be569022be70757c27c86
parent5ed1f93b0e87d961282a4487c81b48cf6f0b127f (diff)
[Bug #21881] Separate the master and root box
This change separates the master box from the root box, as the single master copy of boxes. Before this change, the root box is the source of copies, and also it runs builtin classes' code. Builtin code makes changes by requiring files, thus the source of copies is mutable, be different from the past one. This causes different internal states of boxes, depending on when it is created. After this change, all boxes are created from the master box, and the master box never runs code, so it realizes the immutable source of copies. This also makes is possible to separate RubyGems (and other default gems) from each boxes. All boxes will have their own copies of RubyGems. RubyGems has its internal state, but it'll be separated between boxes after this change.
-rw-r--r--box.c191
-rw-r--r--builtin.c38
-rw-r--r--builtin.h2
-rw-r--r--class.c4
-rw-r--r--depend1
-rw-r--r--doc/language/box.md78
-rw-r--r--eval.c2
-rw-r--r--internal/box.h17
-rw-r--r--internal/class.h10
-rw-r--r--internal/inits.h2
-rw-r--r--mini_builtin.c13
-rw-r--r--ruby.c77
-rw-r--r--test/ruby/test_box.rb89
-rw-r--r--vm.c2
-rw-r--r--vm_core.h1
-rw-r--r--vm_insnhelper.c2
16 files changed, 388 insertions, 141 deletions
diff --git a/box.c b/box.c
index fba494f7ad..9c85426b8c 100644
--- a/box.c
+++ b/box.c
@@ -20,6 +20,8 @@
#include "darray.h"
#include "zjit.h"
+#include "builtin.h"
+
#include <stdio.h>
#ifdef HAVE_FCNTL_H
@@ -36,8 +38,12 @@ VALUE rb_cBox = 0;
VALUE rb_cBoxEntry = 0;
VALUE rb_mBoxLoader = 0;
-static rb_box_t root_box[1]; /* Initialize in initialize_root_box() */
+static rb_box_t master_box[1]; /* Initialize in initialize_master_box() */
+static rb_box_t *root_box;
static rb_box_t *main_box;
+
+static rb_box_gem_flags_t box_gem_flags[1];
+
static char *tmp_dir;
static bool tmp_dir_has_dirsep;
@@ -62,14 +68,32 @@ static VALUE rb_box_inspect(VALUE obj);
static void cleanup_all_local_extensions(VALUE libmap);
void
+rb_box_set_gem_flags(rb_box_gem_flags_t *flags)
+{
+
+ box_gem_flags->gem = flags->gem;
+ box_gem_flags->error_highlight = flags->error_highlight;
+ box_gem_flags->did_you_mean = flags->did_you_mean;
+ box_gem_flags->syntax_suggest = flags->syntax_suggest;
+}
+
+void
rb_box_init_done(void)
{
ruby_box_init_done = true;
}
const rb_box_t *
+rb_master_box(void)
+{
+ return master_box;
+}
+
+const rb_box_t *
rb_root_box(void)
{
+ if (!root_box) // The root box isn't initialized yet - The Ruby runtime is in setup.
+ return master_box;
return root_box;
}
@@ -83,12 +107,14 @@ const rb_box_t *
rb_current_box(void)
{
/*
- * If RUBY_BOX is not set, the root box is the only available one.
+ * If RUBY_BOX is not set, the master box is the only available one.
*
- * Until the main_box is not initialized, the root box is
+ * While the root/main boxes are not initialized, the master box is
* the only valid box.
* This early return is to avoid accessing EC before its setup.
*/
+ if (!root_box)
+ return master_box;
if (!main_box)
return root_box;
@@ -98,6 +124,8 @@ rb_current_box(void)
const rb_box_t *
rb_loading_box(void)
{
+ if (!root_box)
+ return master_box;
if (!main_box)
return root_box;
@@ -133,7 +161,7 @@ box_main_to_s(VALUE obj)
static void
box_entry_initialize(rb_box_t *box)
{
- const rb_box_t *root = rb_root_box();
+ const rb_box_t *master = rb_master_box();
// These will be updated immediately
box->box_object = 0;
@@ -142,15 +170,15 @@ box_entry_initialize(rb_box_t *box)
box->top_self = rb_obj_alloc(rb_cObject);
rb_define_singleton_method(box->top_self, "to_s", box_main_to_s, 0);
rb_define_alias(rb_singleton_class(box->top_self), "inspect", "to_s");
- box->load_path = rb_ary_dup(root->load_path);
- box->expanded_load_path = rb_ary_dup(root->expanded_load_path);
+ box->load_path = rb_ary_dup(master->load_path);
+ box->expanded_load_path = rb_ary_dup(master->expanded_load_path);
box->load_path_snapshot = rb_ary_new();
box->load_path_check_cache = 0;
- box->loaded_features = rb_ary_dup(root->loaded_features);
+ box->loaded_features = rb_ary_dup(master->loaded_features);
box->loaded_features_snapshot = rb_ary_new();
box->loaded_features_index = st_init_numtable();
- box->loaded_features_realpaths = rb_hash_dup(root->loaded_features_realpaths);
- box->loaded_features_realpath_map = rb_hash_dup(root->loaded_features_realpath_map);
+ box->loaded_features_realpaths = rb_hash_dup(master->loaded_features_realpaths);
+ box->loaded_features_realpath_map = rb_hash_dup(master->loaded_features_realpath_map);
box->loading_table = st_init_strtable();
box->ruby_dln_libmap = rb_hash_new_with_size(0);
box->gvar_tbl = rb_hash_new_with_size(0);
@@ -227,7 +255,7 @@ free_loaded_feature_index_i(st_data_t key, st_data_t value, st_data_t arg)
}
static void
-box_root_free(void *ptr)
+free_box_st_tables(void *ptr)
{
rb_box_t *box = (rb_box_t *)ptr;
if (box->loading_table) {
@@ -274,7 +302,7 @@ box_entry_free(void *ptr)
cleanup_all_local_extensions(box->ruby_dln_libmap);
- box_root_free(ptr);
+ free_box_st_tables(ptr);
SIZED_FREE(box);
}
@@ -303,11 +331,11 @@ static const rb_data_type_t rb_box_data_type = {
0, 0, RUBY_TYPED_FREE_IMMEDIATELY // TODO: enable RUBY_TYPED_WB_PROTECTED when inserting write barriers
};
-static const rb_data_type_t rb_root_box_data_type = {
- "Ruby::Box::Root",
+static const rb_data_type_t rb_master_box_data_type = {
+ "Ruby::Box::Master",
{
rb_box_entry_mark,
- box_root_free,
+ free_box_st_tables,
box_entry_memsize,
rb_box_gc_update_references,
},
@@ -340,7 +368,7 @@ rb_get_box_t(VALUE box)
VM_ASSERT(box);
if (NIL_P(box))
- return root_box;
+ return (rb_box_t *)rb_root_box();
VM_ASSERT(BOX_OBJ_P(box));
@@ -394,6 +422,14 @@ box_initialize(VALUE box_value)
rb_ivar_set(box_value, id_box_entry, entry);
+ if (ruby_box_init_done) {
+ if (box_gem_flags->gem) {
+ rb_vm_call_cfunc_in_box(Qnil, rb_define_gem_modules, (VALUE)box_gem_flags, Qnil,
+ rb_str_new_cstr("before_prelude.user.dummy"), (const rb_box_t *)box);
+ rb_load_gem_prelude((VALUE)box);
+ }
+ }
+
// Invalidate ZJIT code that assumes only the root box is active
rb_zjit_invalidate_root_box();
@@ -845,49 +881,52 @@ rb_box_require_relative(VALUE box, VALUE fname)
}
static void
-initialize_root_box(void)
+initialize_master_box(void)
{
rb_vm_t *vm = GET_VM();
- rb_box_t *root = (rb_box_t *)rb_root_box();
+ rb_box_t *master = (rb_box_t *)rb_master_box();
- root->load_path = rb_ary_new();
- root->expanded_load_path = rb_ary_hidden_new(0);
- root->load_path_snapshot = rb_ary_hidden_new(0);
- root->load_path_check_cache = 0;
- rb_define_singleton_method(root->load_path, "resolve_feature_path", rb_resolve_feature_path, 1);
+ master->load_path = rb_ary_new();
+ master->expanded_load_path = rb_ary_hidden_new(0);
+ master->load_path_snapshot = rb_ary_hidden_new(0);
+ master->load_path_check_cache = 0;
+ rb_define_singleton_method(master->load_path, "resolve_feature_path", rb_resolve_feature_path, 1);
- root->loaded_features = rb_ary_new();
- root->loaded_features_snapshot = rb_ary_hidden_new(0);
- root->loaded_features_index = st_init_numtable();
- root->loaded_features_realpaths = rb_hash_new();
- rb_obj_hide(root->loaded_features_realpaths);
- root->loaded_features_realpath_map = rb_hash_new();
- rb_obj_hide(root->loaded_features_realpath_map);
+ master->loaded_features = rb_ary_new();
+ master->loaded_features_snapshot = rb_ary_hidden_new(0);
+ master->loaded_features_index = st_init_numtable();
+ master->loaded_features_realpaths = rb_hash_new();
+ rb_obj_hide(master->loaded_features_realpaths);
+ master->loaded_features_realpath_map = rb_hash_new();
+ rb_obj_hide(master->loaded_features_realpath_map);
- root->ruby_dln_libmap = rb_hash_new_with_size(0);
- root->gvar_tbl = rb_hash_new_with_size(0);
- root->classext_cow_classes = NULL; // classext CoW never happen on the root box
+ master->ruby_dln_libmap = rb_hash_new_with_size(0);
+ master->gvar_tbl = rb_hash_new_with_size(0);
+ master->classext_cow_classes = NULL; // classext CoW never happen on the master box
- vm->root_box = root;
+ vm->master_box = master;
if (rb_box_available()) {
- VALUE root_box, entry;
+ VALUE master_box, entry;
ID id_box_entry;
CONST_ID(id_box_entry, "__box_entry__");
- root_box = rb_obj_alloc(rb_cBox);
- RCLASS_SET_PRIME_CLASSEXT_WRITABLE(root_box, true);
- RCLASS_SET_CONST_TBL(root_box, RCLASSEXT_CONST_TBL(RCLASS_EXT_PRIME(rb_cObject)), true);
+ master_box = rb_obj_alloc(rb_cBox);
+ RCLASS_SET_PRIME_CLASSEXT_WRITABLE(master_box, true);
+ RCLASS_SET_CONST_TBL(master_box, RCLASSEXT_CONST_TBL(RCLASS_EXT_PRIME(rb_cObject)), true);
+
+ master->box_id = box_generate_id();
+ master->box_object = master_box;
- root->box_id = box_generate_id();
- root->box_object = root_box;
+ entry = TypedData_Wrap_Struct(rb_cBoxEntry, &rb_master_box_data_type, master);
+ rb_ivar_set(master_box, id_box_entry, entry);
- entry = TypedData_Wrap_Struct(rb_cBoxEntry, &rb_root_box_data_type, root);
- rb_ivar_set(root_box, id_box_entry, entry);
+ rb_gc_register_mark_object(master_box);
+ rb_gc_register_mark_object(entry);
}
else {
- root->box_id = 1;
- root->box_object = Qnil;
+ master->box_id = 1;
+ master->box_object = Qnil;
}
}
@@ -911,11 +950,26 @@ static int box_experimental_warned = 0;
RUBY_EXTERN const char ruby_api_version_name[];
-void
-rb_initialize_main_box(void)
+static VALUE
+box_value_initialize(bool root, bool user, bool optional)
{
rb_box_t *box;
- VALUE main_box_value;
+ VALUE box_value = rb_class_new_instance(0, NULL, rb_cBox);
+
+ VM_ASSERT(BOX_OBJ_P(box_value));
+
+ box = rb_get_box_t(box_value);
+ box->box_object = box_value;
+ box->is_root = root;
+ box->is_user = user;
+ box->is_optional = optional;
+ return box_value;
+}
+
+void
+rb_initialize_mandatory_boxes(void)
+{
+ VALUE root_box_value, main_box_value;
rb_vm_t *vm = GET_VM();
VM_ASSERT(rb_box_available());
@@ -928,19 +982,18 @@ rb_initialize_main_box(void)
box_experimental_warned = 1;
}
- main_box_value = rb_class_new_instance(0, NULL, rb_cBox);
- VM_ASSERT(BOX_OBJ_P(main_box_value));
- box = rb_get_box_t(main_box_value);
- box->box_object = main_box_value;
- box->is_user = true;
- box->is_optional = false;
+ root_box_value = box_value_initialize(true, false, false);
+ main_box_value = box_value_initialize(false, true, false);
+ rb_const_set(rb_cBox, rb_intern("ROOT"), root_box_value);
rb_const_set(rb_cBox, rb_intern("MAIN"), main_box_value);
- vm->main_box = main_box = box;
+ vm->root_box = root_box = rb_get_box_t(root_box_value);
+ vm->main_box = main_box = rb_get_box_t(main_box_value);
// create the writable classext of ::Object explicitly to finalize the set of visible top-level constants
- RCLASS_EXT_WRITABLE_IN_BOX(rb_cObject, box);
+ RCLASS_EXT_WRITABLE_IN_BOX(rb_cObject, root_box);
+ RCLASS_EXT_WRITABLE_IN_BOX(rb_cObject, main_box);
}
static VALUE
@@ -949,12 +1002,15 @@ rb_box_inspect(VALUE obj)
rb_box_t *box;
VALUE r;
if (obj == Qfalse) {
- r = rb_str_new_cstr("#<Ruby::Box:root>");
+ r = rb_str_new_cstr("#<Ruby::Box:master>");
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_MASTER_P(box)) {
+ rb_str_cat_cstr(r, ",master");
+ }
if (BOX_ROOT_P(box)) {
rb_str_cat_cstr(r, ",root");
}
@@ -986,9 +1042,9 @@ box_define_loader_method(const char *name)
}
void
-Init_root_box(void)
+Init_master_box(void)
{
- root_box->loading_table = st_init_strtable();
+ master_box->loading_table = st_init_strtable();
}
void
@@ -1005,6 +1061,13 @@ Init_enable_box(void)
/* :nodoc: */
static VALUE
+rb_box_s_master(VALUE recv)
+{
+ return master_box->box_object;
+}
+
+/* :nodoc: */
+static VALUE
rb_box_s_root(VALUE recv)
{
return root_box->box_object;
@@ -1019,6 +1082,14 @@ rb_box_s_main(VALUE recv)
/* :nodoc: */
static VALUE
+rb_box_master_p(VALUE box_value)
+{
+ const rb_box_t *box = (const rb_box_t *)rb_get_box_t(box_value);
+ return RBOOL(BOX_MASTER_P(box));
+}
+
+/* :nodoc: */
+static VALUE
rb_box_root_p(VALUE box_value)
{
const rb_box_t *box = (const rb_box_t *)rb_get_box_t(box_value);
@@ -1151,7 +1222,7 @@ rb_f_dump_classext(VALUE recv, VALUE klass)
box = RCLASSEXT_BOX(ext);
snprintf(buf, 2048, "Prime classext box(%ld,%s), readable(%s), writable(%s)\n",
box->box_id,
- BOX_ROOT_P(box) ? "root" : (BOX_MAIN_P(box) ? "main" : "optional"),
+ BOX_MASTER_P(box) ? "master" : (BOX_ROOT_P(box) ? "root" : (BOX_MAIN_P(box) ? "main" : "optional")),
RCLASS_PRIME_CLASSEXT_READABLE_P(klass) ? "t" : "f",
RCLASS_PRIME_CLASSEXT_WRITABLE_P(klass) ? "t" : "f");
rb_str_cat_cstr(res, buf);
@@ -1193,7 +1264,7 @@ Init_Box(void)
rb_cBoxEntry = rb_define_class_under(rb_cBox, "Entry", rb_cObject);
rb_define_alloc_func(rb_cBoxEntry, rb_box_entry_alloc);
- initialize_root_box();
+ initialize_master_box();
/* :nodoc: */
rb_mBoxLoader = rb_define_module_under(rb_cBox, "Loader");
@@ -1204,8 +1275,10 @@ Init_Box(void)
if (rb_box_available()) {
rb_include_module(rb_cObject, rb_mBoxLoader);
+ rb_define_singleton_method(rb_cBox, "master", rb_box_s_master, 0);
rb_define_singleton_method(rb_cBox, "root", rb_box_s_root, 0);
rb_define_singleton_method(rb_cBox, "main", rb_box_s_main, 0);
+ rb_define_method(rb_cBox, "master?", rb_box_master_p, 0);
rb_define_method(rb_cBox, "root?", rb_box_root_p, 0);
rb_define_method(rb_cBox, "main?", rb_box_main_p, 0);
diff --git a/builtin.c b/builtin.c
index 7861aee8ba..03c9d03bc3 100644
--- a/builtin.c
+++ b/builtin.c
@@ -1,4 +1,5 @@
#include "internal.h"
+#include "internal/box.h"
#include "vm_core.h"
#include "iseq.h"
#include "builtin.h"
@@ -56,7 +57,7 @@ builtin_lookup(const char *feature, size_t *psize)
}
static void
-load_with_builtin_functions(const char *feature_name, const struct rb_builtin_function *table)
+load_with_builtin_functions(const char *feature_name, const struct rb_builtin_function *table, const rb_box_t *target_box)
{
// search binary
size_t size;
@@ -74,13 +75,40 @@ load_with_builtin_functions(const char *feature_name, const struct rb_builtin_fu
vm->builtin_function_table = NULL;
// exec
- rb_iseq_eval(rb_iseq_check(iseq), rb_root_box()); // builtin functions are loaded in the root box
+ rb_iseq_eval(rb_iseq_check(iseq), target_box);
}
void
rb_load_with_builtin_functions(const char *feature_name, const struct rb_builtin_function *table)
{
- load_with_builtin_functions(feature_name, table);
+ load_with_builtin_functions(feature_name, table, rb_root_box());
+}
+
+VALUE
+rb_define_gem_modules(VALUE flags_value, VALUE _)
+{
+ rb_box_gem_flags_t *flags = (rb_box_gem_flags_t *)flags_value;
+
+ if (flags->gem) {
+ rb_define_module("Gem");
+ if (flags->error_highlight) {
+ rb_define_module("ErrorHighlight");
+ }
+ if (flags->did_you_mean) {
+ rb_define_module("DidYouMean");
+ }
+ if (flags->syntax_suggest) {
+ rb_define_module("SyntaxSuggest");
+ }
+ }
+
+ return Qnil;
+}
+
+void
+rb_load_gem_prelude(VALUE box)
+{
+ load_with_builtin_functions("gem_prelude", NULL, (const rb_box_t *)box);
}
#endif
@@ -103,7 +131,9 @@ Init_builtin_features(void)
#ifdef BUILTIN_BINARY_SIZE
- load_with_builtin_functions("gem_prelude", NULL);
+ rb_load_gem_prelude((VALUE)rb_root_box());
+
+ rb_load_gem_prelude((VALUE)rb_main_box());
#endif
diff --git a/builtin.h b/builtin.h
index fd1c4c307f..ffd2aad88e 100644
--- a/builtin.h
+++ b/builtin.h
@@ -21,6 +21,8 @@ struct rb_builtin_function {
}
void rb_load_with_builtin_functions(const char *feature_name, const struct rb_builtin_function *table);
+VALUE rb_define_gem_modules(VALUE, VALUE);
+void rb_load_gem_prelude(VALUE box);
#ifndef rb_execution_context_t
typedef struct rb_execution_context_struct rb_execution_context_t;
diff --git a/class.c b/class.c
index 70563c3094..91bea44cd9 100644
--- a/class.c
+++ b/class.c
@@ -194,7 +194,7 @@ rb_class_set_box_classext(VALUE obj, const rb_box_t *box, rb_classext_t *ext)
.ext = ext,
};
- VM_ASSERT(BOX_USER_P(box));
+ VM_ASSERT(BOX_MUTABLE_P(box));
st_update(RCLASS_CLASSEXT_TBL(obj), (st_data_t)box->box_object, set_box_classext_update, (st_data_t)&args);
@@ -700,7 +700,7 @@ class_alloc0(enum ruby_value_type type, VALUE klass, bool boxable)
static VALUE
class_alloc(enum ruby_value_type type, VALUE klass)
{
- bool boxable = rb_box_available() && BOX_ROOT_P(rb_current_box());
+ bool boxable = rb_box_available() && BOX_MASTER_P(rb_current_box());
return class_alloc0(type, klass, boxable);
}
diff --git a/depend b/depend
index a17eb16f75..e0247ffa7e 100644
--- a/depend
+++ b/depend
@@ -821,6 +821,7 @@ box.$(OBJEXT): {$(VPATH)}backward/2/long_long.h
box.$(OBJEXT): {$(VPATH)}backward/2/stdalign.h
box.$(OBJEXT): {$(VPATH)}backward/2/stdarg.h
box.$(OBJEXT): {$(VPATH)}box.c
+box.$(OBJEXT): {$(VPATH)}builtin.h
box.$(OBJEXT): {$(VPATH)}config.h
box.$(OBJEXT): {$(VPATH)}constant.h
box.$(OBJEXT): {$(VPATH)}darray.h
diff --git a/doc/language/box.md b/doc/language/box.md
index 8c7fd20b20..92514b3ec9 100644
--- a/doc/language/box.md
+++ b/doc/language/box.md
@@ -68,16 +68,35 @@ p s.x # 1
### Ruby Box types
-There are two box types:
+There are three box types:
+* Master box
* 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".)
+Ruby bootstrap runs in the root box, and a
-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 box automatically created at the end of Ruby's bootstrap, copied from the root box.
+There is the root box, just a single box in a Ruby process. All builtin classes/modules are defined and run in the root box. (See "Builtin classes and modules".)
-When `Ruby::Box.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.
+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 box automatically created at the end of Ruby's bootstrap. The files specified with `-r` command line option will be required in the main box.
+
+Calling `Ruby::Box.new` creates an "optional" box (a user, non-main box), technically equal to the main box.
+
+Ruby also has the master box. The master box is the "master copy" of all boxes. Boxes will be created as a copy of the master box. The master box is only for the source of box copies, and no code runs in the master box.
+
+
+```
+[master]
+ |
+ |----[root]
+ |
+ |----[main]
+ |
+ |----[user box 1]
+ |
+ |----[user box 2]
+ ...
+```
### Ruby Box class and instances
@@ -125,7 +144,7 @@ box::Foo.foo_is_blank? #=> false (#blank? called in box)
String::BLANK_PATTERN # NameError
```
-The main box and `box` are different boxes, so monkey patches in main are also invisible in `box`.
+The main box and `box` above are different boxes, so monkey patches in main are also invisible in `box`.
### Builtin classes and modules
@@ -133,10 +152,22 @@ 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 RubyGems `Gem`, for example)
Hereafter, "builtin classes and modules" will be referred to as just "builtin classes".
+Builtin classes and modules are loaded in all boxes, and run in the root box.
+
+### Exceptional non-built-in classes/modules
+
+There are some exceptional classes/modules that are enabled in default, but aren't built-in classes. Those classes/modules are:
+
+* `RubyGems`
+* `ErrorHighlight`
+* `DidYouMean`
+* `SyntaxSuggest`
+
+Those classes/modules (part of default gems) are loaded in each boxes independently. If a user box's code calls RubyGems, it calls the RubyGems inside the box itself, instead of the root box's one.
+
### 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`).
@@ -317,41 +348,6 @@ This could potentially conflict with the user's expectations. We should find the
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).
diff --git a/eval.c b/eval.c
index 2b613ffff3..b6fedf11f3 100644
--- a/eval.c
+++ b/eval.c
@@ -81,7 +81,7 @@ ruby_setup(void)
rb_vm_encoded_insn_data_table_init();
Init_enable_box();
Init_vm_objects();
- Init_root_box();
+ Init_master_box();
Init_fstring_table();
EC_PUSH_TAG(GET_EC());
diff --git a/internal/box.h b/internal/box.h
index b62b6a9bc9..c717dc4e24 100644
--- a/internal/box.h
+++ b/internal/box.h
@@ -36,14 +36,25 @@ struct rb_box_struct {
VALUE gvar_tbl;
struct st_table *classext_cow_classes;
+ bool is_root;
bool is_user;
bool is_optional;
};
typedef struct rb_box_struct rb_box_t;
+struct rb_box_gem_flags {
+ bool gem;
+ bool error_highlight;
+ bool did_you_mean;
+ bool syntax_suggest;
+};
+typedef struct rb_box_gem_flags rb_box_gem_flags_t;
+
#define BOX_OBJ_P(obj) (rb_obj_class(obj) == rb_cBox)
-#define BOX_ROOT_P(box) (box && !box->is_user)
+#define BOX_MASTER_P(box) (box && !box->is_root && !box->is_user)
+#define BOX_MUTABLE_P(box) (box && (box->is_root || box->is_user))
+#define BOX_ROOT_P(box) (box && box->is_root)
#define BOX_USER_P(box) (box && box->is_user)
#define BOX_OPTIONAL_P(box) (box && box->is_optional)
#define BOX_MAIN_P(box) (box && box->is_user && !box->is_optional)
@@ -63,6 +74,7 @@ rb_box_available(void)
return ruby_box_enabled;
}
+const rb_box_t * rb_master_box(void);
const rb_box_t * rb_root_box(void);
const rb_box_t * rb_main_box(void);
const rb_box_t * rb_current_box(void);
@@ -78,6 +90,7 @@ VALUE rb_get_box_object(rb_box_t *ns);
VALUE rb_box_local_extension(VALUE box, VALUE fname, VALUE path, VALUE *cleanup);
void rb_box_cleanup_local_extension(VALUE cleanup);
-void rb_initialize_main_box(void);
+void rb_initialize_mandatory_boxes(void);
void rb_box_init_done(void);
+void rb_box_set_gem_flags(rb_box_gem_flags_t *);
#endif /* INTERNAL_BOX_H */
diff --git a/internal/class.h b/internal/class.h
index 8b66f50bc6..51e38b09c8 100644
--- a/internal/class.h
+++ b/internal/class.h
@@ -289,7 +289,7 @@ RCLASS_SET_BOX_CLASSEXT(VALUE obj, const rb_box_t *box, rb_classext_t *ext)
{
int first_set = 0;
st_table *tbl = RCLASS_CLASSEXT_TBL(obj);
- VM_ASSERT(BOX_USER_P(box)); // non-prime classext is only for user box, with box_object
+ VM_ASSERT(BOX_MUTABLE_P(box)); // Setting non-prime classext never happens on the master box
VM_ASSERT(box->box_object);
VM_ASSERT(RCLASSEXT_BOX(ext) == box);
if (!tbl) {
@@ -364,7 +364,7 @@ RCLASS_EXT_READABLE_LOOKUP(VALUE obj, const rb_box_t *box)
static inline rb_classext_t *
RCLASS_EXT_READABLE_IN_BOX(VALUE obj, const rb_box_t *box)
{
- if (BOX_ROOT_P(box)
+ if (BOX_MASTER_P(box)
|| RCLASS_PRIME_CLASSEXT_READABLE_P(obj)) {
return RCLASS_EXT_PRIME(obj);
}
@@ -380,7 +380,7 @@ RCLASS_EXT_READABLE(VALUE obj)
}
// delay determining the current box to optimize for unmodified classes
box = rb_current_box();
- if (BOX_ROOT_P(box)) {
+ if (BOX_MASTER_P(box)) {
return RCLASS_EXT_PRIME(obj);
}
return RCLASS_EXT_READABLE_LOOKUP(obj, box);
@@ -414,7 +414,7 @@ RCLASS_EXT_WRITABLE_LOOKUP(VALUE obj, const rb_box_t *box)
static inline rb_classext_t *
RCLASS_EXT_WRITABLE_IN_BOX(VALUE obj, const rb_box_t *box)
{
- if (BOX_ROOT_P(box)
+ if (BOX_MASTER_P(box)
|| RCLASS_PRIME_CLASSEXT_WRITABLE_P(obj)) {
return RCLASS_EXT_PRIME(obj);
}
@@ -430,7 +430,7 @@ RCLASS_EXT_WRITABLE(VALUE obj)
}
// delay determining the current box to optimize for unmodified classes
box = rb_current_box();
- if (BOX_ROOT_P(box)) {
+ if (BOX_MASTER_P(box)) {
return RCLASS_EXT_PRIME(obj);
}
return RCLASS_EXT_WRITABLE_LOOKUP(obj, box);
diff --git a/internal/inits.h b/internal/inits.h
index dee818285c..be73dac1dc 100644
--- a/internal/inits.h
+++ b/internal/inits.h
@@ -11,7 +11,7 @@
/* box.c */
void Init_enable_box(void);
-void Init_root_box(void);
+void Init_master_box(void);
/* class.c */
void Init_class_hierarchy(void);
diff --git a/mini_builtin.c b/mini_builtin.c
index 1e2c32a67e..75ea94d37d 100644
--- a/mini_builtin.c
+++ b/mini_builtin.c
@@ -103,3 +103,16 @@ rb_load_with_builtin_functions(const char *feature_name, const struct rb_builtin
const rb_iseq_t *iseq = builtin_iseq_load(feature_name, table);
rb_iseq_eval(iseq, rb_root_box());
}
+
+VALUE
+rb_define_gem_modules(VALUE _a, VALUE _b)
+{
+ // do nothing - moniruby doesn't load gem_prelude.rb.
+ return Qnil;
+}
+
+void
+rb_load_gem_prelude(VALUE _)
+{
+ // do nothing - miniruby doesn't support loading RubyGems.
+}
diff --git a/ruby.c b/ruby.c
index 687d769285..162287ca71 100644
--- a/ruby.c
+++ b/ruby.c
@@ -798,6 +798,25 @@ require_libraries(VALUE *req_list)
*req_list = 0;
}
+static void
+require_libraries_in_main_box(VALUE *req_list)
+{
+ const rb_box_t *main_box = rb_main_box();
+ VALUE list = *req_list;
+ ID require;
+ rb_encoding *extenc = rb_default_external_encoding();
+
+ CONST_ID(require, "require");
+ while (list && RARRAY_LEN(list) > 0) {
+ VALUE feature = rb_ary_shift(list);
+ rb_enc_associate(feature, extenc);
+ RBASIC_SET_CLASS_RAW(feature, rb_cString);
+ OBJ_FREEZE(feature);
+ rb_funcallv(main_box->box_object, require, 1, &feature);
+ }
+ *req_list = 0;
+}
+
static const struct rb_block*
toplevel_context(rb_binding_t *bind)
{
@@ -1771,6 +1790,7 @@ proc_options(long argc, char **argv, ruby_cmdline_options_t *opt, int envopt)
return argc0 - argc;
}
+VALUE rb_define_gem_modules(VALUE, VALUE);
void Init_builtin_features(void);
static void
@@ -1798,19 +1818,6 @@ ruby_opt_init(ruby_cmdline_options_t *opt)
if (opt->dump & dump_exit_bits) return;
- if (FEATURE_SET_P(opt->features, gems)) {
- rb_define_module("Gem");
- if (opt->features.set & FEATURE_BIT(error_highlight)) {
- rb_define_module("ErrorHighlight");
- }
- if (opt->features.set & FEATURE_BIT(did_you_mean)) {
- rb_define_module("DidYouMean");
- }
- if (opt->features.set & FEATURE_BIT(syntax_suggest)) {
- rb_define_module("SyntaxSuggest");
- }
- }
-
Init_ext(); /* load statically linked extensions before rubygems */
Init_extra_exts();
@@ -1829,14 +1836,41 @@ ruby_opt_init(ruby_cmdline_options_t *opt)
rb_zjit_init_builtin_cmes();
#endif
- ruby_init_prelude();
+ /**
+ * Initialize the root/main boxes before loading libraries to run them
+ * (including RubyGems, written in Ruby) in those boxes themselves
+ */
+ if (rb_box_available()) {
+ rb_initialize_mandatory_boxes();
+ }
- /* Initialize the main box after loading libraries (including rubygems)
- * to enable those in both root and main */
- if (rb_box_available())
- rb_initialize_main_box();
rb_box_init_done();
+ if (FEATURE_SET_P(opt->features, gems)) {
+ rb_box_gem_flags_t gem_flags = {
+ .gem = FEATURE_SET_P(opt->features, gems),
+ .error_highlight = opt->features.set & FEATURE_BIT(error_highlight),
+ .did_you_mean = opt->features.set & FEATURE_BIT(did_you_mean),
+ .syntax_suggest = opt->features.set & FEATURE_BIT(syntax_suggest)
+ };
+
+ if (rb_box_available()) {
+ rb_vm_call_cfunc_in_box(Qnil, rb_define_gem_modules, (VALUE)&gem_flags, Qnil,
+ rb_str_new_cstr("before_prelude.root.dummy"), rb_root_box());
+ rb_vm_call_cfunc_in_box(Qnil, rb_define_gem_modules, (VALUE)&gem_flags, Qnil,
+ rb_str_new_cstr("before_prelude.main.dummy"), rb_main_box());
+
+ rb_box_set_gem_flags(&gem_flags);
+ }
+ else {
+ rb_define_gem_modules((VALUE)&gem_flags, Qnil);
+ }
+ }
+
+ // The root/main boxes load gem_prelude here.
+ // User boxes will load it in those #initialize instead.
+ ruby_init_prelude();
+
// Enable JITs after ruby_init_prelude() to avoid JITing prelude code.
#if USE_YJIT
rb_yjit_init(opt->yjit);
@@ -1847,7 +1881,12 @@ ruby_opt_init(ruby_cmdline_options_t *opt)
#endif
ruby_set_script_name(opt->script_name);
- require_libraries(&opt->req_list);
+ if (rb_box_available()) {
+ require_libraries_in_main_box(&opt->req_list);
+ }
+ else {
+ require_libraries(&opt->req_list);
+ }
}
static int
diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb
index 9d60941526..34b5d5fa96 100644
--- a/test/ruby/test_box.rb
+++ b/test/ruby/test_box.rb
@@ -2,6 +2,7 @@
require 'test/unit'
require 'rbconfig'
+require 'tempfile'
class TestBox < Test::Unit::TestCase
EXPERIMENTAL_WARNING_LINE_PATTERNS = [
@@ -155,8 +156,7 @@ class TestBox < Test::Unit::TestCase
assert_include Ruby::Box.current.inspect, "main"
end
- def test_class_variables
- # [Bug #21952]
+ def test_class_variables_in_root_are_invisible_in_other_boxes
assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
begin;
Ruby::Box.root.eval(<<~RUBY)
@@ -179,10 +179,9 @@ class TestBox < Test::Unit::TestCase
REPRO
b1 = Ruby::Box.new
- assert_equal 2, b1.eval(code)
-
- b2 = Ruby::Box.new
- assert_equal 2, b2.eval(code)
+ assert_raise(NameError, "uninitialized class variable @@x in B") {
+ b1.eval(code)
+ }
end;
end
@@ -899,6 +898,72 @@ class TestBox < Test::Unit::TestCase
end
end
+ def test_calling_root_box_methods_does_not_change_user_boxes_newly_created
+ assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
+ begin;
+ assert_not_include Object.constants.sort, :Find # required by Pathname#find
+ assert_not_include Ruby::Box.root.eval("Object.constants.sort"), :Find
+ b1 = Ruby::Box.new
+ assert_not_include b1.eval("Object.constants.sort"), :Find
+
+ require 'pathname'
+ Pathname.new('.').find{|path| path.directory?}
+ assert_include Object.constants.sort, :Find # required by Pathname#find
+
+ assert_not_include Ruby::Box.root.eval("Object.constants.sort"), :Find
+ assert_not_include b1.eval("Object.constants.sort"), :Find
+
+ Ruby::Box.root.eval("require 'pathname'; Pathname.new('.').find{|path| path.directory? }")
+ assert_include Ruby::Box.root.eval("Object.constants.sort"), :Find
+
+ assert_not_include b1.eval("Object.constants.sort"), :Find
+ b2 = Ruby::Box.new
+ assert_not_include b2.eval("Object.constants.sort"), :Find
+ end;
+ end
+
+ def test_boxes_have_different_rubygems
+ # assert_separately w/ ENV_ENABLE_BOX and --enable=gems causes timeouts on CI @ Windows
+ assert_in_out_err([ENV_ENABLE_BOX, "--enable=gems"], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error|
+ begin;
+ require "json"
+ h = {main: Gem.object_id, root: Ruby::Box.root.eval("Gem").object_id, box: Ruby::Box.new.eval("Gem").object_id}
+ puts h.to_json
+ end;
+ require "json"
+ result = JSON.parse(output.first, symbolize_names: true)
+ assert_not_equal result[:main], result[:root]
+ assert_not_equal result[:box], result[:root]
+ assert_not_equal result[:main], result[:box]
+ end
+ end
+
+ def test_require_list_loaded_only_in_main_box
+ Tempfile.create(["req_a", ".rb"]) do |t1|
+ Tempfile.create(["req_b", ".rb"]) do |t2|
+ t1.puts "module FooBarA; end"
+ t1.close
+ t2.puts "module FooBarB; end"
+ t2.close
+
+ opts = [ENV_ENABLE_BOX, "-r#{t1.path}", "-r#{t2.path}"]
+ assert_separately(opts, __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
+ begin;
+ main_constants = Object.constants
+ assert_include main_constants, :FooBarA
+ assert_include main_constants, :FooBarB
+
+ root_constants = Ruby::Box.root.eval("Object.constants.sort")
+ master_constants = Ruby::Box.master.eval("Object.constants.sort")
+ assert_not_include root_constants, :FooBarA
+ assert_not_include root_constants, :FooBarB
+ assert_not_include master_constants, :FooBarA
+ assert_not_include master_constants, :FooBarB
+ end;
+ end
+ end
+ end
+
def test_root_and_main_methods
assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
begin;
@@ -997,6 +1062,18 @@ class TestBox < Test::Unit::TestCase
end;
end
+ def test_very_basic_method_calls_and_constants
+ assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
+ begin;
+ code = <<~EOC
+ consts = Object.constants
+ [consts.include?(:String), consts.include?(:Array)]
+ EOC
+ assert_equal([true, true], Ruby::Box.current.eval(code))
+ assert_equal([true, true], Ruby::Box.root.eval(code))
+ end;
+ end
+
def test_loading_extension_libs_in_main_box_1
pend if /mswin|mingw/ =~ RUBY_PLATFORM # timeout on windows environments
assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
diff --git a/vm.c b/vm.c
index 42c2267ea6..c79663cf6b 100644
--- a/vm.c
+++ b/vm.c
@@ -3232,7 +3232,7 @@ find_loader_control_frame(const rb_execution_context_t *ec, const rb_control_fra
while (RUBY_VM_VALID_CONTROL_FRAME_P(cfp, end_cfp)) {
if (!VM_ENV_FRAME_TYPE_P(cfp->ep, VM_FRAME_MAGIC_CFUNC))
break;
- if (!BOX_ROOT_P(current_box_on_cfp(ec, cfp)))
+ if (!BOX_MASTER_P(current_box_on_cfp(ec, cfp)))
break;
cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
}
diff --git a/vm_core.h b/vm_core.h
index 1e3dcfe04f..6190d3220b 100644
--- a/vm_core.h
+++ b/vm_core.h
@@ -774,6 +774,7 @@ typedef struct rb_vm_struct {
const VALUE special_exceptions[ruby_special_error_count];
/* Ruby Box */
+ rb_box_t *master_box;
rb_box_t *root_box;
rb_box_t *main_box;
diff --git a/vm_insnhelper.c b/vm_insnhelper.c
index 24835cda26..82324c72a5 100644
--- a/vm_insnhelper.c
+++ b/vm_insnhelper.c
@@ -2273,6 +2273,8 @@ vm_search_method_fastpath(const struct rb_control_frame_struct *reg_cfp, struct
{
const struct rb_callcache *cc = cd->cc;
+ VM_ASSERT_TYPE2(klass, T_CLASS, T_ICLASS);
+
#if OPT_INLINE_METHOD_CACHE
if (LIKELY(vm_cc_class_check(cc, klass))) {
if (LIKELY(!METHOD_ENTRY_INVALIDATED(vm_cc_cme(cc)))) {