From 55b1ba3bf276ba82173bd961fb8e0f08bf4182a6 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 17 Jul 2025 15:38:54 +0900 Subject: [PATCH 1/9] Ractor.shareable_proc call-seq: Ractor.sharable_proc(self: nil){} -> sharable proc It returns shareable Proc object. The Proc object is shareable and the self in a block will be replaced with the value passed via `self:` keyword. In a shareable Proc, the outer variables should * (1) refer shareable objects * (2) be not be overwritten ```ruby a = 42 Ractor.shareable_proc{ p a } #=> OK b = 43 Ractor.shareable_proc{ p b; b = 44 } #=> Ractor::IsolationError because 'b' is reassigned in the block. c = 44 Ractor.shareable_proc{ p c } #=> Ractor::IsolationError because 'c' will be reassigned outside of the block. c = 45 d = 45 d = 46 if cond Ractor.shareable_proc{ p d } #=> Ractor::IsolationError because 'd' was reassigned outside of the block. ``` The last `d`'s case can be relaxed in a future version. The above check will be done in a static analysis at compile time, so the reflection feature such as `Binding#local_varaible_set` can not be detected. ```ruby e = 42 shpr = Ractor.shareable_proc{ p e } #=> OK binding.local_variable_set(:e, 43) shpr.call #=> 42 (returns captured timing value) ``` Ractor.sharaeble_lambda is also introduced. [Feature #21550] [Feature #21557] --- NEWS.md | 6 ++ bootstraptest/test_ractor.rb | 163 ++++++++++------------------------- compile.c | 91 ++++++++++++++++++- iseq.c | 5 +- prism_compile.c | 3 + ractor.c | 16 +++- ractor.rb | 43 ++++++++- test/ruby/test_iseq.rb | 23 ++--- test/ruby/test_ractor.rb | 30 ++----- thread.c | 2 +- vm.c | 35 ++++++-- vm_core.h | 10 ++- 12 files changed, 254 insertions(+), 173 deletions(-) diff --git a/NEWS.md b/NEWS.md index 5a9277b3992411..7d66e1aaa5185b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -117,6 +117,10 @@ Note: We're only listing outstanding class updates. * `Ractor#close_incoming` and `Ractor#close_outgoing` were removed. + * `Ractor.sharealbe_proc` and `Ractor.shareable_lambda` is introduced + to make shareable Proc or lambda. + [[Feature #21550]], [[Feature #21557]] + * `Set` * `Set` is now a core class, instead of an autoloaded stdlib class. @@ -316,3 +320,5 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21347]: https://bugs.ruby-lang.org/issues/21347 [Feature #21360]: https://bugs.ruby-lang.org/issues/21360 [Feature #21527]: https://bugs.ruby-lang.org/issues/21527 +[Feature #21550]: https://bugs.ruby-lang.org/issues/21550 +[Feature #21557]: https://bugs.ruby-lang.org/issues/21557 diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 74fee197f85a75..634a3e3e6102b9 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -145,28 +145,47 @@ }.map(&:value) } +assert_equal "42", %q{ + a = 42 + Ractor.shareable_lambda{ a }.call +} + # Ractor.make_shareable issue for locals in proc [Bug #18023] assert_equal '[:a, :b, :c, :d, :e]', %q{ v1, v2, v3, v4, v5 = :a, :b, :c, :d, :e - closure = Ractor.current.instance_eval{ Proc.new { [v1, v2, v3, v4, v5] } } - - Ractor.make_shareable(closure).call + closure = Proc.new { [v1, v2, v3, v4, v5] } + Ractor.shareable_proc(&closure).call } -# Ractor.make_shareable issue for locals in proc [Bug #18023] -assert_equal '[:a, :b, :c, :d, :e, :f, :g]', %q{ - a = :a - closure = Ractor.current.instance_eval do - -> { - b, c, d = :b, :c, :d - -> { - e, f, g = :e, :f, :g - -> { [a, b, c, d, e, f, g] } - }.call - }.call +# Ractor::IsolationError cases +assert_equal '3', %q{ + ok = 0 + + begin + a = 1 + Ractor.shareable_proc{a} + a = 2 + rescue Ractor::IsolationError => e + ok += 1 end - Ractor.make_shareable(closure).call + begin + cond = false + a = 1 + a = 2 if cond + Ractor.shareable_proc{a} + rescue Ractor::IsolationError => e + ok += 1 + end + + begin + 1.times{|i| + i = 2 + Ractor.shareable_proc{i} + } + rescue Ractor::IsolationError => e + ok += 1 + end } ### @@ -967,7 +986,7 @@ class C end RUBY -# Constant cache should care about non-sharable constants +# Constant cache should care about non-shareable constants assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false STR = "hello" def str; STR; end @@ -1137,41 +1156,17 @@ def /(other) [a.frozen?, a[0].frozen?] == [true, false] } -# Ractor.make_shareable(a_proc) makes a proc shareable. +# Ractor.make_shareable(a_proc) is not supported now. assert_equal 'true', %q{ - a = [1, [2, 3], {a: "4"}] - - pr = Ractor.current.instance_eval do - Proc.new do - a - end - end + pr = Proc.new{} - Ractor.make_shareable(a) # referred value should be shareable - Ractor.make_shareable(pr) - Ractor.shareable?(pr) -} - -# Ractor.make_shareable(a_proc) makes inner structure shareable and freezes it -assert_equal 'true,true,true,true', %q{ - class Proc - attr_reader :obj - def initialize - @obj = Object.new - end - end - - pr = Ractor.current.instance_eval do - Proc.new {} + begin + Ractor.make_shareable(pr) + rescue Ractor::Error + true + else + false end - - results = [] - Ractor.make_shareable(pr) - results << Ractor.shareable?(pr) - results << pr.frozen? - results << Ractor.shareable?(pr.obj) - results << pr.obj.frozen? - results.map(&:to_s).join(',') } # Ractor.shareable?(recursive_objects) @@ -1202,50 +1197,16 @@ module M; end Ractor.make_shareable(ary = [C, M]) } -# Ractor.make_shareable with curried proc checks isolation of original proc -assert_equal 'isolation error', %q{ - a = Object.new - orig = proc { a } - curried = orig.curry - - begin - Ractor.make_shareable(curried) - rescue Ractor::IsolationError - 'isolation error' - else - 'no error' - end -} - # define_method() can invoke different Ractor's proc if the proc is shareable. assert_equal '1', %q{ class C a = 1 - define_method "foo", Ractor.make_shareable(Proc.new{ a }) - a = 2 + define_method "foo", Ractor.shareable_proc{ a } end Ractor.new{ C.new.foo }.value } -# Ractor.make_shareable(a_proc) makes a proc shareable. -assert_equal 'can not make a Proc shareable because it accesses outer variables (a).', %q{ - a = b = nil - pr = Ractor.current.instance_eval do - Proc.new do - c = b # assign to a is okay because c is block local variable - # reading b is okay - a = b # assign to a is not allowed #=> Ractor::Error - end - end - - begin - Ractor.make_shareable(pr) - rescue => e - e.message - end -} - # Ractor.make_shareable(obj, copy: true) makes copied shareable object. assert_equal '[false, false, true, true]', %q{ r = [] @@ -1471,42 +1432,6 @@ class C "ok" } if !yjit_enabled? && ENV['GITHUB_WORKFLOW'] != 'ModGC' # flaky -assert_equal "ok", %q{ - def foo(*); ->{ super }; end - begin - Ractor.make_shareable(foo) - rescue Ractor::IsolationError - "ok" - end -} - -assert_equal "ok", %q{ - def foo(**); ->{ super }; end - begin - Ractor.make_shareable(foo) - rescue Ractor::IsolationError - "ok" - end -} - -assert_equal "ok", %q{ - def foo(...); ->{ super }; end - begin - Ractor.make_shareable(foo) - rescue Ractor::IsolationError - "ok" - end -} - -assert_equal "ok", %q{ - def foo((x), (y)); ->{ super }; end - begin - Ractor.make_shareable(foo([], [])) - rescue Ractor::IsolationError - "ok" - end -} - # check method cache invalidation assert_equal "ok", %q{ module M diff --git a/compile.c b/compile.c index 0ef0d1b31a5fa3..fb269721f3fae0 100644 --- a/compile.c +++ b/compile.c @@ -494,6 +494,7 @@ static int iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const anchor, const static int iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor); static int iseq_set_exception_table(rb_iseq_t *iseq); static int iseq_set_optargs_table(rb_iseq_t *iseq); +static int iseq_set_parameters_lvar_state(const rb_iseq_t *iseq); static int compile_defined_expr(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, VALUE needstr, bool ignore); static int compile_hash(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int method_call_keywords, int popped); @@ -876,6 +877,7 @@ rb_iseq_compile_node(rb_iseq_t *iseq, const NODE *node) /* iseq type of top, method, class, block */ iseq_set_local_table(iseq, RNODE_SCOPE(node)->nd_tbl, (NODE *)RNODE_SCOPE(node)->nd_args); iseq_set_arguments(iseq, ret, (NODE *)RNODE_SCOPE(node)->nd_args); + iseq_set_parameters_lvar_state(iseq); switch (ISEQ_BODY(iseq)->type) { case ISEQ_TYPE_BLOCK: @@ -1715,6 +1717,7 @@ iseq_set_exception_local_table(rb_iseq_t *iseq) { ISEQ_BODY(iseq)->local_table_size = numberof(rb_iseq_shared_exc_local_tbl); ISEQ_BODY(iseq)->local_table = rb_iseq_shared_exc_local_tbl; + ISEQ_BODY(iseq)->lvar_states = NULL; // $! is read-only, so don't need lvar_states return COMPILE_OK; } @@ -1861,6 +1864,46 @@ iseq_lvar_id(const rb_iseq_t *iseq, int idx, int level) return id; } +static void +update_lvar_state(const rb_iseq_t *iseq, int level, int idx) +{ + for (int i=0; iparent_iseq; + } + + enum lvar_state *states = ISEQ_BODY(iseq)->lvar_states; + int table_idx = ISEQ_BODY(iseq)->local_table_size - idx; + switch (states[table_idx]) { + case lvar_uninitialized: + states[table_idx] = lvar_initialized; + break; + case lvar_initialized: + states[table_idx] = lvar_reassigned; + break; + case lvar_reassigned: + /* nothing */ + break; + default: + rb_bug("unreachable"); + } +} + +static int +iseq_set_parameters_lvar_state(const rb_iseq_t *iseq) +{ + for (unsigned int i=0; iparam.size; i++) { + ISEQ_BODY(iseq)->lvar_states[i] = lvar_initialized; + } + + int lead_num = ISEQ_BODY(iseq)->param.lead_num; + int opt_num = ISEQ_BODY(iseq)->param.opt_num; + for (int i=0; ilvar_states[lead_num + i] = lvar_uninitialized; + } + + return COMPILE_OK; +} + static void iseq_add_getlocal(rb_iseq_t *iseq, LINK_ANCHOR *const seq, const NODE *const line_node, int idx, int level) { @@ -1882,6 +1925,7 @@ iseq_add_setlocal(rb_iseq_t *iseq, LINK_ANCHOR *const seq, const NODE *const lin else { ADD_INSN2(seq, line_node, setlocal, INT2FIX((idx) + VM_ENV_DATA_SIZE - 1), INT2FIX(level)); } + update_lvar_state(iseq, level, idx); if (level > 0) access_outer_variables(iseq, level, iseq_lvar_id(iseq, idx, level), Qtrue); } @@ -2212,6 +2256,14 @@ iseq_set_local_table(rb_iseq_t *iseq, const rb_ast_id_table_t *tbl, const NODE * ID *ids = ALLOC_N(ID, size); MEMCPY(ids, tbl->ids + offset, ID, size); ISEQ_BODY(iseq)->local_table = ids; + + enum lvar_state *states = ALLOC_N(enum lvar_state, size); + // fprintf(stderr, "iseq:%p states:%p size:%d\n", iseq, states, (int)size); + for (unsigned int i=0; ilocal_table[i])); + } + ISEQ_BODY(iseq)->lvar_states = states; } ISEQ_BODY(iseq)->local_table_size = size; @@ -12379,7 +12431,7 @@ typedef uint32_t ibf_offset_t; #define IBF_MAJOR_VERSION ISEQ_MAJOR_VERSION #ifdef RUBY_DEVEL -#define IBF_DEVEL_VERSION 4 +#define IBF_DEVEL_VERSION 5 #define IBF_MINOR_VERSION (ISEQ_MINOR_VERSION * 10000 + IBF_DEVEL_VERSION) #else #define IBF_MINOR_VERSION ISEQ_MINOR_VERSION @@ -13190,7 +13242,7 @@ ibf_dump_local_table(struct ibf_dump *dump, const rb_iseq_t *iseq) return ibf_dump_write(dump, table, sizeof(ID) * size); } -static ID * +static const ID * ibf_load_local_table(const struct ibf_load *load, ibf_offset_t local_table_offset, int size) { if (size > 0) { @@ -13200,13 +13252,42 @@ ibf_load_local_table(const struct ibf_load *load, ibf_offset_t local_table_offse for (i=0; ilocal_table_size; + IBF_W_ALIGN(enum lvar_state); + return ibf_dump_write(dump, body->lvar_states, body->lvar_states ? size : 0); +} + +static enum lvar_state * +ibf_load_lvar_states(const struct ibf_load *load, ibf_offset_t lvar_states_offset, int size, const ID *local_table) +{ + if (local_table == rb_iseq_shared_exc_local_tbl || + size <= 0) { + return NULL; + } + else { + enum lvar_state *states = IBF_R(lvar_states_offset, enum lvar_state, size); + return states; + } +} + static ibf_offset_t ibf_dump_catch_table(struct ibf_dump *dump, const rb_iseq_t *iseq) { @@ -13480,6 +13561,7 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq) ruby_xfree(positions); const ibf_offset_t local_table_offset = ibf_dump_local_table(dump, iseq); + const ibf_offset_t lvar_states_offset = ibf_dump_lvar_states(dump, iseq); const unsigned int catch_table_size = body->catch_table ? body->catch_table->size : 0; const ibf_offset_t catch_table_offset = ibf_dump_catch_table(dump, iseq); const int parent_iseq_index = ibf_dump_iseq(dump, ISEQ_BODY(iseq)->parent_iseq); @@ -13547,6 +13629,7 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq) ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(insns_info_positions_offset)); ibf_dump_write_small_value(dump, body->insns_info.size); ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(local_table_offset)); + ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(lvar_states_offset)); ibf_dump_write_small_value(dump, catch_table_size); ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(catch_table_offset)); ibf_dump_write_small_value(dump, parent_iseq_index); @@ -13658,6 +13741,7 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset) const ibf_offset_t insns_info_positions_offset = (ibf_offset_t)IBF_BODY_OFFSET(ibf_load_small_value(load, &reading_pos)); const unsigned int insns_info_size = (unsigned int)ibf_load_small_value(load, &reading_pos); const ibf_offset_t local_table_offset = (ibf_offset_t)IBF_BODY_OFFSET(ibf_load_small_value(load, &reading_pos)); + const ibf_offset_t lvar_states_offset = (ibf_offset_t)IBF_BODY_OFFSET(ibf_load_small_value(load, &reading_pos)); const unsigned int catch_table_size = (unsigned int)ibf_load_small_value(load, &reading_pos); const ibf_offset_t catch_table_offset = (ibf_offset_t)IBF_BODY_OFFSET(ibf_load_small_value(load, &reading_pos)); const int parent_iseq_index = (int)ibf_load_small_value(load, &reading_pos); @@ -13774,6 +13858,7 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset) load_body->insns_info.body = ibf_load_insns_info_body(load, insns_info_body_offset, insns_info_size); load_body->insns_info.positions = ibf_load_insns_info_positions(load, insns_info_positions_offset, insns_info_size); load_body->local_table = ibf_load_local_table(load, local_table_offset, local_table_size); + load_body->lvar_states = ibf_load_lvar_states(load, lvar_states_offset, local_table_size, load_body->local_table); ibf_load_catch_table(load, catch_table_offset, catch_table_size, iseq); const rb_iseq_t *parent_iseq = ibf_load_iseq(load, (const rb_iseq_t *)(VALUE)parent_iseq_index); diff --git a/iseq.c b/iseq.c index 33b37bade5dece..d8891353f1a365 100644 --- a/iseq.c +++ b/iseq.c @@ -199,8 +199,11 @@ rb_iseq_free(const rb_iseq_t *iseq) } ruby_xfree((void *)body->param.keyword); } - if (LIKELY(body->local_table != rb_iseq_shared_exc_local_tbl)) + if (LIKELY(body->local_table != rb_iseq_shared_exc_local_tbl)) { ruby_xfree((void *)body->local_table); + } + ruby_xfree((void *)body->lvar_states); + compile_data_free(ISEQ_COMPILE_DATA(iseq)); if (body->outer_variables) rb_id_table_free(body->outer_variables); ruby_xfree(body); diff --git a/prism_compile.c b/prism_compile.c index 578e6f240f98dc..cd8287c7621d5b 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -102,6 +102,7 @@ pm_iseq_add_setlocal(rb_iseq_t *iseq, LINK_ANCHOR *const seq, int line, int node else { ADD_ELEM(seq, (LINK_ELEMENT *) new_insn_body(iseq, line, node_id, BIN(setlocal), 2, INT2FIX((idx) + VM_ENV_DATA_SIZE - 1), INT2FIX(level))); } + update_lvar_state(iseq, level, idx); if (level > 0) access_outer_variables(iseq, level, iseq_lvar_id(iseq, idx, level), Qtrue); } @@ -6796,6 +6797,8 @@ pm_compile_scope_node(rb_iseq_t *iseq, pm_scope_node_t *scope_node, const pm_nod // FIXME: args? iseq_set_local_table(iseq, local_table_for_iseq, 0); + iseq_set_parameters_lvar_state(iseq); + scope_node->local_table_for_iseq_size = local_table_for_iseq->size; if (keyword != NULL) { diff --git a/ractor.c b/ractor.c index b5142106385dd8..c439f25e859e39 100644 --- a/ractor.c +++ b/ractor.c @@ -1374,7 +1374,7 @@ make_shareable_check_shareable(VALUE obj) } else if (!allow_frozen_shareable_p(obj)) { if (rb_obj_is_proc(obj)) { - rb_proc_ractor_make_shareable(obj); + rb_proc_ractor_make_shareable(obj, Qundef); return traverse_cont; } else { @@ -2273,6 +2273,20 @@ ractor_local_value_store_if_absent(rb_execution_context_t *ec, VALUE self, VALUE return rb_mutex_synchronize(cr->local_storage_store_lock, ractor_local_value_store_i, (VALUE)&data); } +// sharable_proc + +static VALUE +ractor_shareable_proc(rb_execution_context_t *ec, VALUE replace_self, bool is_lambda) +{ + if (!rb_ractor_shareable_p(replace_self)) { + rb_raise(rb_eRactorIsolationError, "self should be shareable: %" PRIsVALUE, replace_self); + } + else { + VALUE proc = is_lambda ? rb_block_lambda() : rb_block_proc(); + return rb_proc_ractor_make_shareable(proc, replace_self); + } +} + // Ractor#require struct cross_ractor_require { diff --git a/ractor.rb b/ractor.rb index 87d18528769c31..7c00e148a120aa 100644 --- a/ractor.rb +++ b/ractor.rb @@ -1,5 +1,3 @@ -# \Ractor is an Actor-model abstraction for Ruby that provides thread-safe parallel execution. -# # Ractor.new makes a new \Ractor, which can run in parallel. # # # The simplest ractor @@ -606,6 +604,47 @@ def unmonitor port __builtin_ractor_unmonitor(port) end + # + # call-seq: + # Ractor.sharable_proc(self: nil){} -> sharable proc + # + # It returns shareable Proc object. The Proc object is + # shareable and the self in a block will be replaced with + # the value passed via `self:` keyword. + # + # In a shareable Proc, you can not access to the outer variables. + # + # a = 42 + # Ractor.shareable_proc{ p a } + # #=> can not isolate a Proc because it accesses outer variables (a). (ArgumentError) + # + # The `self` should be a sharable object + # + # Ractor.shareable_proc(self: self){} + # #=> self should be shareable: main (Ractor::IsolationError) + # + def self.shareable_proc self: nil + Primitive.attr! :use_block + + __builtin_cexpr!(%Q{ + ractor_shareable_proc(ec, *LOCAL_PTR(self), false) + }) + end + + # + # call-seq: + # Ractor.sharable_proc{} -> sharable proc + # + # Same as Ractor.sharable_proc, but returns lambda proc. + # + def self.shareable_lambda self: nil + Primitive.attr! :use_block + + __builtin_cexpr!(%Q{ + ractor_shareable_proc(ec, *LOCAL_PTR(self), true) + }) + end + # \Port objects transmit messages between Ractors. class Port # diff --git a/test/ruby/test_iseq.rb b/test/ruby/test_iseq.rb index 45223c89da5927..fa716787fe9841 100644 --- a/test/ruby/test_iseq.rb +++ b/test/ruby/test_iseq.rb @@ -139,8 +139,7 @@ def (Object.new).touch(**) # :nodoc: def test_lambda_with_ractor_roundtrip iseq = compile(<<~EOF, __LINE__+1) x = 42 - y = nil.instance_eval{ lambda { x } } - Ractor.make_shareable(y) + y = Ractor.shareable_lambda{x} y.call EOF assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval) @@ -158,22 +157,18 @@ def (Object.new).touch(&) # :nodoc: def test_ractor_unshareable_outer_variable name = "\u{2603 26a1}" - y = nil.instance_eval do - eval("proc {#{name} = nil; proc {|x| #{name} = x}}").call - end assert_raise_with_message(ArgumentError, /\(#{name}\)/) do - Ractor.make_shareable(y) - end - y = nil.instance_eval do - eval("proc {#{name} = []; proc {|x| #{name}}}").call + eval("#{name} = nil; Ractor.shareable_proc{#{name} = nil}") end - assert_raise_with_message(Ractor::IsolationError, /'#{name}'/) do - Ractor.make_shareable(y) + + assert_raise_with_message(Ractor::IsolationError, /\'#{name}\'/) do + eval("#{name} = []; Ractor.shareable_proc{#{name}}") end + obj = Object.new - def obj.foo(*) nil.instance_eval{ ->{super} } end - assert_raise_with_message(Ractor::IsolationError, /refer unshareable object \[\] from variable '\*'/) do - Ractor.make_shareable(obj.foo(*[])) + def obj.foo(*) Ractor.shareable_proc{super} end + assert_raise_with_message(Ractor::IsolationError, /cannot make a shareable Proc because it can refer unshareable object \[\]/) do + obj.foo(*[]) end end diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 70a2ca4bfbd673..c4154cd2632b27 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -3,38 +3,19 @@ class TestRactor < Test::Unit::TestCase def test_shareability_of_iseq_proc - y = nil.instance_eval do + assert_raise Ractor::IsolationError do foo = [] - proc { foo } + Ractor.shareable_proc{ foo } end - assert_unshareable(y, /unshareable object \[\] from variable 'foo'/) - - y = [].instance_eval { proc { self } } - assert_unshareable(y, /Proc's self is not shareable/) - - y = [].freeze.instance_eval { proc { self } } - assert_make_shareable(y) - end - - def test_shareability_of_curried_proc - x = nil.instance_eval do - foo = [] - proc { foo }.curry - end - assert_unshareable(x, /unshareable object \[\] from variable 'foo'/) - - x = nil.instance_eval do - foo = 123 - proc { foo }.curry - end - assert_make_shareable(x) end def test_shareability_of_method_proc + # TODO: fix with Ractor.shareable_proc/lambda +=begin str = +"" x = str.instance_exec { proc { to_s } } - assert_unshareable(x, /Proc's self is not shareable/) + assert_unshareable(x, /Proc\'s self is not shareable/) x = str.instance_exec { method(:to_s) } assert_unshareable(x, "can not make shareable object for #", exception: Ractor::Error) @@ -58,6 +39,7 @@ def test_shareability_of_method_proc x = str.instance_exec { method(:itself).to_proc } assert_unshareable(x, "can not make shareable object for #", exception: Ractor::Error) +=end end def test_shareability_error_uses_inspect diff --git a/thread.c b/thread.c index 97e9561f3af3c5..bab615b9edb27b 100644 --- a/thread.c +++ b/thread.c @@ -841,7 +841,7 @@ thread_create_core(VALUE thval, struct thread_create_params *params) th->invoke_type = thread_invoke_type_ractor_proc; th->ractor = params->g; th->ractor->threads.main = th; - th->invoke_arg.proc.proc = rb_proc_isolate_bang(params->proc); + th->invoke_arg.proc.proc = rb_proc_isolate_bang(params->proc, Qnil); th->invoke_arg.proc.args = INT2FIX(RARRAY_LENINT(params->args)); th->invoke_arg.proc.kw_splat = rb_keyword_given_p(); rb_ractor_send_parameters(ec, params->g, params->args); diff --git a/vm.c b/vm.c index da92c5bd009f83..154a0ba9d16d04 100644 --- a/vm.c +++ b/vm.c @@ -1325,12 +1325,22 @@ env_copy(const VALUE *src_ep, VALUE read_only_variables) for (int i=RARRAY_LENINT(read_only_variables)-1; i>=0; i--) { ID id = NUM2ID(RARRAY_AREF(read_only_variables, i)); - for (unsigned int j=0; jiseq)->local_table_size; j++) { - if (id == ISEQ_BODY(src_env->iseq)->local_table[j]) { + const struct rb_iseq_constant_body *body = ISEQ_BODY(src_env->iseq); + for (unsigned int j=0; jlocal_table_size; j++) { + if (id == body->local_table[j]) { + // check reassignment + if (body->lvar_states[j] == lvar_reassigned) { + VALUE name = rb_id2str(id); + VALUE msg = rb_sprintf("cannot make a shareable Proc because " + "the outer variable '%" PRIsVALUE "' may be reassigned.", name); + rb_exc_raise(rb_exc_new_str(rb_eRactorIsolationError, msg)); + } + + // check shareable VALUE v = src_env->env[j]; if (!rb_ractor_shareable_p(v)) { VALUE name = rb_id2str(id); - VALUE msg = rb_sprintf("can not make shareable Proc because it can refer" + VALUE msg = rb_sprintf("cannot make a shareable Proc because it can refer" " unshareable object %+" PRIsVALUE " from ", v); if (name) rb_str_catf(msg, "variable '%" PRIsVALUE "'", name); @@ -1403,12 +1413,18 @@ proc_shared_outer_variables(struct rb_id_table *outer_variables, bool isolate, c } VALUE -rb_proc_isolate_bang(VALUE self) +rb_proc_isolate_bang(VALUE self, VALUE replace_self) { const rb_iseq_t *iseq = vm_proc_iseq(self); if (iseq) { rb_proc_t *proc = (rb_proc_t *)RTYPEDDATA_DATA(self); + + if (!UNDEF_P(replace_self)) { + VM_ASSERT(rb_ractor_shareable_p(replace_self)); + RB_OBJ_WRITE(self, &proc->block.as.captured.self, replace_self); + } + if (proc->block.type != block_type_iseq) rb_raise(rb_eRuntimeError, "not supported yet"); if (ISEQ_BODY(iseq)->outer_variables) { @@ -1427,17 +1443,22 @@ VALUE rb_proc_isolate(VALUE self) { VALUE dst = rb_proc_dup(self); - rb_proc_isolate_bang(dst); + rb_proc_isolate_bang(dst, Qundef); return dst; } VALUE -rb_proc_ractor_make_shareable(VALUE self) +rb_proc_ractor_make_shareable(VALUE self, VALUE replace_self) { const rb_iseq_t *iseq = vm_proc_iseq(self); if (iseq) { rb_proc_t *proc = (rb_proc_t *)RTYPEDDATA_DATA(self); + + if (!UNDEF_P(replace_self)) { + RB_OBJ_WRITE(self, &proc->block.as.captured.self, replace_self); + } + if (proc->block.type != block_type_iseq) rb_raise(rb_eRuntimeError, "not supported yet"); if (!rb_ractor_shareable_p(vm_block_self(&proc->block))) { @@ -1458,6 +1479,8 @@ rb_proc_ractor_make_shareable(VALUE self) } rb_obj_freeze(self); + FL_SET_RAW(self, RUBY_FL_SHAREABLE); + return self; } diff --git a/vm_core.h b/vm_core.h index 9156b7286891a7..2e77e1073eb959 100644 --- a/vm_core.h +++ b/vm_core.h @@ -496,6 +496,12 @@ struct rb_iseq_constant_body { const ID *local_table; /* must free */ + enum lvar_state { + lvar_uninitialized, + lvar_initialized, + lvar_reassigned, + } *lvar_states; + /* catch table */ struct iseq_catch_table *catch_table; @@ -1285,8 +1291,8 @@ typedef struct { RUBY_SYMBOL_EXPORT_BEGIN VALUE rb_proc_isolate(VALUE self); -VALUE rb_proc_isolate_bang(VALUE self); -VALUE rb_proc_ractor_make_shareable(VALUE self); +VALUE rb_proc_isolate_bang(VALUE self, VALUE replace_self); +VALUE rb_proc_ractor_make_shareable(VALUE proc, VALUE replace_self); RUBY_SYMBOL_EXPORT_END typedef struct { From 8fad3e87beaf5d49f0bf7744c4102144832fe8cc Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Fri, 19 Sep 2025 01:56:13 +0900 Subject: [PATCH 2/9] use commit hash for net-imap to catch up new sharable proc semantics. --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/bundled_gems b/gems/bundled_gems index 71f79f8d0476e4..8de29d9937c808 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -13,7 +13,7 @@ test-unit 3.7.0 https://github.com/test-unit/test-unit rexml 3.4.2 https://github.com/ruby/rexml rss 0.3.1 https://github.com/ruby/rss net-ftp 0.3.8 https://github.com/ruby/net-ftp -net-imap 0.5.10 https://github.com/ruby/net-imap +net-imap 0.5.10 https://github.com/ruby/net-imap 71c0288b9a8f78a7125a4ce2980ab421acdf5836 net-pop 0.1.2 https://github.com/ruby/net-pop net-smtp 0.5.1 https://github.com/ruby/net-smtp matrix 0.4.3 https://github.com/ruby/matrix From 0fe1099d9a0bb3ac2b25828578defba3b5243aad Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 23 Sep 2025 12:05:52 -0700 Subject: [PATCH 3/9] ZJIT: Support gdb for debugging zjit-test (#14637) --- zjit/zjit.mk | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/zjit/zjit.mk b/zjit/zjit.mk index f0bf1b0da59fb2..f31b948845678a 100644 --- a/zjit/zjit.mk +++ b/zjit/zjit.mk @@ -56,7 +56,8 @@ ZJIT_BINDGEN_DIFF_OPTS = # Generate Rust bindings. See source for details. # Needs `./configure --enable-zjit=dev` and Clang. ifneq ($(strip $(CARGO)),) # if configure found Cargo -.PHONY: zjit-bindgen zjit-bindgen-show-unused zjit-test zjit-test-lldb zjit-test-update +.PHONY: zjit-bindgen zjit-bindgen-show-unused zjit-test zjit-test-update +.PHONY: zjit-test-debug zjit-test-lldb zjit-test-gdb zjit-test-rr zjit-bindgen: zjit.$(OBJEXT) ZJIT_SRC_ROOT_PATH='$(top_srcdir)' BINDGEN_JIT_NAME=zjit $(CARGO) run --manifest-path '$(top_srcdir)/zjit/bindgen/Cargo.toml' -- $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(Q) if [ 'x$(HAVE_GIT)' = xyes ]; then $(GIT) -C "$(top_srcdir)" diff $(ZJIT_BINDGEN_DIFF_OPTS) zjit/src/cruby_bindings.inc.rs; fi @@ -93,8 +94,11 @@ zjit-test-update: @$(CARGO) insta --version >/dev/null 2>&1 || { echo "Error: cargo-insta is not installed. Install with: cargo install cargo-insta"; exit 1; } @$(CARGO) insta accept --manifest-path '$(top_srcdir)/zjit/Cargo.toml' -# Run a ZJIT test written with Rust #[test] under LLDB -zjit-test-lldb: libminiruby.a +ZJIT_DEBUGGER = +ZJIT_DEBUGGER_OPTS = + +# Run a ZJIT test written with Rust #[test] under $(ZJIT_DEBUGGER) +zjit-test-debug: libminiruby.a $(Q)set -eu; \ if [ -z '$(ZJIT_TESTS)' ]; then \ echo "Please pass a ZJIT_TESTS=... filter to make."; \ @@ -104,7 +108,19 @@ zjit-test-lldb: libminiruby.a exe_path=`$(ZJIT_NEXTEST_ENV) \ $(CARGO) nextest list --manifest-path '$(top_srcdir)/zjit/Cargo.toml' --message-format json --list-type=binaries-only | \ $(BASERUBY) -rjson -e 'puts JSON.load(STDIN.read).dig("rust-binaries", "zjit", "binary-path")'`; \ - exec lldb $$exe_path -- --test-threads=1 $(ZJIT_TESTS) + exec $(ZJIT_DEBUGGER) $$exe_path $(ZJIT_DEBUGGER_OPTS) --test-threads=1 $(ZJIT_TESTS) + +# Run a ZJIT test written with Rust #[test] under LLDB +zjit-test-lldb: + $(Q) $(MAKE) zjit-test-debug ZJIT_DEBUGGER=lldb ZJIT_DEBUGGER_OPTS=-- + +# Run a ZJIT test written with Rust #[test] under GDB +zjit-test-gdb: libminiruby.a + $(Q) $(MAKE) zjit-test-debug ZJIT_DEBUGGER="gdb --args" + +# Run a ZJIT test written with Rust #[test] under rr-debugger +zjit-test-rr: libminiruby.a + $(Q) $(MAKE) zjit-test-debug ZJIT_DEBUGGER="rr record" # A library for booting miniruby in tests. # Why not use libruby-static.a for this? From 990ec01e7ddf4e465d08a69b76dff33aaa3d9e1d Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 23 Sep 2025 21:18:39 +0200 Subject: [PATCH 4/9] Fix typo in NEWS --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 7d66e1aaa5185b..3e153d8492423d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -117,7 +117,7 @@ Note: We're only listing outstanding class updates. * `Ractor#close_incoming` and `Ractor#close_outgoing` were removed. - * `Ractor.sharealbe_proc` and `Ractor.shareable_lambda` is introduced + * `Ractor.shareable_proc` and `Ractor.shareable_lambda` is introduced to make shareable Proc or lambda. [[Feature #21550]], [[Feature #21557]] From 39d764ed800fa6c930ff23067de323fd2fde4c2a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Tue, 23 Sep 2025 20:46:04 +0100 Subject: [PATCH 5/9] ZJIT: Add stack overflow check to `gen_ccall_variadic` (#14636) ZJIT: Add stack overflow check to gen_ccall_variadic --- zjit/src/codegen.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index bd7ab847861605..2db30448a3914b 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -661,6 +661,9 @@ fn gen_ccall_variadic( ) -> lir::Opnd { gen_prepare_non_leaf_call(jit, asm, state); + let stack_growth = state.stack_size(); + gen_stack_overflow_check(jit, asm, state, stack_growth); + gen_push_frame(asm, args.len(), state, ControlFrame { recv, iseq: None, @@ -1109,17 +1112,8 @@ fn gen_send_without_block_direct( state: &FrameState, ) -> lir::Opnd { let local_size = unsafe { get_iseq_body_local_table_size(iseq) }.as_usize(); - // Stack overflow check: fails if CFP<=SP at any point in the callee. - asm_comment!(asm, "stack overflow check"); let stack_growth = state.stack_size() + local_size + unsafe { get_iseq_body_stack_max(iseq) }.as_usize(); - // vm_push_frame() checks it against a decremented cfp, and CHECK_VM_STACK_OVERFLOW0 - // adds to the margin another control frame with `&bounds[1]`. - const { assert!(RUBY_SIZEOF_CONTROL_FRAME % SIZEOF_VALUE == 0, "sizeof(rb_control_frame_t) is a multiple of sizeof(VALUE)"); } - let cfp_growth = 2 * (RUBY_SIZEOF_CONTROL_FRAME / SIZEOF_VALUE); - let peak_offset = SIZEOF_VALUE * (stack_growth + cfp_growth); - let stack_limit = asm.add(SP, peak_offset.into()); - asm.cmp(CFP, stack_limit); - asm.jbe(side_exit(jit, state, StackOverflow)); + gen_stack_overflow_check(jit, asm, state, stack_growth); // Save cfp->pc and cfp->sp for the caller frame gen_prepare_call_with_gc(asm, state, false); @@ -1713,6 +1707,19 @@ fn gen_push_frame(asm: &mut Assembler, argc: usize, state: &FrameState, frame: C asm.mov(cfp_opnd(RUBY_OFFSET_CFP_BLOCK_CODE), 0.into()); } +/// Stack overflow check: fails if CFP<=SP at any point in the callee. +fn gen_stack_overflow_check(jit: &mut JITState, asm: &mut Assembler, state: &FrameState, stack_growth: usize) { + asm_comment!(asm, "stack overflow check"); + // vm_push_frame() checks it against a decremented cfp, and CHECK_VM_STACK_OVERFLOW0 + // adds to the margin another control frame with `&bounds[1]`. + const { assert!(RUBY_SIZEOF_CONTROL_FRAME % SIZEOF_VALUE == 0, "sizeof(rb_control_frame_t) is a multiple of sizeof(VALUE)"); } + let cfp_growth = 2 * (RUBY_SIZEOF_CONTROL_FRAME / SIZEOF_VALUE); + let peak_offset = (cfp_growth + stack_growth) * SIZEOF_VALUE; + let stack_limit = asm.lea(Opnd::mem(64, SP, peak_offset as i32)); + asm.cmp(CFP, stack_limit); + asm.jbe(side_exit(jit, state, StackOverflow)); +} + /// Return an operand we use for the basic block argument at a given index fn param_opnd(idx: usize) -> Opnd { // To simplify the implementation, allocate a fixed register or a stack slot for each basic block argument for now. From 06b7a70837d831b8628ae2adde9318371c111d82 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 23 Sep 2025 16:20:04 -0400 Subject: [PATCH 6/9] Fix thread_profile_frames crashing due to uninitialized PC ZJIT never sets `cfp->jit_return`, so to avoid crashing while profiling, we need to explicitly validate the PC of the top most frame. Particularly pertinent for profilers that call rb_profile_frames() from within a signal handler such as Vernier and Stackprof since they can sample at any time and observe an invalid PC. --- vm_backtrace.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/vm_backtrace.c b/vm_backtrace.c index aaa0b5051cc637..e81c568dda2e6b 100644 --- a/vm_backtrace.c +++ b/vm_backtrace.c @@ -1761,15 +1761,22 @@ thread_profile_frames(rb_execution_context_t *ec, int start, int limit, VALUE *b } if (lines) { - // The topmost frame may not have an updated PC because the JIT - // may not have set one. The JIT compiler will update the PC - // before entering a new function (so that `caller` will work), - // so only the topmost frame could possibly have an out of date PC - if (cfp == top && cfp->jit_return) { + const VALUE *pc = cfp->pc; + VALUE *iseq_encoded = ISEQ_BODY(cfp->iseq)->iseq_encoded; + VALUE *pc_end = iseq_encoded + ISEQ_BODY(cfp->iseq)->iseq_size; + + // The topmost frame may have an invalid PC because the JIT + // may leave it uninitialized for speed. JIT code must update the PC + // before entering a non-leaf method (so that `caller` will work), + // so only the topmost frame could possibly have an out-of-date PC. + // ZJIT doesn't set `cfp->jit_return`, so it's not a reliable signal. + // + // Avoid passing invalid PC to calc_lineno() to avoid crashing. + if (cfp == top && (pc < iseq_encoded || pc > pc_end)) { lines[i] = 0; } else { - lines[i] = calc_lineno(cfp->iseq, cfp->pc); + lines[i] = calc_lineno(cfp->iseq, pc); } } From 479cdab3378a9597b01a4a3febc382488484f21a Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 23 Sep 2025 17:48:06 -0700 Subject: [PATCH 7/9] ZJIT: Allow testing JIT code on zjit-test (#14639) * ZJIT: Allow testing JIT code on zjit-test * Resurrect TestingAllocator tests --- zjit/src/asm/mod.rs | 10 ++------ zjit/src/backend/arm64/mod.rs | 6 +++-- zjit/src/codegen.rs | 5 ---- zjit/src/cruby.rs | 10 ++++++-- zjit/src/hir.rs | 5 ++-- zjit/src/options.rs | 7 +++--- zjit/src/state.rs | 30 +---------------------- zjit/src/virtualmem.rs | 46 +++++++++++++++++++++++++---------- 8 files changed, 54 insertions(+), 65 deletions(-) diff --git a/zjit/src/asm/mod.rs b/zjit/src/asm/mod.rs index 2ac864047cabb5..95202ad7292fa7 100644 --- a/zjit/src/asm/mod.rs +++ b/zjit/src/asm/mod.rs @@ -317,19 +317,13 @@ impl fmt::LowerHex for CodeBlock { impl CodeBlock { /// Stubbed CodeBlock for testing. Can't execute generated code. pub fn new_dummy() -> Self { - const DEFAULT_MEM_SIZE: usize = 1024; + const DEFAULT_MEM_SIZE: usize = 1024 * 1024; CodeBlock::new_dummy_sized(DEFAULT_MEM_SIZE) } pub fn new_dummy_sized(mem_size: usize) -> Self { - use std::ptr::NonNull; use crate::virtualmem::*; - use crate::virtualmem::tests::TestingAllocator; - - let alloc = TestingAllocator::new(mem_size); - let mem_start: *const u8 = alloc.mem_start(); - let virt_mem = VirtualMem::new(alloc, 1, NonNull::new(mem_start as *mut u8).unwrap(), mem_size, 128 * 1024 * 1024); - + let virt_mem = VirtualMem::alloc(mem_size, None); Self::new(Rc::new(RefCell::new(virt_mem)), false) } } diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index 34e939957f702a..7a8c0eeaa0d009 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -2058,10 +2058,12 @@ mod tests { // `IMMEDIATE_MAX_VALUE` number of dummy instructions will be generated // plus a compare, a jump instruction, and a label. - const MEMORY_REQUIRED: usize = (IMMEDIATE_MAX_VALUE + 8)*4; + // Adding page_size to avoid OOM on the last page. + let page_size = unsafe { rb_jit_get_page_size() } as usize; + let memory_required = (IMMEDIATE_MAX_VALUE + 8) * 4 + page_size; let mut asm = Assembler::new(); - let mut cb = CodeBlock::new_dummy_sized(MEMORY_REQUIRED); + let mut cb = CodeBlock::new_dummy_sized(memory_required); let far_label = asm.new_label("far"); diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 2db30448a3914b..41e196ca8e4f64 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -75,11 +75,6 @@ impl JITState { /// See jit_compile_exception() for details. #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, jit_exception: bool) -> *const u8 { - // Do not test the JIT code in HIR tests - if cfg!(test) { - return std::ptr::null(); - } - // Take a lock to avoid writing to ISEQ in parallel with Ractors. // with_vm_lock() does nothing if the program doesn't use Ractors. with_vm_lock(src_loc!(), || { diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 82d0582da40fd5..85aa3c0b05b435 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -999,7 +999,7 @@ pub use manual_defs::*; pub mod test_utils { use std::{ptr::null, sync::Once}; - use crate::{options::{internal_set_num_profiles, rb_zjit_call_threshold, rb_zjit_prepare_options, DEFAULT_CALL_THRESHOLD}, state::{rb_zjit_enabled_p, ZJITState}}; + use crate::{options::{rb_zjit_call_threshold, rb_zjit_prepare_options, set_call_threshold, DEFAULT_CALL_THRESHOLD}, state::{rb_zjit_enabled_p, ZJITState}}; use super::*; @@ -1026,7 +1026,7 @@ pub mod test_utils { // The default rb_zjit_profile_threshold is too high, so lower it for HIR tests. if rb_zjit_call_threshold == DEFAULT_CALL_THRESHOLD { - internal_set_num_profiles(1); + set_call_threshold(2); } // Pass command line options so the VM loads core library methods defined in @@ -1099,6 +1099,12 @@ pub mod test_utils { }) } + /// Get the #inspect of a given Ruby program in Rust string + pub fn inspect(program: &str) -> String { + let inspect = format!("({program}).inspect"); + ruby_str_to_rust_string(eval(&inspect)) + } + /// Get the ISeq of a specified method pub fn get_method_iseq(recv: &str, name: &str) -> *const rb_iseq_t { let wrapped_iseq = eval(&format!("RubyVM::InstructionSequence.of({}.method(:{}))", recv, name)); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 136764f137b99b..6e3fd78e0a8945 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6545,6 +6545,7 @@ mod graphviz_tests { #[cfg(test)] mod opt_tests { use super::*; + use crate::options::*; use insta::assert_snapshot; #[track_caller] @@ -8481,6 +8482,7 @@ mod opt_tests { CheckInterrupts Return v37 "); + assert_snapshot!(inspect("test"), @"{}"); } #[test] @@ -9580,8 +9582,7 @@ mod opt_tests { #[test] fn test_dont_optimize_getivar_polymorphic() { - crate::options::rb_zjit_prepare_options(); - crate::options::internal_set_num_profiles(3); + set_call_threshold(3); eval(" class C attr_reader :foo, :bar diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 2a9b2e7d279055..b33d18efffd4ab 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -301,12 +301,11 @@ fn update_profile_threshold() { } } +/// Update --zjit-call-threshold for testing #[cfg(test)] -pub fn internal_set_num_profiles(n: u8) { - let options = unsafe { OPTIONS.as_mut().unwrap() }; - options.num_profiles = n; - let call_threshold = n.saturating_add(1); +pub fn set_call_threshold(call_threshold: u64) { unsafe { rb_zjit_call_threshold = call_threshold as u64; } + rb_zjit_prepare_options(); update_profile_threshold(); } diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 0c657f450a25dc..c0f81e1e856dec 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -54,45 +54,17 @@ static mut ZJIT_STATE: Option = None; impl ZJITState { /// Initialize the ZJIT globals pub fn init() { - #[cfg(not(test))] let mut cb = { - use crate::cruby::*; use crate::options::*; - - let exec_mem_bytes: usize = get_option!(exec_mem_bytes); - let virt_block: *mut u8 = unsafe { rb_jit_reserve_addr_space(64 * 1024 * 1024) }; - - // Memory protection syscalls need page-aligned addresses, so check it here. Assuming - // `virt_block` is page-aligned, `second_half` should be page-aligned as long as the - // page size in bytes is a power of two 2¹⁹ or smaller. This is because the user - // requested size is half of mem_option × 2²⁰ as it's in MiB. - // - // Basically, we don't support x86-64 2MiB and 1GiB pages. ARMv8 can do up to 64KiB - // (2¹⁶ bytes) pages, which should be fine. 4KiB pages seem to be the most popular though. - let page_size = unsafe { rb_jit_get_page_size() }; - assert_eq!( - virt_block as usize % page_size as usize, 0, - "Start of virtual address block should be page-aligned", - ); - use crate::virtualmem::*; - use std::ptr::NonNull; use std::rc::Rc; use std::cell::RefCell; - let mem_block = VirtualMem::new( - crate::virtualmem::sys::SystemAllocator {}, - page_size, - NonNull::new(virt_block).unwrap(), - exec_mem_bytes, - get_option!(mem_bytes) - ); + let mem_block = VirtualMem::alloc(get_option!(exec_mem_bytes), Some(get_option!(mem_bytes))); let mem_block = Rc::new(RefCell::new(mem_block)); CodeBlock::new(mem_block.clone(), get_option!(dump_disasm)) }; - #[cfg(test)] - let mut cb = CodeBlock::new_dummy(); let exit_trampoline = gen_exit_trampoline(&mut cb).unwrap(); let function_stub_hit_trampoline = gen_function_stub_hit_trampoline(&mut cb).unwrap(); diff --git a/zjit/src/virtualmem.rs b/zjit/src/virtualmem.rs index 0d19858c8711f5..5af5c0e8b999a4 100644 --- a/zjit/src/virtualmem.rs +++ b/zjit/src/virtualmem.rs @@ -4,18 +4,11 @@ // benefit. use std::ptr::NonNull; - +use crate::cruby::*; use crate::stats::zjit_alloc_size; -#[cfg(test)] -use crate::options::get_option; - -#[cfg(not(test))] pub type VirtualMem = VirtualMemory; -#[cfg(test)] -pub type VirtualMem = VirtualMemory; - /// Memory for generated executable machine code. When not testing, we reserve address space for /// the entire region upfront and map physical memory into the reserved address space as needed. On /// Linux, this is basically done using an `mmap` with `PROT_NONE` upfront and gradually using @@ -33,7 +26,7 @@ pub struct VirtualMemory { region_size_bytes: usize, /// mapped_region_bytes + zjit_alloc_size may not increase beyond this limit. - memory_limit_bytes: usize, + memory_limit_bytes: Option, /// Number of bytes per "page", memory protection permission can only be controlled at this /// granularity. @@ -113,6 +106,28 @@ pub enum WriteError { use WriteError::*; +impl VirtualMem { + /// Allocate a VirtualMem insntace with a requested size + pub fn alloc(exec_mem_bytes: usize, mem_bytes: Option) -> Self { + let virt_block: *mut u8 = unsafe { rb_jit_reserve_addr_space(exec_mem_bytes as u32) }; + + // Memory protection syscalls need page-aligned addresses, so check it here. Assuming + // `virt_block` is page-aligned, `second_half` should be page-aligned as long as the + // page size in bytes is a power of two 2¹⁹ or smaller. This is because the user + // requested size is half of mem_option × 2²⁰ as it's in MiB. + // + // Basically, we don't support x86-64 2MiB and 1GiB pages. ARMv8 can do up to 64KiB + // (2¹⁶ bytes) pages, which should be fine. 4KiB pages seem to be the most popular though. + let page_size = unsafe { rb_jit_get_page_size() }; + assert_eq!( + virt_block as usize % page_size as usize, 0, + "Start of virtual address block should be page-aligned", + ); + + Self::new(sys::SystemAllocator {}, page_size, NonNull::new(virt_block).unwrap(), exec_mem_bytes, mem_bytes) + } +} + impl VirtualMemory { /// Bring a part of the address space under management. pub fn new( @@ -120,7 +135,7 @@ impl VirtualMemory { page_size: u32, virt_region_start: NonNull, region_size_bytes: usize, - memory_limit_bytes: usize, + memory_limit_bytes: Option, ) -> Self { assert_ne!(0, page_size); let page_size_bytes = page_size as usize; @@ -181,6 +196,12 @@ impl VirtualMemory { let whole_region_end = start.wrapping_add(self.region_size_bytes); let alloc = &mut self.allocator; + // Ignore zjit_alloc_size() if self.memory_limit_bytes is None for testing + let mut required_region_bytes = page_addr + page_size - start as usize; + if self.memory_limit_bytes.is_some() { + required_region_bytes += zjit_alloc_size(); + } + assert!((start..=whole_region_end).contains(&mapped_region_end)); if (start..mapped_region_end).contains(&raw) { @@ -193,7 +214,7 @@ impl VirtualMemory { self.current_write_page = Some(page_addr); } else if (start..whole_region_end).contains(&raw) && - (page_addr + page_size - start as usize) + zjit_alloc_size() < self.memory_limit_bytes { + required_region_bytes < self.memory_limit_bytes.unwrap_or(self.region_size_bytes) { // Writing to a brand new page let mapped_region_end_addr = mapped_region_end as usize; let alloc_size = page_addr - mapped_region_end_addr + page_size; @@ -279,7 +300,6 @@ impl CodePtrBase for VirtualMemory { } /// Requires linking with CRuby to work -#[cfg(not(test))] pub mod sys { use crate::cruby::*; @@ -387,7 +407,7 @@ pub mod tests { PAGE_SIZE.try_into().unwrap(), NonNull::new(mem_start as *mut u8).unwrap(), mem_size, - get_option!(mem_bytes), + None, ) } From 5b65e76a7d3aa5d6111fbf2b5bf371bdf478c352 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Wed, 24 Sep 2025 07:10:31 +0900 Subject: [PATCH 8/9] fix lvar_state dump size `ibf_dump_write()` should consider the size of the element. --- compile.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compile.c b/compile.c index fb269721f3fae0..d3b9704cc94d69 100644 --- a/compile.c +++ b/compile.c @@ -13272,7 +13272,7 @@ ibf_dump_lvar_states(struct ibf_dump *dump, const rb_iseq_t *iseq) const struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq); const int size = body->local_table_size; IBF_W_ALIGN(enum lvar_state); - return ibf_dump_write(dump, body->lvar_states, body->lvar_states ? size : 0); + return ibf_dump_write(dump, body->lvar_states, sizeof(enum lvar_state) * (body->lvar_states ? size : 0)); } static enum lvar_state * From 6cd970b428314271e6b8efa746b8c4b6f5f6fcf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Tue, 23 Sep 2025 16:20:18 +0200 Subject: [PATCH 9/9] [ruby/psych] Remove warning by not calling find_library after pkg_config If pkg_config returns a truthy value, it found the library and added it to the global values for the Makefile. Calling `find_library` after a successful `pkg_config` causes -lyaml to appear twice in the LIBS variable in the resulting Makefile, and causes ld on macOS to emit a warning: $ bundle exec rake compile 2>&1 | grep warning ld: warning: ignoring duplicate libraries: '-lyaml' https://github.com/ruby/psych/commit/cb5e3d465c --- ext/psych/extconf.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ext/psych/extconf.rb b/ext/psych/extconf.rb index e7dd0bb60a16fc..589e201c1cdb9d 100644 --- a/ext/psych/extconf.rb +++ b/ext/psych/extconf.rb @@ -36,8 +36,11 @@ libyaml = "libyaml.#$LIBEXT" $cleanfiles << libyaml $LOCAL_LIBS.prepend("$(LIBYAML) ") -else # default to pre-installed libyaml - pkg_config('yaml-0.1') + + # default to pre-installed libyaml +elsif pkg_config('yaml-0.1') + # found with pkg-config +else dir_config('libyaml') find_header('yaml.h') or abort "yaml.h not found" find_library('yaml', 'yaml_get_version') or abort "libyaml not found"