From 74bfb1606d09672a37105c138edf8e0d8acbb5c1 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 18 Dec 2025 13:51:33 -0500 Subject: [PATCH 01/20] Remove assertion in encoded_iseq_trace_instrument (#15616) `encoded_iseq_trace_instrument` is safe to call in a ractor if the iseq is new. In that case, the VM lock is not taken. This assertion was added in 4fb537b1ee28bb37dbe551ac65c279d436c756bc. --- iseq.c | 1 - 1 file changed, 1 deletion(-) diff --git a/iseq.c b/iseq.c index 7457118e07ce96..2e13928e920ed0 100644 --- a/iseq.c +++ b/iseq.c @@ -3936,7 +3936,6 @@ rb_vm_insn_decode(const VALUE encoded) static inline int encoded_iseq_trace_instrument(VALUE *iseq_encoded_insn, rb_event_flag_t turnon, bool remain_traced) { - ASSERT_vm_locking(); st_data_t key = (st_data_t)*iseq_encoded_insn; st_data_t val; From 28c2a5b2a4db5ed6faa7cf3d71baf2765a277184 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 17 Dec 2025 20:21:17 -0800 Subject: [PATCH 02/20] Fix env debug assertion failure w/ Ractors+JITs Previously when using a JIT and Ractors at the same time with debug assertions turned on this could rarely fail with: vm_core.h:1448: Assertion Failed: VM_ENV_FLAGS:FIXNUM_P(flags) When using Ractors, any time the VM lock is acquired, that may join a barrier as another Ractor initiates GC. This could be made to happen reliably by replacing the invalidation with a call to rb_gc(). This assertion failure happens because VM_STACK_ENV_WRITE(ep, 0, (VALUE)env); Is setting VM_ENV_DATA_INDEX_FLAGS to the environment, which is not a valid set of flags (it should be a fixnum). Although we update cfp->ep, rb_execution_context_mark will also mark the PREV_EP, and until the recursive calls to vm_make_env_each all finish the "next" ep may still be pointing to the stack env we've just escaped. I'm not completely sure why we need to store this on the stack - why is setting cfp->ep not enough? I'm also not sure why rb_execution_context_mark needs to mark the prev_ep. --- vm.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/vm.c b/vm.c index 27cb5ae25825e0..15cf64c7b38094 100644 --- a/vm.c +++ b/vm.c @@ -1122,6 +1122,14 @@ vm_make_env_each(const rb_execution_context_t * const ec, rb_control_frame_t *co local_size += VM_ENV_DATA_SIZE; } + // Invalidate JIT code that assumes cfp->ep == vm_base_ptr(cfp). + // This is done before creating the imemo_env because VM_STACK_ENV_WRITE + // below leaves the on-stack ep in a state that is unsafe to GC. + if (VM_FRAME_RUBYFRAME_P(cfp)) { + rb_yjit_invalidate_ep_is_bp(cfp->iseq); + rb_zjit_invalidate_no_ep_escape(cfp->iseq); + } + /* * # local variables on a stack frame (N == local_size) * [lvar1, lvar2, ..., lvarN, SPECVAL] @@ -1165,12 +1173,6 @@ vm_make_env_each(const rb_execution_context_t * const ec, rb_control_frame_t *co } #endif - // Invalidate JIT code that assumes cfp->ep == vm_base_ptr(cfp). - if (env->iseq) { - rb_yjit_invalidate_ep_is_bp(env->iseq); - rb_zjit_invalidate_no_ep_escape(env->iseq); - } - return (VALUE)env; } From 57c4cd9a474ccd32caccee28a850ff8a041babb8 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 17 Dec 2025 09:22:54 +0100 Subject: [PATCH 03/20] thread_sync.c: eliminate GET_EC() from queue_do_pop We receive the ec as argument, it's much cheaper to pass it around that to look it up again. --- thread_sync.c | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/thread_sync.c b/thread_sync.c index 0cf7c1faef97c1..30f3315b0cc098 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -1083,12 +1083,12 @@ szqueue_sleep_done(VALUE p) return Qfalse; } -static VALUE -queue_do_pop(VALUE self, struct rb_queue *q, int should_block, VALUE timeout) +static inline VALUE +queue_do_pop(rb_execution_context_t *ec, VALUE self, struct rb_queue *q, VALUE non_block, VALUE timeout) { check_array(self, q->que); if (RARRAY_LEN(q->que) == 0) { - if (!should_block) { + if (RTEST(non_block)) { rb_raise(rb_eThreadError, "queue empty"); } @@ -1103,8 +1103,6 @@ queue_do_pop(VALUE self, struct rb_queue *q, int should_block, VALUE timeout) return queue_closed_result(self, q); } else { - rb_execution_context_t *ec = GET_EC(); - RUBY_ASSERT(RARRAY_LEN(q->que) == 0); RUBY_ASSERT(queue_closed_p(self) == 0); @@ -1136,7 +1134,7 @@ queue_do_pop(VALUE self, struct rb_queue *q, int should_block, VALUE timeout) static VALUE rb_queue_pop(rb_execution_context_t *ec, VALUE self, VALUE non_block, VALUE timeout) { - return queue_do_pop(self, queue_ptr(self), !RTEST(non_block), timeout); + return queue_do_pop(ec, self, queue_ptr(self), non_block, timeout); } /* @@ -1330,7 +1328,6 @@ rb_szqueue_push(rb_execution_context_t *ec, VALUE self, VALUE object, VALUE non_ raise_closed_queue_error(self); } else { - rb_execution_context_t *ec = GET_EC(); struct queue_waiter queue_waiter = { .w = {.self = self, .th = ec->thread_ptr, .fiber = nonblocking_fiber(ec->fiber_ptr)}, .as = {.sq = sq} @@ -1357,10 +1354,10 @@ rb_szqueue_push(rb_execution_context_t *ec, VALUE self, VALUE object, VALUE non_ } static VALUE -szqueue_do_pop(VALUE self, int should_block, VALUE timeout) +rb_szqueue_pop(rb_execution_context_t *ec, VALUE self, VALUE non_block, VALUE timeout) { struct rb_szqueue *sq = szqueue_ptr(self); - VALUE retval = queue_do_pop(self, &sq->q, should_block, timeout); + VALUE retval = queue_do_pop(ec, self, &sq->q, non_block, timeout); if (queue_length(self, &sq->q) < sq->max) { wakeup_one(szqueue_pushq(sq)); @@ -1368,11 +1365,6 @@ szqueue_do_pop(VALUE self, int should_block, VALUE timeout) return retval; } -static VALUE -rb_szqueue_pop(rb_execution_context_t *ec, VALUE self, VALUE non_block, VALUE timeout) -{ - return szqueue_do_pop(self, !RTEST(non_block), timeout); -} /* * Document-method: Thread::SizedQueue#clear From bbc684d8300fc3fc02020a9a644b857d090f2215 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 18 Dec 2025 09:11:53 +0100 Subject: [PATCH 04/20] thread_sync.c: simplify `check_array` If the queue was allocated without calling initialize, `ary` will be `0`. --- thread_sync.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/thread_sync.c b/thread_sync.c index 30f3315b0cc098..962ca13d2ab940 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -839,13 +839,13 @@ ary_buf_new(void) return rb_ary_hidden_new(1); } -static VALUE +static inline VALUE check_array(VALUE obj, VALUE ary) { - if (!RB_TYPE_P(ary, T_ARRAY)) { - rb_raise(rb_eTypeError, "%+"PRIsVALUE" not initialized", obj); + if (RB_LIKELY(ary)) { + return ary; } - return ary; + rb_raise(rb_eTypeError, "%+"PRIsVALUE" not initialized", obj); } static long From 8cf4f373ff596aaef7aaec993c355b242d4fe2c1 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 18 Dec 2025 09:35:44 +0100 Subject: [PATCH 05/20] thread_sync.c: declare queue_data_type as parent of szqueue_data_type. Allows to remove some duplicated code like szqueue_length, etc. --- include/ruby/internal/core/rtypeddata.h | 19 ++++++-- thread_sync.c | 58 ++++++++----------------- 2 files changed, 32 insertions(+), 45 deletions(-) diff --git a/include/ruby/internal/core/rtypeddata.h b/include/ruby/internal/core/rtypeddata.h index aed4bd89b893f6..72044562df8e3b 100644 --- a/include/ruby/internal/core/rtypeddata.h +++ b/include/ruby/internal/core/rtypeddata.h @@ -615,13 +615,24 @@ RBIMPL_ATTR_ARTIFICIAL() * directly. */ static inline void * -rbimpl_check_typeddata(VALUE obj, const rb_data_type_t *type) +rbimpl_check_typeddata(VALUE obj, const rb_data_type_t *expected_type) { - if (RB_LIKELY(RB_TYPE_P(obj, T_DATA) && RTYPEDDATA_P(obj) && RTYPEDDATA_TYPE(obj) == type)) { - return RTYPEDDATA_GET_DATA(obj); + if (RB_LIKELY(RB_TYPE_P(obj, T_DATA) && RTYPEDDATA_P(obj))) { + const rb_data_type_t *actual_type = RTYPEDDATA_TYPE(obj); + void *data = RTYPEDDATA_GET_DATA(obj); + if (RB_LIKELY(actual_type == expected_type)) { + return data; + } + + while (actual_type) { + actual_type = actual_type->parent; + if (actual_type == expected_type) { + return data; + } + } } - return rb_check_typeddata(obj, type); + return rb_check_typeddata(obj, expected_type); } diff --git a/thread_sync.c b/thread_sync.c index 962ca13d2ab940..d0e356b161d5b2 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -728,9 +728,14 @@ queue_memsize(const void *ptr) } static const rb_data_type_t queue_data_type = { - "queue", - {queue_mark_and_move, RUBY_TYPED_DEFAULT_FREE, queue_memsize, queue_mark_and_move}, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY|RUBY_TYPED_WB_PROTECTED + .wrap_struct_name = "Thread::Queue", + .function = { + .dmark = queue_mark_and_move, + .dfree = RUBY_TYPED_DEFAULT_FREE, + .dsize = queue_memsize, + .dcompact = queue_mark_and_move, + }, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, }; static VALUE @@ -803,9 +808,15 @@ szqueue_memsize(const void *ptr) } static const rb_data_type_t szqueue_data_type = { - "sized_queue", - {szqueue_mark_and_move, RUBY_TYPED_DEFAULT_FREE, szqueue_memsize, szqueue_mark_and_move}, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY|RUBY_TYPED_WB_PROTECTED + .wrap_struct_name = "Thread::SizedQueue", + .function = { + .dmark = szqueue_mark_and_move, + .dfree = RUBY_TYPED_DEFAULT_FREE, + .dsize = szqueue_memsize, + .dcompact = szqueue_mark_and_move, + }, + .parent = &queue_data_type, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, }; static VALUE @@ -1382,23 +1393,6 @@ rb_szqueue_clear(VALUE self) return self; } -/* - * Document-method: Thread::SizedQueue#length - * call-seq: - * length - * size - * - * Returns the length of the queue. - */ - -static VALUE -rb_szqueue_length(VALUE self) -{ - struct rb_szqueue *sq = szqueue_ptr(self); - - return LONG2NUM(queue_length(self, &sq->q)); -} - /* * Document-method: Thread::SizedQueue#num_waiting * @@ -1413,21 +1407,6 @@ rb_szqueue_num_waiting(VALUE self) return INT2NUM(sq->q.num_waiting + sq->num_waiting_push); } -/* - * Document-method: Thread::SizedQueue#empty? - * call-seq: empty? - * - * Returns +true+ if the queue is empty. - */ - -static VALUE -rb_szqueue_empty_p(VALUE self) -{ - struct rb_szqueue *sq = szqueue_ptr(self); - - return RBOOL(queue_length(self, &sq->q) == 0); -} - /* ConditionalVariable */ struct rb_condvar { @@ -1678,11 +1657,8 @@ Init_thread_sync(void) rb_define_method(rb_cSizedQueue, "close", rb_szqueue_close, 0); rb_define_method(rb_cSizedQueue, "max", rb_szqueue_max_get, 0); rb_define_method(rb_cSizedQueue, "max=", rb_szqueue_max_set, 1); - rb_define_method(rb_cSizedQueue, "empty?", rb_szqueue_empty_p, 0); rb_define_method(rb_cSizedQueue, "clear", rb_szqueue_clear, 0); - rb_define_method(rb_cSizedQueue, "length", rb_szqueue_length, 0); rb_define_method(rb_cSizedQueue, "num_waiting", rb_szqueue_num_waiting, 0); - rb_define_alias(rb_cSizedQueue, "size", "length"); /* CVar */ DEFINE_CLASS(ConditionVariable, Object); From fb1dd92d30a8df93f6fe2746aacc097f4c3ea62b Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 18 Dec 2025 20:17:45 +0100 Subject: [PATCH 06/20] thread_sync.c: rename mutex_trylock internal function [Bug #21793] To fix a naming conflict on solaris. --- thread_sync.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/thread_sync.c b/thread_sync.c index d0e356b161d5b2..a93888fad02ae6 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -235,7 +235,7 @@ mutex_locked(rb_mutex_t *mutex, rb_thread_t *th, rb_serial_t ec_serial) } static inline bool -mutex_trylock(rb_mutex_t *mutex, rb_thread_t *th, rb_serial_t ec_serial) +do_mutex_trylock(rb_mutex_t *mutex, rb_thread_t *th, rb_serial_t ec_serial) { if (mutex->ec_serial == 0) { RUBY_DEBUG_LOG("%p ok", mutex); @@ -252,7 +252,7 @@ mutex_trylock(rb_mutex_t *mutex, rb_thread_t *th, rb_serial_t ec_serial) static VALUE rb_mut_trylock(rb_execution_context_t *ec, VALUE self) { - return RBOOL(mutex_trylock(mutex_ptr(self), ec->thread_ptr, rb_ec_serial(ec))); + return RBOOL(do_mutex_trylock(mutex_ptr(self), ec->thread_ptr, rb_ec_serial(ec))); } VALUE @@ -315,7 +315,7 @@ do_mutex_lock(struct mutex_args *args, int interruptible_p) rb_raise(rb_eThreadError, "can't be called from trap context"); } - if (!mutex_trylock(mutex, th, ec_serial)) { + if (!do_mutex_trylock(mutex, th, ec_serial)) { if (mutex->ec_serial == ec_serial) { rb_raise(rb_eThreadError, "deadlock; recursive locking"); } From a7eb1879ad35a0b5d9d32fcdbf2f840bd2c8858c Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 18 Dec 2025 15:08:36 -0500 Subject: [PATCH 07/20] [DOC] small improvements to ractor class docs (#15584) * Ractor.yield no longer exists * Ractor.shareable_proc returns a copy of the given proc * Improve wording for monitoring/unmonitoring ports --- ractor.rb | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/ractor.rb b/ractor.rb index 70ce1a9fffecb9..e380d04f873aad 100644 --- a/ractor.rb +++ b/ractor.rb @@ -47,7 +47,7 @@ # frozen objects can be unshareable if they contain (through their instance variables) unfrozen # objects. # -# Shareable objects are those which can be used by several threads without compromising +# Shareable objects are those which can be used by several ractors at once without compromising # thread-safety, for example numbers, +true+ and +false+. Ractor.shareable? allows you to check this, # and Ractor.make_shareable tries to make the object shareable if it's not already, and gives an error # if it can't do it. @@ -65,12 +65,12 @@ # ary[0].frozen? #=> true # ary[1].frozen? #=> true # -# When a shareable object is sent (via #send or Ractor.yield), no additional processing occurs -# on it. It just becomes usable by both ractors. When an unshareable object is sent, it can be +# When a shareable object is sent via #send, no additional processing occurs +# on it and it becomes usable by both ractors. When an unshareable object is sent, it can be # either _copied_ or _moved_. The first is the default, and it copies the object fully by # deep cloning (Object#clone) the non-shareable parts of its structure. # -# data = ['foo', 'bar'.freeze] +# data = ['foo'.dup, 'bar'.freeze] # r = Ractor.new do # data2 = Ractor.receive # puts "In ractor: #{data2.object_id}, #{data2[0].object_id}, #{data2[1].object_id}" @@ -81,8 +81,8 @@ # # This will output something like: # -# In ractor: 340, 360, 320 -# Outside : 380, 400, 320 +# In ractor: 8, 16, 24 +# Outside : 32, 40, 24 # # Note that the object ids of the array and the non-frozen string inside the array have changed in # the ractor because they are different objects. The second array's element, which is a @@ -460,9 +460,9 @@ def self.[]=(sym, val) # call-seq: # Ractor.store_if_absent(key){ init_block } # - # If the corresponding value is not set, yield a value with - # init_block and store the value in thread-safe manner. - # This method returns corresponding stored value. + # If the corresponding ractor-local value is not set, yield a value with + # init_block and store the value in a thread-safe manner. + # This method returns the stored value. # # (1..10).map{ # Thread.new(it){|i| @@ -578,10 +578,10 @@ def value # call-seq: # ractor.monitor(port) -> self # - # Register port as a monitoring port. If the ractor terminated, - # the port received a Symbol object. + # Add another ractor's port to the monitored list of the receiver. If +self+ terminates, + # the port is sent a Symbol object. # :exited will be sent if the ractor terminated without an exception. - # :aborted will be sent if the ractor terminated with a exception. + # :aborted will be sent if the ractor terminated with an exception. # # r = Ractor.new{ some_task() } # r.monitor(port = Ractor::Port.new) @@ -599,7 +599,7 @@ def monitor port # call-seq: # ractor.unmonitor(port) -> self # - # Unregister port from the monitoring ports. + # Remove the given port from the ractor's monitored list. # def unmonitor port __builtin_ractor_unmonitor(port) @@ -609,11 +609,11 @@ def unmonitor port # call-seq: # Ractor.shareable_proc(self: nil){} -> shareable 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. + # Returns a shareable copy of the given block's Proc. The value of +self+ + # in the Proc will be replaced with the value passed via the `self:` keyword, + # or +nil+ if not given. # - # In a shareable Proc, you can not access to the outer variables. + # In a shareable Proc, you can not access any outer variables. # # a = 42 # Ractor.shareable_proc{ p a } @@ -636,7 +636,7 @@ def self.shareable_proc self: nil # call-seq: # Ractor.shareable_proc{} -> shareable proc # - # Same as Ractor.shareable_proc, but returns lambda proc. + # Same as Ractor.shareable_proc, but returns a lambda. # def self.shareable_lambda self: nil Primitive.attr! :use_block @@ -652,7 +652,7 @@ class Port # call-seq: # port.receive -> msg # - # Receive a message to the port (which was sent there by Port#send). + # Receive a message from the port (which was sent there by Port#send). # # port = Ractor::Port.new # r = Ractor.new port do |port| @@ -662,7 +662,7 @@ class Port # v1 = port.receive # puts "Received: #{v1}" # r.join - # # Here will be printed: "Received: message1" + # # This will print: "Received: message1" # # The method blocks if the message queue is empty. # From aace29d485559e38ca06923a6af335dbb5fb28f1 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 18 Dec 2025 15:42:49 -0500 Subject: [PATCH 08/20] Check for NULL fields in TYPEDDATA memsize functions (#15633) Some TYPEDDATA objects allocate struct fields using the GC right after they get created, and in that case the VM can try to perform a GC and join a barrier if another ractor started one. If we're dumping the heap in another ractor, this acquires a barrier and it will call the `rb_obj_memsize` function on this object. We can't assume these struct fields are non-null. This also goes for C extensions, which may cause problems with heap dumping from a ractor if their memsize functions aren't coded correctly to check for NULL fields. Because dumping the heap from a ractor is likely a rare scenario and it has only recently been introduced, we'll have to see how this works in practice and if it causes bugs. --- ast.c | 8 ++++++-- box.c | 11 ++++++++--- id_table.c | 2 +- ractor_sync.c | 6 +++++- st.c | 1 + weakmap.c | 16 ++++++++++------ 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/ast.c b/ast.c index 449b21aa2d1023..5357aa38a5ae09 100644 --- a/ast.c +++ b/ast.c @@ -32,9 +32,13 @@ static size_t node_memsize(const void *ptr) { struct ASTNodeData *data = (struct ASTNodeData *)ptr; - rb_ast_t *ast = rb_ruby_ast_data_get(data->ast_value); + size_t size = sizeof(struct ASTNodeData); + if (data->ast_value) { + rb_ast_t *ast = rb_ruby_ast_data_get(data->ast_value); + size += rb_ast_memsize(ast); + } - return sizeof(struct ASTNodeData) + rb_ast_memsize(ast); + return size; } static const rb_data_type_t rb_node_type = { diff --git a/box.c b/box.c index 14f6acdd8267b5..3182fb4eb20758 100644 --- a/box.c +++ b/box.c @@ -276,10 +276,15 @@ box_entry_free(void *ptr) static size_t box_entry_memsize(const void *ptr) { + size_t size = sizeof(rb_box_t); const rb_box_t *box = (const rb_box_t *)ptr; - return sizeof(rb_box_t) + \ - rb_st_memsize(box->loaded_features_index) + \ - rb_st_memsize(box->loading_table); + if (box->loaded_features_index) { + size += rb_st_memsize(box->loaded_features_index); + } + if (box->loading_table) { + size += rb_st_memsize(box->loading_table); + } + return size; } static const rb_data_type_t rb_box_data_type = { diff --git a/id_table.c b/id_table.c index 4629af553505b7..cece14c3891153 100644 --- a/id_table.c +++ b/id_table.c @@ -374,7 +374,7 @@ rb_managed_id_table_create(const rb_data_type_t *type, size_t capa) struct rb_id_table *tbl; VALUE obj = TypedData_Make_Struct(0, struct rb_id_table, type, tbl); RB_OBJ_SET_SHAREABLE(obj); - rb_id_table_init(tbl, capa); + rb_id_table_init(tbl, capa); // NOTE: this can cause GC, so dmark and dsize need to check tbl->items return obj; } diff --git a/ractor_sync.c b/ractor_sync.c index 8c7c144c3fda97..a63df7c407b194 100644 --- a/ractor_sync.c +++ b/ractor_sync.c @@ -1267,7 +1267,11 @@ static size_t ractor_selector_memsize(const void *ptr) { const struct ractor_selector *s = ptr; - return sizeof(struct ractor_selector) + st_memsize(s->ports); + size_t size = sizeof(struct ractor_selector); + if (s->ports) { + size += st_memsize(s->ports); + } + return size; } static const rb_data_type_t ractor_selector_data_type = { diff --git a/st.c b/st.c index ef9ffbec5e1ea3..8937f7935f6b22 100644 --- a/st.c +++ b/st.c @@ -674,6 +674,7 @@ st_free_table(st_table *tab) size_t st_memsize(const st_table *tab) { + RUBY_ASSERT(tab != NULL); return(sizeof(st_table) + (tab->bins == NULL ? 0 : bins_size(tab)) + get_allocated_entries(tab) * sizeof(st_table_entry)); diff --git a/weakmap.c b/weakmap.c index 2ebf7d204f7195..ecdd219f62a21e 100644 --- a/weakmap.c +++ b/weakmap.c @@ -139,9 +139,11 @@ wmap_memsize(const void *ptr) const struct weakmap *w = ptr; size_t size = 0; - size += st_memsize(w->table); - /* The key and value of the table each take sizeof(VALUE) in size. */ - size += st_table_size(w->table) * (2 * sizeof(VALUE)); + if (w->table) { + size += st_memsize(w->table); + /* The key and value of the table each take sizeof(VALUE) in size. */ + size += st_table_size(w->table) * (2 * sizeof(VALUE)); + } return size; } @@ -689,9 +691,11 @@ wkmap_memsize(const void *ptr) const struct weakkeymap *w = ptr; size_t size = 0; - size += st_memsize(w->table); - /* Each key of the table takes sizeof(VALUE) in size. */ - size += st_table_size(w->table) * sizeof(VALUE); + if (w->table) { + size += st_memsize(w->table); + /* Each key of the table takes sizeof(VALUE) in size. */ + size += st_table_size(w->table) * sizeof(VALUE); + } return size; } From 63b082cf0e87942dcea28cbdeb1c8a9e616e903a Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 17 Dec 2025 12:12:17 -0800 Subject: [PATCH 09/20] Store ractor_id directly on EC This is easier to access as ec->ractor_id instead of pointer-chasing through ec->thread->ractor->ractor_id Co-authored-by: Luke Gruber --- thread.c | 1 + vm.c | 1 + vm_core.h | 8 ++++++++ vm_insnhelper.c | 4 ++-- vm_method.c | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/thread.c b/thread.c index 3e1bb1dbe7ad17..788a0e9ad791a2 100644 --- a/thread.c +++ b/thread.c @@ -860,6 +860,7 @@ thread_create_core(VALUE thval, struct thread_create_params *params) #endif th->invoke_type = thread_invoke_type_ractor_proc; th->ractor = params->g; + th->ec->ractor_id = rb_ractor_id(th->ractor); th->ractor->threads.main = th; th->invoke_arg.proc.proc = rb_proc_isolate_bang(params->proc, Qnil); th->invoke_arg.proc.args = INT2FIX(RARRAY_LENINT(params->args)); diff --git a/vm.c b/vm.c index 15cf64c7b38094..f78a779c3f3655 100644 --- a/vm.c +++ b/vm.c @@ -3955,6 +3955,7 @@ th_init(rb_thread_t *th, VALUE self, rb_vm_t *vm) th->ec->local_storage_recursive_hash_for_trace = Qnil; th->ec->storage = Qnil; + th->ec->ractor_id = rb_ractor_id(th->ractor); #if OPT_CALL_THREADED_CODE th->retval = Qundef; diff --git a/vm_core.h b/vm_core.h index 839c054ab399f5..999f06d403bbe5 100644 --- a/vm_core.h +++ b/vm_core.h @@ -1048,6 +1048,7 @@ struct rb_execution_context_struct { rb_fiber_t *fiber_ptr; struct rb_thread_struct *thread_ptr; rb_serial_t serial; + rb_serial_t ractor_id; /* storage (ec (fiber) local) */ struct rb_id_table *local_storage; @@ -2070,6 +2071,13 @@ rb_ec_ractor_ptr(const rb_execution_context_t *ec) } } +static inline rb_serial_t +rb_ec_ractor_id(const rb_execution_context_t *ec) +{ + VM_ASSERT(ec->ractor_id == rb_ractor_id(rb_ec_ractor_ptr(ec))); + return ec->ractor_id; +} + static inline rb_vm_t * rb_ec_vm_ptr(const rb_execution_context_t *ec) { diff --git a/vm_insnhelper.c b/vm_insnhelper.c index aa67e54d0ac44b..2ad67461bb7694 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -4120,7 +4120,7 @@ vm_call_bmethod_body(rb_execution_context_t *ec, struct rb_calling_info *calling VALUE procv = cme->def->body.bmethod.proc; if (!RB_OBJ_SHAREABLE_P(procv) && - cme->def->body.bmethod.defined_ractor_id != rb_ractor_id(rb_ec_ractor_ptr(ec))) { + cme->def->body.bmethod.defined_ractor_id != rb_ec_ractor_id(ec)) { rb_raise(rb_eRuntimeError, "defined with an un-shareable Proc in a different Ractor"); } @@ -4143,7 +4143,7 @@ vm_call_iseq_bmethod(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct VALUE procv = cme->def->body.bmethod.proc; if (!RB_OBJ_SHAREABLE_P(procv) && - cme->def->body.bmethod.defined_ractor_id != rb_ractor_id(rb_ec_ractor_ptr(ec))) { + cme->def->body.bmethod.defined_ractor_id != rb_ec_ractor_id(ec)) { rb_raise(rb_eRuntimeError, "defined with an un-shareable Proc in a different Ractor"); } diff --git a/vm_method.c b/vm_method.c index 9f569df7fa6e51..2a6323e59300b5 100644 --- a/vm_method.c +++ b/vm_method.c @@ -1030,7 +1030,7 @@ rb_method_definition_set(const rb_method_entry_t *me, rb_method_definition_t *de } case VM_METHOD_TYPE_BMETHOD: RB_OBJ_WRITE(me, &def->body.bmethod.proc, (VALUE)opts); - def->body.bmethod.defined_ractor_id = rb_ractor_id(rb_ec_ractor_ptr(GET_EC())); + def->body.bmethod.defined_ractor_id = rb_ec_ractor_id(GET_EC()); return; case VM_METHOD_TYPE_NOTIMPLEMENTED: setup_method_cfunc_struct(UNALIGNED_MEMBER_PTR(def, body.cfunc), (VALUE(*)(ANYARGS))rb_f_notimplement_internal, -1); From 345ea0c8e18d3ce8fed332137d225769df619f2b Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 17 Dec 2025 12:08:43 -0800 Subject: [PATCH 10/20] YJIT: Support calling bmethods in Ractors Co-authored-by: Luke Gruber --- vm_core.h | 5 +++-- yjit.c | 6 ++++++ yjit/bindgen/src/main.rs | 1 + yjit/src/codegen.rs | 11 ++++++----- yjit/src/cruby.rs | 6 ++++++ yjit/src/cruby_bindings.inc.rs | 1 + 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/vm_core.h b/vm_core.h index 999f06d403bbe5..68adc5eac16f32 100644 --- a/vm_core.h +++ b/vm_core.h @@ -2074,8 +2074,9 @@ rb_ec_ractor_ptr(const rb_execution_context_t *ec) static inline rb_serial_t rb_ec_ractor_id(const rb_execution_context_t *ec) { - VM_ASSERT(ec->ractor_id == rb_ractor_id(rb_ec_ractor_ptr(ec))); - return ec->ractor_id; + rb_serial_t ractor_id = ec->ractor_id; + RUBY_ASSERT(ractor_id); + return ractor_id; } static inline rb_vm_t * diff --git a/yjit.c b/yjit.c index 6c3c9cd00161ce..1cd934c42eee63 100644 --- a/yjit.c +++ b/yjit.c @@ -473,6 +473,12 @@ rb_yjit_invokeblock_sp_pops(const struct rb_callinfo *ci) return 1 - sp_inc_of_invokeblock(ci); // + 1 to ignore return value push } +rb_serial_t +rb_yjit_cme_ractor_serial(const rb_callable_method_entry_t *cme) +{ + return cme->def->body.bmethod.defined_ractor_id; +} + // Setup jit_return to avoid returning a non-Qundef value on a non-FINISH frame. // See [jit_compile_exception] for details. void diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index 06c475f3c8b493..fd99d529041077 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -272,6 +272,7 @@ fn main() { .allowlist_function("rb_optimized_call") .allowlist_function("rb_yjit_sendish_sp_pops") .allowlist_function("rb_yjit_invokeblock_sp_pops") + .allowlist_function("rb_yjit_cme_ractor_serial") .allowlist_function("rb_yjit_set_exception_return") .allowlist_function("rb_jit_str_concat_codepoint") .allowlist_type("rstring_offsets") diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 620bdb82800a78..606fe6ed702842 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -7396,11 +7396,12 @@ fn gen_send_bmethod( let capture = unsafe { proc_block.as_.captured.as_ref() }; let iseq = unsafe { *capture.code.iseq.as_ref() }; - // Optimize for single ractor mode and avoid runtime check for - // "defined with an un-shareable Proc in a different Ractor" - if !assume_single_ractor_mode(jit, asm) { - gen_counter_incr(jit, asm, Counter::send_bmethod_ractor); - return None; + if !procv.shareable_p() { + let ractor_serial = unsafe { rb_yjit_cme_ractor_serial(cme) }; + asm_comment!(asm, "guard current ractor == {}", ractor_serial); + let current_ractor_serial = asm.load(Opnd::mem(64, EC, RUBY_OFFSET_EC_RACTOR_ID)); + asm.cmp(current_ractor_serial, Opnd::UImm(ractor_serial)); + asm.jne(Target::side_exit(Counter::send_bmethod_ractor)); } // Passing a block to a block needs logic different from passing diff --git a/yjit/src/cruby.rs b/yjit/src/cruby.rs index 6e6a1810c67dda..d8497e41e39321 100644 --- a/yjit/src/cruby.rs +++ b/yjit/src/cruby.rs @@ -361,6 +361,11 @@ impl VALUE { !self.special_const_p() } + /// Shareability between ractors. `RB_OBJ_SHAREABLE_P()`. + pub fn shareable_p(self) -> bool { + (self.builtin_flags() & RUBY_FL_SHAREABLE as usize) != 0 + } + /// Return true if the value is a Ruby Fixnum (immediate-size integer) pub fn fixnum_p(self) -> bool { let VALUE(cval) = self; @@ -772,6 +777,7 @@ mod manual_defs { pub const RUBY_OFFSET_EC_INTERRUPT_FLAG: i32 = 32; // rb_atomic_t (u32) pub const RUBY_OFFSET_EC_INTERRUPT_MASK: i32 = 36; // rb_atomic_t (u32) pub const RUBY_OFFSET_EC_THREAD_PTR: i32 = 48; + pub const RUBY_OFFSET_EC_RACTOR_ID: i32 = 64; // Constants from rb_thread_t in vm_core.h pub const RUBY_OFFSET_THREAD_SELF: i32 = 16; diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index d2347671bbacb8..0cab97ebf4a603 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1198,6 +1198,7 @@ extern "C" { pub fn rb_yjit_shape_index(shape_id: shape_id_t) -> attr_index_t; pub fn rb_yjit_sendish_sp_pops(ci: *const rb_callinfo) -> usize; pub fn rb_yjit_invokeblock_sp_pops(ci: *const rb_callinfo) -> usize; + pub fn rb_yjit_cme_ractor_serial(cme: *const rb_callable_method_entry_t) -> rb_serial_t; pub fn rb_yjit_set_exception_return( cfp: *mut rb_control_frame_t, leave_exit: *mut ::std::os::raw::c_void, From b1c3060beda4bd659d21a2b2ea598bf9ee819d70 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 18 Dec 2025 12:31:07 -0800 Subject: [PATCH 11/20] Co-authored-by: Luke Gruber Co-authored-by: Alan Wu YJIT: Support calling bmethods in Ractors Co-authored-by: Luke Gruber Suggestion from Alan --- yjit/src/codegen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 606fe6ed702842..11f961f8c781e2 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -7400,7 +7400,7 @@ fn gen_send_bmethod( let ractor_serial = unsafe { rb_yjit_cme_ractor_serial(cme) }; asm_comment!(asm, "guard current ractor == {}", ractor_serial); let current_ractor_serial = asm.load(Opnd::mem(64, EC, RUBY_OFFSET_EC_RACTOR_ID)); - asm.cmp(current_ractor_serial, Opnd::UImm(ractor_serial)); + asm.cmp(current_ractor_serial, ractor_serial.into()); asm.jne(Target::side_exit(Counter::send_bmethod_ractor)); } From 73e930f9f911cf71ecb416c3112a7818bae41cd6 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 18 Dec 2025 12:31:34 -0800 Subject: [PATCH 12/20] JIT: Move EC offsets to jit_bindgen_constants Co-authored-by: Alan Wu --- jit.c | 9 ++++++++- yjit/src/backend/tests.rs | 4 ++-- yjit/src/codegen.rs | 14 +++++++------- yjit/src/cruby.rs | 7 ------- yjit/src/cruby_bindings.inc.rs | 5 +++++ zjit/src/backend/tests.rs | 4 ++-- zjit/src/codegen.rs | 14 +++++++------- zjit/src/cruby.rs | 6 ------ zjit/src/cruby_bindings.inc.rs | 5 +++++ 9 files changed, 36 insertions(+), 32 deletions(-) diff --git a/jit.c b/jit.c index ff44ac5b2e565d..fb7a5bd47d4919 100644 --- a/jit.c +++ b/jit.c @@ -23,7 +23,14 @@ enum jit_bindgen_constants { ROBJECT_OFFSET_AS_ARY = offsetof(struct RObject, as.ary), // Field offsets for the RString struct - RUBY_OFFSET_RSTRING_LEN = offsetof(struct RString, len) + RUBY_OFFSET_RSTRING_LEN = offsetof(struct RString, len), + + // Field offsets for rb_execution_context_t + RUBY_OFFSET_EC_CFP = offsetof(rb_execution_context_t, cfp), + RUBY_OFFSET_EC_INTERRUPT_FLAG = offsetof(rb_execution_context_t, interrupt_flag), + RUBY_OFFSET_EC_INTERRUPT_MASK = offsetof(rb_execution_context_t, interrupt_mask), + RUBY_OFFSET_EC_THREAD_PTR = offsetof(rb_execution_context_t, thread_ptr), + RUBY_OFFSET_EC_RACTOR_ID = offsetof(rb_execution_context_t, ractor_id), }; // Manually bound in rust since this is out-of-range of `int`, diff --git a/yjit/src/backend/tests.rs b/yjit/src/backend/tests.rs index ac2f35b3d9c9e4..bfeea5163a2654 100644 --- a/yjit/src/backend/tests.rs +++ b/yjit/src/backend/tests.rs @@ -232,9 +232,9 @@ fn test_jcc_ptr() let (mut asm, mut cb) = setup_asm(); let side_exit = Target::CodePtr(cb.get_write_ptr().add_bytes(4)); - let not_mask = asm.not(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_MASK)); + let not_mask = asm.not(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_MASK as i32)); asm.test( - Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG), + Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG as i32), not_mask, ); asm.jnz(side_exit); diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 11f961f8c781e2..61de6a32dabe8a 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -1208,7 +1208,7 @@ fn gen_check_ints( // Not checking interrupt_mask since it's zero outside finalize_deferred_heap_pages, // signal_exec, or rb_postponed_job_flush. - let interrupt_flag = asm.load(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG)); + let interrupt_flag = asm.load(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG as i32)); asm.test(interrupt_flag, interrupt_flag); asm.jnz(Target::side_exit(counter)); @@ -6659,7 +6659,7 @@ fn jit_thread_s_current( asm.stack_pop(1); // ec->thread_ptr - let ec_thread_opnd = asm.load(Opnd::mem(64, EC, RUBY_OFFSET_EC_THREAD_PTR)); + let ec_thread_opnd = asm.load(Opnd::mem(64, EC, RUBY_OFFSET_EC_THREAD_PTR as i32)); // thread->self let thread_self = Opnd::mem(64, ec_thread_opnd, RUBY_OFFSET_THREAD_SELF); @@ -7124,7 +7124,7 @@ fn gen_send_cfunc( asm_comment!(asm, "set ec->cfp"); let new_cfp = asm.lea(Opnd::mem(64, CFP, -(RUBY_SIZEOF_CONTROL_FRAME as i32))); - asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), new_cfp); + asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), new_cfp); if !kw_arg.is_null() { // Build a hash from all kwargs passed @@ -7220,7 +7220,7 @@ fn gen_send_cfunc( // Pop the stack frame (ec->cfp++) // Instead of recalculating, we can reuse the previous CFP, which is stored in a callee-saved // register - let ec_cfp_opnd = Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP); + let ec_cfp_opnd = Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32); asm.store(ec_cfp_opnd, CFP); // cfunc calls may corrupt types @@ -7399,7 +7399,7 @@ fn gen_send_bmethod( if !procv.shareable_p() { let ractor_serial = unsafe { rb_yjit_cme_ractor_serial(cme) }; asm_comment!(asm, "guard current ractor == {}", ractor_serial); - let current_ractor_serial = asm.load(Opnd::mem(64, EC, RUBY_OFFSET_EC_RACTOR_ID)); + let current_ractor_serial = asm.load(Opnd::mem(64, EC, RUBY_OFFSET_EC_RACTOR_ID as i32)); asm.cmp(current_ractor_serial, ractor_serial.into()); asm.jne(Target::side_exit(Counter::send_bmethod_ractor)); } @@ -8359,7 +8359,7 @@ fn gen_send_iseq( asm_comment!(asm, "switch to new CFP"); let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); asm.mov(CFP, new_cfp); - asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP); // Directly jump to the entry point of the callee gen_direct_jump( @@ -9937,7 +9937,7 @@ fn gen_leave( asm_comment!(asm, "pop stack frame"); let incr_cfp = asm.add(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); asm.mov(CFP, incr_cfp); - asm.mov(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.mov(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP); // Load the return value let retval_opnd = asm.stack_pop(1); diff --git a/yjit/src/cruby.rs b/yjit/src/cruby.rs index d8497e41e39321..d34b049a453625 100644 --- a/yjit/src/cruby.rs +++ b/yjit/src/cruby.rs @@ -772,13 +772,6 @@ mod manual_defs { pub const RUBY_OFFSET_CFP_JIT_RETURN: i32 = 48; pub const RUBY_SIZEOF_CONTROL_FRAME: usize = 56; - // Constants from rb_execution_context_t vm_core.h - pub const RUBY_OFFSET_EC_CFP: i32 = 16; - pub const RUBY_OFFSET_EC_INTERRUPT_FLAG: i32 = 32; // rb_atomic_t (u32) - pub const RUBY_OFFSET_EC_INTERRUPT_MASK: i32 = 36; // rb_atomic_t (u32) - pub const RUBY_OFFSET_EC_THREAD_PTR: i32 = 48; - pub const RUBY_OFFSET_EC_RACTOR_ID: i32 = 64; - // Constants from rb_thread_t in vm_core.h pub const RUBY_OFFSET_THREAD_SELF: i32 = 16; diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 0cab97ebf4a603..952cf88c205115 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -978,6 +978,11 @@ pub type rb_seq_param_keyword_struct = pub const ROBJECT_OFFSET_AS_HEAP_FIELDS: jit_bindgen_constants = 16; pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RUBY_OFFSET_RSTRING_LEN: jit_bindgen_constants = 16; +pub const RUBY_OFFSET_EC_CFP: jit_bindgen_constants = 16; +pub const RUBY_OFFSET_EC_INTERRUPT_FLAG: jit_bindgen_constants = 32; +pub const RUBY_OFFSET_EC_INTERRUPT_MASK: jit_bindgen_constants = 36; +pub const RUBY_OFFSET_EC_THREAD_PTR: jit_bindgen_constants = 48; +pub const RUBY_OFFSET_EC_RACTOR_ID: jit_bindgen_constants = 64; pub type jit_bindgen_constants = u32; pub type rb_iseq_param_keyword_struct = rb_iseq_constant_body_rb_iseq_parameters_rb_iseq_param_keyword; diff --git a/zjit/src/backend/tests.rs b/zjit/src/backend/tests.rs index 6e62b3068d5a29..ece6f8605f1540 100644 --- a/zjit/src/backend/tests.rs +++ b/zjit/src/backend/tests.rs @@ -229,9 +229,9 @@ fn test_jcc_ptr() let (mut asm, mut cb) = setup_asm(); let side_exit = Target::CodePtr(cb.get_write_ptr().add_bytes(4)); - let not_mask = asm.not(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_MASK)); + let not_mask = asm.not(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_MASK as i32)); asm.test( - Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG), + Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG as i32), not_mask, ); asm.jnz(side_exit); diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index e81732f3d54ff7..b05c0110909e62 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -841,7 +841,7 @@ fn gen_ccall_with_frame( asm_comment!(asm, "switch to new CFP"); let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); asm.mov(CFP, new_cfp); - asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP); let mut cfunc_args = vec![recv]; cfunc_args.extend(args); @@ -851,7 +851,7 @@ fn gen_ccall_with_frame( asm_comment!(asm, "pop C frame"); let new_cfp = asm.add(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); asm.mov(CFP, new_cfp); - asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP); asm_comment!(asm, "restore SP register for the caller"); let new_sp = asm.sub(SP, sp_offset.into()); @@ -926,7 +926,7 @@ fn gen_ccall_variadic( asm_comment!(asm, "switch to new CFP"); let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); asm.mov(CFP, new_cfp); - asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP); let argv_ptr = gen_push_opnds(asm, &args); asm.count_call_to(&name.contents_lossy()); @@ -936,7 +936,7 @@ fn gen_ccall_variadic( asm_comment!(asm, "pop C frame"); let new_cfp = asm.add(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); asm.mov(CFP, new_cfp); - asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP); asm_comment!(asm, "restore SP register for the caller"); let new_sp = asm.sub(SP, sp_offset.into()); @@ -1051,7 +1051,7 @@ fn gen_check_interrupts(jit: &mut JITState, asm: &mut Assembler, state: &FrameSt asm_comment!(asm, "RUBY_VM_CHECK_INTS(ec)"); // Not checking interrupt_mask since it's zero outside finalize_deferred_heap_pages, // signal_exec, or rb_postponed_job_flush. - let interrupt_flag = asm.load(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG)); + let interrupt_flag = asm.load(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG as i32)); asm.test(interrupt_flag, interrupt_flag); asm.jnz(side_exit(jit, state, SideExitReason::Interrupt)); } @@ -1382,7 +1382,7 @@ fn gen_send_without_block_direct( asm_comment!(asm, "switch to new CFP"); let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); asm.mov(CFP, new_cfp); - asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP); // Set up arguments let mut c_args = vec![recv]; @@ -1741,7 +1741,7 @@ fn gen_return(asm: &mut Assembler, val: lir::Opnd) { asm_comment!(asm, "pop stack frame"); let incr_cfp = asm.add(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); asm.mov(CFP, incr_cfp); - asm.mov(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.mov(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP); // Order here is important. Because we're about to tear down the frame, // we need to load the return value, which might be part of the frame. diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 68b6810125ea25..57a3bee7e01d8c 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1073,12 +1073,6 @@ mod manual_defs { pub const RUBY_OFFSET_CFP_JIT_RETURN: i32 = 48; pub const RUBY_SIZEOF_CONTROL_FRAME: usize = 56; - // Constants from rb_execution_context_t vm_core.h - pub const RUBY_OFFSET_EC_CFP: i32 = 16; - pub const RUBY_OFFSET_EC_INTERRUPT_FLAG: i32 = 32; // rb_atomic_t (u32) - pub const RUBY_OFFSET_EC_INTERRUPT_MASK: i32 = 36; // rb_atomic_t (u32) - pub const RUBY_OFFSET_EC_THREAD_PTR: i32 = 48; - // Constants from rb_thread_t in vm_core.h pub const RUBY_OFFSET_THREAD_SELF: i32 = 16; diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 56ec724dd9b29c..2689ec30cf8b2d 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1828,6 +1828,11 @@ pub type zjit_struct_offsets = u32; pub const ROBJECT_OFFSET_AS_HEAP_FIELDS: jit_bindgen_constants = 16; pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RUBY_OFFSET_RSTRING_LEN: jit_bindgen_constants = 16; +pub const RUBY_OFFSET_EC_CFP: jit_bindgen_constants = 16; +pub const RUBY_OFFSET_EC_INTERRUPT_FLAG: jit_bindgen_constants = 32; +pub const RUBY_OFFSET_EC_INTERRUPT_MASK: jit_bindgen_constants = 36; +pub const RUBY_OFFSET_EC_THREAD_PTR: jit_bindgen_constants = 48; +pub const RUBY_OFFSET_EC_RACTOR_ID: jit_bindgen_constants = 64; pub type jit_bindgen_constants = u32; pub const rb_invalid_shape_id: shape_id_t = 4294967295; pub type rb_iseq_param_keyword_struct = From d0b72429a93e54f1f956b4aedfc25c57dc7001aa Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 16 Dec 2025 09:10:45 -0800 Subject: [PATCH 13/20] Add support for signed and unsigned LEB128 to pack/unpack. This commit adds a new pack format command `R` and `r` for unsigned and signed LEB128 encoding. The "r" mnemonic is because this is a "vaRiable" length encoding scheme. LEB128 is used in various formats including DWARF, WebAssembly, MQTT, and Protobuf. [Feature #21785] --- doc/language/packed_data.rdoc | 2 + pack.c | 83 ++++++++++++++ spec/ruby/core/array/pack/r_spec.rb | 23 ++++ spec/ruby/core/array/pack/shared/basic.rb | 4 +- spec/ruby/core/string/unpack/shared/basic.rb | 2 +- test/ruby/test_pack.rb | 112 +++++++++++++++++++ 6 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 spec/ruby/core/array/pack/r_spec.rb diff --git a/doc/language/packed_data.rdoc b/doc/language/packed_data.rdoc index 3a762c03829a74..dcb4d557352d88 100644 --- a/doc/language/packed_data.rdoc +++ b/doc/language/packed_data.rdoc @@ -53,6 +53,8 @@ These tables summarize the directives for packing and unpacking. U | UTF-8 character w | BER-compressed integer + R | LEB128 encoded unsigned integer + r | LEB128 encoded signed integer === For Floats diff --git a/pack.c b/pack.c index 3a5c1bfb9677cf..6f68b13ccac6c4 100644 --- a/pack.c +++ b/pack.c @@ -667,6 +667,56 @@ pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) } break; + case 'r': /* r for SLEB128 encoding (signed) */ + case 'R': /* R for ULEB128 encoding (unsigned) */ + { + int pack_flags = INTEGER_PACK_LITTLE_ENDIAN; + + if (type == 'r') { + pack_flags |= INTEGER_PACK_2COMP; + } + + while (len-- > 0) { + size_t numbytes; + int sign; + char *cp; + + from = NEXTFROM; + from = rb_to_int(from); + numbytes = rb_absint_numwords(from, 7, NULL); + if (numbytes == 0) + numbytes = 1; + VALUE buf = rb_str_new(NULL, numbytes); + + sign = rb_integer_pack(from, RSTRING_PTR(buf), RSTRING_LEN(buf), 1, 1, pack_flags); + + if (sign < 0 && type == 'R') { + rb_raise(rb_eArgError, "can't encode negative numbers in ULEB128"); + } + + if (type == 'r') { + /* Check if we need an extra byte for sign extension */ + unsigned char last_byte = (unsigned char)RSTRING_PTR(buf)[numbytes - 1]; + if ((sign >= 0 && (last_byte & 0x40)) || /* positive but sign bit set */ + (sign < 0 && !(last_byte & 0x40))) { /* negative but sign bit clear */ + /* Need an extra byte */ + rb_str_resize(buf, numbytes + 1); + RSTRING_PTR(buf)[numbytes] = sign < 0 ? 0x7f : 0x00; + numbytes++; + } + } + + cp = RSTRING_PTR(buf); + while (1 < numbytes) { + *cp |= 0x80; + cp++; + numbytes--; + } + + rb_str_buf_cat(res, RSTRING_PTR(buf), RSTRING_LEN(buf)); + } + } + break; case 'u': /* uuencoded string */ case 'm': /* base64 encoded string */ from = NEXTFROM; @@ -1558,6 +1608,39 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) } break; + case 'r': + case 'R': + { + int pack_flags = INTEGER_PACK_LITTLE_ENDIAN; + + if (type == 'r') { + pack_flags |= INTEGER_PACK_2COMP; + } + char *s0 = s; + while (len > 0 && s < send) { + if (*s & 0x80) { + s++; + } + else { + s++; + UNPACK_PUSH(rb_integer_unpack(s0, s-s0, 1, 1, pack_flags)); + len--; + s0 = s; + } + } + /* Handle incomplete value and remaining expected values with nil (only if not using *) */ + if (!star) { + if (s0 != s && len > 0) { + UNPACK_PUSH(Qnil); + len--; + } + while (len-- > 0) { + UNPACK_PUSH(Qnil); + } + } + } + break; + case 'w': { char *s0 = s; diff --git a/spec/ruby/core/array/pack/r_spec.rb b/spec/ruby/core/array/pack/r_spec.rb new file mode 100644 index 00000000000000..22be6fa6400cfa --- /dev/null +++ b/spec/ruby/core/array/pack/r_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/integer' + +ruby_version_is "4.0" do + describe "Array#pack with format 'R'" do + it_behaves_like :array_pack_basic, 'R' + it_behaves_like :array_pack_basic_non_float, 'R' + it_behaves_like :array_pack_arguments, 'R' + it_behaves_like :array_pack_numeric_basic, 'R' + it_behaves_like :array_pack_integer, 'R' + end + + describe "Array#pack with format 'r'" do + it_behaves_like :array_pack_basic, 'r' + it_behaves_like :array_pack_basic_non_float, 'r' + it_behaves_like :array_pack_arguments, 'r' + it_behaves_like :array_pack_numeric_basic, 'r' + it_behaves_like :array_pack_integer, 'r' + end +end diff --git a/spec/ruby/core/array/pack/shared/basic.rb b/spec/ruby/core/array/pack/shared/basic.rb index ebd9f75d9dc66a..77d7f2f71c0873 100644 --- a/spec/ruby/core/array/pack/shared/basic.rb +++ b/spec/ruby/core/array/pack/shared/basic.rb @@ -37,7 +37,7 @@ # NOTE: it's just a plan of the Ruby core team it "warns that a directive is unknown" do # additional directive ('a') is required for the X directive - -> { [@obj, @obj].pack("a R" + pack_format) }.should complain(/unknown pack directive 'R'/) + -> { [@obj, @obj].pack("a K" + pack_format) }.should complain(/unknown pack directive 'K'/) -> { [@obj, @obj].pack("a 0" + pack_format) }.should complain(/unknown pack directive '0'/) -> { [@obj, @obj].pack("a :" + pack_format) }.should complain(/unknown pack directive ':'/) end @@ -48,7 +48,7 @@ # NOTE: Added this case just to not forget about the decision in the ticket it "raise ArgumentError when a directive is unknown" do # additional directive ('a') is required for the X directive - -> { [@obj, @obj].pack("a R" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive 'R'/) + -> { [@obj, @obj].pack("a K" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive 'K'/) -> { [@obj, @obj].pack("a 0" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive '0'/) -> { [@obj, @obj].pack("a :" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive ':'/) end diff --git a/spec/ruby/core/string/unpack/shared/basic.rb b/spec/ruby/core/string/unpack/shared/basic.rb index b37a447683a95c..0ac2a951ed7220 100644 --- a/spec/ruby/core/string/unpack/shared/basic.rb +++ b/spec/ruby/core/string/unpack/shared/basic.rb @@ -12,7 +12,7 @@ ruby_version_is "3.3" do # https://bugs.ruby-lang.org/issues/19150 it 'raise ArgumentError when a directive is unknown' do - -> { "abcdefgh".unpack("a R" + unpack_format) }.should raise_error(ArgumentError, /unknown unpack directive 'R'/) + -> { "abcdefgh".unpack("a K" + unpack_format) }.should raise_error(ArgumentError, /unknown unpack directive 'K'/) -> { "abcdefgh".unpack("a 0" + unpack_format) }.should raise_error(ArgumentError, /unknown unpack directive '0'/) -> { "abcdefgh".unpack("a :" + unpack_format) }.should raise_error(ArgumentError, /unknown unpack directive ':'/) end diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index ca089f09c3dc4b..9c40cfaa204f86 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -936,4 +936,116 @@ class Array assert_equal "oh no", v end; end + + def test_unpack_broken_R + assert_equal([nil], "\xFF".unpack("R")) + assert_nil("\xFF".unpack1("R")) + assert_equal([nil], "\xFF".unpack("r")) + assert_nil("\xFF".unpack1("r")) + + bytes = [256].pack("r") + assert_equal([256, nil, nil, nil], (bytes + "\xFF").unpack("rrrr")) + + bytes = [256].pack("R") + assert_equal([256, nil, nil, nil], (bytes + "\xFF").unpack("RRRR")) + + assert_equal([], "\xFF".unpack("R*")) + assert_equal([], "\xFF".unpack("r*")) + end + + def test_pack_unpack_R + # ULEB128 encoding (unsigned) + assert_equal("\x00", [0].pack("R")) + assert_equal("\x01", [1].pack("R")) + assert_equal("\x7f", [127].pack("R")) + assert_equal("\x80\x01", [128].pack("R")) + assert_equal("\xff\x7f", [0x3fff].pack("R")) + assert_equal("\x80\x80\x01", [0x4000].pack("R")) + assert_equal("\xff\xff\xff\xff\x0f", [0xffffffff].pack("R")) + assert_equal("\x80\x80\x80\x80\x10", [0x100000000].pack("R")) + assert_equal("\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01", [0xffff_ffff_ffff_ffff].pack("R")) + assert_equal("\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f", [0xffff_ffff_ffff_ffff_ffff_ffff].pack("R")) + + # Multiple values + assert_equal("\x01\x02", [1, 2].pack("R*")) + assert_equal("\x7f\x80\x01", [127, 128].pack("R*")) + + # Negative numbers should raise an error + assert_raise(ArgumentError) { [-1].pack("R") } + assert_raise(ArgumentError) { [-100].pack("R") } + + # Unpack tests + assert_equal([0], "\x00".unpack("R")) + assert_equal([1], "\x01".unpack("R")) + assert_equal([127], "\x7f".unpack("R")) + assert_equal([128], "\x80\x01".unpack("R")) + assert_equal([0x3fff], "\xff\x7f".unpack("R")) + assert_equal([0x4000], "\x80\x80\x01".unpack("R")) + assert_equal([0xffffffff], "\xff\xff\xff\xff\x0f".unpack("R")) + assert_equal([0x100000000], "\x80\x80\x80\x80\x10".unpack("R")) + assert_equal([0xffff_ffff_ffff_ffff], "\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01".unpack("R")) + assert_equal([0xffff_ffff_ffff_ffff_ffff_ffff].pack("R"), "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f") + + # Multiple values + assert_equal([1, 2], "\x01\x02".unpack("R*")) + assert_equal([127, 128], "\x7f\x80\x01".unpack("R*")) + + # Round-trip test + values = [0, 1, 127, 128, 0x3fff, 0x4000, 0xffffffff, 0x100000000] + assert_equal(values, values.pack("R*").unpack("R*")) + end + + def test_pack_unpack_r + # SLEB128 encoding (signed) + assert_equal("\x00", [0].pack("r")) + assert_equal("\x01", [1].pack("r")) + assert_equal("\x7f", [-1].pack("r")) + assert_equal("\x7e", [-2].pack("r")) + assert_equal("\xff\x00", [127].pack("r")) + assert_equal("\x80\x01", [128].pack("r")) + assert_equal("\x81\x7f", [-127].pack("r")) + assert_equal("\x80\x7f", [-128].pack("r")) + + # Larger positive numbers + assert_equal("\xff\xff\x00", [0x3fff].pack("r")) + assert_equal("\x80\x80\x01", [0x4000].pack("r")) + + # Larger negative numbers + assert_equal("\x81\x80\x7f", [-0x3fff].pack("r")) + assert_equal("\x80\x80\x7f", [-0x4000].pack("r")) + + # Very large numbers + assert_equal("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x1F", [0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + assert_equal("\x81\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80`", [-0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + + # Multiple values + assert_equal("\x00\x01\x7f", [0, 1, -1].pack("r*")) + + # Unpack tests + assert_equal([0], "\x00".unpack("r")) + assert_equal([1], "\x01".unpack("r")) + assert_equal([-1], "\x7f".unpack("r")) + assert_equal([-2], "\x7e".unpack("r")) + assert_equal([127], "\xff\x00".unpack("r")) + assert_equal([128], "\x80\x01".unpack("r")) + assert_equal([-127], "\x81\x7f".unpack("r")) + assert_equal([-128], "\x80\x7f".unpack("r")) + + # Larger numbers + assert_equal([0x3fff], "\xff\xff\x00".unpack("r")) + assert_equal([0x4000], "\x80\x80\x01".unpack("r")) + assert_equal([-0x3fff], "\x81\x80\x7f".unpack("r")) + assert_equal([-0x4000], "\x80\x80\x7f".unpack("r")) + + # Very large numbers + assert_equal("\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f", [0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + assert_equal("\x81\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80`", [-0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + + # Multiple values + assert_equal([0, 1, -1], "\x00\x01\x7f".unpack("r*")) + + # Round-trip test + values = [0, 1, -1, 127, -127, 128, -128, 0x3fff, -0x3fff, 0x4000, -0x4000] + assert_equal(values, values.pack("r*").unpack("r*")) + end end From 99b915944f1d47c1a47b1a3e894013869c7c27a7 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Tue, 16 Dec 2025 23:21:14 +0000 Subject: [PATCH 14/20] [DOC] Russian strings should look Russian --- doc/string/aset.rdoc | 6 +++--- doc/string/bytes.rdoc | 4 ++-- doc/string/bytesize.rdoc | 6 +++--- doc/string/center.rdoc | 2 +- doc/string/chars.rdoc | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/string/aset.rdoc b/doc/string/aset.rdoc index cac3b67ef580dd..db9079ebfb8188 100644 --- a/doc/string/aset.rdoc +++ b/doc/string/aset.rdoc @@ -170,9 +170,9 @@ With string argument +substring+ given: s['ll'] = 'foo' # => "foo" s # => "hefooo" - s = 'тест' - s['ес'] = 'foo' # => "foo" - s # => "тfooт" + s = 'Привет' + s['ив'] = 'foo' # => "foo" + s # => "Прfooет" s = 'こんにちは' s['んにち'] = 'foo' # => "foo" diff --git a/doc/string/bytes.rdoc b/doc/string/bytes.rdoc index f4b071f6306394..3815f13276fa11 100644 --- a/doc/string/bytes.rdoc +++ b/doc/string/bytes.rdoc @@ -1,7 +1,7 @@ Returns an array of the bytes in +self+: - 'hello'.bytes # => [104, 101, 108, 108, 111] - 'тест'.bytes # => [209, 130, 208, 181, 209, 129, 209, 130] + 'hello'.bytes # => [104, 101, 108, 108, 111] + 'Привет'.bytes # => [208, 159, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130] 'こんにちは'.bytes # => [227, 129, 147, 227, 130, 147, 227, 129, 171, 227, 129, 161, 227, 129, 175] diff --git a/doc/string/bytesize.rdoc b/doc/string/bytesize.rdoc index 5166dd7dc614ac..cbb7f439fcb448 100644 --- a/doc/string/bytesize.rdoc +++ b/doc/string/bytesize.rdoc @@ -5,9 +5,9 @@ Note that the byte count may be different from the character count (returned by s = 'foo' s.bytesize # => 3 s.size # => 3 - s = 'тест' - s.bytesize # => 8 - s.size # => 4 + s = 'Привет' + s.bytesize # => 12 + s.size # => 6 s = 'こんにちは' s.bytesize # => 15 s.size # => 5 diff --git a/doc/string/center.rdoc b/doc/string/center.rdoc index 343f6ba263acae..3116d211174286 100644 --- a/doc/string/center.rdoc +++ b/doc/string/center.rdoc @@ -9,7 +9,7 @@ centered and padded on one or both ends with +pad_string+: 'hello'.center(20, '-|') # => "-|-|-|-hello-|-|-|-|" # Some padding repeated. 'hello'.center(10, 'abcdefg') # => "abhelloabc" # Some padding not used. ' hello '.center(13) # => " hello " - 'тест'.center(10) # => " тест " + 'Привет'.center(10) # => " Привет " 'こんにちは'.center(10) # => " こんにちは " # Multi-byte characters. If +size+ is less than or equal to the size of +self+, returns an unpadded copy of +self+: diff --git a/doc/string/chars.rdoc b/doc/string/chars.rdoc index 094384271b1a77..97ea07331f4c46 100644 --- a/doc/string/chars.rdoc +++ b/doc/string/chars.rdoc @@ -1,7 +1,7 @@ Returns an array of the characters in +self+: 'hello'.chars # => ["h", "e", "l", "l", "o"] - 'тест'.chars # => ["т", "е", "с", "т"] + 'Привет'.chars # => ["П", "р", "и", "в", "е", "т"] 'こんにちは'.chars # => ["こ", "ん", "に", "ち", "は"] ''.chars # => [] From 3c6a6afa1c166b9fd24761c65fbd1268cb020341 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 18 Dec 2025 15:33:34 -0800 Subject: [PATCH 15/20] [DOC] Update ractor.c docs --- ractor.c | 42 +++++++++++------------------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/ractor.c b/ractor.c index a95468880708d0..f4c2e243be02e3 100644 --- a/ractor.c +++ b/ractor.c @@ -953,36 +953,16 @@ ractor_moved_missing(int argc, VALUE *argv, VALUE self) * * Raised when an attempt is made to send a message to a closed port, * or to retrieve a message from a closed and empty port. - * Ports may be closed explicitly with Ractor#close_outgoing/close_incoming + * Ports may be closed explicitly with Ractor::Port#close * and are closed implicitly when a Ractor terminates. * - * r = Ractor.new { sleep(500) } - * r.close_outgoing - * r.take # Ractor::ClosedError + * port = Ractor::Port.new + * port.close + * port << "test" # Ractor::ClosedError + * port.receive # Ractor::ClosedError * - * ClosedError is a descendant of StopIteration, so the closing of the ractor will break - * the loops without propagating the error: - * - * r = Ractor.new do - * loop do - * msg = receive # raises ClosedError and loop traps it - * puts "Received: #{msg}" - * end - * puts "loop exited" - * end - * - * 3.times{|i| r << i} - * r.close_incoming - * r.take - * puts "Continue successfully" - * - * This will print: - * - * Received: 0 - * Received: 1 - * Received: 2 - * loop exited - * Continue successfully + * ClosedError is a descendant of StopIteration, so the closing of a port will break + * out of loops without propagating the error. */ /* @@ -995,14 +975,14 @@ ractor_moved_missing(int argc, VALUE *argv, VALUE self) /* * Document-class: Ractor::RemoteError * - * Raised on attempt to Ractor#take if there was an uncaught exception in the Ractor. + * Raised on Ractor#join or Ractor#value if there was an uncaught exception in the Ractor. * Its +cause+ will contain the original exception, and +ractor+ is the original ractor * it was raised in. * * r = Ractor.new { raise "Something weird happened" } * * begin - * r.take + * r.value * rescue => e * p e # => # * p e.ractor == r # => true @@ -1014,7 +994,7 @@ ractor_moved_missing(int argc, VALUE *argv, VALUE self) /* * Document-class: Ractor::MovedError * - * Raised on an attempt to access an object which was moved in Ractor#send or Ractor.yield. + * Raised on an attempt to access an object which was moved in Ractor#send or Ractor::Port#send. * * r = Ractor.new { sleep } * @@ -1029,7 +1009,7 @@ ractor_moved_missing(int argc, VALUE *argv, VALUE self) * Document-class: Ractor::MovedObject * * A special object which replaces any value that was moved to another ractor in Ractor#send - * or Ractor.yield. Any attempt to access the object results in Ractor::MovedError. + * or Ractor::Port#send. Any attempt to access the object results in Ractor::MovedError. * * r = Ractor.new { receive } * From 535233c68c30546c81d22694fa3f5911d74c14b6 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 18 Dec 2025 15:48:29 -0800 Subject: [PATCH 16/20] [DOC] Update ractor.rb docs --- ractor.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ractor.rb b/ractor.rb index e380d04f873aad..136445880251c3 100644 --- a/ractor.rb +++ b/ractor.rb @@ -371,7 +371,7 @@ def close # # Checks if the object is shareable by ractors. # - # Ractor.shareable?(1) #=> true -- numbers and other immutable basic values are frozen + # Ractor.shareable?(1) #=> true -- numbers and other immutable basic values are shareable # Ractor.shareable?('foo') #=> false, unless the string is frozen due to # frozen_string_literal: true # Ractor.shareable?('foo'.freeze) #=> true # @@ -589,7 +589,7 @@ def value # # r = Ractor.new{ raise "foo" } # r.monitor(port = Ractor::Port.new) - # port.receive #=> :terminated and r is terminated with an exception "foo" + # port.receive #=> :aborted and r is terminated with an exception "foo" # def monitor port __builtin_ractor_monitor(port) @@ -634,7 +634,7 @@ def self.shareable_proc self: nil # # call-seq: - # Ractor.shareable_proc{} -> shareable proc + # Ractor.shareable_lambda{} -> shareable lambda # # Same as Ractor.shareable_proc, but returns a lambda. # @@ -690,7 +690,7 @@ class Port # Still received only one # Received: message2 # - # If close_incoming was called on the ractor, the method raises Ractor::ClosedError + # If the port is closed, the method raises Ractor::ClosedError # if there are no more messages in the message queue: # # port = Ractor::Port.new @@ -710,8 +710,8 @@ def receive # Send a message to a port to be accepted by port.receive. # # port = Ractor::Port.new - # r = Ractor.new do - # r.send 'message' + # r = Ractor.new(port) do |port| + # port.send 'message' # end # value = port.receive # puts "Received #{value}" @@ -722,7 +722,7 @@ def receive # # port = Ractor::Port.new # r = Ractor.new(port) do |port| - # port.send 'test'} + # port.send 'test' # puts "Sent successfully" # # Prints: "Sent successfully" immediately # end @@ -752,7 +752,7 @@ def send obj, move: false # call-seq: # port.close # - # Close the port. On the closed port, sending is not prohibited. + # Close the port. On the closed port, sending is prohibited. # Receiving is also not allowed if there is no sent messages arrived before closing. # # port = Ractor::Port.new From 805f53a9b12d82830db2f523d975eff2bd71bfa5 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Tue, 16 Dec 2025 16:13:44 -0800 Subject: [PATCH 17/20] [DOC] Various improvements to NEWS --- NEWS.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index a7a94d94287f2c..f51b49416fc745 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,7 +12,7 @@ Note that each entry is kept to a minimum, see links for details. * Logical binary operators (`||`, `&&`, `and` and `or`) at the beginning of a line continue the previous line, like fluent dot. - The following two code are equal: + The following two code examples are equal: ```ruby if condition1 @@ -165,7 +165,7 @@ Note: We're only listing outstanding class updates. * `Ractor::Port#close` * `Ractor::Port#closed?` - As result, `Ractor.yield` and `Ractor#take` were removed. + As a result, `Ractor.yield` and `Ractor#take` were removed. * `Ractor#join` and `Ractor#value` were added to wait for the termination of a Ractor. These are similar to `Thread#join` @@ -182,7 +182,7 @@ Note: We're only listing outstanding class updates. * `Ractor#close_incoming` and `Ractor#close_outgoing` were removed. - * `Ractor.shareable_proc` and `Ractor.shareable_lambda` is introduced + * `Ractor.shareable_proc` and `Ractor.shareable_lambda` are introduced to make shareable Proc or lambda. [[Feature #21550]], [[Feature #21557]] @@ -214,7 +214,7 @@ Note: We're only listing outstanding class updates. * `Set` is now a core class, instead of an autoloaded stdlib class. [[Feature #21216]] - * `Set#inspect` now uses a simpler displays, similar to literal arrays. + * `Set#inspect` now uses a simpler display, similar to literal arrays. (e.g., `Set[1, 2, 3]` instead of `#`). [[Feature #21389]] * Passing arguments to `Set#to_set` and `Enumerable#to_set` is now deprecated. @@ -369,7 +369,7 @@ The following bundled gems are updated. * `rb_path_check` has been removed. This function was used for `$SAFE` path checking which was removed in Ruby 2.7, - and was already deprecated,. + and was already deprecated. [[Feature #20971]] * A backtrace for `ArgumentError` of "wrong number of arguments" now @@ -523,6 +523,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21527]: https://bugs.ruby-lang.org/issues/21527 [Feature #21543]: https://bugs.ruby-lang.org/issues/21543 [Feature #21550]: https://bugs.ruby-lang.org/issues/21550 +[Feature #21552]: https://bugs.ruby-lang.org/issues/21552 [Feature #21557]: https://bugs.ruby-lang.org/issues/21557 [Bug #21654]: https://bugs.ruby-lang.org/issues/21654 [Feature #21678]: https://bugs.ruby-lang.org/issues/21678 From 084b916a5b9da0367b077add3203b924e868970b Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Tue, 16 Dec 2025 16:10:11 -0800 Subject: [PATCH 18/20] [DOC] Update NEWS for implementation improvements --- NEWS.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index f51b49416fc745..3d1807beb6100a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -358,7 +358,7 @@ The following bundled gems are updated. * `Ractor.yield` * `Ractor#take` * `Ractor#close_incoming` - * `Ractor#close_outgoging` + * `Ractor#close_outgoing` [[Feature #21262]] @@ -449,15 +449,27 @@ The following bundled gems are updated. ## Implementation improvements +* `Class#new` (ex. `Object.new`) is faster in all cases, but especially when passing keyword arguments. This has also been integrated into YJIT and ZJIT. [[Feature #21254]] +* GC heaps of different size pools now grow independently, reducing memory usage when only some pools contain long-lived objects +* GC sweeping is faster on pages of large objects +* "Generic ivar" objects (String, Array, `TypedData`, etc.) now use a new internal "fields" object for faster instance variable access +* The GC avoids maintaining an internal `id2ref` table until it is first used, making `object_id` allocation and GC sweeping faster +* `object_id` and `hash` are faster on Class and Module objects +* Larger bignum Integers can remain embedded using variable width allocation +* `Random`, `Enumerator::Product`, `Enumerator::Chain`, `Addrinfo`, + `StringScanner`, and some internal objects are now write-barrier protected, + which reduces GC overhead. + ### Ractor -A lot of work has gone into making Ractors more stable, performant, and usable. These improvements bring Ractors implementation closer to leaving experimental status. +A lot of work has gone into making Ractors more stable, performant, and usable. These improvements bring Ractor implementation closer to leaving experimental status. * Performance improvements - * Frozen strings and the symbol table internally use a lock-free hash set + * Frozen strings and the symbol table internally use a lock-free hash set [[Feature #21268]] * Method cache lookups avoid locking in most cases - * Class (and geniv) instance variable access is faster and avoids locking - * Cache contention is avoided during object allocation + * Class (and generic ivar) instance variable access is faster and avoids locking + * CPU cache contention is avoided in object allocation by using a per-ractor counter + * CPU cache contention is avoided in xmalloc/xfree by using a thread-local counter * `object_id` avoids locking in most cases * Bug fixes and stability * Fixed possible deadlocks when combining Ractors and Threads @@ -465,6 +477,8 @@ A lot of work has gone into making Ractors more stable, performant, and usable. * Fixed encoding/transcoding issues across Ractors * Fixed race conditions in GC operations and method invalidation * Fixed issues with processes forking after starting a Ractor + * GC allocation counts are now accurate under Ractors + * Fixed TracePoints not working after GC [[Bug #19112]] ## JIT @@ -488,6 +502,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #15408]: https://bugs.ruby-lang.org/issues/15408 [Feature #17473]: https://bugs.ruby-lang.org/issues/17473 [Feature #18455]: https://bugs.ruby-lang.org/issues/18455 +[Bug #19112]: https://bugs.ruby-lang.org/issues/19112 [Feature #19630]: https://bugs.ruby-lang.org/issues/19630 [Bug #19868]: https://bugs.ruby-lang.org/issues/19868 [Feature #19908]: https://bugs.ruby-lang.org/issues/19908 @@ -510,6 +525,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21219]: https://bugs.ruby-lang.org/issues/21219 [Feature #21254]: https://bugs.ruby-lang.org/issues/21254 [Feature #21258]: https://bugs.ruby-lang.org/issues/21258 +[Feature #21268]: https://bugs.ruby-lang.org/issues/21268 [Feature #21262]: https://bugs.ruby-lang.org/issues/21262 [Feature #21275]: https://bugs.ruby-lang.org/issues/21275 [Feature #21287]: https://bugs.ruby-lang.org/issues/21287 From b14f2f0116e6eccbfff57edae1283b3c53247752 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 18 Dec 2025 18:21:00 -0600 Subject: [PATCH 19/20] [DOC] Harmonize lt methods --- compar.c | 10 +++++++--- hash.c | 5 ++--- numeric.c | 9 ++++----- object.c | 15 ++++++++------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/compar.c b/compar.c index f5da6178cf4358..c577e4741ffa13 100644 --- a/compar.c +++ b/compar.c @@ -123,10 +123,14 @@ cmp_ge(VALUE x, VALUE y) /* * call-seq: - * obj < other -> true or false + * self < other -> true or false + * + * Returns whether +self+ is "less than" +other+; + * equivalent to (self <=> other) < 0: + * + * 'foo' < 'foo' # => false + * 'foo' < 'food' # => true * - * Compares two objects based on the receiver's <=> - * method, returning true if it returns a value less than 0. */ static VALUE diff --git a/hash.c b/hash.c index ac9a71794c8430..9e1555518ec037 100644 --- a/hash.c +++ b/hash.c @@ -4915,10 +4915,9 @@ rb_hash_le(VALUE hash, VALUE other) /* * call-seq: - * self < other_hash -> true or false + * self < other -> true or false * - * Returns +true+ if the entries of +self+ are a proper subset of the entries of +other_hash+, - * +false+ otherwise: + * Returns whether the entries of +self+ are a proper subset of the entries of +other+: * * h = {foo: 0, bar: 1} * h < {foo: 0, bar: 1, baz: 2} # => true # Proper subset. diff --git a/numeric.c b/numeric.c index 63e10fbe9f9141..3e770ceed3bc88 100644 --- a/numeric.c +++ b/numeric.c @@ -1702,7 +1702,8 @@ flo_ge(VALUE x, VALUE y) * call-seq: * self < other -> true or false * - * Returns +true+ if +self+ is numerically less than +other+: + * Returns whether the value of +self+ is less than the value of +other+; + * +other+ must be numeric, but may not be Complex: * * 2.0 < 3 # => true * 2.0 < 3.0 # => true @@ -1710,7 +1711,6 @@ flo_ge(VALUE x, VALUE y) * 2.0 < 2.0 # => false * * Float::NAN < Float::NAN returns an implementation-dependent value. - * */ static VALUE @@ -5038,7 +5038,8 @@ fix_lt(VALUE x, VALUE y) * call-seq: * self < other -> true or false * - * Returns +true+ if the value of +self+ is less than that of +other+: + * Returns whether the value of +self+ is less than the value of +other+; + * +other+ must be numeric, but may not be Complex: * * 1 < 0 # => false * 1 < 1 # => false @@ -5046,8 +5047,6 @@ fix_lt(VALUE x, VALUE y) * 1 < 0.5 # => false * 1 < Rational(1, 2) # => false * - * Raises an exception if the comparison cannot be made. - * */ static VALUE diff --git a/object.c b/object.c index 158bb0b219256c..b2b7a9d96038f0 100644 --- a/object.c +++ b/object.c @@ -1948,14 +1948,15 @@ rb_class_inherited_p(VALUE mod, VALUE arg) /* * call-seq: - * mod < other -> true, false, or nil + * self < other -> true, false, or nil * - * Returns true if mod is a subclass of other. Returns - * false if mod is the same as other - * or mod is an ancestor of other. - * Returns nil if there's no relationship between the two. - * (Think of the relationship in terms of the class definition: - * "class A < B" implies "A < B".) + * Returns whether +self+ is a subclass of +other+, + * or +nil+ if there is no relationship between the two: + * + * Float < Numeric # => true + * Numeric < Float # => false + * Float < Float # => false + * Float < Hash # => nil * */ From 0c4fcdff3252cca0d50986fc5b54a41bff809c85 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 18 Dec 2025 15:48:06 -0800 Subject: [PATCH 20/20] Update ArgumentError message for Ractor.select --- bootstraptest/test_ractor.rb | 2 +- ractor.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index a1169f9d29c54f..e2a3e8dd5beff1 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -316,7 +316,7 @@ def test n } unless (ENV.key?('TRAVIS') && ENV['TRAVIS_CPU_ARCH'] == 'arm64') # https://bugs.ruby-lang.org/issues/17878 # Exception for empty select -assert_match /specify at least one ractor/, %q{ +assert_match /specify at least one Ractor::Port or Ractor/, %q{ begin Ractor.select rescue ArgumentError => e diff --git a/ractor.rb b/ractor.rb index 136445880251c3..0002eece2ce700 100644 --- a/ractor.rb +++ b/ractor.rb @@ -267,7 +267,7 @@ def self.count # # TBD def self.select(*ports) - raise ArgumentError, 'specify at least one ractor or `yield_value`' if ports.empty? + raise ArgumentError, 'specify at least one Ractor::Port or Ractor' if ports.empty? monitors = {} # Ractor::Port => Ractor