Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det

## Compatibility issues

* `Kernel#at_exit` and `END {}` now raise `Ractor::IsolationError` when called
in a non-main Ractor. Previously the registered handler ran in the main
Ractor at process exit, which was confusing. [[Feature #22139]]

## Stdlib compatibility issues

## C API updates
Expand Down Expand Up @@ -207,6 +211,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
[Feature #21861]: https://bugs.ruby-lang.org/issues/21861
[Feature #21932]: https://bugs.ruby-lang.org/issues/21932
[Feature #21981]: https://bugs.ruby-lang.org/issues/21981
[Feature #22139]: https://bugs.ruby-lang.org/issues/22139
[PR #17201]: https://github.com/ruby/ruby/pull/17201
[RubyGems-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/v4.0.4
[RubyGems-v4.0.5]: https://github.com/rubygems/rubygems/releases/tag/v4.0.5
Expand Down
2 changes: 2 additions & 0 deletions depend
Original file line number Diff line number Diff line change
Expand Up @@ -5452,6 +5452,7 @@ eval.$(OBJEXT): $(top_srcdir)/internal/imemo.h
eval.$(OBJEXT): $(top_srcdir)/internal/inits.h
eval.$(OBJEXT): $(top_srcdir)/internal/io.h
eval.$(OBJEXT): $(top_srcdir)/internal/object.h
eval.$(OBJEXT): $(top_srcdir)/internal/ractor.h
eval.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h
eval.$(OBJEXT): $(top_srcdir)/internal/serial.h
eval.$(OBJEXT): $(top_srcdir)/internal/set_table.h
Expand Down Expand Up @@ -19742,6 +19743,7 @@ vm.$(OBJEXT): $(top_srcdir)/internal/numeric.h
vm.$(OBJEXT): $(top_srcdir)/internal/object.h
vm.$(OBJEXT): $(top_srcdir)/internal/parse.h
vm.$(OBJEXT): $(top_srcdir)/internal/proc.h
vm.$(OBJEXT): $(top_srcdir)/internal/ractor.h
vm.$(OBJEXT): $(top_srcdir)/internal/random.h
vm.$(OBJEXT): $(top_srcdir)/internal/rational.h
vm.$(OBJEXT): $(top_srcdir)/internal/re.h
Expand Down
2 changes: 2 additions & 0 deletions eval_jump.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

#include "eval_intern.h"
#include "internal/ractor.h"

/* exit */

Expand Down Expand Up @@ -42,6 +43,7 @@ rb_f_at_exit(VALUE _)
if (!rb_block_given_p()) {
rb_raise(rb_eArgError, "called without a block");
}
rb_ractor_ensure_main_ractor("can not call at_exit from non-main Ractors");
proc = rb_block_proc();
rb_set_end_proc(rb_call_end_proc, proc);
return proc;
Expand Down
22 changes: 22 additions & 0 deletions test/ruby/test_ractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,28 @@ def test_fork_raise_isolation_error
RUBY
end if Process.respond_to?(:fork)

def test_at_exit_raise_isolation_error
assert_ractor(<<~'RUBY')
ractor = Ractor.new do
at_exit { }
rescue Ractor::IsolationError => e
e
end
assert_equal Ractor::IsolationError, ractor.value.class
RUBY
end

def test_END_raise_isolation_error
assert_ractor(<<~'RUBY', ignore_stderr: true)
ractor = Ractor.new do
END { nil }
rescue Ractor::IsolationError => e
e
end
assert_equal Ractor::IsolationError, ractor.value.class
RUBY
end

def test_require_raises_and_no_ractor_belonging_issue
assert_ractor(<<~'RUBY')
require "tempfile"
Expand Down
46 changes: 24 additions & 22 deletions thread_pthread.c
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,9 @@ struct rb_thread_context {
bool dead;
};

static bool thread_sched_reclaim_from(struct coroutine_context *returning_from);
static bool thread_sched_reclaim(struct coroutine_context *dead_co);
#endif
static struct coroutine_context *coroutine_transfer0(struct coroutine_context *transfer_from,
static void coroutine_transfer0(struct coroutine_context *transfer_from,
struct coroutine_context *transfer_to, bool to_dead);

#define thread_sched_dump(s) thread_sched_dump_(__FILE__, __LINE__, s)
Expand Down Expand Up @@ -1264,7 +1264,7 @@ rb_thread_sched_init(struct rb_thread_sched *sched, bool atfork)
#endif
}

static struct coroutine_context *
static void
coroutine_transfer0(struct coroutine_context *transfer_from, struct coroutine_context *transfer_to, bool to_dead)
{
#ifdef RUBY_ASAN_ENABLED
Expand All @@ -1279,6 +1279,7 @@ coroutine_transfer0(struct coroutine_context *transfer_from, struct coroutine_co
__tsan_switch_to_fiber(transfer_to->tsan_fiber, 0);
#endif

RBIMPL_ATTR_MAYBE_UNUSED()
struct coroutine_context *returning_from = coroutine_transfer(transfer_from, transfer_to);

/* if to_dead was passed, the caller is promising that this coroutine is finished and it should
Expand All @@ -1288,11 +1289,9 @@ coroutine_transfer0(struct coroutine_context *transfer_from, struct coroutine_co
__sanitizer_finish_switch_fiber(transfer_from->fake_stack,
(const void**)&returning_from->stack_base, &returning_from->stack_size);
#endif

return returning_from;
}

static struct coroutine_context *
static void
thread_sched_switch0(struct coroutine_context *current_cont, rb_thread_t *next_th, struct rb_native_thread *nt, bool to_dead)
{
VM_ASSERT(!nt->dedicated);
Expand All @@ -1307,7 +1306,7 @@ thread_sched_switch0(struct coroutine_context *current_cont, rb_thread_t *next_t
ruby_thread_set_native(next_th);
native_thread_assign(nt, next_th);

return coroutine_transfer0(current_cont, next_th->sched.context, to_dead);
coroutine_transfer0(current_cont, next_th->sched.context, to_dead);
}

static void
Expand Down Expand Up @@ -2436,12 +2435,16 @@ nt_start(void *ptr)
if (next_th && next_th->nt == NULL) {
RUBY_DEBUG_LOG("nt:%d next_th:%d", (int)nt->serial, (int)next_th->serial);
#if USE_MN_THREADS
struct coroutine_context *from = thread_sched_switch0(nt->nt_context, next_th, nt, false);
if (thread_sched_reclaim_from(from)) {
// A terminal transfer: the dying thread released
// the sched lock before transferring (its Ractor
// may be gone by now); we resumed with NO lock
// held and must not touch the sched again.
thread_sched_switch0(nt->nt_context, next_th, nt, false);

// If a coroutine terminated during the transfer, co_start
// recorded it in nt->dead_co (switch0's return value is
// backend-dependent, unusable; see thread_pthread.h).
struct coroutine_context *dead_co = nt->dead_co;
nt->dead_co = NULL;
if (thread_sched_reclaim(dead_co)) {
// it already released the sched lock before its
// transfer (its Ractor may be gone): leave sched be.
locked = false;
}
#else
Expand Down Expand Up @@ -2477,16 +2480,15 @@ static int native_thread_create_shared(rb_thread_t *th);
static void nt_free_stack(void *mstack);


// Reclaim the coroutine context the nt scheduling loop just resumed from,
// if its thread terminated. A dying thread always returns to its native
// thread's own context (co_start's epilogue), so this is the only place
// terminal transfers arrive -- and our running here proves the transfer's
// register save into the block has completed. Returns true for a terminal
// transfer, whose dying thread RELEASED the sched lock before transferring.
// Reclaim the context a coroutine thread recorded in nt->dead_co before its
// final transfer (co_start's epilogue). Our running here proves that transfer's
// register save into the block completed. Returns true when a thread did
// terminate -- it RELEASED the sched lock before transferring; NULL/false means
// a live yield, where the loop still owns the lock.
static bool
thread_sched_reclaim_from(struct coroutine_context *returning_from)
thread_sched_reclaim(struct coroutine_context *dead_co)
{
struct rb_thread_context *tctx = (struct rb_thread_context *)returning_from;
struct rb_thread_context *tctx = (struct rb_thread_context *)dead_co;

if (tctx != NULL && tctx->dead) {
nt_free_stack(tctx->stack);
Expand All @@ -2512,7 +2514,7 @@ rb_threadptr_sched_free(rb_thread_t *th)
else if (th->sched.context != NULL) {
// a coroutine thread that never reached its epilogue (never started);
// a terminated one is reclaimed by whoever resumed from its final
// transfer (thread_sched_reclaim_from), and cleared this pointer.
// transfer (thread_sched_reclaim), and cleared this pointer.
struct rb_thread_context *tctx = (struct rb_thread_context *)th->sched.context;
nt_free_stack(tctx->stack);
SIZED_FREE(tctx);
Expand Down
5 changes: 5 additions & 0 deletions thread_pthread.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ struct rb_native_thread {

struct coroutine_context *nt_context;
int dedicated;

// A terminating coroutine records its context here before its final
// transfer; this nt's loop reclaims it. (Not via coroutine_transfer()'s
// return value: its meaning differs between the amd64 asm and ucontext.)
struct coroutine_context *dead_co;
};

#undef except
Expand Down
3 changes: 3 additions & 0 deletions thread_pthread_mn.c
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,9 @@ co_start(struct coroutine_context *from, struct coroutine_context *self)
// this native thread's own context, where the nt loop reclaims tctx.
struct rb_thread_context *tctx = (struct rb_thread_context *)self;
tctx->dead = true;
// Hand this context to the nt's loop, which reclaims it right after the
// final transfer returns from switch0.
tctx->nt->dead_co = &tctx->co;
coroutine_transfer0(&tctx->co, tctx->nt->nt_context, true);

rb_bug("unreachable");
Expand Down
2 changes: 2 additions & 0 deletions vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "internal/missing.h"
#include "internal/object.h"
#include "internal/proc.h"
#include "internal/ractor.h"
#include "internal/re.h"
#include "internal/ruby_parser.h"
#include "internal/st.h"
Expand Down Expand Up @@ -4115,6 +4116,7 @@ m_core_undef_method(VALUE self, VALUE cbase, VALUE sym)
static VALUE
m_core_set_postexe(VALUE self)
{
rb_ractor_ensure_main_ractor("can not use END{} in non-main Ractors");
rb_set_end_proc(rb_call_end_proc, rb_block_proc());
return Qnil;
}
Expand Down
3 changes: 3 additions & 0 deletions zjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,12 @@ def stats_string
:compile_hir_build_time_ns,
:compile_hir_strength_reduce_time_ns,
:compile_hir_inline_methods_time_ns,
:compile_hir_optimize_load_store_time_ns,
:compile_hir_canonicalize_time_ns,
:compile_hir_fold_constants_time_ns,
:compile_hir_clean_cfg_time_ns,
:compile_hir_remove_redundant_patch_points_time_ns,
:compile_hir_remove_duplicate_check_interrupts_time_ns,
:compile_hir_eliminate_dead_code_time_ns,
:compile_lir_time_ns,
:profile_time_ns,
Expand Down
2 changes: 1 addition & 1 deletion zjit/bindgen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ fn main() {
.allowlist_type("ruby_rstring_private_flags")
.allowlist_function("rb_ec_str_resurrect")
.allowlist_function("rb_str_concat_literals")
.allowlist_function("rb_obj_as_string_result")
.allowlist_function("rb_any_to_s")
.allowlist_function("rb_str_byte_substr")
.allowlist_function("rb_str_substr_two_fixnums")
.allowlist_function("rb_backref_get")
Expand Down
18 changes: 9 additions & 9 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
Insn::FixnumBitCheck { val, index } => gen_fixnum_bit_check(asm, opnd!(val), *index),
Insn::SideExit { state, reason, recompile } => no_output!(gen_side_exit(jit, asm, function, reason, *recompile, &function.frame_state(*state))),
Insn::PutSpecialObject { value_type, state } => gen_putspecialobject(jit, asm, function, *value_type, &function.frame_state(*state)),
Insn::AnyToString { val, str, state } => gen_anytostring(asm, opnd!(val), opnd!(str), &function.frame_state(*state)),
Insn::AnyToString { val, state } => gen_anytostring(asm, opnd!(val), &function.frame_state(*state)),
Insn::Defined { op_type, obj, pushval, v, lep_level, state } => gen_defined(jit, asm, function, *op_type, *obj, *pushval, opnd!(v), *lep_level, &function.frame_state(*state)),
Insn::CheckMatch { target, pattern, flag, state } => gen_checkmatch(jit, asm, function, opnd!(target), opnd!(pattern), *flag, &function.frame_state(*state)),
Insn::GetSpecialSymbol { symbol_type, state } => gen_getspecial_symbol(asm, *symbol_type, &function.frame_state(*state)),
Expand All @@ -768,8 +768,8 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
Insn::LoadSP => gen_load_sp(),
&Insn::GetEP { level } => gen_get_ep(asm, level),
Insn::LoadSelf => gen_load_self(asm),
&Insn::LoadField { recv, id, offset, return_type } => gen_load_field(asm, opnd!(recv), id, offset, return_type),
&Insn::StoreField { recv, id, offset, val } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val), function.type_of(val))),
&Insn::LoadField { recv, id, offset, return_type: _, num_bits } => gen_load_field(asm, opnd!(recv), id, offset, num_bits),
&Insn::StoreField { recv, id, offset, val, num_bits } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val), num_bits)),
&Insn::WriteBarrier { recv, val } => no_output!(gen_write_barrier(jit, asm, opnd!(recv), opnd!(val), function.type_of(val))),
&Insn::IsBlockGiven { lep } => gen_is_block_given(asm, opnd!(lep)),
Insn::ArrayInclude { elements, target, state } => gen_array_include(jit, asm, function, opnds!(elements), opnd!(target), &function.frame_state(*state)),
Expand Down Expand Up @@ -1370,18 +1370,18 @@ fn gen_load_self(asm: &mut Assembler) -> Opnd {
asm.load(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SELF))
}

fn gen_load_field(asm: &mut Assembler, recv: Opnd, id: FieldName, offset: i32, return_type: Type) -> Opnd {
fn gen_load_field(asm: &mut Assembler, recv: Opnd, id: FieldName, offset: i32, num_bits: u8) -> Opnd {
gen_incr_counter(asm, Counter::load_field_count);
asm_comment!(asm, "Load field id={id} offset={offset}");
let recv = asm.load_mem(recv);
asm.load(Opnd::mem(return_type.num_bits(), recv, offset))
asm.load(Opnd::mem(num_bits, recv, offset))
}

fn gen_store_field(asm: &mut Assembler, recv: Opnd, id: FieldName, offset: i32, val: Opnd, val_type: Type) {
fn gen_store_field(asm: &mut Assembler, recv: Opnd, id: FieldName, offset: i32, val: Opnd, num_bits: u8) {
gen_incr_counter(asm, Counter::store_field_count);
asm_comment!(asm, "Store field id={id} offset={offset}");
let recv = asm.load_mem(recv);
asm.store(Opnd::mem(val_type.num_bits(), recv, offset), val);
asm.store(Opnd::mem(num_bits, recv, offset), val);
}

fn gen_write_barrier(jit: &mut JITState, asm: &mut Assembler, recv: Opnd, val: Opnd, val_type: Type) {
Expand Down Expand Up @@ -2604,10 +2604,10 @@ fn gen_box_fixnum(jit: &mut JITState, asm: &mut Assembler, function: &Function,
asm.or(shifted, Opnd::UImm(RUBY_FIXNUM_FLAG as u64))
}

fn gen_anytostring(asm: &mut Assembler, val: lir::Opnd, str: lir::Opnd, state: &FrameState) -> lir::Opnd {
fn gen_anytostring(asm: &mut Assembler, val: lir::Opnd, state: &FrameState) -> lir::Opnd {
gen_prepare_leaf_call_with_gc(asm, state);

asm_ccall!(asm, rb_obj_as_string_result, str, val)
asm_ccall!(asm, rb_any_to_s, val)
}

/// Evaluate if a value is truthy
Expand Down
2 changes: 1 addition & 1 deletion zjit/src/cruby_bindings.inc.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 8 additions & 38 deletions zjit/src/cruby_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,20 +322,10 @@ fn inline_string_to_s(fun: &mut hir::Function, block: hir::BlockId, recv: hir::I
fn inline_thread_current(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option<hir::InsnId> {
let &[] = args else { return None; };
let ec = fun.push_insn(block, hir::Insn::LoadEC);
let thread_ptr = fun.push_insn(block, hir::Insn::LoadField {
recv: ec,
id: FieldName::thread_ptr,
offset: RUBY_OFFSET_EC_THREAD_PTR,
return_type: types::CPtr,
});
let thread_self = fun.push_insn(block, hir::Insn::LoadField {
recv: thread_ptr,
id: FieldName::SelfParam,
offset: RUBY_OFFSET_THREAD_SELF,
// TODO(max): Add Thread type. But Thread.current is not guaranteed to be an exact Thread.
// You can make subclasses...
return_type: types::BasicObject,
});
let thread_ptr = fun.load_field(block, ec, FieldName::thread_ptr, RUBY_OFFSET_EC_THREAD_PTR, types::CPtr);
// TODO(max): Add Thread type. But Thread.current is not guaranteed to be an exact Thread.
// You can make subclasses...
let thread_self = fun.load_field(block, thread_ptr, FieldName::SelfParam, RUBY_OFFSET_THREAD_SELF, types::BasicObject);
Some(thread_self)
}

Expand Down Expand Up @@ -468,12 +458,7 @@ fn inline_hash_aset(fun: &mut hir::Function, block: hir::BlockId, recv: hir::Ins
fn inline_string_bytesize(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
if args.is_empty() && fun.likely_a(recv, types::String, state) {
let recv = fun.coerce_to(block, recv, types::String, state);
let len = fun.push_insn(block, hir::Insn::LoadField {
recv,
id: FieldName::len,
offset: RUBY_OFFSET_RSTRING_LEN,
return_type: types::CInt64,
});
let len = fun.load_string_length(block, recv);

let result = fun.push_insn(block, hir::Insn::BoxFixnum {
val: len,
Expand All @@ -492,12 +477,7 @@ fn inline_string_getbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir
// when converting the index to a C integer.
let index = fun.coerce_to(block, index, types::Fixnum, state);
let unboxed_index = fun.push_insn(block, hir::Insn::UnboxFixnum { val: index });
let len = fun.push_insn(block, hir::Insn::LoadField {
recv,
id: FieldName::len,
offset: RUBY_OFFSET_RSTRING_LEN,
return_type: types::CInt64,
});
let len = fun.load_string_length(block, recv);
// TODO(max): Find a way to mark these guards as not needed for correctness... as in, once
// the data dependency is gone (say, the StringGetbyte is elided), they can also be elided.
//
Expand All @@ -520,12 +500,7 @@ fn inline_string_setbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir
let value = fun.coerce_to(block, value, types::Fixnum, state);

let unboxed_index = fun.push_insn(block, hir::Insn::UnboxFixnum { val: index });
let len = fun.push_insn(block, hir::Insn::LoadField {
recv,
id: FieldName::len,
offset: RUBY_OFFSET_RSTRING_LEN,
return_type: types::CInt64,
});
let len = fun.load_string_length(block, recv);
let unboxed_index = fun.push_insn(block, hir::Insn::GuardLess { left: unboxed_index, right: len, reason: Box::new(SideExitReason::GuardLess), state });
let unboxed_index = fun.push_insn(block, hir::Insn::AdjustBounds { index: unboxed_index, length: len });
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
Expand All @@ -543,12 +518,7 @@ fn inline_string_setbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir

fn inline_string_empty_p(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option<hir::InsnId> {
let &[] = args else { return None; };
let len = fun.push_insn(block, hir::Insn::LoadField {
recv,
id: FieldName::len,
offset: RUBY_OFFSET_RSTRING_LEN,
return_type: types::CInt64,
});
let len = fun.load_string_length(block, recv);
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
let is_zero = fun.push_insn(block, hir::Insn::IsBitEqual { left: len, right: zero });
let result = fun.push_insn(block, hir::Insn::BoxBool { val: is_zero });
Expand Down
Loading