diff --git a/box.c b/box.c index fba494f7ad2e1c..9c85426b8c51b8 100644 --- a/box.c +++ b/box.c @@ -20,6 +20,8 @@ #include "darray.h" #include "zjit.h" +#include "builtin.h" + #include #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; @@ -61,15 +67,33 @@ VALUE rb_resolve_feature_path(VALUE klass, VALUE fname); 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("#"); + r = rb_str_new_cstr("#"); return r; } box = rb_get_box_t(obj); r = rb_str_new_cstr("#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 @@ -1003,6 +1059,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) @@ -1017,6 +1080,14 @@ rb_box_s_main(VALUE recv) return main_box->box_object; } +/* :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) @@ -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 6cc9790466a9f1..03c9d03bc3183c 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" @@ -22,18 +23,41 @@ bin4feature(const struct builtin_binary *bb, const char *feature, size_t *psize) static const unsigned char* builtin_lookup(const char *feature, size_t *psize) { - static int index = 0; - const unsigned char *bin = bin4feature(&builtin_binary[index++], feature, psize); + static size_t index = 0; + const unsigned char *bin = NULL; + + /* + * Fast path: + * builtin_binary is usually arranged in the same order + * as features are looked up in miniruby, so try the next entry first. + */ + if (builtin_binary[index].feature) { + bin = bin4feature(&builtin_binary[index], feature, psize); + index++; + } + if (bin) { + return bin; + } - // usually, `builtin_binary` order is loading order at miniruby. - for (const struct builtin_binary *bb = &builtin_binary[0]; bb->feature &&! bin; bb++) { - bin = bin4feature(bb++, feature, psize); + /* + * Fallback: + * In case the lookup order does not match the array order, + * scan the entire table to find the feature. + */ + for (const struct builtin_binary *bb = &builtin_binary[0]; + bb->feature; + bb++) { + bin = bin4feature(bb, feature, psize); + if (bin) { + break; + } } + return bin; } 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; @@ -51,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 @@ -80,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 fd1c4c307fc62b..ffd2aad88eb054 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 70563c3094a00e..91bea44cd93069 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 a17eb16f758660..e0247ffa7eb3cb 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 8c7fd20b202f09..92514b3ec9e2f1 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 2b613ffff3e007..b6fedf11f36702 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 b62b6a9bc946f2..c717dc4e2416b7 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 8b66f50bc6a6cb..51e38b09c84243 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 dee818285c75ad..be73dac1dcbe8a 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 1e2c32a67efa3c..75ea94d37dfd5c 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 687d769285cbfb..162287ca71f495 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 9d609415266eec..34b5d5fa9623d5 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 @@ def test_raising_errors_in_require 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 ::B 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 @@ def test_prelude_gems_and_loaded_features_with_disable_gems 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 @@ def foo_box = Ruby::Box.current 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 42c2267ea65aa0..c79663cf6b85da 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 1e3dcfe04f21ac..6190d3220b93d5 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 24835cda265cca..82324c72a5c6ca 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)))) {