diff --git a/ruby.c b/ruby.c index 3d53610c2473c9..105657c2206683 100644 --- a/ruby.c +++ b/ruby.c @@ -1819,6 +1819,16 @@ ruby_opt_init(ruby_cmdline_options_t *opt) GET_VM()->running = 1; memset(ruby_vm_redefined_flag, 0, sizeof(ruby_vm_redefined_flag)); + // Register JIT-optimized builtin CMEs before the prelude, which may + // redefine core methods (e.g. Kernel.prepend via bundler/setup). +#if USE_YJIT + rb_yjit_init_builtin_cmes(); +#endif +#if USE_ZJIT + extern void rb_zjit_init_builtin_cmes(void); + rb_zjit_init_builtin_cmes(); +#endif + ruby_init_prelude(); /* Initialize the main box after loading libraries (including rubygems) @@ -1827,7 +1837,7 @@ ruby_opt_init(ruby_cmdline_options_t *opt) rb_initialize_main_box(); rb_box_init_done(); - // Initialize JITs after ruby_init_prelude() because JITing prelude is typically not optimal. + // Enable JITs after ruby_init_prelude() to avoid JITing prelude code. #if USE_YJIT rb_yjit_init(opt->yjit); #endif diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index 2670db181c7795..e7d0ae50a508ad 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -1850,6 +1850,17 @@ def test_yjit_dump_insns assert_not_predicate(status, :signaled?) end + def test_yjit_prelude_kernel_prepend + # Simulate what bundler/setup can do: prepend a module to Kernel during + # the prelude via the BUNDLER_SETUP mechanism in rubygems.rb: + # require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler) + Tempfile.create(["kernel_prepend", ".rb"]) do |f| + f.write("Kernel.prepend(Module.new)\n") + f.flush + assert_separately([{ "BUNDLER_SETUP" => f.path }, "--enable=gems", "--yjit"], "", ignore_stderr: true) + end + end + private def code_gc_helpers diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index ff7c1673278300..0895a1cfad79ac 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -112,6 +112,17 @@ def test_zjit_disable RUBY end + def test_zjit_prelude_kernel_prepend + # Simulate what bundler/setup can do: prepend a module to Kernel during + # the prelude via the BUNDLER_SETUP mechanism in rubygems.rb: + # require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler) + Tempfile.create(["kernel_prepend", ".rb"]) do |f| + f.write("Kernel.prepend(Module.new)\n") + f.flush + assert_separately([{ "BUNDLER_SETUP" => f.path }, "--enable=gems", "--zjit"], "", ignore_stderr: true) + end + end + def test_zjit_enable_respects_existing_options assert_separately(['--zjit-disable', '--zjit-stats-quiet'], <<~RUBY) refute_predicate RubyVM::ZJIT, :enabled? diff --git a/yjit.h b/yjit.h index 645f0af49d8fd3..45a9f74c8692d6 100644 --- a/yjit.h +++ b/yjit.h @@ -35,6 +35,7 @@ void rb_yjit_cme_invalidate(rb_callable_method_entry_t *cme); void rb_yjit_collect_binding_alloc(void); void rb_yjit_collect_binding_set(void); void rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception); +void rb_yjit_init_builtin_cmes(void); void rb_yjit_init(bool yjit_enabled); void rb_yjit_free_at_exit(void); void rb_yjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop); @@ -63,6 +64,7 @@ static inline void rb_yjit_cme_invalidate(rb_callable_method_entry_t *cme) {} static inline void rb_yjit_collect_binding_alloc(void) {} static inline void rb_yjit_collect_binding_set(void) {} static inline void rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) {} +static inline void rb_yjit_init_builtin_cmes(void) {} static inline void rb_yjit_init(bool yjit_enabled) {} static inline void rb_yjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop) {} static inline void rb_yjit_constant_state_changed(ID id) {} diff --git a/yjit/src/yjit.rs b/yjit/src/yjit.rs index 517a0daae5b9e2..0e0ad31c8e83ad 100644 --- a/yjit/src/yjit.rs +++ b/yjit/src/yjit.rs @@ -37,12 +37,17 @@ pub fn yjit_enabled_p() -> bool { unsafe { rb_yjit_enabled_p } } -/// This function is called from C code +/// Register specialized codegen for builtin C method entries. +/// Must be called at boot before ruby_init_prelude() since the prelude +/// could redefine core methods (e.g. Kernel.prepend via bundler). #[no_mangle] -pub extern "C" fn rb_yjit_init(yjit_enabled: bool) { - // Register the method codegen functions. This must be done at boot. +pub extern "C" fn rb_yjit_init_builtin_cmes() { yjit_reg_method_codegen_fns(); +} +/// This function is called from C code +#[no_mangle] +pub extern "C" fn rb_yjit_init(yjit_enabled: bool) { // If --yjit-disable, yjit_init() will not be called until RubyVM::YJIT.enable. if yjit_enabled { yjit_init(); diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 04411e7efbac4e..a7851b2607132a 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -292,25 +292,28 @@ impl ZJITState { } } -/// Initialize ZJIT at boot. This is called even if ZJIT is disabled. +/// Initialize IDs and annotate builtin C method entries. +/// Must be called at boot before ruby_init_prelude() since the prelude +/// could redefine core methods (e.g. Kernel.prepend via bundler). #[unsafe(no_mangle)] -pub extern "C" fn rb_zjit_init(zjit_enabled: bool) { +pub extern "C" fn rb_zjit_init_builtin_cmes() { use InitializationState::*; debug_assert!( matches!(unsafe { &ZJIT_STATE }, Uninitialized), - "rb_zjit_init should only be called once during boot", + "rb_zjit_init_builtin_cmes should only be called once during boot", ); - // Initialize IDs and method annotations. - // cruby_methods::init() must be called at boot, - // as cmes could have been re-defined after boot. cruby::ids::init(); let method_annotations = cruby_methods::init(); unsafe { ZJIT_STATE = Initialized(method_annotations); } +} +/// Initialize ZJIT at boot. This is called even if ZJIT is disabled. +#[unsafe(no_mangle)] +pub extern "C" fn rb_zjit_init(zjit_enabled: bool) { // If --zjit, enable ZJIT immediately if zjit_enabled { zjit_enable();