From a9a3c2bbfa35d82d97cbbb4738c7e8c3bd381a69 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Sun, 23 Aug 2026 16:48:04 +0000 Subject: [PATCH 1/7] ractor: skip blocking_cnt bookkeeping on pthread rb_ractor_blocking_threads_inc/dec took the VM lock on every blocking region boundary of a ractor's last runnable thread -- for a 1-thread ractor, twice per IO operation -- to maintain vm->ractor.blocking_cnt. That counter is only consumed by the win32 scheduler, so the pthread build now skips those VM-lock sections; the per-ractor threads.blocking_cnt and the ractor creation/exit protocol stay. Ractor#inspect's status becomes running/terminated only ("blocking" is gone). The finer states were really the win32 scheduler's in-blocking-region flag leaking into inspect (under M:N, receive/sleep/ IO waits always showed "running" anyway, and the skip above stops the remaining flips on pthread entirely). R ractors x 1 thread, pipe write/read round-trips each, 16-HT machine (Ryzen 9 5900HX), RUBY_MN_THREADS=1, best of 3, mean of 2 alternating same-tree runs: before after 1R 342k 356k 4R 877k 1016k 8R 884k 976k 16R 835k 884k 24R 793k 839k The remaining ceiling is the scheduler-lock contention addressed by "thread: keep context switches off the scheduler lock"; combined, 16R reaches 3.6M rt/s. Suggested-by: Koichi Sasada Co-Authored-By: Claude Fable 5 --- ractor.c | 11 +++++++++++ ractor.rb | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ractor.c b/ractor.c index 5bf4e41b748b63..f774e6acfba61c 100644 --- a/ractor.c +++ b/ractor.c @@ -1006,6 +1006,14 @@ ractor_check_blocking(rb_ractor_t *cr, unsigned int remained_thread_cnt, const c { VM_ASSERT(cr == GET_RACTOR()); +#ifdef RUBY_THREAD_PTHREAD_H + // vm->ractor.blocking_cnt is only consumed by the win32 scheduler; the + // pthread one must not pay a VM lock per blocking region for it. The + // running<->blocking status flips stop with it (all callers), matching + // rb_ractor_blocking_threads_dec skipping the reverse transition. + return; +#endif + RUBY_DEBUG_LOG2(file, line, "cr->threads.cnt:%u cr->threads.blocking_cnt:%u vm->ractor.cnt:%u vm->ractor.blocking_cnt:%u", cr->threads.cnt, cr->threads.blocking_cnt, @@ -1106,6 +1114,8 @@ rb_ractor_blocking_threads_dec(rb_ractor_t *cr, const char *file, int line) VM_ASSERT(cr == GET_RACTOR()); +#ifndef RUBY_THREAD_PTHREAD_H + // see rb_ractor_blocking_threads_inc if (cr->threads.cnt == cr->threads.blocking_cnt) { rb_vm_t *vm = GET_VM(); @@ -1113,6 +1123,7 @@ rb_ractor_blocking_threads_dec(rb_ractor_t *cr, const char *file, int line) rb_vm_ractor_blocking_cnt_dec(vm, cr, __FILE__, __LINE__); } } +#endif cr->threads.blocking_cnt--; } diff --git a/ractor.rb b/ractor.rb index f0c9cc9aa6968b..e826e61b37655a 100644 --- a/ractor.rb +++ b/ractor.rb @@ -382,7 +382,7 @@ def inspect name = __builtin_cexpr! %q{ RACTOR_PTR(self)->name } id = __builtin_cexpr! %q{ UINT2NUM(rb_ractor_id(RACTOR_PTR(self))) } status = __builtin_cexpr! %q{ - rb_str_new2(ractor_status_str(RACTOR_PTR(self)->status_)) + rb_str_new2(RACTOR_PTR(self)->status_ == ractor_terminated ? "terminated" : "running") } "#" end From 99312bd9ffaedce0b22680089f226e5efddd65ac Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 24 Aug 2026 07:47:46 +1200 Subject: [PATCH 2/7] Resize `IO::Buffer` slices without reallocating. (#18446) Resize IO::Buffer slices as views --- io_buffer.c | 61 +++++++++++++++++++-- spec/ruby/core/io/buffer/resize_spec.rb | 71 +++++++++++++++++++++++-- test/ruby/test_io_buffer.rb | 55 ++++++++++++++++++- 3 files changed, 178 insertions(+), 9 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 6b8d834f9b28d6..f9fa1cf23d5e16 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -345,13 +345,19 @@ get_io_buffer(VALUE self) return buffer; } +static bool +io_buffer_slice_p(struct rb_io_buffer *buffer) +{ + return rb_typeddata_is_kind_of(buffer->source, &rb_io_buffer_type); +} + // Return the buffer which owns the lock count. A slice backed by another // buffer shares that source buffer's lock count. Other external sources, such // as strings, manage their own lifetime and do not share buffer lock state. static struct rb_io_buffer * io_buffer_lock_owner(struct rb_io_buffer *buffer) { - if (rb_typeddata_is_kind_of(buffer->source, &rb_io_buffer_type)) { + if (io_buffer_slice_p(buffer)) { return get_io_buffer(buffer->source); } @@ -1775,7 +1781,7 @@ rb_io_buffer_slice(struct rb_io_buffer *buffer, VALUE self, size_t offset, size_ // Slices retain their root buffer. If this buffer is already a slice, // retain its root directly rather than building a chain of slices: - if (rb_typeddata_is_kind_of(buffer->source, &rb_io_buffer_type)) { + if (io_buffer_slice_p(buffer)) { RB_OBJ_WRITE(instance, &slice->source, buffer->source); } else { @@ -1911,11 +1917,52 @@ io_buffer_resize_copy(VALUE self, struct rb_io_buffer *buffer, size_t size) *buffer = resized; } +static void +io_buffer_resize_slice(struct rb_io_buffer *slice, size_t size) +{ + struct rb_io_buffer *source = get_io_buffer(slice->source); + + if (!io_buffer_validate(source)) { + rb_raise(rb_eIOBufferInvalidatedError, "Buffer is invalid!"); + } + + if (source->base == NULL || slice->base == NULL) { + rb_raise(rb_eIOBufferInvalidatedError, "Buffer is invalid!"); + } + + uintptr_t source_address = (uintptr_t)source->base; + uintptr_t slice_address = (uintptr_t)slice->base; + + if (slice_address < source_address) { + rb_raise(rb_eIOBufferInvalidatedError, "Buffer is invalid!"); + } + + uintptr_t offset = slice_address - source_address; + + if (offset > source->size) { + rb_raise(rb_eIOBufferInvalidatedError, "Buffer is invalid!"); + } + + if (size > source->size - (size_t)offset) { + rb_raise(rb_eArgError, "Resized slice exceeds its source buffer!"); + } + + // Validate the requested range rather than the current range so that + // shrinking a slice can restore its validity after the source shrinks. + slice->size = size; +} + void rb_io_buffer_resize(VALUE self, size_t size) { struct rb_io_buffer *buffer = get_io_buffer(self); + if (io_buffer_slice_p(buffer)) { + // Resizing a slice only changes the view, not the locked allocation. + io_buffer_resize_slice(buffer, size); + return; + } + io_buffer_validate_for_reading(buffer); if (io_buffer_locked(buffer)) { @@ -1986,8 +2033,14 @@ rb_io_buffer_resize(VALUE self, size_t size) * # # * # 0x00000000 74 65 73 74 00 00 00 00 test.... * - * External buffer (created with ::for), and locked buffer - * can not be resized. + * When the buffer is a slice, resizing changes the size of the view without + * modifying the source buffer or allocating new storage. The resized view + * must remain within the source buffer. Growing the view exposes the existing + * bytes in the source; they are not cleared. Because the source allocation + * does not change, a slice can be resized while its source is locked. + * + * External owning buffers (created with ::for), and locked owning buffers + * cannot be resized. */ static VALUE io_buffer_resize(VALUE self, VALUE size) diff --git a/spec/ruby/core/io/buffer/resize_spec.rb b/spec/ruby/core/io/buffer/resize_spec.rb index 6e684475f34067..35ff38145214aa 100644 --- a/spec/ruby/core/io/buffer/resize_spec.rb +++ b/spec/ruby/core/io/buffer/resize_spec.rb @@ -143,9 +143,72 @@ -> { @buffer.resize(10.0) }.should.raise(TypeError, "not an Integer") end - context "with a slice of a buffer" do - # Current behavior of slice resizing seems unintended (it's undocumented, too). - # It either creates a completely new buffer, or breaks the slice on size 0. - it "needs to be reviewed for spec completeness" + ruby_version_is "4.1" do + context "with a slice of a buffer" do + it "changes the size of the view without modifying the source" do + @buffer = IO::Buffer.for("abcdef").dup + slice = @buffer.slice(2, 2) + + slice.resize(4).should.equal?(slice) + slice.get_string.should == "cdef" + @buffer.get_string.should == "abcdef" + + slice.resize(1) + slice.get_string.should == "c" + end + + it "does not allocate when resized to zero" do + @buffer = IO::Buffer.for("abcdef").dup + slice = @buffer.slice(2, 2) + + slice.resize(0) + slice.should_not.null? + slice.should.empty? + slice.should.valid? + + slice.resize(4) + slice.get_string.should == "cdef" + end + + it "raises ArgumentError when the resized view exceeds the source" do + @buffer = IO::Buffer.for("abcdef").dup + slice = @buffer.slice(2, 2) + + -> { slice.resize(5) }.should.raise( + ArgumentError, + "Resized slice exceeds its source buffer!" + ) + + slice.get_string.should == "cd" + end + + it "can be resized while the source allocation is locked" do + @buffer = IO::Buffer.for("abcdef").dup + slice = @buffer.slice(2, 2) + + slice.locked do + slice.resize(4) + slice.get_string.should == "cdef" + end + end + + it "preserves read-only access" do + @buffer = IO::Buffer.for("abcdef") + slice = @buffer.slice(2, 2) + + slice.resize(4) + slice.should.readonly? + slice.get_string.should == "cdef" + end + + it "uses the retained root buffer as the boundary for nested slices" do + @buffer = IO::Buffer.for("abcdef").dup + parent = @buffer.slice(1, 2) + slice = parent.slice(1, 1) + + slice.resize(4) + slice.get_string.should == "cdef" + end + end end end diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 53c4fd7a2ba6fa..78a34bcc0307cf 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -326,11 +326,64 @@ def test_resize_zero_slice slice = buffer.slice(0, 8) slice.resize(0) - assert_predicate slice, :null? + refute_predicate slice, :null? + assert_predicate slice, :empty? + assert_predicate slice, :valid? assert_equal 64, buffer.size slice.resize(1) assert_equal 1, slice.size + assert_predicate slice, :valid? + end + + def test_resize_slice_changes_view + buffer = IO::Buffer.for("abcdef").dup + slice = buffer.slice(2, 2) + + assert_same slice, slice.resize(4) + assert_equal "cdef", slice.get_string + + slice.set_string("X") + assert_equal "abXdef", buffer.get_string + + slice.resize(1) + assert_equal "X", slice.get_string + end + + def test_resize_slice_beyond_source + buffer = IO::Buffer.for("abcdef").dup + slice = buffer.slice(2, 2) + + error = assert_raise(ArgumentError) do + slice.resize(5) + end + + assert_equal "Resized slice exceeds its source buffer!", error.message + assert_equal 2, slice.size + assert_equal "cd", slice.get_string + end + + def test_resize_locked_slice + buffer = IO::Buffer.for("abcdef").dup + slice = buffer.slice(2, 2) + + slice.locked do + slice.resize(4) + assert_equal "cdef", slice.get_string + + assert_raise(IO::Buffer::LockedError) do + buffer.resize(8) + end + end + end + + def test_resize_nested_slice_uses_root_bounds + buffer = IO::Buffer.for("abcdef").dup + parent = buffer.slice(1, 2) + slice = parent.slice(1, 1) + + slice.resize(4) + assert_equal "cdef", slice.get_string end def test_resize_zero_external From 3c19a7c1cdaf31c24805f177878d054520bcff88 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Sun, 23 Aug 2026 07:36:57 +0000 Subject: [PATCH 3/7] thread: shard the io-wait bookkeeping by fd Every io wait took timer_th.waiting_lock to register and again to wake: one global lock serialized the fd map, the epoll_ctl calls made under it, the timer wheel and the wake-pending flags, across all fds. Split the ownership: - an fd's waiter list, arming state and the flags of its io waits move under one of 16 fd shard locks; unrelated fds register and wake in parallel, and the epoll_ctl calls leave the global lock entirely - waiting_lock keeps the wheel and fd-less timed waits - wake_pending becomes a count under its own small lock, so an expiry hold and an fd event's wake can pin one thread at once - the untimed waiter list is dropped (its only reader was a VM_ASSERT pass), so untimed io waits touch no global lock at all - the fd map's chunk table becomes a fixed array with CAS-installed chunks, since chunks span shards An io+timeout wait is registered in both structures; whoever clears its flags under the fd shard owns the wakeup. The expiry pass pops wheel nodes under waiting_lock, pins the thread with the pending count, and claims under the shard by (serial, fd) -- lock order is always shard -> waiting_lock -> wake_pending_lock. R ractor pairs (1 thread each) ping-ponging over pipes with blocking reads, total round-trips/sec, 16-HT machine (Ryzen 9 5900HX), RUBY_MN_THREADS=1, mean of 2 alternating same-tree runs: before after 8 pairs 63k 134k 16 pairs 64k 143k Messaging without fds (port ping-pong pairs) is unchanged. A thread-heavy shape (256 threads in few ractors doing memcached round trips) is unchanged: its wakes stay inside each ractor's readyq and the old lock was not saturated there. Co-Authored-By: Claude Fable 5 --- thread_pthread.c | 40 ++++- thread_pthread.h | 5 +- thread_pthread_mn.c | 346 +++++++++++++++++++++++++++++--------------- 3 files changed, 264 insertions(+), 127 deletions(-) diff --git a/thread_pthread.c b/thread_pthread.c index e624555c682e52..789fd1989de7dc 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -3299,18 +3299,26 @@ static struct { } wheel[TIMER_WHEEL_LEVELS]; uint64_t wheel_cursor_tick; // slots for ticks <= this are drained rb_hrtime_t next_expiry; // never later than the earliest deadline - struct ccan_list_head waiting_untimed; - pthread_mutex_t waiting_lock; + pthread_mutex_t waiting_lock; // the wheel and the flags of fd-less timed waits + + /* The fd map entries and the flags of io waits are guarded per fd by a + * shard lock, so unrelated fds register and wake in parallel. Whoever + * clears an io wait's flags under its shard owns the wakeup. Lock order: + * shard -> waiting_lock -> wake_pending_lock, never the other way. */ +#define IO_WAIT_SHARDS 16 + pthread_mutex_t fd_shard_locks[IO_WAIT_SHARDS]; // signaled when wake_pending clears on a thread; see timer_thread_wake_fence + pthread_mutex_t wake_pending_lock; rb_nativethread_cond_t wake_pending_cond; #endif #if (HAVE_SYS_EPOLL_H || HAVE_SYS_EVENT_H) && USE_MN_THREADS - // fd -> struct rb_fd_waiters, in chunks so entries never move. - // Protected by waiting_lock. - struct rb_fd_waiters **fdmap_chunks; - unsigned int fdmap_nchunks; + // fd -> struct rb_fd_waiters, in chunks so entries never move. The chunk + // table installs slots by CAS (chunks span shards); entry state is guarded + // by the fd's shard lock. +#define FDMAP_MAX_CHUNKS 1024 // fds up to FDMAP_MAX_CHUNKS * FDMAP_CHUNK_SIZE + struct rb_fd_waiters *fdmap_chunks[FDMAP_MAX_CHUNKS]; #endif } timer_th = { .created_fork_gen = 0, @@ -3539,6 +3547,21 @@ rb_thread_create_timer_thread(void) RUBY_DEBUG_LOG("forked child process"); CLOSE_INVALIDATE_PAIR(timer_th.comm_fds); +#if USE_MN_THREADS + // The parent's waiters do not exist in the child, and the armings + // belong to the closed event backend: a stale entry would satisfy + // fd_waiters_arm's want == armed_flags check and never arm the new + // backend (a lost wake the old dynamic map also had). + for (unsigned int ci = 0; ci < FDMAP_MAX_CHUNKS; ci++) { + struct rb_fd_waiters *chunk = timer_th.fdmap_chunks[ci]; + if (chunk == NULL) continue; + for (unsigned int i = 0; i < FDMAP_CHUNK_SIZE; i++) { + ccan_list_head_init(&chunk[i].waiters); + chunk[i].armed_flags = 0; + chunk[i].generation++; + } + } +#endif #if HAVE_SYS_EPOLL_H && USE_MN_THREADS close_invalidate(&timer_th.event_fd, "close event_fd"); #elif HAVE_SYS_EVENT_H && USE_MN_THREADS @@ -3561,8 +3584,11 @@ rb_thread_create_timer_thread(void) } timer_th.wheel_cursor_tick = timer_wheel_tick(rb_hrtime_now()); timer_th.next_expiry = TIMER_WHEEL_NO_EXPIRY; - ccan_list_head_init(&timer_th.waiting_untimed); rb_native_mutex_initialize(&timer_th.waiting_lock); + for (int i = 0; i < IO_WAIT_SHARDS; i++) { + rb_native_mutex_initialize(&timer_th.fd_shard_locks[i]); + } + rb_native_mutex_initialize(&timer_th.wake_pending_lock); rb_native_cond_initialize(&timer_th.wake_pending_cond); #endif diff --git a/thread_pthread.h b/thread_pthread.h index 4d34941647dd13..040ae55be4697e 100644 --- a/thread_pthread.h +++ b/thread_pthread.h @@ -95,8 +95,9 @@ struct rb_thread_sched_item { struct rb_thread_sched_waiting waiting_reason; uint32_t event_serial; - // the timer thread has a wake pending for this thread; under waiting_lock - bool wake_pending; + // wakes pending on this thread (timer thread or an fd shard claim); + // under timer_th.wake_pending_lock + uint32_t wake_pending_cnt; // parked on its own condvar with a deadline; under the sched lock (see // ubf_waiting). Always false for an M:N thread: its deadline lives on the diff --git a/thread_pthread_mn.c b/thread_pthread_mn.c index 4226a39cb045db..f2f0860f8cfdda 100644 --- a/thread_pthread_mn.c +++ b/thread_pthread_mn.c @@ -23,6 +23,23 @@ thread_sched_waiting_thread(struct rb_thread_sched_waiting *w) } } +#define FD_WAIT_IO_MASK (thread_sched_waiting_io_read | thread_sched_waiting_io_write) + +// Guards the fd map entries and the flags of io waits for its fds. Whoever +// clears an io wait's flags under its shard owns that wakeup. +// Lock order: shard -> waiting_lock -> wake_pending_lock. +static void +fd_shard_lock(int fd) +{ + rb_native_mutex_lock(&timer_th.fd_shard_locks[(unsigned int)fd % IO_WAIT_SHARDS]); +} + +static void +fd_shard_unlock(int fd) +{ + rb_native_mutex_unlock(&timer_th.fd_shard_locks[(unsigned int)fd % IO_WAIT_SHARDS]); +} + #define TIMER_WHEEL_NO_EXPIRY RB_HRTIME_MAX #ifndef TIMER_WHEEL_TICK_MS #define TIMER_WHEEL_TICK_MS 1 // L0 slot width; coarser trades sleep accuracy for fewer drains @@ -178,20 +195,13 @@ timer_wheel_drain(rb_hrtime_t now, uint64_t now_tick, struct ccan_list_head *exp struct rb_thread_sched_waiting *w; while ((w = ccan_list_pop(&pending, struct rb_thread_sched_waiting, node)) != NULL) { if (timer_thread_check_exceed(w->data.timeout, now)) { - rb_thread_t *th = thread_sched_waiting_thread(w); + RUBY_DEBUG_LOG("expired th:%u", rb_th_serial(thread_sched_waiting_thread(w))); - RUBY_DEBUG_LOG("wakeup th:%u", rb_th_serial(th)); - -#if HAVE_SYS_EPOLL_H || HAVE_SYS_EVENT_H - // An fd+timeout waiter is also on its fd's waiter list. - timer_thread_unregister_waiting(th, w->data.fd, w->flags); -#endif - /* flags stay set until the wakeup below takes them under - * the lock: a waiter whose flags are already cleared may + /* flags stay set until the wakeup takes them under the + * owning lock (the fd shard for io waits, this one + * otherwise): a waiter whose flags are already cleared may * run and re-register through this same `w`, which would * relink the node we are still holding on `expired`. */ - w->data.result = 0; - ccan_list_add_tail(expired, &w->node); } else { @@ -263,39 +273,48 @@ timer_thread_wakeup_thread(rb_thread_t *th, uint32_t event_serial) // One thread the timer thread is about to wake, with the serial it was armed at. struct timer_wake { rb_thread_t *th; uint32_t serial; }; -// Mark each thread while a wake is pending for it, so a dying thread can wait -// (timer_thread_wake_fence). Set under waiting_lock before the lock is dropped. +// Count a pending wake against a thread, so a dying thread can wait them out +// (timer_thread_wake_fence). A count, not a flag: an expiry hold and an fd +// event's wake can be pending on one thread at once. Take it while the lock +// that pinned the thread (shard or waiting_lock) is still held. static void -timer_wake_pending_set(struct timer_wake *batch, int n) +timer_wake_pending_inc(rb_thread_t *th) { - for (int i = 0; i < n; i++) { - batch[i].th->sched.wake_pending = true; - } + rb_native_mutex_lock(&timer_th.wake_pending_lock); + th->sched.wake_pending_cnt++; + rb_native_mutex_unlock(&timer_th.wake_pending_lock); +} + +static void +timer_wake_pending_dec(rb_thread_t *th) +{ + rb_native_mutex_lock(&timer_th.wake_pending_lock); + VM_ASSERT(th->sched.wake_pending_cnt > 0); + th->sched.wake_pending_cnt--; + rb_native_cond_broadcast(&timer_th.wake_pending_cond); + rb_native_mutex_unlock(&timer_th.wake_pending_lock); } static void timer_wake_pending_clear(struct timer_wake *batch, int n) { - rb_native_mutex_lock(&timer_th.waiting_lock); for (int i = 0; i < n; i++) { - batch[i].th->sched.wake_pending = false; + timer_wake_pending_dec(batch[i].th); } - rb_native_cond_broadcast(&timer_th.wake_pending_cond); - rb_native_mutex_unlock(&timer_th.waiting_lock); } -// Wait out a pending wake before a thread is freed: it would touch freed memory, -// or wake a reused thread whose first serial matches the stale entry. +// Wait out pending wakes before a thread is freed: they would touch freed +// memory, or wake a reused thread whose first serial matches a stale entry. static void timer_thread_wake_fence(rb_thread_t *th) { if (!TIMER_THREAD_CREATED_P()) return; - rb_native_mutex_lock(&timer_th.waiting_lock); - while (th->sched.wake_pending) { - rb_native_cond_wait(&timer_th.wake_pending_cond, &timer_th.waiting_lock); + rb_native_mutex_lock(&timer_th.wake_pending_lock); + while (th->sched.wake_pending_cnt > 0) { + rb_native_cond_wait(&timer_th.wake_pending_cond, &timer_th.wake_pending_lock); } - rb_native_mutex_unlock(&timer_th.waiting_lock); + rb_native_mutex_unlock(&timer_th.wake_pending_lock); } static void @@ -312,6 +331,8 @@ timer_thread_check_timeout(rb_vm_t *vm) while (more) { int n = 0; + struct { rb_thread_t *th; uint32_t serial; int fd; } io_claims[TIMEOUT_WAKE_BATCH]; + int n_io = 0; rb_native_mutex_lock(&timer_th.waiting_lock); { @@ -321,18 +342,37 @@ timer_thread_check_timeout(rb_vm_t *vm) } struct rb_thread_sched_waiting *w; - while (n < TIMEOUT_WAKE_BATCH && + while (n + n_io < TIMEOUT_WAKE_BATCH && (w = ccan_list_pop(&expired, struct rb_thread_sched_waiting, node)) != NULL) { // Name the thread and its serial here, then release it: once the // flags are clear the thread may run and re-register through `w`, // and a serial read after that would match the new registration. - batch[n].th = thread_sched_waiting_thread(w); - batch[n].serial = w->data.event_serial; - w->flags = thread_sched_waiting_none; - n++; + // the pop leaves the node dangling; a concurrent claimer's + // wheel del (under this lock) must find it self-linked + ccan_list_node_init(&w->node); + + if (w->flags & FD_WAIT_IO_MASK) { + // The fd shard owns these flags; claim below, once this + // lock is dropped. The pending count pins the thread: it + // was parked when we popped its node (a claimed entry + // leaves the wheel before its thread can wake). + io_claims[n_io].th = thread_sched_waiting_thread(w); + io_claims[n_io].serial = w->data.event_serial; + io_claims[n_io].fd = w->data.fd; + timer_wake_pending_inc(io_claims[n_io].th); + n_io++; + } + else { + // pin before the flags clear; see timer_thread_wake_fd_waiters + batch[n].th = thread_sched_waiting_thread(w); + batch[n].serial = w->data.event_serial; + timer_wake_pending_inc(batch[n].th); + w->flags = thread_sched_waiting_none; + w->data.result = 0; + n++; + } } more = !ccan_list_empty(&expired); - timer_wake_pending_set(batch, n); } rb_native_mutex_unlock(&timer_th.waiting_lock); @@ -340,26 +380,77 @@ timer_thread_check_timeout(rb_vm_t *vm) timer_thread_wakeup_thread(batch[i].th, batch[i].serial); } timer_wake_pending_clear(batch, n); + + // The io entries race the fd event and the ubf for their flags. + for (int i = 0; i < n_io; i++) { + rb_thread_t *th = io_claims[i].th; + int fd = io_claims[i].fd; + bool claimed = false; + + fd_shard_lock(fd); + { + struct rb_thread_sched_waiting *w = &th->sched.waiting_reason; + + if (w->flags != thread_sched_waiting_none && + w->data.event_serial == io_claims[i].serial) { + VM_ASSERT(w->data.fd == fd); + timer_thread_unregister_waiting(th, fd, w->flags); + w->flags = thread_sched_waiting_none; + w->data.result = 0; + claimed = true; + } + } + fd_shard_unlock(fd); + + if (claimed) { + timer_thread_wakeup_thread(th, io_claims[i].serial); + } + timer_wake_pending_dec(th); + } } } static bool timer_thread_cancel_waiting(rb_thread_t *th) { - bool canceled = false; + struct rb_thread_sched_waiting *w = &th->sched.waiting_reason; - rb_native_mutex_lock(&timer_th.waiting_lock); - { - if (th->sched.waiting_reason.flags) { - canceled = true; - timer_wheel_del(&th->sched.waiting_reason); - timer_thread_unregister_waiting(th, th->sched.waiting_reason.data.fd, th->sched.waiting_reason.flags); - th->sched.waiting_reason.flags = thread_sched_waiting_none; + while (1) { + // Racy routing read; the claim is re-verified under the owning lock. + enum thread_sched_waiting_flag flags = w->flags; + + if (flags == thread_sched_waiting_none) { + return false; + } + else if (flags & FD_WAIT_IO_MASK) { + int fd = w->data.fd; + + fd_shard_lock(fd); + if ((w->flags & FD_WAIT_IO_MASK) && w->data.fd == fd) { + if (w->flags & thread_sched_waiting_timeout) { + rb_native_mutex_lock(&timer_th.waiting_lock); + timer_wheel_del(w); + rb_native_mutex_unlock(&timer_th.waiting_lock); + } + timer_thread_unregister_waiting(th, fd, w->flags); + w->flags = thread_sched_waiting_none; + fd_shard_unlock(fd); + return true; + } + fd_shard_unlock(fd); + } + else { + rb_native_mutex_lock(&timer_th.waiting_lock); + if (w->flags && !(w->flags & FD_WAIT_IO_MASK)) { + timer_wheel_del(w); + w->flags = thread_sched_waiting_none; + rb_native_mutex_unlock(&timer_th.waiting_lock); + return true; + } + rb_native_mutex_unlock(&timer_th.waiting_lock); } + // lost the routing race; look again } - rb_native_mutex_unlock(&timer_th.waiting_lock); - - return canceled; } static void @@ -1041,52 +1132,45 @@ native_thread_create_shared(rb_thread_t *th) /// -- a reader and a writer on one socket -- so the backend is armed with their /// union, and an event is dispatched to every waiter it concerns. -#define FD_WAIT_IO_MASK (thread_sched_waiting_io_read | thread_sched_waiting_io_write) +// (FD_WAIT_IO_MASK and the fd shard helpers are defined near the top.) #define FDMAP_CHUNK_BITS 10 #define FDMAP_CHUNK_SIZE (1u << FDMAP_CHUNK_BITS) #define FDMAP_CHUNK_MASK (FDMAP_CHUNK_SIZE - 1) -// timer_th.waiting_lock must be held. +// Callable under any fd shard lock: a chunk spans every shard, so the chunk +// table is not guarded by them; slots install by CAS and are never freed. static struct rb_fd_waiters * fd_waiters_lookup(int fd, bool create) { if (fd < 0) return NULL; unsigned int ci = (unsigned int)fd >> FDMAP_CHUNK_BITS; + if (ci >= FDMAP_MAX_CHUNKS) return NULL; // the caller falls back to a blocking wait - if (ci >= timer_th.fdmap_nchunks) { - if (!create) return NULL; - - unsigned int n = timer_th.fdmap_nchunks ? timer_th.fdmap_nchunks : 8; - while (n <= ci) n *= 2; - - // Only the chunk pointers are reallocated; the entries themselves never - // move, so the list heads inside them stay valid. - struct rb_fd_waiters **chunks = realloc(timer_th.fdmap_chunks, sizeof(*chunks) * n); - if (chunks == NULL) rb_bug("fd_waiters_lookup: realloc failed"); + struct rb_fd_waiters *chunk = RUBY_ATOMIC_PTR_LOAD(timer_th.fdmap_chunks[ci]); - for (unsigned int i = timer_th.fdmap_nchunks; i < n; i++) chunks[i] = NULL; - timer_th.fdmap_chunks = chunks; - timer_th.fdmap_nchunks = n; - } - - if (timer_th.fdmap_chunks[ci] == NULL) { + if (chunk == NULL) { if (!create) return NULL; - struct rb_fd_waiters *chunk = calloc(FDMAP_CHUNK_SIZE, sizeof(*chunk)); + chunk = calloc(FDMAP_CHUNK_SIZE, sizeof(*chunk)); if (chunk == NULL) rb_bug("fd_waiters_lookup: calloc failed"); for (unsigned int i = 0; i < FDMAP_CHUNK_SIZE; i++) { ccan_list_head_init(&chunk[i].waiters); } - timer_th.fdmap_chunks[ci] = chunk; + + struct rb_fd_waiters *prev = RUBY_ATOMIC_PTR_CAS(timer_th.fdmap_chunks[ci], NULL, chunk); + if (prev != NULL) { + free(chunk); // another shard installed it first + chunk = prev; + } } - return &timer_th.fdmap_chunks[ci][(unsigned int)fd & FDMAP_CHUNK_MASK]; + return &chunk[(unsigned int)fd & FDMAP_CHUNK_MASK]; } -// timer_th.waiting_lock must be held. +// The fd's shard lock must be held. static uint32_t fd_waiters_union(struct rb_fd_waiters *e) { @@ -1112,7 +1196,7 @@ fd_event_tag(int fd, uint32_t generation) // Make the backend match `want`. Returns false if the fd cannot be registered // at all (closed, or unsupported by the backend), leaving the entry untouched. -// timer_th.waiting_lock must be held. +// The fd's shard lock must be held. static bool fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want) { @@ -1247,18 +1331,16 @@ verify_waiting_list(void) VM_ASSERT(occupied == !ccan_list_empty(&lv->slots[slot])); ccan_list_for_each(&lv->slots[slot], w, node) { - VM_ASSERT(w->flags & thread_sched_waiting_timeout); - VM_ASSERT(w->data.timeout != 0); + // an io entry's flags belong to its fd shard: do not read them here + if (!(w->flags & FD_WAIT_IO_MASK)) { + VM_ASSERT(w->flags & thread_sched_waiting_timeout); + VM_ASSERT(w->data.timeout != 0); + } VM_ASSERT(w->wheel_lvl == lvl); VM_ASSERT(w->wheel_slot == slot); } } } - - ccan_list_for_each(&timer_th.waiting_untimed, w, node) { - VM_ASSERT(!(w->flags & thread_sched_waiting_timeout)); - VM_ASSERT(w->data.timeout == 0); - } #endif } @@ -1359,66 +1441,85 @@ timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting VM_ASSERT(fd >= 0); } - rb_native_mutex_lock(&timer_th.waiting_lock); - { - if (flags & FD_WAIT_IO_MASK) { - VM_ASSERT(th != NULL); - + if (flags & FD_WAIT_IO_MASK) { + fd_shard_lock(fd); + { struct rb_fd_waiters *e = fd_waiters_lookup(fd, true); + if (e == NULL) { // fd beyond the map: fall back to a blocking wait + fd_shard_unlock(fd); + return timer_thread_unavailable; + } + // Arm the union of what this fd's waiters want, so a second waiter // on the same fd extends the arming instead of colliding with it. if (!fd_waiters_arm(fd, e, fd_waiters_union(e) | (uint32_t)(flags & FD_WAIT_IO_MASK))) { - rb_native_mutex_unlock(&timer_th.waiting_lock); + fd_shard_unlock(fd); return timer_thread_unavailable; } - ccan_list_add_tail(&e->waiters, &th->sched.waiting_reason.fd_node); - RUBY_DEBUG_LOG("armed fd:%d want:%u", fd, e->armed_flags); - } + if (th) { + ccan_list_add_tail(&e->waiters, &th->sched.waiting_reason.fd_node); - if (th) { - VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none); - - // setup waiting information - { + VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none); th->sched.waiting_reason.flags = flags; th->sched.waiting_reason.data.timeout = abs; th->sched.waiting_reason.data.fd = fd; th->sched.waiting_reason.data.result = 0; th->sched.waiting_reason.data.event_serial = event_serial; - } - if (abs == 0) { // no timeout - VM_ASSERT(!(flags & thread_sched_waiting_timeout)); - ccan_list_add_tail(&timer_th.waiting_untimed, &th->sched.waiting_reason.node); - } - else { - RUBY_DEBUG_LOG("abs:%lu", (unsigned long)abs); - VM_ASSERT(flags & thread_sched_waiting_timeout); - - rb_hrtime_t prev_expiry = timer_th.next_expiry; - timer_wheel_insert(&th->sched.waiting_reason); - - verify_waiting_list(); - - if (timer_th.next_expiry < prev_expiry) { - // an earlier deadline than the timer thread is armed for - timer_thread_wakeup_force(); + if (abs != 0) { + RUBY_DEBUG_LOG("abs:%lu", (unsigned long)abs); + VM_ASSERT(flags & thread_sched_waiting_timeout); + + rb_native_mutex_lock(&timer_th.waiting_lock); + { + rb_hrtime_t prev_expiry = timer_th.next_expiry; + timer_wheel_insert(&th->sched.waiting_reason); + verify_waiting_list(); + if (timer_th.next_expiry < prev_expiry) { + // an earlier deadline than the timer thread is armed for + timer_thread_wakeup_force(); + } + } + rb_native_mutex_unlock(&timer_th.waiting_lock); } } + RUBY_DEBUG_LOG("armed fd:%d want:%u", fd, e->armed_flags); } - else { - VM_ASSERT(abs == 0); + fd_shard_unlock(fd); + } + else if (th) { + // fd-less timed wait: the wheel lock owns it end to end + VM_ASSERT(abs != 0 && (flags & thread_sched_waiting_timeout)); + + rb_native_mutex_lock(&timer_th.waiting_lock); + { + VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none); + th->sched.waiting_reason.flags = flags; + th->sched.waiting_reason.data.timeout = abs; + th->sched.waiting_reason.data.fd = fd; + th->sched.waiting_reason.data.result = 0; + th->sched.waiting_reason.data.event_serial = event_serial; + + rb_hrtime_t prev_expiry = timer_th.next_expiry; + timer_wheel_insert(&th->sched.waiting_reason); + verify_waiting_list(); + if (timer_th.next_expiry < prev_expiry) { + timer_thread_wakeup_force(); + } } + rb_native_mutex_unlock(&timer_th.waiting_lock); + } + else { + VM_ASSERT(abs == 0); } - rb_native_mutex_unlock(&timer_th.waiting_lock); return timer_thread_registered; } // Drop `th` from its fd's waiter list and re-arm the backend for whoever is -// left. timer_th.waiting_lock must be held. +// left. The fd's shard lock must be held. static void timer_thread_unregister_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags) { @@ -1502,11 +1603,11 @@ timer_thread_wake_fd_waiters(int fd, uint32_t generation, uint32_t wake_flags, i if (wake_flags == 0) return; - for (;;) { + while (1) { int n = 0; bool more = false; - rb_native_mutex_lock(&timer_th.waiting_lock); + fd_shard_lock(fd); { struct rb_fd_waiters *e = fd_waiters_lookup(fd, false); @@ -1524,25 +1625,34 @@ timer_thread_wake_fd_waiters(int fd, uint32_t generation, uint32_t wake_flags, i } ccan_list_del_init(&w->fd_node); - timer_wheel_del(w); // also leaves the timer wheel - w->flags = thread_sched_waiting_none; - w->data.fd = -1; - w->data.result = result; + if (w->flags & thread_sched_waiting_timeout) { + // also leaves the timer wheel (or the expiry pass's + // batch, whose claim will then find the flags gone) + rb_native_mutex_lock(&timer_th.waiting_lock); + timer_wheel_del(w); + rb_native_mutex_unlock(&timer_th.waiting_lock); + } + // The pin must be visible before the flags clear: a waiter + // that sees the clear may skip parking, finish and die, + // and the fence only waits on the pending count. batch[n].th = thread_sched_waiting_thread(w); batch[n].serial = w->data.event_serial; + timer_wake_pending_inc(batch[n].th); n++; + + w->flags = thread_sched_waiting_none; + w->data.fd = -1; + w->data.result = result; } // Re-arm for whoever is still waiting on this fd (nothing, if // they all just woke up). fd_waiters_arm(fd, e, fd_waiters_union(e)); } - - timer_wake_pending_set(batch, n); } - rb_native_mutex_unlock(&timer_th.waiting_lock); + fd_shard_unlock(fd); for (int i = 0; i < n; i++) { timer_thread_wakeup_thread(batch[i].th, batch[i].serial); From 05cb652056cdb04f14901434cfecc069c3ac5683 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 21 Aug 2026 08:40:37 +0900 Subject: [PATCH 4/7] Reduce memory for proc objects We know the type of the proc object at allocation time, so we can allocate exactly the amount of memory needed for that type. We can also shrink the type from a 4 byte enum since there are only 4 types of procs. This allows us to put some flags after it, which saves us 8 bytes. This commit reduces symbol and proc procs by 24 bytes (80 bytes to 56 bytes). It also reduces iseq and ifunc procs by 8 bytes (80 bytes to 72 bytes). --- proc.c | 105 ++++++++++++------- vm.c | 42 ++++---- vm_core.h | 37 ++++++- vm_eval.c | 4 +- vm_insnhelper.c | 6 +- yjit/bindgen/src/main.rs | 1 + yjit/src/codegen.rs | 4 +- yjit/src/cruby_bindings.inc.rs | 104 +++++++++++++++---- zjit/bindgen/src/main.rs | 1 + zjit/src/codegen.rs | 4 +- zjit/src/cruby.rs | 1 + zjit/src/cruby_bindings.inc.rs | 180 ++++++++++++++++++++++++++------- zjit/src/hir.rs | 4 +- 13 files changed, 363 insertions(+), 130 deletions(-) diff --git a/proc.c b/proc.c index aeaf6de448027f..6206f2c5da668e 100644 --- a/proc.c +++ b/proc.c @@ -309,7 +309,7 @@ rb_proc_refinements_recipe(VALUE procval) { rb_proc_t *proc; GetProcPtr(procval, proc); - if (!proc->is_refined) return Qnil; + if (!proc->header.is_refined) return Qnil; return rb_ivar_get(procval, id_refinements_recipe); } @@ -319,7 +319,7 @@ rb_proc_set_refinements_recipe(VALUE procval, VALUE recipe) rb_proc_t *proc; GetProcPtr(procval, proc); rb_ivar_set(procval, id_refinements_recipe, recipe); - proc->is_refined = 1; + proc->header.is_refined = 1; } typedef struct { @@ -331,9 +331,19 @@ static size_t proc_memsize(const void *ptr) { const rb_proc_t *proc = ptr; - if (proc->block.as.captured.ep == ((const cfunc_proc_t *)ptr)->env+1) - return sizeof(cfunc_proc_t); - return sizeof(rb_proc_t); + switch (proc->block.type) { + case block_type_iseq: + case block_type_ifunc: + if (proc->block.as.captured.ep == ((const cfunc_proc_t *)ptr)->env+1) + return sizeof(cfunc_proc_t); + return sizeof(rb_proc_captured_t); + case block_type_symbol: + return sizeof(rb_proc_symbol_t); + case block_type_proc: + return sizeof(rb_proc_proc_t); + } + VM_UNREACHABLE(proc_memsize); + return 0; } const rb_data_type_t ruby_proc_data_type = { @@ -350,10 +360,26 @@ const rb_data_type_t ruby_proc_data_type = { #define proc_data_type ruby_proc_data_type VALUE -rb_proc_alloc(VALUE klass) +rb_proc_alloc(VALUE klass, enum rb_block_type block_type) { - rb_proc_t *proc; - return TypedData_Make_Struct(klass, rb_proc_t, &proc_data_type, proc); + size_t size; + switch (block_type) { + case block_type_symbol: + size = sizeof(rb_proc_symbol_t); + break; + case block_type_proc: + size = sizeof(rb_proc_proc_t); + break; + case block_type_iseq: + case block_type_ifunc: + size = sizeof(rb_proc_captured_t); + break; + default: + VM_UNREACHABLE(rb_proc_alloc); + return Qundef; + } + + return rb_data_typed_object_zalloc(klass, size, &proc_data_type); } VALUE @@ -559,7 +585,7 @@ rb_proc_refinements_cref_for_call(VALUE procval) { rb_proc_t *proc; GetProcPtr(procval, proc); - if (!proc->is_refined) return NULL; + if (!proc->header.is_refined) return NULL; refinement_iseq_ensure(procval, proc); VALUE recipe = rb_ivar_get(procval, id_refinements_recipe); @@ -629,7 +655,7 @@ proc_refined(int argc, VALUE *argv, VALUE self) return self; } - if (vm_block_type(&src->block) != block_type_iseq || src->is_from_method) { + if (vm_block_type(&src->block) != block_type_iseq || src->header.is_from_method) { rb_raise(rb_eArgError, "can't apply refinements to a Proc without a Ruby block"); } @@ -798,7 +824,7 @@ rb_proc_lambda_p(VALUE procval) rb_proc_t *proc; GetProcPtr(procval, proc); - return RBOOL(proc->is_lambda); + return RBOOL(proc->header.is_lambda); } /* Binding */ @@ -1396,7 +1422,7 @@ cfunc_proc_new(VALUE klass, VALUE ifunc) /* self? */ RB_OBJ_WRITE(procval, &proc->block.as.captured.code.ifunc, ifunc); - proc->is_lambda = TRUE; + proc->header.is_lambda = TRUE; return procval; } @@ -1429,13 +1455,13 @@ rb_func_proc_dup(VALUE src_obj) static VALUE sym_proc_new(VALUE klass, VALUE sym) { - VALUE procval = rb_proc_alloc(klass); + VALUE procval = rb_proc_alloc(klass, block_type_symbol); rb_proc_t *proc; GetProcPtr(procval, proc); vm_block_type_set(&proc->block, block_type_symbol); - proc->is_lambda = TRUE; - RB_OBJ_WRITE(procval, &proc->block.as.symbol, sym); + proc->header.is_lambda = TRUE; + RB_OBJ_WRITE(procval, &proc->symbol.symbol, sym); return procval; } @@ -1847,7 +1873,7 @@ rb_proc_arity(VALUE self) int max, min; GetProcPtr(self, proc); min = rb_vm_block_min_max_arity(&proc->block, &max); - return (proc->is_lambda ? min == max : max != UNLIMITED_ARGUMENTS) ? min : -min-1; + return (proc->header.is_lambda ? min == max : max != UNLIMITED_ARGUMENTS) ? min : -min-1; } static void @@ -1897,7 +1923,7 @@ rb_block_pair_yield_optimizable(void) VALUE procval = block_handler; rb_proc_t *proc; GetProcPtr(procval, proc); - if (proc->is_lambda) return 0; + if (proc->header.is_lambda) return 0; if (min != max) return 0; return min > 1; } @@ -1965,7 +1991,7 @@ rb_proc_get_iseq(VALUE self, int *is_proc) GetProcPtr(self, proc); block = &proc->block; - if (is_proc) *is_proc = !proc->is_lambda; + if (is_proc) *is_proc = !proc->header.is_lambda; switch (vm_block_type(block)) { case block_type_iseq: @@ -2031,9 +2057,9 @@ proc_eq(VALUE self, VALUE other) GetProcPtr(self, self_proc); GetProcPtr(other, other_proc); - if (self_proc->is_from_method != other_proc->is_from_method || - self_proc->is_lambda != other_proc->is_lambda || - self_proc->is_refined != other_proc->is_refined) { + if (self_proc->header.is_from_method != other_proc->header.is_from_method || + self_proc->header.is_lambda != other_proc->header.is_lambda || + self_proc->header.is_refined != other_proc->header.is_refined) { return Qfalse; } @@ -2052,7 +2078,7 @@ proc_eq(VALUE self, VALUE other) } /* a refined Proc's block iseq flips from the source to the copy on * the first call; compare what the Procs were built from instead */ - if (self_proc->is_refined) { + if (self_proc->header.is_refined) { if (!refinement_recipe_eq(rb_proc_refinements_recipe(self), rb_proc_refinements_recipe(other))) { return Qfalse; @@ -2232,7 +2258,7 @@ rb_hash_proc(st_index_t hash, VALUE prc) switch (vm_block_type(&proc->block)) { case block_type_iseq: - if (proc->is_refined) { + if (proc->header.is_refined) { /* from the recipe, not the block iseq: the latter flips from the * source to the copy on the first call, and the hash must not */ VALUE recipe = rb_proc_refinements_recipe(prc); @@ -2263,8 +2289,9 @@ rb_hash_proc(st_index_t hash, VALUE prc) /* ifunc procs have their own allocated ep. If an ifunc is duplicated, they * will point to different ep but they should return the same hash code, so - * we cannot include the ep in the hash. */ - if (vm_block_type(&proc->block) != block_type_ifunc) { + * we cannot include the ep in the hash. Symbol and proc type blocks are + * smaller and do not have an ep at all. */ + if (vm_block_type(&proc->block) == block_type_iseq) { hash = rb_hash_uint(hash, (st_index_t)proc->block.as.captured.ep); } @@ -2384,7 +2411,7 @@ proc_to_s(VALUE self) { const rb_proc_t *proc; GetProcPtr(self, proc); - return rb_block_to_s(self, &proc->block, proc->is_lambda ? " (lambda)" : NULL); + return rb_block_to_s(self, &proc->block, proc->header.is_lambda ? " (lambda)" : NULL); } /* @@ -3098,7 +3125,7 @@ rb_mod_define_method_with_visibility(int argc, VALUE *argv, VALUE mod, const str GetProcPtr(body, body_proc); /* A bmethod never reads the refinement cref carried on the proc; * reject rather than silently drop the refinements. */ - if (body_proc->is_refined) { + if (body_proc->header.is_refined) { rb_raise(rb_eArgError, "can't define a method from a Proc with refinements"); } @@ -3106,8 +3133,8 @@ rb_mod_define_method_with_visibility(int argc, VALUE *argv, VALUE mod, const str if (vm_proc_iseq(procval) != NULL) { rb_proc_t *proc; GetProcPtr(procval, proc); - proc->is_lambda = TRUE; - proc->is_from_method = TRUE; + proc->header.is_lambda = TRUE; + proc->header.is_from_method = TRUE; } rb_add_method(mod, id, VM_METHOD_TYPE_BMETHOD, (void *)procval, scope_visi->method_visi); if (scope_visi->module_func) { @@ -4231,7 +4258,7 @@ method_to_proc(VALUE method) */ procval = rb_block_call(rb_mRubyVMFrozenCore, idLambda, 0, 0, bmcall, method); GetProcPtr(procval, proc); - proc->is_from_method = 1; + proc->header.is_from_method = 1; return procval; } @@ -4421,7 +4448,7 @@ proc_binding(VALUE self) GetProcPtr(self, proc); block = &proc->block; - if (proc->is_isolated) rb_raise(rb_eArgError, "Can't create Binding from isolated Proc"); + if (proc->header.is_isolated) rb_raise(rb_eArgError, "Can't create Binding from isolated Proc"); again: switch (vm_block_type(block)) { @@ -4488,12 +4515,12 @@ make_curry_proc(VALUE proc, VALUE passed, VALUE arity) int is_lambda; GetProcPtr(proc, procp); - is_lambda = procp->is_lambda; + is_lambda = procp->header.is_lambda; rb_ary_freeze(passed); rb_ary_freeze(args); proc = rb_proc_new(curry, args); GetProcPtr(proc, procp); - procp->is_lambda = is_lambda; + procp->header.is_lambda = is_lambda; return proc; } @@ -4698,7 +4725,7 @@ rb_proc_compose_to_left(VALUE self, VALUE g) if (rb_obj_is_proc(g)) { GetProcPtr(g, procp); - is_lambda = procp->is_lambda; + is_lambda = procp->header.is_lambda; } else { VM_ASSERT(rb_obj_is_method(g) || rb_obj_respond_to(g, idCall, TRUE)); @@ -4707,7 +4734,7 @@ rb_proc_compose_to_left(VALUE self, VALUE g) proc = rb_proc_new(compose, args); GetProcPtr(proc, procp); - procp->is_lambda = is_lambda; + procp->header.is_lambda = is_lambda; return proc; } @@ -4756,11 +4783,11 @@ rb_proc_compose_to_right(VALUE self, VALUE g) args = rb_ary_tmp_new_from_values(0, 2, procs); GetProcPtr(self, procp); - is_lambda = procp->is_lambda; + is_lambda = procp->header.is_lambda; proc = rb_proc_new(compose, args); GetProcPtr(proc, procp); - procp->is_lambda = is_lambda; + procp->header.is_lambda = is_lambda; return proc; } @@ -4843,7 +4870,7 @@ proc_ruby2_keywords(VALUE procval) rb_check_frozen(procval); - if (proc->is_from_method) { + if (proc->header.is_from_method) { rb_warn("Skipping set of ruby2_keywords flag for proc (proc created from method)"); return procval; } @@ -4854,7 +4881,7 @@ proc_ruby2_keywords(VALUE procval) !ISEQ_BODY(proc->block.as.captured.code.iseq)->param.flags.has_post && !ISEQ_BODY(proc->block.as.captured.code.iseq)->param.flags.has_kw && !ISEQ_BODY(proc->block.as.captured.code.iseq)->param.flags.has_kwrest) { - if (proc->is_refined) { + if (proc->header.is_refined) { /* on a copy of this Proc's own: the block is shared with the * source Proc until the first call, and the copy installed by * it may be memoized and shared with sibling Procs */ diff --git a/vm.c b/vm.c index 749fe5cc1a760c..f0bebc00bd7ada 100644 --- a/vm.c +++ b/vm.c @@ -1301,7 +1301,7 @@ vm_proc_create_from_captured(VALUE klass, enum rb_block_type block_type, int8_t is_from_method, int8_t is_lambda) { - VALUE procval = rb_proc_alloc(klass); + VALUE procval = rb_proc_alloc(klass, block_type); rb_proc_t *proc = RTYPEDDATA_DATA(procval); VM_ASSERT(VM_EP_IN_HEAP_P(GET_EC(), captured->ep)); @@ -1312,8 +1312,8 @@ vm_proc_create_from_captured(VALUE klass, rb_vm_block_ep_update(procval, &proc->block, captured->ep); vm_block_type_set(&proc->block, block_type); - proc->is_from_method = is_from_method; - proc->is_lambda = is_lambda; + proc->header.is_from_method = is_from_method; + proc->header.is_lambda = is_lambda; return procval; } @@ -1341,14 +1341,14 @@ rb_vm_block_copy(VALUE obj, const struct rb_block *dst, const struct rb_block *s static VALUE proc_create(VALUE klass, const struct rb_block *block, int8_t is_from_method, int8_t is_lambda) { - VALUE procval = rb_proc_alloc(klass); + VALUE procval = rb_proc_alloc(klass, block->type); rb_proc_t *proc = RTYPEDDATA_DATA(procval); VM_ASSERT(VM_EP_IN_HEAP_P(GET_EC(), vm_block_ep(block))); rb_vm_block_copy(procval, &proc->block, block); vm_block_type_set(&proc->block, block->type); - proc->is_from_method = is_from_method; - proc->is_lambda = is_lambda; + proc->header.is_from_method = is_from_method; + proc->header.is_lambda = is_lambda; return procval; } @@ -1366,14 +1366,14 @@ rb_proc_dup_0(VALUE self) procval = rb_func_proc_dup(self); break; default: - procval = proc_create(rb_obj_class(self), &src->block, src->is_from_method, src->is_lambda); + procval = proc_create(rb_obj_class(self), &src->block, src->header.is_from_method, src->header.is_lambda); break; } - if (src->is_refined) { + if (src->header.is_refined) { rb_proc_t *dst; GetProcPtr(procval, dst); - dst->is_refined = 1; + dst->header.is_refined = 1; } if (RB_OBJ_SHAREABLE_P(self)) RB_OBJ_SET_SHAREABLE(procval); @@ -1403,7 +1403,7 @@ rb_proc_dup_with_iseq_and_recipe(VALUE self, const rb_iseq_t *iseq, VALUE recipe struct rb_block block = src->block; block.as.captured.code.iseq = iseq; - VALUE procval = proc_create(rb_obj_class(self), &block, src->is_from_method, src->is_lambda); + VALUE procval = proc_create(rb_obj_class(self), &block, src->header.is_from_method, src->header.is_lambda); rb_proc_set_refinements_recipe(procval, recipe); RB_GC_GUARD(self); @@ -1587,19 +1587,19 @@ rb_proc_isolate_bang(VALUE self, VALUE replace_self) if (iseq) { rb_proc_t *proc = (rb_proc_t *)RTYPEDDATA_DATA(self); + if (proc->block.type != block_type_iseq) rb_raise(rb_eRuntimeError, "not supported yet"); + 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) { proc_shared_outer_variables(ISEQ_BODY(iseq)->outer_variables, true, "isolate a Proc"); } proc_isolate_env(self, proc, Qfalse); - proc->is_isolated = TRUE; + proc->header.is_isolated = TRUE; RB_OBJ_WRITE(self, &proc->block.as.captured.self, Qnil); } @@ -1623,12 +1623,12 @@ rb_proc_ractor_make_shareable(VALUE self, VALUE replace_self) if (iseq) { rb_proc_t *proc = (rb_proc_t *)RTYPEDDATA_DATA(self); + if (proc->block.type != block_type_iseq) rb_raise(rb_eRuntimeError, "not supported yet"); + 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))) { rb_raise(rb_eRactorIsolationError, "Proc's self is not shareable: %" PRIsVALUE, @@ -1643,7 +1643,7 @@ rb_proc_ractor_make_shareable(VALUE self, VALUE replace_self) } proc_isolate_env(self, proc, read_only_variables); - proc->is_isolated = TRUE; + proc->header.is_isolated = TRUE; } else { const struct rb_block *block = vm_proc_block(self); @@ -1896,9 +1896,9 @@ invoke_block_from_c_bh(rb_execution_context_t *ec, VALUE block_handler, VALUE procval = VM_BH_TO_PROC(block_handler); rb_proc_t *po; GetProcPtr(procval, po); - if (po->is_refined) cref = rb_proc_refinements_cref_for_call(procval); + if (po->header.is_refined) cref = rb_proc_refinements_cref_for_call(procval); if (force_blockarg == FALSE) { - is_lambda = po->is_lambda; + is_lambda = po->header.is_lambda; } block_handler = vm_block_to_block_handler(&po->block); goto again; @@ -1999,7 +1999,7 @@ vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, const rb_cref_t *cref) { - return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, proc->is_lambda, cref, NULL); + return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, proc->header.is_lambda, cref, NULL); } static VALUE @@ -2018,7 +2018,7 @@ rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self = vm_block_self(&proc->block); vm_block_handler_verify(passed_block_handler); - if (proc->is_from_method) { + if (proc->header.is_from_method) { return vm_invoke_bmethod(ec, proc, self, argc, argv, kw_splat, passed_block_handler, NULL); } else { @@ -2033,7 +2033,7 @@ rb_vm_invoke_proc_with_self(rb_execution_context_t *ec, rb_proc_t *proc, VALUE s { vm_block_handler_verify(passed_block_handler); - if (proc->is_from_method) { + if (proc->header.is_from_method) { return vm_invoke_bmethod(ec, proc, self, argc, argv, kw_splat, passed_block_handler, NULL); } else { diff --git a/vm_core.h b/vm_core.h index e867230f66d475..3dbfe7442a7f1d 100644 --- a/vm_core.h +++ b/vm_core.h @@ -995,12 +995,12 @@ enum rb_block_type { }; struct rb_block { + enum rb_block_type type : 8; union { struct rb_captured_block captured; VALUE symbol; VALUE proc; } as; - enum rb_block_type type; }; typedef struct rb_control_frame_struct { @@ -1398,13 +1398,44 @@ extern const rb_data_type_t ruby_proc_data_type; GetCoreDataFromValue((obj), rb_proc_t, &ruby_proc_data_type, (ptr)) typedef struct { - const struct rb_block block; + enum rb_block_type type : 8; unsigned int is_from_method: 1; /* bool */ unsigned int is_lambda: 1; /* bool */ unsigned int is_isolated: 1; /* bool */ unsigned int is_refined: 1; /* bool: Proc#refined */ +} rb_proc_header_t; + +typedef struct { + rb_proc_header_t header; + struct rb_captured_block captured; +} rb_proc_captured_t; + +typedef struct { + rb_proc_header_t header; + VALUE symbol; +} rb_proc_symbol_t; + +typedef struct { + rb_proc_header_t header; + VALUE proc; +} rb_proc_proc_t; + +/* A Proc of any block type. */ +typedef union { + const struct rb_block block; + rb_proc_header_t header; + rb_proc_captured_t captured; + rb_proc_symbol_t symbol; + rb_proc_proc_t proc; } rb_proc_t; +STATIC_ASSERT(rb_proc_captured_offset, + offsetof(rb_proc_captured_t, captured) == offsetof(rb_proc_t, block.as.captured)); +STATIC_ASSERT(rb_proc_symbol_offset, + offsetof(rb_proc_symbol_t, symbol) == offsetof(rb_proc_t, block.as.symbol)); +STATIC_ASSERT(rb_proc_proc_offset, + offsetof(rb_proc_proc_t, proc) == offsetof(rb_proc_t, block.as.proc)); + /* A refined proc's refinements recipe (see Proc#refined) lives in a hidden * ivar on the proc object; the accessors return nil/NULL unless is_refined is * set. rb_proc_refinements_cref_for_call also makes the copy of the block @@ -2046,7 +2077,7 @@ VM_BH_FROM_PROC(VALUE procval) /* VM related object allocate functions */ VALUE rb_thread_alloc(VALUE klass); VALUE rb_binding_alloc(VALUE klass); -VALUE rb_proc_alloc(VALUE klass); +VALUE rb_proc_alloc(VALUE klass, enum rb_block_type block_type); VALUE rb_proc_dup(VALUE self); VALUE rb_proc_dup_0(VALUE self); diff --git a/vm_eval.c b/vm_eval.c index 372a9549d58e42..eccf3bf8256909 100644 --- a/vm_eval.c +++ b/vm_eval.c @@ -2239,8 +2239,8 @@ yield_under(VALUE self, int singleton, int argc, const VALUE *argv, int kw_splat VALUE procval = VM_BH_TO_PROC(block_handler); rb_proc_t *po; GetProcPtr(procval, po); - is_lambda = po->is_lambda; - if (po->is_refined) proc_cref = rb_proc_refinements_cref_for_call(procval); + is_lambda = po->header.is_lambda; + if (po->header.is_refined) proc_cref = rb_proc_refinements_cref_for_call(procval); block_handler = vm_block_to_block_handler(&po->block); } goto again; diff --git a/vm_insnhelper.c b/vm_insnhelper.c index ce8e21d0fde2ff..15e21792c36a60 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -5205,7 +5205,7 @@ block_proc_is_lambda(const VALUE procval) if (procval) { GetProcPtr(procval, proc); - return proc->is_lambda; + return proc->header.is_lambda; } else { return 0; @@ -5497,8 +5497,8 @@ vm_invoke_proc_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, VALUE procval = VM_BH_TO_PROC(block_handler); rb_proc_t *po; GetProcPtr(procval, po); - if (po->is_refined) refined_procval = procval; - is_lambda = po->is_lambda; + if (po->header.is_refined) refined_procval = procval; + is_lambda = po->header.is_lambda; block_handler = vm_block_to_block_handler(&po->block); } diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index 93dc9b4be6a5b4..1b1a833eed9216 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -266,6 +266,7 @@ fn main() { .allowlist_function("rb_RSTRING_LEN") .allowlist_function("rb_ENCODING_GET") .allowlist_function("rb_jit_get_proc_ptr") + .allowlist_type("rb_block_type") .allowlist_function("rb_yjit_exit_locations_dict") .allowlist_function("rb_jit_icache_invalidate") .allowlist_function("rb_optimized_call") diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 2fefb0319406bc..9483b715a69671 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -7450,9 +7450,9 @@ fn gen_send_bmethod( let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) }; let proc = unsafe { rb_jit_get_proc_ptr(procv) }; - let proc_block = unsafe { &(*proc).block }; + let proc_block = unsafe { (*proc).block.as_ref() }; - if proc_block.type_ != block_type_iseq { + if proc_block.type_() != block_type_iseq { return None; } diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 75ff2642244b79..e00aec76e277e9 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -517,8 +517,9 @@ pub const block_type_proc: rb_block_type = 3; pub type rb_block_type = u32; #[repr(C)] pub struct rb_block { + pub _bitfield_align_1: [u8; 0], + pub _bitfield_1: __BindgenBitfieldUnit<[u8; 1usize]>, pub as_: rb_block__bindgen_ty_1, - pub type_: rb_block_type, } #[repr(C)] pub struct rb_block__bindgen_ty_1 { @@ -527,86 +528,149 @@ pub struct rb_block__bindgen_ty_1 { pub proc_: __BindgenUnionField, pub bindgen_union_field: [u64; 3usize], } +impl rb_block { + #[inline] + pub fn type_(&self) -> rb_block_type { + unsafe { ::std::mem::transmute(self._bitfield_1.get(0usize, 8u8) as u32) } + } + #[inline] + pub fn set_type(&mut self, val: rb_block_type) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + self._bitfield_1.set(0usize, 8u8, val as u64) + } + } + #[inline] + pub fn new_bitfield_1(type_: rb_block_type) -> __BindgenBitfieldUnit<[u8; 1usize]> { + let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 1usize]> = Default::default(); + __bindgen_bitfield_unit.set(0usize, 8u8, { + let type_: u32 = unsafe { ::std::mem::transmute(type_) }; + type_ as u64 + }); + __bindgen_bitfield_unit + } +} pub type rb_control_frame_t = rb_control_frame_struct; #[repr(C)] -pub struct rb_proc_t { - pub block: rb_block, +#[repr(align(4))] +#[derive(Debug, Copy, Clone)] +pub struct rb_proc_header_t { pub _bitfield_align_1: [u8; 0], - pub _bitfield_1: __BindgenBitfieldUnit<[u8; 1usize]>, - pub __bindgen_padding_0: [u8; 7usize], + pub _bitfield_1: __BindgenBitfieldUnit<[u8; 2usize]>, + pub __bindgen_padding_0: u16, } -impl rb_proc_t { +impl rb_proc_header_t { + #[inline] + pub fn type_(&self) -> rb_block_type { + unsafe { ::std::mem::transmute(self._bitfield_1.get(0usize, 8u8) as u32) } + } + #[inline] + pub fn set_type(&mut self, val: rb_block_type) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + self._bitfield_1.set(0usize, 8u8, val as u64) + } + } #[inline] pub fn is_from_method(&self) -> ::std::os::raw::c_uint { - unsafe { ::std::mem::transmute(self._bitfield_1.get(0usize, 1u8) as u32) } + unsafe { ::std::mem::transmute(self._bitfield_1.get(8usize, 1u8) as u32) } } #[inline] pub fn set_is_from_method(&mut self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - self._bitfield_1.set(0usize, 1u8, val as u64) + self._bitfield_1.set(8usize, 1u8, val as u64) } } #[inline] pub fn is_lambda(&self) -> ::std::os::raw::c_uint { - unsafe { ::std::mem::transmute(self._bitfield_1.get(1usize, 1u8) as u32) } + unsafe { ::std::mem::transmute(self._bitfield_1.get(9usize, 1u8) as u32) } } #[inline] pub fn set_is_lambda(&mut self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - self._bitfield_1.set(1usize, 1u8, val as u64) + self._bitfield_1.set(9usize, 1u8, val as u64) } } #[inline] pub fn is_isolated(&self) -> ::std::os::raw::c_uint { - unsafe { ::std::mem::transmute(self._bitfield_1.get(2usize, 1u8) as u32) } + unsafe { ::std::mem::transmute(self._bitfield_1.get(10usize, 1u8) as u32) } } #[inline] pub fn set_is_isolated(&mut self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - self._bitfield_1.set(2usize, 1u8, val as u64) + self._bitfield_1.set(10usize, 1u8, val as u64) } } #[inline] pub fn is_refined(&self) -> ::std::os::raw::c_uint { - unsafe { ::std::mem::transmute(self._bitfield_1.get(3usize, 1u8) as u32) } + unsafe { ::std::mem::transmute(self._bitfield_1.get(11usize, 1u8) as u32) } } #[inline] pub fn set_is_refined(&mut self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - self._bitfield_1.set(3usize, 1u8, val as u64) + self._bitfield_1.set(11usize, 1u8, val as u64) } } #[inline] pub fn new_bitfield_1( + type_: rb_block_type, is_from_method: ::std::os::raw::c_uint, is_lambda: ::std::os::raw::c_uint, is_isolated: ::std::os::raw::c_uint, is_refined: ::std::os::raw::c_uint, - ) -> __BindgenBitfieldUnit<[u8; 1usize]> { - let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 1usize]> = Default::default(); - __bindgen_bitfield_unit.set(0usize, 1u8, { + ) -> __BindgenBitfieldUnit<[u8; 2usize]> { + let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 2usize]> = Default::default(); + __bindgen_bitfield_unit.set(0usize, 8u8, { + let type_: u32 = unsafe { ::std::mem::transmute(type_) }; + type_ as u64 + }); + __bindgen_bitfield_unit.set(8usize, 1u8, { let is_from_method: u32 = unsafe { ::std::mem::transmute(is_from_method) }; is_from_method as u64 }); - __bindgen_bitfield_unit.set(1usize, 1u8, { + __bindgen_bitfield_unit.set(9usize, 1u8, { let is_lambda: u32 = unsafe { ::std::mem::transmute(is_lambda) }; is_lambda as u64 }); - __bindgen_bitfield_unit.set(2usize, 1u8, { + __bindgen_bitfield_unit.set(10usize, 1u8, { let is_isolated: u32 = unsafe { ::std::mem::transmute(is_isolated) }; is_isolated as u64 }); - __bindgen_bitfield_unit.set(3usize, 1u8, { + __bindgen_bitfield_unit.set(11usize, 1u8, { let is_refined: u32 = unsafe { ::std::mem::transmute(is_refined) }; is_refined as u64 }); __bindgen_bitfield_unit } } +#[repr(C)] +pub struct rb_proc_captured_t { + pub header: rb_proc_header_t, + pub captured: rb_captured_block, +} +#[repr(C)] +pub struct rb_proc_symbol_t { + pub header: rb_proc_header_t, + pub symbol: VALUE, +} +#[repr(C)] +pub struct rb_proc_proc_t { + pub header: rb_proc_header_t, + pub proc_: VALUE, +} +#[repr(C)] +pub struct rb_proc_t { + pub block: __BindgenUnionField, + pub header: __BindgenUnionField, + pub captured: __BindgenUnionField, + pub symbol: __BindgenUnionField, + pub proc_: __BindgenUnionField, + pub bindgen_union_field: [u64; 4usize], +} pub const VM_CHECKMATCH_TYPE_WHEN: vm_check_match_type = 1; pub const VM_CHECKMATCH_TYPE_CASE: vm_check_match_type = 2; pub const VM_CHECKMATCH_TYPE_RESCUE: vm_check_match_type = 3; diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 773c24aff45d04..ceafd6bbb0ddbf 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -422,6 +422,7 @@ fn main() { .allowlist_function("rb_get_def_iseq_ptr") .allowlist_function("rb_get_def_bmethod_proc") .allowlist_function("rb_jit_get_proc_ptr") + .allowlist_type("rb_block_type") .allowlist_function("rb_iseq_encoded_size") .allowlist_function("rb_get_iseq_body_total_calls") .allowlist_function("rb_get_iseq_body_local_iseq") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index e39822242b523d..170f597735fbe8 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1656,7 +1656,7 @@ fn gen_push_inline_frame( // Extract EP from the Proc instance let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) }; let proc = unsafe { rb_jit_get_proc_ptr(procv) }; - let proc_block = unsafe { &(*proc).block }; + let proc_block = unsafe { (*proc).block.as_ref() }; let capture = unsafe { proc_block.as_.captured.as_ref() }; let bmethod_frame_type = VM_FRAME_MAGIC_BLOCK | VM_FRAME_FLAG_BMETHOD | VM_FRAME_FLAG_LAMBDA; // Tag the captured EP like VM_GUARDED_PREV_EP() in vm_call_iseq_bmethod() @@ -1796,7 +1796,7 @@ fn gen_send_iseq_direct( // Extract EP from the Proc instance let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) }; let proc = unsafe { rb_jit_get_proc_ptr(procv) }; - let proc_block = unsafe { &(*proc).block }; + let proc_block = unsafe { (*proc).block.as_ref() }; let capture = unsafe { proc_block.as_.captured.as_ref() }; let bmethod_frame_type = VM_FRAME_MAGIC_BLOCK | VM_FRAME_FLAG_BMETHOD | VM_FRAME_FLAG_LAMBDA; // Tag the captured EP like VM_GUARDED_PREV_EP() in vm_call_iseq_bmethod() diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index c2789778fbffac..dfa994c02c5dac 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -102,6 +102,7 @@ pub type RedefinitionFlag = u32; #[allow(unsafe_op_in_unsafe_fn)] #[allow(dead_code)] +#[allow(non_snake_case)] // bindgen names bitfield raw accessors like `type__raw` #[allow(clippy::all)] // warning meant to help with reading; not useful for generated code mod autogened { use super::*; diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 08d4181f77e1f6..437121381c7009 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1326,8 +1326,9 @@ pub const block_type_proc: rb_block_type = 3; pub type rb_block_type = u32; #[repr(C)] pub struct rb_block { + pub _bitfield_align_1: [u8; 0], + pub _bitfield_1: __BindgenBitfieldUnit<[u8; 1usize]>, pub as_: rb_block__bindgen_ty_1, - pub type_: rb_block_type, } #[repr(C)] pub struct rb_block__bindgen_ty_1 { @@ -1336,6 +1337,50 @@ pub struct rb_block__bindgen_ty_1 { pub proc_: __BindgenUnionField, pub bindgen_union_field: [u64; 3usize], } +impl rb_block { + #[inline] + pub fn type_(&self) -> rb_block_type { + unsafe { ::std::mem::transmute(self._bitfield_1.get(0usize, 8u8) as u32) } + } + #[inline] + pub fn set_type(&mut self, val: rb_block_type) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + self._bitfield_1.set(0usize, 8u8, val as u64) + } + } + #[inline] + pub unsafe fn type__raw(this: *const Self) -> rb_block_type { + unsafe { + ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 1usize]>>::raw_get( + ::std::ptr::addr_of!((*this)._bitfield_1), + 0usize, + 8u8, + ) as u32) + } + } + #[inline] + pub unsafe fn set_type_raw(this: *mut Self, val: rb_block_type) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + <__BindgenBitfieldUnit<[u8; 1usize]>>::raw_set( + ::std::ptr::addr_of_mut!((*this)._bitfield_1), + 0usize, + 8u8, + val as u64, + ) + } + } + #[inline] + pub fn new_bitfield_1(type_: rb_block_type) -> __BindgenBitfieldUnit<[u8; 1usize]> { + let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 1usize]> = Default::default(); + __bindgen_bitfield_unit.set(0usize, 8u8, { + let type_: u32 = unsafe { ::std::mem::transmute(type_) }; + type_ as u64 + }); + __bindgen_bitfield_unit + } +} #[repr(C)] pub struct rb_control_frame_struct { pub pc: *const VALUE, @@ -1348,30 +1393,64 @@ pub struct rb_control_frame_struct { } pub type rb_control_frame_t = rb_control_frame_struct; #[repr(C)] -pub struct rb_proc_t { - pub block: rb_block, +#[repr(align(4))] +#[derive(Debug, Copy, Clone)] +pub struct rb_proc_header_t { pub _bitfield_align_1: [u8; 0], - pub _bitfield_1: __BindgenBitfieldUnit<[u8; 1usize]>, - pub __bindgen_padding_0: [u8; 7usize], + pub _bitfield_1: __BindgenBitfieldUnit<[u8; 2usize]>, + pub __bindgen_padding_0: u16, } -impl rb_proc_t { +impl rb_proc_header_t { + #[inline] + pub fn type_(&self) -> rb_block_type { + unsafe { ::std::mem::transmute(self._bitfield_1.get(0usize, 8u8) as u32) } + } + #[inline] + pub fn set_type(&mut self, val: rb_block_type) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + self._bitfield_1.set(0usize, 8u8, val as u64) + } + } + #[inline] + pub unsafe fn type__raw(this: *const Self) -> rb_block_type { + unsafe { + ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 2usize]>>::raw_get( + ::std::ptr::addr_of!((*this)._bitfield_1), + 0usize, + 8u8, + ) as u32) + } + } + #[inline] + pub unsafe fn set_type_raw(this: *mut Self, val: rb_block_type) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + <__BindgenBitfieldUnit<[u8; 2usize]>>::raw_set( + ::std::ptr::addr_of_mut!((*this)._bitfield_1), + 0usize, + 8u8, + val as u64, + ) + } + } #[inline] pub fn is_from_method(&self) -> ::std::os::raw::c_uint { - unsafe { ::std::mem::transmute(self._bitfield_1.get(0usize, 1u8) as u32) } + unsafe { ::std::mem::transmute(self._bitfield_1.get(8usize, 1u8) as u32) } } #[inline] pub fn set_is_from_method(&mut self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - self._bitfield_1.set(0usize, 1u8, val as u64) + self._bitfield_1.set(8usize, 1u8, val as u64) } } #[inline] pub unsafe fn is_from_method_raw(this: *const Self) -> ::std::os::raw::c_uint { unsafe { - ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 1usize]>>::raw_get( + ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 2usize]>>::raw_get( ::std::ptr::addr_of!((*this)._bitfield_1), - 0usize, + 8usize, 1u8, ) as u32) } @@ -1380,9 +1459,9 @@ impl rb_proc_t { pub unsafe fn set_is_from_method_raw(this: *mut Self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - <__BindgenBitfieldUnit<[u8; 1usize]>>::raw_set( + <__BindgenBitfieldUnit<[u8; 2usize]>>::raw_set( ::std::ptr::addr_of_mut!((*this)._bitfield_1), - 0usize, + 8usize, 1u8, val as u64, ) @@ -1390,21 +1469,21 @@ impl rb_proc_t { } #[inline] pub fn is_lambda(&self) -> ::std::os::raw::c_uint { - unsafe { ::std::mem::transmute(self._bitfield_1.get(1usize, 1u8) as u32) } + unsafe { ::std::mem::transmute(self._bitfield_1.get(9usize, 1u8) as u32) } } #[inline] pub fn set_is_lambda(&mut self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - self._bitfield_1.set(1usize, 1u8, val as u64) + self._bitfield_1.set(9usize, 1u8, val as u64) } } #[inline] pub unsafe fn is_lambda_raw(this: *const Self) -> ::std::os::raw::c_uint { unsafe { - ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 1usize]>>::raw_get( + ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 2usize]>>::raw_get( ::std::ptr::addr_of!((*this)._bitfield_1), - 1usize, + 9usize, 1u8, ) as u32) } @@ -1413,9 +1492,9 @@ impl rb_proc_t { pub unsafe fn set_is_lambda_raw(this: *mut Self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - <__BindgenBitfieldUnit<[u8; 1usize]>>::raw_set( + <__BindgenBitfieldUnit<[u8; 2usize]>>::raw_set( ::std::ptr::addr_of_mut!((*this)._bitfield_1), - 1usize, + 9usize, 1u8, val as u64, ) @@ -1423,21 +1502,21 @@ impl rb_proc_t { } #[inline] pub fn is_isolated(&self) -> ::std::os::raw::c_uint { - unsafe { ::std::mem::transmute(self._bitfield_1.get(2usize, 1u8) as u32) } + unsafe { ::std::mem::transmute(self._bitfield_1.get(10usize, 1u8) as u32) } } #[inline] pub fn set_is_isolated(&mut self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - self._bitfield_1.set(2usize, 1u8, val as u64) + self._bitfield_1.set(10usize, 1u8, val as u64) } } #[inline] pub unsafe fn is_isolated_raw(this: *const Self) -> ::std::os::raw::c_uint { unsafe { - ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 1usize]>>::raw_get( + ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 2usize]>>::raw_get( ::std::ptr::addr_of!((*this)._bitfield_1), - 2usize, + 10usize, 1u8, ) as u32) } @@ -1446,9 +1525,9 @@ impl rb_proc_t { pub unsafe fn set_is_isolated_raw(this: *mut Self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - <__BindgenBitfieldUnit<[u8; 1usize]>>::raw_set( + <__BindgenBitfieldUnit<[u8; 2usize]>>::raw_set( ::std::ptr::addr_of_mut!((*this)._bitfield_1), - 2usize, + 10usize, 1u8, val as u64, ) @@ -1456,21 +1535,21 @@ impl rb_proc_t { } #[inline] pub fn is_refined(&self) -> ::std::os::raw::c_uint { - unsafe { ::std::mem::transmute(self._bitfield_1.get(3usize, 1u8) as u32) } + unsafe { ::std::mem::transmute(self._bitfield_1.get(11usize, 1u8) as u32) } } #[inline] pub fn set_is_refined(&mut self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - self._bitfield_1.set(3usize, 1u8, val as u64) + self._bitfield_1.set(11usize, 1u8, val as u64) } } #[inline] pub unsafe fn is_refined_raw(this: *const Self) -> ::std::os::raw::c_uint { unsafe { - ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 1usize]>>::raw_get( + ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 2usize]>>::raw_get( ::std::ptr::addr_of!((*this)._bitfield_1), - 3usize, + 11usize, 1u8, ) as u32) } @@ -1479,9 +1558,9 @@ impl rb_proc_t { pub unsafe fn set_is_refined_raw(this: *mut Self, val: ::std::os::raw::c_uint) { unsafe { let val: u32 = ::std::mem::transmute(val); - <__BindgenBitfieldUnit<[u8; 1usize]>>::raw_set( + <__BindgenBitfieldUnit<[u8; 2usize]>>::raw_set( ::std::ptr::addr_of_mut!((*this)._bitfield_1), - 3usize, + 11usize, 1u8, val as u64, ) @@ -1489,31 +1568,60 @@ impl rb_proc_t { } #[inline] pub fn new_bitfield_1( + type_: rb_block_type, is_from_method: ::std::os::raw::c_uint, is_lambda: ::std::os::raw::c_uint, is_isolated: ::std::os::raw::c_uint, is_refined: ::std::os::raw::c_uint, - ) -> __BindgenBitfieldUnit<[u8; 1usize]> { - let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 1usize]> = Default::default(); - __bindgen_bitfield_unit.set(0usize, 1u8, { + ) -> __BindgenBitfieldUnit<[u8; 2usize]> { + let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 2usize]> = Default::default(); + __bindgen_bitfield_unit.set(0usize, 8u8, { + let type_: u32 = unsafe { ::std::mem::transmute(type_) }; + type_ as u64 + }); + __bindgen_bitfield_unit.set(8usize, 1u8, { let is_from_method: u32 = unsafe { ::std::mem::transmute(is_from_method) }; is_from_method as u64 }); - __bindgen_bitfield_unit.set(1usize, 1u8, { + __bindgen_bitfield_unit.set(9usize, 1u8, { let is_lambda: u32 = unsafe { ::std::mem::transmute(is_lambda) }; is_lambda as u64 }); - __bindgen_bitfield_unit.set(2usize, 1u8, { + __bindgen_bitfield_unit.set(10usize, 1u8, { let is_isolated: u32 = unsafe { ::std::mem::transmute(is_isolated) }; is_isolated as u64 }); - __bindgen_bitfield_unit.set(3usize, 1u8, { + __bindgen_bitfield_unit.set(11usize, 1u8, { let is_refined: u32 = unsafe { ::std::mem::transmute(is_refined) }; is_refined as u64 }); __bindgen_bitfield_unit } } +#[repr(C)] +pub struct rb_proc_captured_t { + pub header: rb_proc_header_t, + pub captured: rb_captured_block, +} +#[repr(C)] +pub struct rb_proc_symbol_t { + pub header: rb_proc_header_t, + pub symbol: VALUE, +} +#[repr(C)] +pub struct rb_proc_proc_t { + pub header: rb_proc_header_t, + pub proc_: VALUE, +} +#[repr(C)] +pub struct rb_proc_t { + pub block: __BindgenUnionField, + pub header: __BindgenUnionField, + pub captured: __BindgenUnionField, + pub symbol: __BindgenUnionField, + pub proc_: __BindgenUnionField, + pub bindgen_union_field: [u64; 4usize], +} pub const VM_CHECKMATCH_TYPE_WHEN: vm_check_match_type = 1; pub const VM_CHECKMATCH_TYPE_CASE: vm_check_match_type = 2; pub const VM_CHECKMATCH_TYPE_RESCUE: vm_check_match_type = 3; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index c9d76288204939..abe17e006691c5 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4603,10 +4603,10 @@ impl Function { } else if !has_block && def_type == VM_METHOD_TYPE_BMETHOD { let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) }; let proc = unsafe { rb_jit_get_proc_ptr(procv) }; - let proc_block = unsafe { &(*proc).block }; + let proc_block = unsafe { (*proc).block.as_ref() }; // Target ISEQ bmethods. Can't handle for example, `define_method(:foo, &:foo)` // which makes a `block_type_symbol` bmethod. - if proc_block.type_ != block_type_iseq { + if proc_block.type_() != block_type_iseq { self.set_dynamic_send_reason(insn_id, BmethodNonIseqProc); self.push_insn_id(block, insn_id); continue; } From 92bce8ad3300b3d7d4111224e6c2adfd139420c3 Mon Sep 17 00:00:00 2001 From: Issy Long Date: Sun, 23 Aug 2026 13:04:08 +0100 Subject: [PATCH 5/7] [ruby/rubygems] Use `delete_prefix` to strip the `subjectAltName` email tag - `Gem::Security::Signer#extract_name` matched `/\Aemail:/` then read the non-matching part of the string out of `$'`. - Prefer `delete_prefix("email:")` as it's understandable by mortals. - Consequently get rid of a RuboCop disable for `Performance/StartWith`. https://github.com/ruby/rubygems/commit/96978c3f4b --- lib/rubygems/security/signer.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index 044562137cb856..c90f3a93301ddc 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -109,9 +109,7 @@ def extract_name(cert) # :nodoc: subject_alt_name = cert.extensions.find {|e| e.oid == "subjectAltName" } if subject_alt_name - /\Aemail:/ =~ subject_alt_name.value # rubocop:disable Performance/StartWith - - $' || subject_alt_name.value + subject_alt_name.value.delete_prefix("email:") else cert.subject end From 446729fb319217b469e2e79da53524951aceb4c8 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sun, 23 Aug 2026 19:45:25 -0500 Subject: [PATCH 6/7] [DOC] Tweaks for File::blockdev? --- file.c | 14 ++++++++++---- pathname_builtin.rb | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/file.c b/file.c index 41f3c372cef995..9f8087eabecb58 100644 --- a/file.c +++ b/file.c @@ -1945,13 +1945,19 @@ rb_file_socket_p(VALUE obj, VALUE fname) /* * call-seq: - * File.blockdev?(filepath) -> true or false + * File.blockdev?(object) -> true or false * - * Returns +true+ if +filepath+ points to a block device, +false+ otherwise: + * Returns whether +object+ (a path or IO object) + * represents a block device (i.e., a direct-access device): * - * File.blockdev?('/dev/sda1') # => true - * File.blockdev?(File.new('t.tmp')) # => false + * File.blockdev?('/dev/nvme0n1') # => true + * File.blockdev?('/dev/loop0') # => true + * File.blockdev?('/dev/tty') # => false + * File.blockdev?('/dev/null') # => false + * File.blockdev?('nosuch') # => false + * File.blockdev?($stdin) # => false * + * The returned value is filesystem-dependent; on Windows, always +false+. */ static VALUE diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 9deb0c860ef2d6..0b3a96154acc0b 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2220,7 +2220,7 @@ class Pathname # * FileTest * # Pathname($stdin).blockdev? # => false # ``` # - # The returned value is OS-dependent; on Windows, almost always `false`. + # The returned value is filesystem-dependent; on Windows, always `false`. def blockdev?() FileTest.blockdev?(@path) end # :markup: markdown From b4809ba48784e8f079db6a10c4947d6f0a9b1d4e Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sun, 23 Aug 2026 19:45:45 -0500 Subject: [PATCH 7/7] [DOC] Tweaks for File::chardev? (#18427) --- file.c | 15 +++++++++++---- pathname_builtin.rb | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/file.c b/file.c index 9f8087eabecb58..41275eeebc0c84 100644 --- a/file.c +++ b/file.c @@ -1983,13 +1983,20 @@ rb_file_blockdev_p(VALUE obj, VALUE fname) /* * call-seq: - * File.chardev?(filepath) -> true or false + * File.chardev?(object) -> true or false * - * Returns +true+ if +filepath+ points to a character device, +false+ otherwise. + * Returns whether +object+ (a path or IO object) + * represents a character device (i.e., a sequential-access device): + * + * File.chardev?('/dev/tty') # => true + * File.chardev?('/dev/null') # => true + * File.chardev?($stdin) # => true + * File.chardev?('/dev/nvme0n1') # => false + * File.chardev?('/dev/loop0') # => false + * File.chardev?('nosuch') # => false * - * File.chardev?($stdin) # => true - * File.chardev?('t.txt') # => false * + * The returned value is filesystem-dependent; on Windows, always +false+. */ static VALUE rb_file_chardev_p(VALUE obj, VALUE fname) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 0b3a96154acc0b..d2300ab56bebb3 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2240,7 +2240,7 @@ def blockdev?() FileTest.blockdev?(@path) end # Pathname('nosuch').chardev? # => false # ``` # - # The returned value is OS-dependent; on Windows, almost always `false`. + # The returned value is filesystem-dependent; on Windows, always `false`. def chardev?() FileTest.chardev?(@path) end # :markup: markdown