From d8b8a95af9d6aab1e8da2b6f1808bc3ffd406889 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 13 Dec 2025 06:35:58 +0100 Subject: [PATCH 1/5] Make Monitor a core class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Feature #21788] It allows monitor to access internal routines and remove some overhead. Before: ``` ruby 4.0.0dev (2025-12-13T04:52:13Z master 71dd272506) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- Mutex 2.111M i/100ms Monitor 1.736M i/100ms Calculating ------------------------------------- Mutex 25.050M (± 0.4%) i/s (39.92 ns/i) - 126.631M in 5.055208s Monitor 19.809M (± 0.1%) i/s (50.48 ns/i) - 100.672M in 5.082015s ``` After: ``` ruby 4.0.0dev (2025-12-13T06:49:18Z core-monitor 6fabf389fd) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- Mutex 2.144M i/100ms Monitor 1.859M i/100ms Calculating ------------------------------------- Mutex 24.771M (± 0.4%) i/s (40.37 ns/i) - 124.342M in 5.019716s Monitor 23.722M (± 0.4%) i/s (42.15 ns/i) - 118.998M in 5.016361s ``` Bench: ```ruby require 'bundler/inline' gemfile do gem "benchmark-ips" end mutex = Mutex.new require "monitor" monitor = Monitor.new Benchmark.ips do |x| x.report("Mutex") { mutex.synchronize { } } x.report("Monitor") { monitor.synchronize { } } end ``` --- ext/monitor/depend | 162 -------------- ext/monitor/extconf.rb | 2 - ext/monitor/monitor.c | 301 -------------------------- {ext/monitor/lib => lib}/monitor.rb | 79 +------ spec/ruby/core/kernel/require_spec.rb | 9 +- thread_sync.c | 262 ++++++++++++++++++++++ thread_sync.rb | 186 +++++++++++++++- 7 files changed, 455 insertions(+), 546 deletions(-) delete mode 100644 ext/monitor/depend delete mode 100644 ext/monitor/extconf.rb delete mode 100644 ext/monitor/monitor.c rename {ext/monitor/lib => lib}/monitor.rb (78%) diff --git a/ext/monitor/depend b/ext/monitor/depend deleted file mode 100644 index 0c7d54afc8dc57..00000000000000 --- a/ext/monitor/depend +++ /dev/null @@ -1,162 +0,0 @@ -# AUTOGENERATED DEPENDENCIES START -monitor.o: $(RUBY_EXTCONF_H) -monitor.o: $(arch_hdrdir)/ruby/config.h -monitor.o: $(hdrdir)/ruby/assert.h -monitor.o: $(hdrdir)/ruby/backward.h -monitor.o: $(hdrdir)/ruby/backward/2/assume.h -monitor.o: $(hdrdir)/ruby/backward/2/attributes.h -monitor.o: $(hdrdir)/ruby/backward/2/bool.h -monitor.o: $(hdrdir)/ruby/backward/2/inttypes.h -monitor.o: $(hdrdir)/ruby/backward/2/limits.h -monitor.o: $(hdrdir)/ruby/backward/2/long_long.h -monitor.o: $(hdrdir)/ruby/backward/2/stdalign.h -monitor.o: $(hdrdir)/ruby/backward/2/stdarg.h -monitor.o: $(hdrdir)/ruby/defines.h -monitor.o: $(hdrdir)/ruby/intern.h -monitor.o: $(hdrdir)/ruby/internal/abi.h -monitor.o: $(hdrdir)/ruby/internal/anyargs.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/char.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/double.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/fixnum.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/gid_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/int.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/intptr_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/long.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/long_long.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/mode_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/off_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/pid_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/short.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/size_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/st_data_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/uid_t.h -monitor.o: $(hdrdir)/ruby/internal/assume.h -monitor.o: $(hdrdir)/ruby/internal/attr/alloc_size.h -monitor.o: $(hdrdir)/ruby/internal/attr/artificial.h -monitor.o: $(hdrdir)/ruby/internal/attr/cold.h -monitor.o: $(hdrdir)/ruby/internal/attr/const.h -monitor.o: $(hdrdir)/ruby/internal/attr/constexpr.h -monitor.o: $(hdrdir)/ruby/internal/attr/deprecated.h -monitor.o: $(hdrdir)/ruby/internal/attr/diagnose_if.h -monitor.o: $(hdrdir)/ruby/internal/attr/enum_extensibility.h -monitor.o: $(hdrdir)/ruby/internal/attr/error.h -monitor.o: $(hdrdir)/ruby/internal/attr/flag_enum.h -monitor.o: $(hdrdir)/ruby/internal/attr/forceinline.h -monitor.o: $(hdrdir)/ruby/internal/attr/format.h -monitor.o: $(hdrdir)/ruby/internal/attr/maybe_unused.h -monitor.o: $(hdrdir)/ruby/internal/attr/noalias.h -monitor.o: $(hdrdir)/ruby/internal/attr/nodiscard.h -monitor.o: $(hdrdir)/ruby/internal/attr/noexcept.h -monitor.o: $(hdrdir)/ruby/internal/attr/noinline.h -monitor.o: $(hdrdir)/ruby/internal/attr/nonnull.h -monitor.o: $(hdrdir)/ruby/internal/attr/noreturn.h -monitor.o: $(hdrdir)/ruby/internal/attr/packed_struct.h -monitor.o: $(hdrdir)/ruby/internal/attr/pure.h -monitor.o: $(hdrdir)/ruby/internal/attr/restrict.h -monitor.o: $(hdrdir)/ruby/internal/attr/returns_nonnull.h -monitor.o: $(hdrdir)/ruby/internal/attr/warning.h -monitor.o: $(hdrdir)/ruby/internal/attr/weakref.h -monitor.o: $(hdrdir)/ruby/internal/cast.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/apple.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/clang.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/gcc.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/intel.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/msvc.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/sunpro.h -monitor.o: $(hdrdir)/ruby/internal/compiler_since.h -monitor.o: $(hdrdir)/ruby/internal/config.h -monitor.o: $(hdrdir)/ruby/internal/constant_p.h -monitor.o: $(hdrdir)/ruby/internal/core.h -monitor.o: $(hdrdir)/ruby/internal/core/rarray.h -monitor.o: $(hdrdir)/ruby/internal/core/rbasic.h -monitor.o: $(hdrdir)/ruby/internal/core/rbignum.h -monitor.o: $(hdrdir)/ruby/internal/core/rclass.h -monitor.o: $(hdrdir)/ruby/internal/core/rdata.h -monitor.o: $(hdrdir)/ruby/internal/core/rfile.h -monitor.o: $(hdrdir)/ruby/internal/core/rhash.h -monitor.o: $(hdrdir)/ruby/internal/core/robject.h -monitor.o: $(hdrdir)/ruby/internal/core/rregexp.h -monitor.o: $(hdrdir)/ruby/internal/core/rstring.h -monitor.o: $(hdrdir)/ruby/internal/core/rstruct.h -monitor.o: $(hdrdir)/ruby/internal/core/rtypeddata.h -monitor.o: $(hdrdir)/ruby/internal/ctype.h -monitor.o: $(hdrdir)/ruby/internal/dllexport.h -monitor.o: $(hdrdir)/ruby/internal/dosish.h -monitor.o: $(hdrdir)/ruby/internal/error.h -monitor.o: $(hdrdir)/ruby/internal/eval.h -monitor.o: $(hdrdir)/ruby/internal/event.h -monitor.o: $(hdrdir)/ruby/internal/fl_type.h -monitor.o: $(hdrdir)/ruby/internal/gc.h -monitor.o: $(hdrdir)/ruby/internal/glob.h -monitor.o: $(hdrdir)/ruby/internal/globals.h -monitor.o: $(hdrdir)/ruby/internal/has/attribute.h -monitor.o: $(hdrdir)/ruby/internal/has/builtin.h -monitor.o: $(hdrdir)/ruby/internal/has/c_attribute.h -monitor.o: $(hdrdir)/ruby/internal/has/cpp_attribute.h -monitor.o: $(hdrdir)/ruby/internal/has/declspec_attribute.h -monitor.o: $(hdrdir)/ruby/internal/has/extension.h -monitor.o: $(hdrdir)/ruby/internal/has/feature.h -monitor.o: $(hdrdir)/ruby/internal/has/warning.h -monitor.o: $(hdrdir)/ruby/internal/intern/array.h -monitor.o: $(hdrdir)/ruby/internal/intern/bignum.h -monitor.o: $(hdrdir)/ruby/internal/intern/class.h -monitor.o: $(hdrdir)/ruby/internal/intern/compar.h -monitor.o: $(hdrdir)/ruby/internal/intern/complex.h -monitor.o: $(hdrdir)/ruby/internal/intern/cont.h -monitor.o: $(hdrdir)/ruby/internal/intern/dir.h -monitor.o: $(hdrdir)/ruby/internal/intern/enum.h -monitor.o: $(hdrdir)/ruby/internal/intern/enumerator.h -monitor.o: $(hdrdir)/ruby/internal/intern/error.h -monitor.o: $(hdrdir)/ruby/internal/intern/eval.h -monitor.o: $(hdrdir)/ruby/internal/intern/file.h -monitor.o: $(hdrdir)/ruby/internal/intern/hash.h -monitor.o: $(hdrdir)/ruby/internal/intern/io.h -monitor.o: $(hdrdir)/ruby/internal/intern/load.h -monitor.o: $(hdrdir)/ruby/internal/intern/marshal.h -monitor.o: $(hdrdir)/ruby/internal/intern/numeric.h -monitor.o: $(hdrdir)/ruby/internal/intern/object.h -monitor.o: $(hdrdir)/ruby/internal/intern/parse.h -monitor.o: $(hdrdir)/ruby/internal/intern/proc.h -monitor.o: $(hdrdir)/ruby/internal/intern/process.h -monitor.o: $(hdrdir)/ruby/internal/intern/random.h -monitor.o: $(hdrdir)/ruby/internal/intern/range.h -monitor.o: $(hdrdir)/ruby/internal/intern/rational.h -monitor.o: $(hdrdir)/ruby/internal/intern/re.h -monitor.o: $(hdrdir)/ruby/internal/intern/ruby.h -monitor.o: $(hdrdir)/ruby/internal/intern/select.h -monitor.o: $(hdrdir)/ruby/internal/intern/select/largesize.h -monitor.o: $(hdrdir)/ruby/internal/intern/set.h -monitor.o: $(hdrdir)/ruby/internal/intern/signal.h -monitor.o: $(hdrdir)/ruby/internal/intern/sprintf.h -monitor.o: $(hdrdir)/ruby/internal/intern/string.h -monitor.o: $(hdrdir)/ruby/internal/intern/struct.h -monitor.o: $(hdrdir)/ruby/internal/intern/thread.h -monitor.o: $(hdrdir)/ruby/internal/intern/time.h -monitor.o: $(hdrdir)/ruby/internal/intern/variable.h -monitor.o: $(hdrdir)/ruby/internal/intern/vm.h -monitor.o: $(hdrdir)/ruby/internal/interpreter.h -monitor.o: $(hdrdir)/ruby/internal/iterator.h -monitor.o: $(hdrdir)/ruby/internal/memory.h -monitor.o: $(hdrdir)/ruby/internal/method.h -monitor.o: $(hdrdir)/ruby/internal/module.h -monitor.o: $(hdrdir)/ruby/internal/newobj.h -monitor.o: $(hdrdir)/ruby/internal/scan_args.h -monitor.o: $(hdrdir)/ruby/internal/special_consts.h -monitor.o: $(hdrdir)/ruby/internal/static_assert.h -monitor.o: $(hdrdir)/ruby/internal/stdalign.h -monitor.o: $(hdrdir)/ruby/internal/stdbool.h -monitor.o: $(hdrdir)/ruby/internal/stdckdint.h -monitor.o: $(hdrdir)/ruby/internal/symbol.h -monitor.o: $(hdrdir)/ruby/internal/value.h -monitor.o: $(hdrdir)/ruby/internal/value_type.h -monitor.o: $(hdrdir)/ruby/internal/variable.h -monitor.o: $(hdrdir)/ruby/internal/warning_push.h -monitor.o: $(hdrdir)/ruby/internal/xmalloc.h -monitor.o: $(hdrdir)/ruby/missing.h -monitor.o: $(hdrdir)/ruby/ruby.h -monitor.o: $(hdrdir)/ruby/st.h -monitor.o: $(hdrdir)/ruby/subst.h -monitor.o: monitor.c -# AUTOGENERATED DEPENDENCIES END diff --git a/ext/monitor/extconf.rb b/ext/monitor/extconf.rb deleted file mode 100644 index 78c53fa0c5ed62..00000000000000 --- a/ext/monitor/extconf.rb +++ /dev/null @@ -1,2 +0,0 @@ -require 'mkmf' -create_makefile('monitor') diff --git a/ext/monitor/monitor.c b/ext/monitor/monitor.c deleted file mode 100644 index c43751c4e21f72..00000000000000 --- a/ext/monitor/monitor.c +++ /dev/null @@ -1,301 +0,0 @@ -#include "ruby/ruby.h" - -/* Thread::Monitor */ - -struct rb_monitor { - long count; - VALUE owner; - VALUE mutex; -}; - -static void -monitor_mark(void *ptr) -{ - struct rb_monitor *mc = ptr; - rb_gc_mark_movable(mc->owner); - rb_gc_mark_movable(mc->mutex); -} - -static void -monitor_compact(void *ptr) -{ - struct rb_monitor *mc = ptr; - mc->owner = rb_gc_location(mc->owner); - mc->mutex = rb_gc_location(mc->mutex); -} - -static const rb_data_type_t monitor_data_type = { - .wrap_struct_name = "monitor", - .function = { - .dmark = monitor_mark, - .dfree = RUBY_TYPED_DEFAULT_FREE, - .dsize = NULL, // Fully embeded - .dcompact = monitor_compact, - }, - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, -}; - -static VALUE -monitor_alloc(VALUE klass) -{ - struct rb_monitor *mc; - VALUE obj; - - obj = TypedData_Make_Struct(klass, struct rb_monitor, &monitor_data_type, mc); - RB_OBJ_WRITE(obj, &mc->mutex, rb_mutex_new()); - RB_OBJ_WRITE(obj, &mc->owner, Qnil); - mc->count = 0; - - return obj; -} - -static struct rb_monitor * -monitor_ptr(VALUE monitor) -{ - struct rb_monitor *mc; - TypedData_Get_Struct(monitor, struct rb_monitor, &monitor_data_type, mc); - return mc; -} - -static bool -mc_owner_p(struct rb_monitor *mc, VALUE current_fiber) -{ - return mc->owner == current_fiber; -} - -/* - * call-seq: - * try_enter -> true or false - * - * Attempts to enter exclusive section. Returns +false+ if lock fails. - */ -static VALUE -monitor_try_enter(VALUE monitor) -{ - struct rb_monitor *mc = monitor_ptr(monitor); - - VALUE current_fiber = rb_fiber_current(); - if (!mc_owner_p(mc, current_fiber)) { - if (!rb_mutex_trylock(mc->mutex)) { - return Qfalse; - } - RB_OBJ_WRITE(monitor, &mc->owner, current_fiber); - mc->count = 0; - } - mc->count += 1; - return Qtrue; -} - - -struct monitor_args { - VALUE monitor; - struct rb_monitor *mc; - VALUE current_fiber; -}; - -static inline void -monitor_args_init(struct monitor_args *args, VALUE monitor) -{ - args->monitor = monitor; - args->mc = monitor_ptr(monitor); - args->current_fiber = rb_fiber_current(); -} - -static void -monitor_enter0(struct monitor_args *args) -{ - if (!mc_owner_p(args->mc, args->current_fiber)) { - rb_mutex_lock(args->mc->mutex); - RB_OBJ_WRITE(args->monitor, &args->mc->owner, args->current_fiber); - args->mc->count = 0; - } - args->mc->count++; -} - -/* - * call-seq: - * enter -> nil - * - * Enters exclusive section. - */ -static VALUE -monitor_enter(VALUE monitor) -{ - struct monitor_args args; - monitor_args_init(&args, monitor); - monitor_enter0(&args); - return Qnil; -} - -static inline void -monitor_check_owner0(struct monitor_args *args) -{ - if (!mc_owner_p(args->mc, args->current_fiber)) { - rb_raise(rb_eThreadError, "current fiber not owner"); - } -} - -/* :nodoc: */ -static VALUE -monitor_check_owner(VALUE monitor) -{ - struct monitor_args args; - monitor_args_init(&args, monitor); - monitor_check_owner0(&args); - return Qnil; -} - -static void -monitor_exit0(struct monitor_args *args) -{ - monitor_check_owner0(args); - - if (args->mc->count <= 0) rb_bug("monitor_exit: count:%d", (int)args->mc->count); - args->mc->count--; - - if (args->mc->count == 0) { - RB_OBJ_WRITE(args->monitor, &args->mc->owner, Qnil); - rb_mutex_unlock(args->mc->mutex); - } -} - -/* - * call-seq: - * exit -> nil - * - * Leaves exclusive section. - */ -static VALUE -monitor_exit(VALUE monitor) -{ - struct monitor_args args; - monitor_args_init(&args, monitor); - monitor_exit0(&args); - return Qnil; -} - -/* :nodoc: */ -static VALUE -monitor_locked_p(VALUE monitor) -{ - struct rb_monitor *mc = monitor_ptr(monitor); - return rb_mutex_locked_p(mc->mutex); -} - -/* :nodoc: */ -static VALUE -monitor_owned_p(VALUE monitor) -{ - struct rb_monitor *mc = monitor_ptr(monitor); - return rb_mutex_locked_p(mc->mutex) && mc_owner_p(mc, rb_fiber_current()) ? Qtrue : Qfalse; -} - -static VALUE -monitor_exit_for_cond(VALUE monitor) -{ - struct rb_monitor *mc = monitor_ptr(monitor); - long cnt = mc->count; - RB_OBJ_WRITE(monitor, &mc->owner, Qnil); - mc->count = 0; - return LONG2NUM(cnt); -} - -struct wait_for_cond_data { - VALUE monitor; - VALUE cond; - VALUE timeout; - VALUE count; -}; - -static VALUE -monitor_wait_for_cond_body(VALUE v) -{ - struct wait_for_cond_data *data = (struct wait_for_cond_data *)v; - struct rb_monitor *mc = monitor_ptr(data->monitor); - // cond.wait(monitor.mutex, timeout) - VALUE signaled = rb_funcall(data->cond, rb_intern("wait"), 2, mc->mutex, data->timeout); - return RTEST(signaled) ? Qtrue : Qfalse; -} - -static VALUE -monitor_enter_for_cond(VALUE v) -{ - // assert(rb_mutex_owned_p(mc->mutex) == Qtrue) - // but rb_mutex_owned_p is not exported... - - struct wait_for_cond_data *data = (struct wait_for_cond_data *)v; - struct rb_monitor *mc = monitor_ptr(data->monitor); - RB_OBJ_WRITE(data->monitor, &mc->owner, rb_fiber_current()); - mc->count = NUM2LONG(data->count); - return Qnil; -} - -/* :nodoc: */ -static VALUE -monitor_wait_for_cond(VALUE monitor, VALUE cond, VALUE timeout) -{ - VALUE count = monitor_exit_for_cond(monitor); - struct wait_for_cond_data data = { - monitor, - cond, - timeout, - count, - }; - - return rb_ensure(monitor_wait_for_cond_body, (VALUE)&data, - monitor_enter_for_cond, (VALUE)&data); -} - -static VALUE -monitor_sync_body(VALUE monitor) -{ - return rb_yield_values(0); -} - -static VALUE -monitor_sync_ensure(VALUE v_args) -{ - monitor_exit0((struct monitor_args *)v_args); - return Qnil; -} - -/* - * call-seq: - * synchronize { } -> result of the block - * - * Enters exclusive section and executes the block. Leaves the exclusive - * section automatically when the block exits. See example under - * +MonitorMixin+. - */ -static VALUE -monitor_synchronize(VALUE monitor) -{ - struct monitor_args args; - monitor_args_init(&args, monitor); - monitor_enter0(&args); - return rb_ensure(monitor_sync_body, (VALUE)&args, monitor_sync_ensure, (VALUE)&args); -} - -void -Init_monitor(void) -{ -#ifdef HAVE_RB_EXT_RACTOR_SAFE - rb_ext_ractor_safe(true); -#endif - - VALUE rb_cMonitor = rb_define_class("Monitor", rb_cObject); - rb_define_alloc_func(rb_cMonitor, monitor_alloc); - - rb_define_method(rb_cMonitor, "try_enter", monitor_try_enter, 0); - rb_define_method(rb_cMonitor, "enter", monitor_enter, 0); - rb_define_method(rb_cMonitor, "exit", monitor_exit, 0); - rb_define_method(rb_cMonitor, "synchronize", monitor_synchronize, 0); - - /* internal methods for MonitorMixin */ - rb_define_method(rb_cMonitor, "mon_locked?", monitor_locked_p, 0); - rb_define_method(rb_cMonitor, "mon_check_owner", monitor_check_owner, 0); - rb_define_method(rb_cMonitor, "mon_owned?", monitor_owned_p, 0); - - /* internal methods for MonitorMixin::ConditionVariable */ - rb_define_method(rb_cMonitor, "wait_for_cond", monitor_wait_for_cond, 2); -} diff --git a/ext/monitor/lib/monitor.rb b/lib/monitor.rb similarity index 78% rename from ext/monitor/lib/monitor.rb rename to lib/monitor.rb index 82d0a75c5665f4..21329a5de754e4 100644 --- a/ext/monitor/lib/monitor.rb +++ b/lib/monitor.rb @@ -6,9 +6,6 @@ # This library is distributed under the terms of the Ruby license. # You can freely distribute/modify this library. # - -require 'monitor.so' - # # In concurrent programming, a monitor is an object or module intended to be # used safely by more than one thread. The defining characteristic of a @@ -89,65 +86,14 @@ # MonitorMixin module. # module MonitorMixin + ConditionVariable = Monitor::ConditionVariable # :nodoc: + # # FIXME: This isn't documented in Nutshell. # # Since MonitorMixin.new_cond returns a ConditionVariable, and the example # above calls while_wait and signal, this class should be documented. # - class ConditionVariable - # - # Releases the lock held in the associated monitor and waits; reacquires the lock on wakeup. - # - # If +timeout+ is given, this method returns after +timeout+ seconds passed, - # even if no other thread doesn't signal. - # - def wait(timeout = nil) - @monitor.mon_check_owner - @monitor.wait_for_cond(@cond, timeout) - end - - # - # Calls wait repeatedly while the given block yields a truthy value. - # - def wait_while - while yield - wait - end - end - - # - # Calls wait repeatedly until the given block yields a truthy value. - # - def wait_until - until yield - wait - end - end - - # - # Wakes up the first thread in line waiting for this lock. - # - def signal - @monitor.mon_check_owner - @cond.signal - end - - # - # Wakes up all threads waiting for this lock. - # - def broadcast - @monitor.mon_check_owner - @cond.broadcast - end - - private - - def initialize(monitor) # :nodoc: - @monitor = monitor - @cond = Thread::ConditionVariable.new - end - end def self.extend_object(obj) # :nodoc: super(obj) @@ -245,26 +191,7 @@ def mon_check_owner end end -# Use the Monitor class when you want to have a lock object for blocks with -# mutual exclusion. -# -# require 'monitor' -# -# lock = Monitor.new -# lock.synchronize do -# # exclusive access -# end -# -class Monitor - # - # Creates a new MonitorMixin::ConditionVariable associated with the - # Monitor object. - # - def new_cond - ::MonitorMixin::ConditionVariable.new(self) - end - - # for compatibility +class Monitor # :nodoc: alias try_mon_enter try_enter alias mon_try_enter try_enter alias mon_enter enter diff --git a/spec/ruby/core/kernel/require_spec.rb b/spec/ruby/core/kernel/require_spec.rb index 60d17242fe1a41..da2f48fb61500b 100644 --- a/spec/ruby/core/kernel/require_spec.rb +++ b/spec/ruby/core/kernel/require_spec.rb @@ -21,13 +21,16 @@ provided << "set" provided << "pathname" end + ruby_version_is "4.1" do + provided << "monitor" + end it "#{provided.join(', ')} are already required" do out = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems --disable-did-you-mean') features = out.lines.map { |line| File.basename(line.chomp, '.*') } # Ignore CRuby internals - features -= %w[encdb transdb windows_1252 windows_31j] + features -= %w[encdb transdb windows_1252 windows_31] features.reject! { |feature| feature.end_with?('-fake') } features.sort.should == provided.sort @@ -37,6 +40,10 @@ requires = requires.map { |f| f == "pathname" ? "pathname.so" : f } end + ruby_version_is "4.1" do + requires = requires.map { |f| f == "monitor" ? "monitor.so" : f } + end + code = requires.map { |f| "puts require #{f.inspect}\n" }.join required = ruby_exe(code, options: '--disable-gems') required.should == "false\n" * requires.size diff --git a/thread_sync.c b/thread_sync.c index fa6a60ab62db36..cf4e3843ff6c2f 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -1261,6 +1261,263 @@ rb_condvar_broadcast(rb_execution_context_t *ec, VALUE self) return self; } +/* Thread::Monitor */ + +struct rb_monitor { + long count; + rb_serial_t ec_serial; + VALUE mutex; +}; + +static void +monitor_mark(void *ptr) +{ + struct rb_monitor *mc = ptr; + rb_gc_mark_movable(mc->mutex); +} + +static void +monitor_compact(void *ptr) +{ + struct rb_monitor *mc = ptr; + mc->mutex = rb_gc_location(mc->mutex); +} + +static const rb_data_type_t monitor_data_type = { + .wrap_struct_name = "monitor", + .function = { + .dmark = monitor_mark, + .dfree = RUBY_TYPED_DEFAULT_FREE, + .dsize = NULL, // Fully embeded + .dcompact = monitor_compact, + }, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, +}; + +static VALUE +monitor_alloc(VALUE klass) +{ + struct rb_monitor *mc; + VALUE obj; + + obj = TypedData_Make_Struct(klass, struct rb_monitor, &monitor_data_type, mc); + RB_OBJ_WRITE(obj, &mc->mutex, rb_mutex_new()); + mc->ec_serial = 0; + mc->count = 0; + + return obj; +} + +static struct rb_monitor * +monitor_ptr(VALUE monitor) +{ + struct rb_monitor *mc; + TypedData_Get_Struct(monitor, struct rb_monitor, &monitor_data_type, mc); + return mc; +} + +static bool +mc_owner_p(struct rb_monitor *mc, rb_serial_t current_fiber_serial) +{ + return mc->ec_serial == current_fiber_serial; +} + +static VALUE +rb_monitor_try_enter(rb_execution_context_t *ec, VALUE monitor) +{ + struct rb_monitor *mc = monitor_ptr(monitor); + + rb_serial_t current_fiber_serial = rb_ec_serial(ec); + if (!mc_owner_p(mc, current_fiber_serial)) { + if (!rb_mut_trylock(ec, mc->mutex)) { + return Qfalse; + } + mc->ec_serial = current_fiber_serial; + mc->count = 0; + } + mc->count += 1; + return Qtrue; +} + +struct monitor_args { + VALUE monitor; + struct rb_monitor *mc; + rb_serial_t current_fiber_serial; + rb_execution_context_t *ec; +}; + +static void +monitor_enter0(struct monitor_args *args) +{ + if (!mc_owner_p(args->mc, args->current_fiber_serial)) { + struct mutex_args mut_args = { + .self = args->mc->mutex, + .mutex = mutex_ptr(args->mc->mutex), + .ec= args->ec, + }; + do_mutex_lock(&mut_args, 1); + args->mc->ec_serial = args->current_fiber_serial; + args->mc->count = 0; + } + args->mc->count++; +} + +static VALUE +rb_monitor_enter(rb_execution_context_t *ec, VALUE monitor) +{ + struct monitor_args args = { + .monitor = monitor, + .mc = monitor_ptr(monitor), + .ec = ec, + .current_fiber_serial = rb_ec_serial(ec), + }; + monitor_enter0(&args); + return Qnil; +} + +static inline void +monitor_check_owner0(struct monitor_args *args) +{ + if (!mc_owner_p(args->mc, args->current_fiber_serial)) { + rb_raise(rb_eThreadError, "current fiber not owner"); + } +} + +static VALUE +rb_monitor_check_owner(rb_execution_context_t *ec, VALUE monitor) +{ + struct monitor_args args = { + .monitor = monitor, + .mc = monitor_ptr(monitor), + .ec = ec, + .current_fiber_serial = rb_ec_serial(ec), + }; + monitor_check_owner0(&args); + return Qnil; +} + +static void +monitor_exit0(struct monitor_args *args) +{ + monitor_check_owner0(args); + + if (args->mc->count <= 0) rb_bug("monitor_exit: count:%d", (int)args->mc->count); + args->mc->count--; + + if (args->mc->count == 0) { + args->mc->ec_serial = 0; + + struct mutex_args mut_args = { + .self = args->mc->mutex, + .mutex = mutex_ptr(args->mc->mutex), + .ec= args->ec, + }; + do_mutex_unlock(&mut_args); + } +} + +static VALUE +rb_monitor_exit(rb_execution_context_t *ec, VALUE monitor) +{ + struct monitor_args args = { + .monitor = monitor, + .mc = monitor_ptr(monitor), + .ec = ec, + .current_fiber_serial = rb_ec_serial(ec), + }; + monitor_exit0(&args); + return Qnil; +} + +static VALUE +rb_monitor_locked_p(rb_execution_context_t *ec, VALUE monitor) +{ + struct rb_monitor *mc = monitor_ptr(monitor); + return rb_mutex_locked_p(mc->mutex); +} + +static VALUE +rb_monitor_owned_p(rb_execution_context_t *ec, VALUE monitor) +{ + struct rb_monitor *mc = monitor_ptr(monitor); + return RBOOL(rb_mutex_locked_p(mc->mutex) && mc_owner_p(mc, rb_ec_serial(ec))); +} + +static VALUE +monitor_exit_for_cond(VALUE monitor) +{ + struct rb_monitor *mc = monitor_ptr(monitor); + long cnt = mc->count; + mc->ec_serial = 0; + mc->count = 0; + return LONG2NUM(cnt); +} + +struct wait_for_cond_data { + VALUE monitor; + VALUE cond; + VALUE timeout; + VALUE count; +}; + +static VALUE +monitor_wait_for_cond_body(VALUE v) +{ + struct wait_for_cond_data *data = (struct wait_for_cond_data *)v; + struct rb_monitor *mc = monitor_ptr(data->monitor); + // cond.wait(monitor.mutex, timeout) + VALUE signaled = rb_funcall(data->cond, rb_intern("wait"), 2, mc->mutex, data->timeout); + return RTEST(signaled) ? Qtrue : Qfalse; +} + +static VALUE +monitor_enter_for_cond(VALUE v) +{ + // assert(rb_mutex_owned_p(mc->mutex) == Qtrue) + // but rb_mutex_owned_p is not exported... + + struct wait_for_cond_data *data = (struct wait_for_cond_data *)v; + struct rb_monitor *mc = monitor_ptr(data->monitor); + mc->ec_serial = rb_ec_serial(GET_EC()); + mc->count = NUM2LONG(data->count); + return Qnil; +} + +static VALUE +rb_monitor_wait_for_cond(rb_execution_context_t *ec, VALUE monitor, VALUE cond, VALUE timeout) +{ + VALUE count = monitor_exit_for_cond(monitor); + struct wait_for_cond_data data = { + monitor, + cond, + timeout, + count, + }; + + return rb_ensure(monitor_wait_for_cond_body, (VALUE)&data, + monitor_enter_for_cond, (VALUE)&data); +} + +static VALUE +monitor_sync_ensure(VALUE v_args) +{ + monitor_exit0((struct monitor_args *)v_args); + return Qnil; +} + +static VALUE +rb_monitor_synchronize(rb_execution_context_t *ec, VALUE monitor) +{ + struct monitor_args args = { + .monitor = monitor, + .mc = monitor_ptr(monitor), + .ec = ec, + .current_fiber_serial = rb_ec_serial(ec), + }; + monitor_enter0(&args); + return rb_ec_ensure(ec, do_ec_yield, (VALUE)ec, monitor_sync_ensure, (VALUE)&args); +} + static void Init_thread_sync(void) { @@ -1283,6 +1540,11 @@ Init_thread_sync(void) id_sleep = rb_intern("sleep"); + /* Monitor */ + VALUE rb_cMonitor = rb_define_class_id_under_no_pin(rb_cThread, rb_intern("Monitor"), rb_cObject); + rb_define_alloc_func(rb_cMonitor, monitor_alloc); + + rb_provide("monitor.so"); rb_provide("thread.rb"); } diff --git a/thread_sync.rb b/thread_sync.rb index 398c0d02b7b75a..c9d37772d7c19f 100644 --- a/thread_sync.rb +++ b/thread_sync.rb @@ -529,9 +529,187 @@ def wait(mutex, timeout=nil) Primitive.rb_condvar_wait(mutex, timeout) end end + + # Use the Monitor class when you want to have a lock object for blocks with + # mutual exclusion. + # + # lock = Monitor.new + # lock.synchronize do + # # exclusive access + # end + # + # Contrary to Mutex, Monitor is reentrant: + # + # lock = Monitor.new + # lock.synchronize do + # lock.synchronize do + # # exclusive access + # end + # end + class Monitor + # call-seq: + # synchronize { } -> result of the block + # + # Enters exclusive section and executes the block. Leaves the exclusive + # section automatically when the block exits. See example under + # +MonitorMixin+. + def synchronize(&) + Primitive.rb_monitor_synchronize + end + + # call-seq: + # try_enter -> true or false + # + # Attempts to enter exclusive section. Returns +false+ if lock fails. + def try_enter + Primitive.rb_monitor_try_enter + end + + # call-seq: + # enter -> nil + # + # Enters exclusive section. + def enter + Primitive.rb_monitor_enter + end + + # call-seq: + # exit -> nil + # + # Leaves exclusive section. + def exit + Primitive.rb_monitor_exit + end + + # internal methods for MonitorMixin + def mon_check_owner # :nodoc: + Primitive.rb_monitor_check_owner + end + + def mon_locked? # :nodoc: + Primitive.rb_monitor_locked_p + end + + def mon_owned? # :nodoc: + Primitive.rb_monitor_owned_p + end + + # internal methods for MonitorMixin::ConditionVariable + def wait_for_cond(cond, timeout) # :nodoc: + Primitive.rb_monitor_wait_for_cond(cond, timeout) + end + + # Creates a new Monitor::ConditionVariable associated with the + # Monitor object. + # + def new_cond + ConditionVariable.new(self) + end + + # Condition variables, allow to suspend the current thread while in + # the middle of a critical section until a condition is met, such as + # a resource being available. + # + # Example: + # + # monitor = Thread::Monitor.new + # + # resource_available = false + # condvar = monitor.new_cond + # + # a1 = Thread.new { + # # Thread 'a1' waits for the resource to become available and consumes + # # the resource. + # monitor.synchronize { + # condvar.wait_until { resource_available } + # # After the loop, 'resource_available' is guaranteed to be true. + # + # resource_available = false + # puts "a1 consumed the resource" + # } + # } + # + # a2 = Thread.new { + # # Thread 'a2' behaves like 'a1'. + # monitor.synchronize { + # condvar.wait_until { resource_available } + # resource_available = false + # puts "a2 consumed the resource" + # } + # } + # + # b = Thread.new { + # # Thread 'b' periodically makes the resource available. + # loop { + # monitor.synchronize { + # resource_available = true + # + # # Notify one waiting thread if any. It is possible that neither + # # 'a1' nor 'a2 is waiting on 'condvar' at this moment. That's OK. + # condvar.signal + # } + # sleep 1 + # } + # } + # + # # Eventually both 'a1' and 'a2' will have their resources, albeit in an + # # unspecified order. + # [a1, a2].each {|th| th.join} + class ConditionVariable + def initialize(monitor) # :nodoc: + @monitor = monitor + @cond = Thread::ConditionVariable.new + end + + # Releases the lock held in the associated monitor and waits; reacquires the lock on wakeup. + # + # If +timeout+ is given, this method returns after +timeout+ seconds passed, + # even if no other thread doesn't signal. + # + def wait(timeout = nil) + @monitor.mon_check_owner + @monitor.wait_for_cond(@cond, timeout) + end + + # + # Calls wait repeatedly while the given block yields a truthy value. + # + def wait_while + while yield + wait + end + end + + # + # Calls wait repeatedly until the given block yields a truthy value. + # + def wait_until + until yield + wait + end + end + + # + # Wakes up the first thread in line waiting for this lock. + # + def signal + @monitor.mon_check_owner + @cond.signal + end + + # + # Wakes up all threads waiting for this lock. + # + def broadcast + @monitor.mon_check_owner + @cond.broadcast + end + end + end end -Mutex = Thread::Mutex -ConditionVariable = Thread::ConditionVariable -Queue = Thread::Queue -SizedQueue = Thread::SizedQueue +Mutex = Thread::Mutex # :nodoc: +Monitor = Thread::Monitor # :nodoc: +ConditionVariable = Thread::ConditionVariable # :nodoc: +Queue = Thread::Queue # :nodoc: +SizedQueue = Thread::SizedQueue # :nodoc: From c61f52a012f0a390a869db4825143187ea468d21 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 12 Feb 2026 18:11:39 -0500 Subject: [PATCH 2/5] [Feature #21785] Add LEB128 again (#16123) * Revert "Revert pack/unpack support for LEB128" This reverts commit 77c3a9e447ec477be39e00072e1ce3348d0f4533. * Update specs for LEB128 --- NEWS.md | 3 + doc/language/packed_data.rdoc | 2 + pack.c | 83 ++++++++++++++++ spec/ruby/core/array/pack/r_spec.rb | 89 +++++++++++++++++ spec/ruby/core/array/pack/shared/basic.rb | 2 +- spec/ruby/core/string/unpack/r_spec.rb | 85 ++++++++++++++++ test/ruby/test_pack.rb | 112 ++++++++++++++++++++++ 7 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 spec/ruby/core/array/pack/r_spec.rb create mode 100644 spec/ruby/core/string/unpack/r_spec.rb diff --git a/NEWS.md b/NEWS.md index cdeb44054e1d3f..47deb9c38892ce 100644 --- a/NEWS.md +++ b/NEWS.md @@ -19,6 +19,8 @@ Note: We're only listing outstanding class updates. end_line, end_column]`. The previous 2-element format `[path, line]` can still be obtained by calling `.take(2)` on the result. [[Feature #6012]] + * `Array#pack` accepts a new format `R` and `r` for unpacking unsigned + and signed LEB128 encoded integers. [[Feature #21785]] * Set @@ -91,3 +93,4 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #6012]: https://bugs.ruby-lang.org/issues/6012 [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 +[Feature #21785]: https://bugs.ruby-lang.org/issues/21785 diff --git a/doc/language/packed_data.rdoc b/doc/language/packed_data.rdoc index 597db5139f87b2..2dce7bb57ce498 100644 --- a/doc/language/packed_data.rdoc +++ b/doc/language/packed_data.rdoc @@ -53,6 +53,8 @@ These tables summarize the directives for packing and unpacking. U | UTF-8 character w | BER-compressed integer + R | LEB128 encoded unsigned integer + r | LEB128 encoded signed integer === For Floats diff --git a/pack.c b/pack.c index 2ef826b0e2a17f..d2fb4f633fd22b 100644 --- a/pack.c +++ b/pack.c @@ -667,6 +667,56 @@ pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) } break; + case 'r': /* r for SLEB128 encoding (signed) */ + case 'R': /* R for ULEB128 encoding (unsigned) */ + { + int pack_flags = INTEGER_PACK_LITTLE_ENDIAN; + + if (type == 'r') { + pack_flags |= INTEGER_PACK_2COMP; + } + + while (len-- > 0) { + size_t numbytes; + int sign; + char *cp; + + from = NEXTFROM; + from = rb_to_int(from); + numbytes = rb_absint_numwords(from, 7, NULL); + if (numbytes == 0) + numbytes = 1; + VALUE buf = rb_str_new(NULL, numbytes); + + sign = rb_integer_pack(from, RSTRING_PTR(buf), RSTRING_LEN(buf), 1, 1, pack_flags); + + if (sign < 0 && type == 'R') { + rb_raise(rb_eArgError, "can't encode negative numbers in ULEB128"); + } + + if (type == 'r') { + /* Check if we need an extra byte for sign extension */ + unsigned char last_byte = (unsigned char)RSTRING_PTR(buf)[numbytes - 1]; + if ((sign >= 0 && (last_byte & 0x40)) || /* positive but sign bit set */ + (sign < 0 && !(last_byte & 0x40))) { /* negative but sign bit clear */ + /* Need an extra byte */ + rb_str_resize(buf, numbytes + 1); + RSTRING_PTR(buf)[numbytes] = sign < 0 ? 0x7f : 0x00; + numbytes++; + } + } + + cp = RSTRING_PTR(buf); + while (1 < numbytes) { + *cp |= 0x80; + cp++; + numbytes--; + } + + rb_str_buf_cat(res, RSTRING_PTR(buf), RSTRING_LEN(buf)); + } + } + break; case 'u': /* uuencoded string */ case 'm': /* base64 encoded string */ from = NEXTFROM; @@ -1558,6 +1608,39 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) } break; + case 'r': + case 'R': + { + int pack_flags = INTEGER_PACK_LITTLE_ENDIAN; + + if (type == 'r') { + pack_flags |= INTEGER_PACK_2COMP; + } + char *s0 = s; + while (len > 0 && s < send) { + if (*s & 0x80) { + s++; + } + else { + s++; + UNPACK_PUSH(rb_integer_unpack(s0, s-s0, 1, 1, pack_flags)); + len--; + s0 = s; + } + } + /* Handle incomplete value and remaining expected values with nil (only if not using *) */ + if (!star) { + if (s0 != s && len > 0) { + UNPACK_PUSH(Qnil); + len--; + } + while (len-- > 0) { + UNPACK_PUSH(Qnil); + } + } + } + break; + case 'w': { char *s0 = s; diff --git a/spec/ruby/core/array/pack/r_spec.rb b/spec/ruby/core/array/pack/r_spec.rb new file mode 100644 index 00000000000000..62211e3a8efb1e --- /dev/null +++ b/spec/ruby/core/array/pack/r_spec.rb @@ -0,0 +1,89 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/integer' + +ruby_version_is "4.1" do + describe "Array#pack with format 'R'" do + it_behaves_like :array_pack_basic, 'R' + it_behaves_like :array_pack_basic_non_float, 'R' + it_behaves_like :array_pack_arguments, 'R' + it_behaves_like :array_pack_numeric_basic, 'R' + it_behaves_like :array_pack_integer, 'R' + + it "encodes a ULEB128 integer" do + [ [[0], "\x00"], + [[1], "\x01"], + [[127], "\x7f"], + [[128], "\x80\x01"], + [[0x3fff], "\xff\x7f"], + [[0x4000], "\x80\x80\x01"], + [[0xffffffff], "\xff\xff\xff\xff\x0f"], + [[0x100000000], "\x80\x80\x80\x80\x10"], + [[0xffff_ffff_ffff_ffff], "\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01"], + [[0xffff_ffff_ffff_ffff_ffff_ffff], "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f"], + ].should be_computed_by(:pack, "R") + end + + it "encodes multiple values with '*' modifier" do + [1, 2].pack("R*").should == "\x01\x02" + [127, 128].pack("R*").should == "\x7f\x80\x01" + end + + it "raises an ArgumentError when passed a negative value" do + -> { [-1].pack("R") }.should raise_error(ArgumentError) + -> { [-100].pack("R") }.should raise_error(ArgumentError) + end + + it "round-trips values through pack and unpack" do + values = [0, 1, 127, 128, 0x3fff, 0x4000, 0xffffffff, 0x100000000] + values.pack("R*").unpack("R*").should == values + end + end + + describe "Array#pack with format 'r'" do + it_behaves_like :array_pack_basic, 'r' + it_behaves_like :array_pack_basic_non_float, 'r' + it_behaves_like :array_pack_arguments, 'r' + it_behaves_like :array_pack_numeric_basic, 'r' + it_behaves_like :array_pack_integer, 'r' + + it "encodes a SLEB128 integer" do + [ [[0], "\x00"], + [[1], "\x01"], + [[-1], "\x7f"], + [[-2], "\x7e"], + [[127], "\xff\x00"], + [[128], "\x80\x01"], + [[-127], "\x81\x7f"], + [[-128], "\x80\x7f"], + ].should be_computed_by(:pack, "r") + end + + it "encodes larger positive numbers" do + [0x3fff].pack("r").should == "\xff\xff\x00" + [0x4000].pack("r").should == "\x80\x80\x01" + end + + it "encodes larger negative numbers" do + [-0x3fff].pack("r").should == "\x81\x80\x7f" + [-0x4000].pack("r").should == "\x80\x80\x7f" + end + + it "encodes very large numbers" do + [0xffff_ffff_ffff_ffff_ffff_ffff].pack("r").should == "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x1F" + [-0xffff_ffff_ffff_ffff_ffff_ffff].pack("r").should == "\x81\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x60" + end + + it "encodes multiple values with '*' modifier" do + [0, 1, -1].pack("r*").should == "\x00\x01\x7f" + end + + it "round-trips values through pack and unpack" do + values = [0, 1, -1, 127, -127, 128, -128, 0x3fff, -0x3fff, 0x4000, -0x4000] + values.pack("r*").unpack("r*").should == values + end + end +end diff --git a/spec/ruby/core/array/pack/shared/basic.rb b/spec/ruby/core/array/pack/shared/basic.rb index 2ebd75f6c5ed79..280212ecb86229 100644 --- a/spec/ruby/core/array/pack/shared/basic.rb +++ b/spec/ruby/core/array/pack/shared/basic.rb @@ -34,7 +34,7 @@ it "raise ArgumentError when a directive is unknown" do # additional directive ('a') is required for the X directive - -> { [@obj, @obj].pack("a R" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive 'R'/) + -> { [@obj, @obj].pack("a K" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive 'K'/) -> { [@obj, @obj].pack("a 0" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive '0'/) -> { [@obj, @obj].pack("a :" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive ':'/) end diff --git a/spec/ruby/core/string/unpack/r_spec.rb b/spec/ruby/core/string/unpack/r_spec.rb new file mode 100644 index 00000000000000..a385951aa852b4 --- /dev/null +++ b/spec/ruby/core/string/unpack/r_spec.rb @@ -0,0 +1,85 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' + +ruby_version_is "4.1" do + describe "String#unpack with format 'R'" do + it_behaves_like :string_unpack_basic, 'R' + it_behaves_like :string_unpack_no_platform, 'R' + + it "decodes a ULEB128 integer" do + [ ["\x00", [0]], + ["\x01", [1]], + ["\x7f", [127]], + ["\x80\x01", [128]], + ["\xff\x7f", [0x3fff]], + ["\x80\x80\x01", [0x4000]], + ["\xff\xff\xff\xff\x0f", [0xffffffff]], + ["\x80\x80\x80\x80\x10", [0x100000000]], + ["\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01", [0xffff_ffff_ffff_ffff]], + ].should be_computed_by(:unpack, "R") + end + + it "decodes multiple values with '*' modifier" do + "\x01\x02".unpack("R*").should == [1, 2] + "\x7f\x80\x01".unpack("R*").should == [127, 128] + end + + it "returns nil for incomplete data" do + "\xFF".unpack("R").should == [nil] + "\xFF".unpack1("R").should == nil + end + + it "returns nil for remaining incomplete values after a valid one" do + bytes = [256].pack("R") + (bytes + "\xFF").unpack("RRRR").should == [256, nil, nil, nil] + end + + it "skips incomplete values with '*' modifier" do + "\xFF".unpack("R*").should == [] + end + end + + describe "String#unpack with format 'r'" do + it_behaves_like :string_unpack_basic, 'r' + it_behaves_like :string_unpack_no_platform, 'r' + + it "decodes a SLEB128 integer" do + [ ["\x00", [0]], + ["\x01", [1]], + ["\x7f", [-1]], + ["\x7e", [-2]], + ["\xff\x00", [127]], + ["\x80\x01", [128]], + ["\x81\x7f", [-127]], + ["\x80\x7f", [-128]], + ].should be_computed_by(:unpack, "r") + end + + it "decodes larger numbers" do + "\xff\xff\x00".unpack("r").should == [0x3fff] + "\x80\x80\x01".unpack("r").should == [0x4000] + "\x81\x80\x7f".unpack("r").should == [-0x3fff] + "\x80\x80\x7f".unpack("r").should == [-0x4000] + end + + it "decodes multiple values with '*' modifier" do + "\x00\x01\x7f".unpack("r*").should == [0, 1, -1] + end + + it "returns nil for incomplete data" do + "\xFF".unpack("r").should == [nil] + "\xFF".unpack1("r").should == nil + end + + it "returns nil for remaining incomplete values after a valid one" do + bytes = [256].pack("r") + (bytes + "\xFF").unpack("rrrr").should == [256, nil, nil, nil] + end + + it "skips incomplete values with '*' modifier" do + "\xFF".unpack("r*").should == [] + end + end +end diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index ca089f09c3dc4b..9c40cfaa204f86 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -936,4 +936,116 @@ class Array assert_equal "oh no", v end; end + + def test_unpack_broken_R + assert_equal([nil], "\xFF".unpack("R")) + assert_nil("\xFF".unpack1("R")) + assert_equal([nil], "\xFF".unpack("r")) + assert_nil("\xFF".unpack1("r")) + + bytes = [256].pack("r") + assert_equal([256, nil, nil, nil], (bytes + "\xFF").unpack("rrrr")) + + bytes = [256].pack("R") + assert_equal([256, nil, nil, nil], (bytes + "\xFF").unpack("RRRR")) + + assert_equal([], "\xFF".unpack("R*")) + assert_equal([], "\xFF".unpack("r*")) + end + + def test_pack_unpack_R + # ULEB128 encoding (unsigned) + assert_equal("\x00", [0].pack("R")) + assert_equal("\x01", [1].pack("R")) + assert_equal("\x7f", [127].pack("R")) + assert_equal("\x80\x01", [128].pack("R")) + assert_equal("\xff\x7f", [0x3fff].pack("R")) + assert_equal("\x80\x80\x01", [0x4000].pack("R")) + assert_equal("\xff\xff\xff\xff\x0f", [0xffffffff].pack("R")) + assert_equal("\x80\x80\x80\x80\x10", [0x100000000].pack("R")) + assert_equal("\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01", [0xffff_ffff_ffff_ffff].pack("R")) + assert_equal("\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f", [0xffff_ffff_ffff_ffff_ffff_ffff].pack("R")) + + # Multiple values + assert_equal("\x01\x02", [1, 2].pack("R*")) + assert_equal("\x7f\x80\x01", [127, 128].pack("R*")) + + # Negative numbers should raise an error + assert_raise(ArgumentError) { [-1].pack("R") } + assert_raise(ArgumentError) { [-100].pack("R") } + + # Unpack tests + assert_equal([0], "\x00".unpack("R")) + assert_equal([1], "\x01".unpack("R")) + assert_equal([127], "\x7f".unpack("R")) + assert_equal([128], "\x80\x01".unpack("R")) + assert_equal([0x3fff], "\xff\x7f".unpack("R")) + assert_equal([0x4000], "\x80\x80\x01".unpack("R")) + assert_equal([0xffffffff], "\xff\xff\xff\xff\x0f".unpack("R")) + assert_equal([0x100000000], "\x80\x80\x80\x80\x10".unpack("R")) + assert_equal([0xffff_ffff_ffff_ffff], "\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01".unpack("R")) + assert_equal([0xffff_ffff_ffff_ffff_ffff_ffff].pack("R"), "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f") + + # Multiple values + assert_equal([1, 2], "\x01\x02".unpack("R*")) + assert_equal([127, 128], "\x7f\x80\x01".unpack("R*")) + + # Round-trip test + values = [0, 1, 127, 128, 0x3fff, 0x4000, 0xffffffff, 0x100000000] + assert_equal(values, values.pack("R*").unpack("R*")) + end + + def test_pack_unpack_r + # SLEB128 encoding (signed) + assert_equal("\x00", [0].pack("r")) + assert_equal("\x01", [1].pack("r")) + assert_equal("\x7f", [-1].pack("r")) + assert_equal("\x7e", [-2].pack("r")) + assert_equal("\xff\x00", [127].pack("r")) + assert_equal("\x80\x01", [128].pack("r")) + assert_equal("\x81\x7f", [-127].pack("r")) + assert_equal("\x80\x7f", [-128].pack("r")) + + # Larger positive numbers + assert_equal("\xff\xff\x00", [0x3fff].pack("r")) + assert_equal("\x80\x80\x01", [0x4000].pack("r")) + + # Larger negative numbers + assert_equal("\x81\x80\x7f", [-0x3fff].pack("r")) + assert_equal("\x80\x80\x7f", [-0x4000].pack("r")) + + # Very large numbers + assert_equal("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x1F", [0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + assert_equal("\x81\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80`", [-0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + + # Multiple values + assert_equal("\x00\x01\x7f", [0, 1, -1].pack("r*")) + + # Unpack tests + assert_equal([0], "\x00".unpack("r")) + assert_equal([1], "\x01".unpack("r")) + assert_equal([-1], "\x7f".unpack("r")) + assert_equal([-2], "\x7e".unpack("r")) + assert_equal([127], "\xff\x00".unpack("r")) + assert_equal([128], "\x80\x01".unpack("r")) + assert_equal([-127], "\x81\x7f".unpack("r")) + assert_equal([-128], "\x80\x7f".unpack("r")) + + # Larger numbers + assert_equal([0x3fff], "\xff\xff\x00".unpack("r")) + assert_equal([0x4000], "\x80\x80\x01".unpack("r")) + assert_equal([-0x3fff], "\x81\x80\x7f".unpack("r")) + assert_equal([-0x4000], "\x80\x80\x7f".unpack("r")) + + # Very large numbers + assert_equal("\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f", [0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + assert_equal("\x81\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80`", [-0xffff_ffff_ffff_ffff_ffff_ffff].pack("r")) + + # Multiple values + assert_equal([0, 1, -1], "\x00\x01\x7f".unpack("r*")) + + # Round-trip test + values = [0, 1, -1, 127, -127, 128, -128, 0x3fff, -0x3fff, 0x4000, -0x4000] + assert_equal(values, values.pack("r*").unpack("r*")) + end end From f0ff0df584b75b2588952e516d16ff8f134f9832 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 12 Feb 2026 18:21:59 -0500 Subject: [PATCH 3/5] Introduce suport for `autoload_relative`. (#16148) --- NEWS.md | 10 ++ load.c | 77 +++++++++++ .../core/kernel/autoload_relative_spec.rb | 114 ++++++++++++++++ .../kernel/fixtures/autoload_relative_b.rb | 7 + .../kernel/fixtures/autoload_relative_d.rb | 5 + .../core/module/autoload_relative_spec.rb | 128 ++++++++++++++++++ .../module/fixtures/autoload_relative_a.rb | 9 ++ test/ruby/test_autoload.rb | 89 ++++++++++++ 8 files changed, 439 insertions(+) create mode 100644 spec/ruby/core/kernel/autoload_relative_spec.rb create mode 100644 spec/ruby/core/kernel/fixtures/autoload_relative_b.rb create mode 100644 spec/ruby/core/kernel/fixtures/autoload_relative_d.rb create mode 100644 spec/ruby/core/module/autoload_relative_spec.rb create mode 100644 spec/ruby/core/module/fixtures/autoload_relative_a.rb diff --git a/NEWS.md b/NEWS.md index 47deb9c38892ce..b09eadeb055137 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,15 @@ Note that each entry is kept to a minimum, see links for details. Note: We're only listing outstanding class updates. +* Kernel + + * `Kernel#autoload_relative` and `Module#autoload_relative` are added. + These methods work like `autoload`, but resolve the file path relative + to the file where the method is called, similar to `require_relative`. + This makes it easier to autoload constants from files in the same + directory without hardcoding absolute paths or manipulating `$LOAD_PATH`. + [[Feature #15330]] + * Method * `Method#source_location`, `Proc#source_location`, and @@ -92,5 +101,6 @@ A lot of work has gone into making Ractors more stable, performant, and usable. ## JIT [Feature #6012]: https://bugs.ruby-lang.org/issues/6012 +[Feature #15330]: https://bugs.ruby-lang.org/issues/15330 [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 [Feature #21785]: https://bugs.ruby-lang.org/issues/21785 diff --git a/load.c b/load.c index 391a6c13372fc1..e33abeb0df7451 100644 --- a/load.c +++ b/load.c @@ -1536,6 +1536,49 @@ rb_mod_autoload(VALUE mod, VALUE sym, VALUE file) return Qnil; } +/* + * call-seq: + * mod.autoload_relative(const, filename) -> nil + * + * Registers _filename_ to be loaded (using Kernel::require) + * the first time that _const_ (which may be a String or + * a symbol) is accessed in the namespace of _mod_. The _filename_ + * is interpreted as relative to the directory of the file where + * autoload_relative is called. + * + * module A + * end + * A.autoload_relative(:B, "b.rb") + * + * If _const_ in _mod_ is defined as autoload, the file name to be + * loaded is replaced with _filename_. If _const_ is defined but not + * as autoload, does nothing. + * + * The relative path is converted to an absolute path, which is what + * will be returned by Module#autoload? for the constant. + * + * Raises LoadError if called without file context (e.g., from eval). + */ + +static VALUE +rb_mod_autoload_relative(VALUE mod, VALUE sym, VALUE file) +{ + ID id = rb_to_id(sym); + VALUE base, absolute_path; + + FilePathValue(file); + + base = rb_current_realfilepath(); + if (NIL_P(base)) { + rb_loaderror("cannot infer basepath (autoload_relative called without file context)"); + } + base = rb_file_dirname(base); + absolute_path = rb_file_absolute_path(file, base); + + rb_autoload_str(mod, id, absolute_path); + return Qnil; +} + /* * call-seq: * mod.autoload?(name, inherit=true) -> String or nil @@ -1603,6 +1646,38 @@ rb_f_autoload(VALUE obj, VALUE sym, VALUE file) return rb_mod_autoload(klass, sym, file); } +/* + * call-seq: + * autoload_relative(const, filename) -> nil + * + * Registers _filename_ to be loaded (using Kernel::require) + * the first time that _const_ (which may be a String or + * a symbol) is accessed. The _filename_ is interpreted as + * relative to the directory of the file where autoload_relative + * is called. + * + * autoload_relative(:MyModule, "my_module.rb") + * + * If _const_ is defined as autoload, the file name to be loaded is + * replaced with _filename_. If _const_ is defined but not as + * autoload, does nothing. + * + * The relative path is converted to an absolute path, which is what + * will be returned by Kernel#autoload? for the constant. + * + * Raises LoadError if called without file context (e.g., from eval). + */ + +static VALUE +rb_f_autoload_relative(VALUE obj, VALUE sym, VALUE file) +{ + VALUE klass = rb_class_real(rb_vm_cbase()); + if (!klass) { + rb_raise(rb_eTypeError, "Can not set autoload on singleton class"); + } + return rb_mod_autoload_relative(klass, sym, file); +} + /* * call-seq: * autoload?(name, inherit=true) -> String or nil @@ -1689,7 +1764,9 @@ Init_load(void) rb_define_global_function("require", rb_f_require, 1); rb_define_global_function("require_relative", rb_f_require_relative, 1); rb_define_method(rb_cModule, "autoload", rb_mod_autoload, 2); + rb_define_method(rb_cModule, "autoload_relative", rb_mod_autoload_relative, 2); rb_define_method(rb_cModule, "autoload?", rb_mod_autoload_p, -1); rb_define_global_function("autoload", rb_f_autoload, 2); + rb_define_global_function("autoload_relative", rb_f_autoload_relative, 2); rb_define_global_function("autoload?", rb_f_autoload_p, -1); } diff --git a/spec/ruby/core/kernel/autoload_relative_spec.rb b/spec/ruby/core/kernel/autoload_relative_spec.rb new file mode 100644 index 00000000000000..c21e7803a458c2 --- /dev/null +++ b/spec/ruby/core/kernel/autoload_relative_spec.rb @@ -0,0 +1,114 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# Specs for Kernel#autoload_relative + +ruby_version_is "4.1" do + describe "Kernel#autoload_relative" do + before :each do + @loaded_features = $".dup + end + + after :each do + $".replace @loaded_features + # Clean up constants defined by these tests + [:KSAutoloadRelativeA, :KSAutoloadRelativeB, :KSAutoloadRelativeC, + :KSAutoloadRelativeE, :KSAutoloadRelativeF, :KSAutoloadRelativeG, + :KSAutoloadRelativeH, :KSAutoloadRelativeI].each do |const| + KernelSpecs.send(:remove_const, const) if KernelSpecs.const_defined?(const, false) + end + [:KSAutoloadRelativeD, :NestedTest].each do |const| + Object.send(:remove_const, const) if Object.const_defined?(const, false) + end + end + + it "is a private method" do + Kernel.should have_private_instance_method(:autoload_relative) + end + + it "registers a file to load relative to the current file" do + KernelSpecs.autoload_relative :KSAutoloadRelativeA, "fixtures/autoload_relative_b.rb" + path = KernelSpecs.autoload?(:KSAutoloadRelativeA) + path.should_not be_nil + path.should.end_with?("autoload_relative_b.rb") + File.exist?(path).should be_true + end + + it "loads the file when the constant is accessed" do + KernelSpecs.autoload_relative :KSAutoloadRelativeB, "fixtures/autoload_relative_b.rb" + KernelSpecs::KSAutoloadRelativeB.loaded.should == :ksautoload_b + end + + it "sets the autoload constant in the constant table" do + KernelSpecs.autoload_relative :KSAutoloadRelativeC, "fixtures/autoload_relative_b.rb" + KernelSpecs.should have_constant(:KSAutoloadRelativeC) + end + + it "can autoload in instance_eval with a file context" do + result = Object.new.instance_eval(<<-CODE, __FILE__, __LINE__) + autoload_relative :KSAutoloadRelativeD, "fixtures/autoload_relative_d.rb" + KSAutoloadRelativeD.loaded + CODE + result.should == :ksautoload_d + end + + it "raises LoadError if called from eval without file context" do + -> { + eval('autoload_relative :Foo, "foo.rb"') + }.should raise_error(LoadError, /autoload_relative called without file context/) + end + + it "accepts both string and symbol for constant name" do + KernelSpecs.autoload_relative :KSAutoloadRelativeE, "fixtures/autoload_relative_b.rb" + KernelSpecs.autoload_relative "KSAutoloadRelativeF", "fixtures/autoload_relative_b.rb" + + KernelSpecs.should have_constant(:KSAutoloadRelativeE) + KernelSpecs.should have_constant(:KSAutoloadRelativeF) + end + + it "returns nil" do + KernelSpecs.autoload_relative(:KSAutoloadRelativeG, "fixtures/autoload_relative_b.rb").should be_nil + end + + it "resolves nested directory paths correctly" do + -> { + autoload_relative :NestedTest, "../kernel/fixtures/autoload_relative_b.rb" + autoload?(:NestedTest) + }.should_not raise_error + end + + it "resolves paths starting with ./" do + KernelSpecs.autoload_relative :KSAutoloadRelativeH, "./fixtures/autoload_relative_b.rb" + path = KernelSpecs.autoload?(:KSAutoloadRelativeH) + path.should_not be_nil + path.should.end_with?("autoload_relative_b.rb") + end + + it "ignores $LOAD_PATH and uses only relative path resolution" do + original_load_path = $LOAD_PATH.dup + $LOAD_PATH.clear + begin + KernelSpecs.autoload_relative :KSAutoloadRelativeI, "fixtures/autoload_relative_b.rb" + path = KernelSpecs.autoload?(:KSAutoloadRelativeI) + path.should_not be_nil + # Should still resolve even with empty $LOAD_PATH + File.exist?(path).should be_true + ensure + $LOAD_PATH.replace(original_load_path) + end + end + + describe "when Object is frozen" do + it "raises a FrozenError before defining the constant" do + ruby_exe(<<-RUBY).should include("FrozenError") + Object.freeze + begin + autoload_relative :Foo, "autoload_b.rb" + rescue => e + puts e.class + end + RUBY + end + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/autoload_relative_b.rb b/spec/ruby/core/kernel/fixtures/autoload_relative_b.rb new file mode 100644 index 00000000000000..6de6f5091d45fc --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_relative_b.rb @@ -0,0 +1,7 @@ +module KernelSpecs + module KSAutoloadRelativeB + def self.loaded + :ksautoload_b + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/autoload_relative_d.rb b/spec/ruby/core/kernel/fixtures/autoload_relative_d.rb new file mode 100644 index 00000000000000..5b6b5e1fa2f629 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_relative_d.rb @@ -0,0 +1,5 @@ +module KSAutoloadRelativeD + def self.loaded + :ksautoload_d + end +end diff --git a/spec/ruby/core/module/autoload_relative_spec.rb b/spec/ruby/core/module/autoload_relative_spec.rb new file mode 100644 index 00000000000000..ecbab9b4d56eea --- /dev/null +++ b/spec/ruby/core/module/autoload_relative_spec.rb @@ -0,0 +1,128 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# Specs for Module#autoload_relative +module ModuleSpecs + module AutoloadRelative + # Will be used for testing + end +end + +ruby_version_is "4.1" do + describe "Module#autoload_relative" do + before :each do + @loaded_features = $".dup + end + + after :each do + $".replace @loaded_features + end + + it "is a public method" do + Module.should have_public_instance_method(:autoload_relative, false) + end + + it "registers a file to load relative to the current file the first time the named constant is accessed" do + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeA, "fixtures/autoload_relative_a.rb" + path = ModuleSpecs::Autoload.autoload?(:AutoloadRelativeA) + path.should_not be_nil + path.should.end_with?("autoload_relative_a.rb") + File.exist?(path).should be_true + end + + it "loads the registered file when the constant is accessed" do + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeB, "fixtures/autoload_relative_a.rb" + ModuleSpecs::Autoload::AutoloadRelativeB.should be_kind_of(Module) + end + + it "returns nil" do + ModuleSpecs::Autoload.autoload_relative(:AutoloadRelativeC, "fixtures/autoload_relative_a.rb").should be_nil + end + + it "registers a file to load the first time the named constant is accessed" do + module ModuleSpecs::Autoload::AutoloadRelativeTest + autoload_relative :D, "fixtures/autoload_relative_a.rb" + end + path = ModuleSpecs::Autoload::AutoloadRelativeTest.autoload?(:D) + path.should_not be_nil + path.should.end_with?("autoload_relative_a.rb") + end + + it "sets the autoload constant in the constants table" do + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeTableTest, "fixtures/autoload_relative_a.rb" + ModuleSpecs::Autoload.should have_constant(:AutoloadRelativeTableTest) + end + + it "calls #to_path on non-String filenames" do + name = mock("autoload_relative mock") + name.should_receive(:to_path).and_return("fixtures/autoload_relative_a.rb") + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeToPath, name + ModuleSpecs::Autoload.autoload?(:AutoloadRelativeToPath).should_not be_nil + end + + it "calls #to_str on non-String filenames" do + name = mock("autoload_relative mock") + name.should_receive(:to_str).and_return("fixtures/autoload_relative_a.rb") + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeToStr, name + ModuleSpecs::Autoload.autoload?(:AutoloadRelativeToStr).should_not be_nil + end + + it "raises a TypeError if the filename argument is not a String or pathname" do + -> { + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeTypError, nil + }.should raise_error(TypeError) + end + + it "raises a NameError if the constant name is not valid" do + -> { + ModuleSpecs::Autoload.autoload_relative :invalid_name, "fixtures/autoload_relative_a.rb" + }.should raise_error(NameError) + end + + it "raises an ArgumentError if the constant name starts with a lowercase letter" do + -> { + ModuleSpecs::Autoload.autoload_relative :autoload, "fixtures/autoload_relative_a.rb" + }.should raise_error(NameError) + end + + it "raises LoadError if called from eval without file context" do + -> { + ModuleSpecs::Autoload.module_eval('autoload_relative :EvalTest, "fixtures/autoload_relative_a.rb"') + }.should raise_error(LoadError, /autoload_relative called without file context/) + end + + it "can autoload in instance_eval with a file context" do + path = nil + ModuleSpecs::Autoload.instance_eval(<<-CODE, __FILE__, __LINE__) + autoload_relative :InstanceEvalTest, "fixtures/autoload_relative_a.rb" + path = autoload?(:InstanceEvalTest) + CODE + path.should_not be_nil + path.should.end_with?("autoload_relative_a.rb") + end + + it "resolves paths relative to the file where it's called" do + # Using fixtures/autoload_relative_a.rb which exists + ModuleSpecs::Autoload.autoload_relative :RelativePathTest, "fixtures/autoload_relative_a.rb" + path = ModuleSpecs::Autoload.autoload?(:RelativePathTest) + path.should.include?("fixtures") + path.should.end_with?("autoload_relative_a.rb") + end + + it "can load nested directory paths" do + ModuleSpecs::Autoload.autoload_relative :NestedPath, "fixtures/autoload_relative_a.rb" + path = ModuleSpecs::Autoload.autoload?(:NestedPath) + path.should_not be_nil + File.exist?(path).should be_true + end + + describe "interoperability with autoload?" do + it "returns the absolute path with autoload?" do + ModuleSpecs::Autoload.autoload_relative :QueryTest, "fixtures/autoload_relative_a.rb" + path = ModuleSpecs::Autoload.autoload?(:QueryTest) + # Should be an absolute path + Pathname.new(path).absolute?.should be_true + end + end +end +end diff --git a/spec/ruby/core/module/fixtures/autoload_relative_a.rb b/spec/ruby/core/module/fixtures/autoload_relative_a.rb new file mode 100644 index 00000000000000..494181adc2c3e2 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_relative_a.rb @@ -0,0 +1,9 @@ +module ModuleSpecs + module Autoload + class AutoloadRelativeA + end + + class AutoloadRelativeB + end + end +end diff --git a/test/ruby/test_autoload.rb b/test/ruby/test_autoload.rb index 82bf2d9d2c0992..de08be96e4d792 100644 --- a/test/ruby/test_autoload.rb +++ b/test/ruby/test_autoload.rb @@ -614,6 +614,95 @@ module SomeNamespace end end + def test_autoload_relative_toplevel + Dir.mktmpdir('autoload_relative') do |tmpdir| + main_file = File.join(tmpdir, 'main.rb') + module_file = File.join(tmpdir, 'test_module.rb') + + File.write(module_file, <<-RUBY) + module AutoloadRelativeTest + VERSION = '1.0' + end + RUBY + + File.write(main_file, <<-RUBY) + autoload_relative :AutoloadRelativeTest, 'test_module.rb' + puts AutoloadRelativeTest::VERSION + RUBY + + assert_in_out_err([main_file], '', ['1.0'], []) + end + end + + def test_autoload_relative_module_level + Dir.mktmpdir('autoload_relative') do |tmpdir| + main_file = File.join(tmpdir, 'main_mod.rb') + module_file = File.join(tmpdir, 'nested_module.rb') + + File.write(module_file, <<-RUBY) + module Container + module NestedModule + MSG = 'loaded' + end + end + RUBY + + File.write(main_file, <<-RUBY) + module Container + autoload_relative :NestedModule, 'nested_module.rb' + end + puts Container::NestedModule::MSG + RUBY + + assert_in_out_err([main_file], '', ['loaded'], []) + end + end + + def test_autoload_relative_query + Dir.mktmpdir('autoload_relative') do |tmpdir| + main_file = File.join(tmpdir, 'query_test.rb') + module_file = File.join(tmpdir, 'query_module.rb') + + File.write(module_file, 'module QueryModule; end') + + File.write(main_file, <<-RUBY) + autoload_relative :QueryModule, 'query_module.rb' + path = autoload?(:QueryModule) + # Use realpath for comparison to handle symlinks (e.g., /var -> /private/var on macOS) + real_tmpdir = File.realpath('#{tmpdir}') + puts path.start_with?(real_tmpdir) && path.end_with?('query_module.rb') + RUBY + + assert_in_out_err([main_file], '', ['true'], []) + end + end + + def test_autoload_relative_nested_directory + Dir.mktmpdir('autoload_relative') do |tmpdir| + nested_dir = File.join(tmpdir, 'nested') + Dir.mkdir(nested_dir) + + main_file = File.join(tmpdir, 'nested_test.rb') + module_file = File.join(nested_dir, 'deep_module.rb') + + File.write(module_file, 'module DeepModule; VALUE = 42; end') + + File.write(main_file, <<-RUBY) + autoload_relative :DeepModule, 'nested/deep_module.rb' + puts DeepModule::VALUE + RUBY + + assert_in_out_err([main_file], '', ['42'], []) + end + end + + def test_autoload_relative_no_basepath + # Test that autoload_relative raises an error when called from eval without file context + assert_raise(LoadError) do + eval('autoload_relative :TestConst, "test.rb"') + end + end + private def assert_separately(*args, **kwargs) From 98269b6d64f26d1e8f22f3d8fddd30393f009e17 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 13 Feb 2026 08:40:46 +0900 Subject: [PATCH 4/5] [Feature #21796] unpack variant `^` that returns the final offset (#15647) [Feature #21796] unpack variant `^` that returns the current offset --- doc/language/packed_data.rdoc | 5 +++ pack.c | 4 ++ spec/ruby/core/string/unpack/carret_spec.rb | 43 +++++++++++++++++++++ test/ruby/test_pack.rb | 28 ++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 spec/ruby/core/string/unpack/carret_spec.rb diff --git a/doc/language/packed_data.rdoc b/doc/language/packed_data.rdoc index 2dce7bb57ce498..1a6d80bd4e2689 100644 --- a/doc/language/packed_data.rdoc +++ b/doc/language/packed_data.rdoc @@ -100,6 +100,7 @@ These tables summarize the directives for packing and unpacking. @ | skip to the offset given by the length argument X | skip backward one byte x | skip forward one byte + ^ | return the current offset == Packing and Unpacking @@ -722,3 +723,7 @@ for one byte in the input or output string. "\x00\x00\x02".unpack("CxC") # => [0, 2] "\x00\x00\x02".unpack("x3C") # => [nil] "\x00\x00\x02".unpack("x4C") # Raises ArgumentError + +- '^' - Only for unpacking; the current position: + + "foo\0\0\0".unpack("Z*C") # => ["foo", 6] diff --git a/pack.c b/pack.c index d2fb4f633fd22b..f956e686e396eb 100644 --- a/pack.c +++ b/pack.c @@ -1570,6 +1570,10 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) s += len; break; + case '^': + UNPACK_PUSH(SSIZET2NUM(s - RSTRING_PTR(str))); + break; + case 'P': if (sizeof(char *) <= (size_t)(send - s)) { VALUE tmp = Qnil; diff --git a/spec/ruby/core/string/unpack/carret_spec.rb b/spec/ruby/core/string/unpack/carret_spec.rb new file mode 100644 index 00000000000000..815df0c7185eb6 --- /dev/null +++ b/spec/ruby/core/string/unpack/carret_spec.rb @@ -0,0 +1,43 @@ +# encoding: binary +ruby_version_is "4.1" do + require_relative '../../../spec_helper' + require_relative '../fixtures/classes' + require_relative 'shared/basic' + + describe "String#unpack with format '^'" do + it_behaves_like :string_unpack_basic, '^' + it_behaves_like :string_unpack_no_platform, '^' + + it "returns the current offset that start from 0" do + "".unpack("^").should == [0] + end + + it "returns the current offset after the last decode ended" do + "a".unpack("CC^").should == [97, nil, 1] + end + + it "returns the current offset that start from the given offset" do + "abc".unpack("^", offset: 1).should == [1] + end + + it "returns the offset moved by 'X'" do + "\x01\x02\x03\x04".unpack("C3X2^").should == [1, 2, 3, 1] + end + + it "returns the offset moved by 'x'" do + "\x01\x02\x03\x04".unpack("Cx2^").should == [1, 3] + end + + it "returns the offset to the position the previous decode ended" do + "foo".unpack("A4^").should == ["foo", 3] + "foo".unpack("a4^").should == ["foo", 3] + "foo".unpack("Z5^").should == ["foo", 3] + end + + it "returns the offset including truncated part" do + "foo ".unpack("A*^").should == ["foo", 6] + "foo\0".unpack("Z*^").should == ["foo", 4] + "foo\0\0\0".unpack("Z5^").should == ["foo", 5] + end + end +end diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index 9c40cfaa204f86..c23b2832f5b0e0 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -283,6 +283,15 @@ def test_pack_unpack_aA assert_equal(["foo "], "foo ".unpack("a4")) assert_equal(["foo"], "foo".unpack("A4")) assert_equal(["foo"], "foo".unpack("a4")) + + assert_equal(["foo", 4], "foo\0 ".unpack("A4^")) + assert_equal(["foo\0", 4], "foo\0 ".unpack("a4^")) + assert_equal(["foo", 4], "foo ".unpack("A4^")) + assert_equal(["foo ", 4], "foo ".unpack("a4^")) + assert_equal(["foo", 3], "foo".unpack("A4^")) + assert_equal(["foo", 3], "foo".unpack("a4^")) + assert_equal(["foo", 6], "foo\0 ".unpack("A*^")) + assert_equal(["foo", 6], "foo ".unpack("A*^")) end def test_pack_unpack_Z @@ -298,6 +307,11 @@ def test_pack_unpack_Z assert_equal(["foo"], "foo".unpack("Z*")) assert_equal(["foo"], "foo\0".unpack("Z*")) assert_equal(["foo"], "foo".unpack("Z5")) + + assert_equal(["foo", 3], "foo".unpack("Z*^")) + assert_equal(["foo", 4], "foo\0".unpack("Z*^")) + assert_equal(["foo", 3], "foo".unpack("Z5^")) + assert_equal(["foo", 5], "foo\0\0\0".unpack("Z5^")) end def test_pack_unpack_bB @@ -549,6 +563,8 @@ def test_pack_unpack_x assert_equal([0, 2], "\x00\x00\x02".unpack("CxC")) assert_raise(ArgumentError) { "".unpack("x") } + + assert_equal([0, 1, 2, 2, 3], "\x00\x00\x02".unpack("C^x^C^")) end def test_pack_unpack_X @@ -558,6 +574,7 @@ def test_pack_unpack_X assert_equal([0, 2, 2], "\x00\x02".unpack("CCXC")) assert_raise(ArgumentError) { "".unpack("X") } + assert_equal([0, 1, 2, 2, 1, 2, 2], "\x00\x02".unpack("C^C^X^C^")) end def test_pack_unpack_atmark @@ -571,6 +588,17 @@ def test_pack_unpack_atmark pos = RbConfig::LIMITS["UINTPTR_MAX"] - 99 # -100 assert_raise(RangeError) {"0123456789".unpack("@#{pos}C10")} + + assert_equal([1, 3, 4], "\x01\x00\x00\x02".unpack("x^@3^x^")) + end + + def test_unpack_carret + assert_equal([0], "abc".unpack("^")) + assert_equal([2], "abc".unpack("^", offset: 2)) + assert_equal([97, nil, 1], "a".unpack("CC^")) + + assert_raise(ArgumentError) { "".unpack("^!") } + assert_raise(ArgumentError) { "".unpack("^_") } end def test_pack_unpack_percent From a88475d6cb6c6a917b56be496cb83733cadd1d27 Mon Sep 17 00:00:00 2001 From: Prajjwal Singh Date: Wed, 11 Feb 2026 17:28:09 -0500 Subject: [PATCH 5/5] Report all missing modular GC exports before exiting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make `ruby_modular_gc_init()` print all symbol import errors instead of bailing on the first one. That way, GC authors don't have to fix one symbol at a time. Also accumulates and prints the total error count across all imports. Eg. On a sample GC ripped from the built-in GC implementation, renamed to "testgc", with the symbols `rb_gc_impl_gc_enable` and `rb_gc_impl_gc_disable` removed: **Before:** ``` λ batify build → 74a4e2e66d → ~/.rubies/modgc/bin/ruby ruby_modular_gc_init: rb_gc_impl_gc_enable function not exported by library /Users/prajjwal/.rubies/modgc/lib/ruby/gc/librubygc.testgc.bundle ``` **After:** ``` λ batify build → master → ~/.rubies/modgc/bin/ruby ruby_modular_gc_init: rb_gc_impl_gc_enable function not exported by library /Users/prajjwal/.rubies/modgc/lib/ruby/gc/librubygc.testgc.bundle ruby_modular_gc_init: rb_gc_impl_gc_disable function not exported by library /Users/prajjwal/.rubies/modgc/lib/ruby/gc/librubygc.testgc.bundle ruby_modular_gc_init: found 2 missing exports in library /Users/prajjwal/.rubies/modgc/lib/ruby/gc/librubygc.testgc.bundle ``` --- gc.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gc.c b/gc.c index c0f8eebbab0137..2a4dec32e9119f 100644 --- a/gc.c +++ b/gc.c @@ -767,13 +767,15 @@ ruby_modular_gc_init(void) gc_functions.modular_gc_loaded_p = true; } + unsigned int err_count = 0; + # define load_modular_gc_func(name) do { \ if (handle) { \ const char *func_name = "rb_gc_impl_" #name; \ gc_functions.name = dlsym(handle, func_name); \ if (!gc_functions.name) { \ fprintf(stderr, "ruby_modular_gc_init: %s function not exported by library %s\n", func_name, gc_so_path); \ - exit(EXIT_FAILURE); \ + err_count++; \ } \ } \ else { \ @@ -858,6 +860,11 @@ ruby_modular_gc_init(void) load_modular_gc_func(set_event_hook); load_modular_gc_func(copy_attributes); + if (err_count > 0) { + fprintf(stderr, "ruby_modular_gc_init: found %u missing exports in library %s\n", err_count, gc_so_path); + exit(EXIT_FAILURE); + } + # undef load_modular_gc_func rb_gc_functions = gc_functions;