diff --git a/NEWS.md b/NEWS.md
index af3e78ddaaba3f..b302e188520f41 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -105,6 +105,15 @@ Note: We're only listing outstanding class updates.
waiting on a blocking IO operation when the IO operation is closed.
[[Feature #21166]]
+ * Introduce `Fiber::Scheduler#yield` to allow the fiber scheduler to
+ continue processing when signal exceptions are disabled.
+ [[Bug #21633]]
+
+ * Reintroduce the `Fiber::Scheduler#io_close` hook for asynchronous `IO#close`.
+
+ * Invoke `Fiber::Scheduler#io_write` when flushing the IO write buffer.
+ [[Bug #21789]]
+
* File
* `File::Stat#birthtime` is now available on Linux via the statx
@@ -266,6 +275,11 @@ Note: We're only listing outstanding class updates.
* Introduce support for `Thread#raise(cause:)` argument similar to
`Kernel#raise`. [[Feature #21360]]
+* Pathname
+
+ * Pathname has been promoted from a default gem to a core class of Ruby.
+ [[Feature #17473]]
+
## Stdlib updates
The following bundled gems are promoted from default gems.
@@ -284,7 +298,7 @@ The following bundled gems are promoted from default gems.
We only list stdlib changes that are notable feature changes.
Other changes are listed in the following sections. We also listed release
-history from the previous bundled version that is Ruby 3.3.0 if it has GitHub
+history from the previous bundled version that is Ruby 3.4.0 if it has GitHub
releases.
The following default gem is added.
@@ -324,9 +338,6 @@ The following default gems are updated.
* weakref 0.1.4
* zlib 3.2.2
-The following bundled gems are added.
-
-
The following bundled gems are updated.
* minitest 6.0.0
@@ -349,6 +360,15 @@ The following bundled gems are updated.
* csv 3.3.5
* repl_type_completor 0.1.12
+### RubyGems and Bundler
+
+see the following links for details.
+
+* [Upgrading to RubyGems/Bundler 4 - RubyGems Blog](https://blog.rubygems.org/2025/12/03/upgrade-to-rubygems-bundler-4.html)
+* [4.0.0 Released - RubyGems Blog](https://blog.rubygems.org/2025/12/03/4.0.0-released.html)
+* [4.0.1 Released - RubyGems Blog](https://blog.rubygems.org/2025/12/09/4.0.1-released.html)
+* [4.0.2 Released - RubyGems Blog](https://blog.rubygems.org/2025/12/17/4.0.2-released.html)
+
## Supported platforms
* Windows
@@ -546,8 +566,10 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
[Feature #21550]: https://bugs.ruby-lang.org/issues/21550
[Feature #21552]: https://bugs.ruby-lang.org/issues/21552
[Feature #21557]: https://bugs.ruby-lang.org/issues/21557
+[Bug #21633]: https://bugs.ruby-lang.org/issues/21633
[Bug #21654]: https://bugs.ruby-lang.org/issues/21654
[Feature #21678]: https://bugs.ruby-lang.org/issues/21678
[Bug #21698]: https://bugs.ruby-lang.org/issues/21698
[Feature #21701]: https://bugs.ruby-lang.org/issues/21701
[Feature #21785]: https://bugs.ruby-lang.org/issues/21785
+[Bug #21789]: https://bugs.ruby-lang.org/issues/21789
diff --git a/box.c b/box.c
index 3182fb4eb20758..54bd5c6258343f 100644
--- a/box.c
+++ b/box.c
@@ -9,6 +9,7 @@
#include "internal/file.h"
#include "internal/gc.h"
#include "internal/hash.h"
+#include "internal/io.h"
#include "internal/load.h"
#include "internal/st.h"
#include "internal/variable.h"
@@ -627,8 +628,14 @@ copy_ext_file(const char *src_path, const char *dst_path)
# else
const int bin = 0;
# endif
- const int src_fd = open(src_path, O_RDONLY|bin);
+# ifdef O_CLOEXEC
+ const int cloexec = O_CLOEXEC;
+# else
+ const int cloexec = 0;
+# endif
+ const int src_fd = open(src_path, O_RDONLY|cloexec|bin);
if (src_fd < 0) return COPY_ERROR_SRC_OPEN;
+ if (!cloexec) rb_maygvl_fd_fix_cloexec(src_fd);
struct stat src_st;
if (fstat(src_fd, &src_st)) {
@@ -636,11 +643,12 @@ copy_ext_file(const char *src_path, const char *dst_path)
return COPY_ERROR_SRC_STAT;
}
- const int dst_fd = open(dst_path, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|bin, S_IRWXU);
+ const int dst_fd = open(dst_path, O_WRONLY|O_CREAT|O_EXCL|cloexec|bin, S_IRWXU);
if (dst_fd < 0) {
close(src_fd);
return COPY_ERROR_DST_OPEN;
}
+ if (!cloexec) rb_maygvl_fd_fix_cloexec(dst_fd);
enum copy_error_type ret = COPY_ERROR_NONE;
diff --git a/depend b/depend
index 8834814227b0e8..11397bf647adf1 100644
--- a/depend
+++ b/depend
@@ -745,6 +745,7 @@ box.$(OBJEXT): $(top_srcdir)/internal/file.h
box.$(OBJEXT): $(top_srcdir)/internal/gc.h
box.$(OBJEXT): $(top_srcdir)/internal/hash.h
box.$(OBJEXT): $(top_srcdir)/internal/imemo.h
+box.$(OBJEXT): $(top_srcdir)/internal/io.h
box.$(OBJEXT): $(top_srcdir)/internal/load.h
box.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h
box.$(OBJEXT): $(top_srcdir)/internal/serial.h
@@ -951,6 +952,7 @@ box.$(OBJEXT): {$(VPATH)}internal/value_type.h
box.$(OBJEXT): {$(VPATH)}internal/variable.h
box.$(OBJEXT): {$(VPATH)}internal/warning_push.h
box.$(OBJEXT): {$(VPATH)}internal/xmalloc.h
+box.$(OBJEXT): {$(VPATH)}io.h
box.$(OBJEXT): {$(VPATH)}iseq.h
box.$(OBJEXT): {$(VPATH)}method.h
box.$(OBJEXT): {$(VPATH)}missing.h
diff --git a/doc/language/packed_data.rdoc b/doc/language/packed_data.rdoc
index dcb4d557352d88..97079f7f08dd8f 100644
--- a/doc/language/packed_data.rdoc
+++ b/doc/language/packed_data.rdoc
@@ -342,6 +342,22 @@ for one element in the input or output array.
s.unpack('U*')
# => [4194304]
+- 'r' - Signed LEB128-encoded integer
+ (see {Signed LEB128}[https://en.wikipedia.org/wiki/LEB128#Signed_LEB128])
+
+ s = [1, 127, -128, 16383, -16384].pack("r*")
+ # => "\x01\xFF\x00\x80\x7F\xFF\xFF\x00\x80\x80\x7F"
+ s.unpack('r*')
+ # => [1, 127, -128, 16383, -16384]
+
+- 'R' - Unsigned LEB128-encoded integer
+ (see {Unsigned LEB128}[https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128])
+
+ s = [1, 127, 128, 16383, 16384].pack("R*")
+ # => "\x01\x7F\x80\x01\xFF\x7F\x80\x80\x01"
+ s.unpack('R*')
+ # => [1, 127, 128, 16383, 16384]
+
- 'w' - BER-encoded integer
(see {BER encoding}[https://en.wikipedia.org/wiki/X.690#BER_encoding]):
diff --git a/io.c b/io.c
index e739cee3795eec..99268e8f05b1a4 100644
--- a/io.c
+++ b/io.c
@@ -1418,10 +1418,34 @@ io_flush_buffer_sync(void *arg)
return (VALUE)-1;
}
+static inline VALUE
+io_flush_buffer_fiber_scheduler(VALUE scheduler, rb_io_t *fptr)
+{
+ VALUE ret = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, fptr->wbuf.ptr+fptr->wbuf.off, fptr->wbuf.len, 0);
+ if (!UNDEF_P(ret)) {
+ ssize_t result = rb_fiber_scheduler_io_result_apply(ret);
+ if (result > 0) {
+ fptr->wbuf.off += result;
+ fptr->wbuf.len -= result;
+ }
+ return result >= 0 ? (VALUE)0 : (VALUE)-1;
+ }
+ return ret;
+}
+
static VALUE
io_flush_buffer_async(VALUE arg)
{
rb_io_t *fptr = (rb_io_t *)arg;
+
+ VALUE scheduler = rb_fiber_scheduler_current();
+ if (scheduler != Qnil) {
+ VALUE result = io_flush_buffer_fiber_scheduler(scheduler, fptr);
+ if (!UNDEF_P(result)) {
+ return result;
+ }
+ }
+
return rb_io_blocking_region_wait(fptr, io_flush_buffer_sync, fptr, RUBY_IO_WRITABLE);
}
diff --git a/test/fiber/scheduler.rb b/test/fiber/scheduler.rb
index 029c5043dc1b14..07b15c5ce4b86a 100644
--- a/test/fiber/scheduler.rb
+++ b/test/fiber/scheduler.rb
@@ -488,6 +488,29 @@ def blocking(&block)
end
end
+class IOScheduler < Scheduler
+ def __io_ops__
+ @__io_ops__ ||= []
+ end
+
+ def io_write(io, buffer, length, offset)
+ fd = io.fileno
+ str = buffer.get_string
+ __io_ops__ << [:io_write, fd, str]
+ Fiber.blocking { buffer.write(IO.for_fd(fd), 0, offset) }
+ end
+end
+
+class IOErrorScheduler < Scheduler
+ def io_read(io, buffer, length, offset)
+ return -Errno::EBADF::Errno
+ end
+
+ def io_write(io, buffer, length, offset)
+ return -Errno::EINVAL::Errno
+ end
+end
+
# This scheduler has a broken implementation of `unblock`` in the sense that it
# raises an exception. This is used to test the behavior of the scheduler when
# unblock raises an exception.
diff --git a/test/fiber/test_scheduler.rb b/test/fiber/test_scheduler.rb
index 97ce94bd270b69..4a8b4ee62d8e49 100644
--- a/test/fiber/test_scheduler.rb
+++ b/test/fiber/test_scheduler.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'test/unit'
+require 'securerandom'
+require 'fileutils'
require_relative 'scheduler'
class TestFiberScheduler < Test::Unit::TestCase
@@ -283,4 +285,91 @@ def test_post_fork_fiber_blocking
ensure
thread.kill rescue nil
end
+
+ def test_io_write_on_flush
+ fn = File.join(Dir.tmpdir, "ruby_test_io_write_on_flush_#{SecureRandom.hex}")
+ write_fd = nil
+ io_ops = nil
+ thread = Thread.new do
+ scheduler = IOScheduler.new
+ Fiber.set_scheduler scheduler
+
+ Fiber.schedule do
+ File.open(fn, 'w+') do |f|
+ write_fd = f.fileno
+ f << 'foo'
+ f.flush
+ f << 'bar'
+ end
+ end
+ io_ops = scheduler.__io_ops__
+ end
+ thread.join
+ assert_equal [
+ [:io_write, write_fd, 'foo'],
+ [:io_write, write_fd, 'bar']
+ ], io_ops
+
+ assert_equal 'foobar', IO.read(fn)
+ ensure
+ thread.kill rescue nil
+ FileUtils.rm_f(fn)
+ end
+
+ def test_io_read_error
+ fn = File.join(Dir.tmpdir, "ruby_test_io_read_error_#{SecureRandom.hex}")
+ exception = nil
+ thread = Thread.new do
+ scheduler = IOErrorScheduler.new
+ Fiber.set_scheduler scheduler
+ Fiber.schedule do
+ File.open(fn, 'w+') { it.read }
+ rescue => e
+ exception = e
+ end
+ end
+ thread.join
+ assert_kind_of Errno::EBADF, exception
+ ensure
+ thread.kill rescue nil
+ FileUtils.rm_f(fn)
+ end
+
+ def test_io_write_error
+ fn = File.join(Dir.tmpdir, "ruby_test_io_write_error_#{SecureRandom.hex}")
+ exception = nil
+ thread = Thread.new do
+ scheduler = IOErrorScheduler.new
+ Fiber.set_scheduler scheduler
+ Fiber.schedule do
+ File.open(fn, 'w+') { it.sync = true; it << 'foo' }
+ rescue => e
+ exception = e
+ end
+ end
+ thread.join
+ assert_kind_of Errno::EINVAL, exception
+ ensure
+ thread.kill rescue nil
+ FileUtils.rm_f(fn)
+ end
+
+ def test_io_write_flush_error
+ fn = File.join(Dir.tmpdir, "ruby_test_io_write_flush_error_#{SecureRandom.hex}")
+ exception = nil
+ thread = Thread.new do
+ scheduler = IOErrorScheduler.new
+ Fiber.set_scheduler scheduler
+ Fiber.schedule do
+ File.open(fn, 'w+') { it << 'foo' }
+ rescue => e
+ exception = e
+ end
+ end
+ thread.join
+ assert_kind_of Errno::EINVAL, exception
+ ensure
+ thread.kill rescue nil
+ FileUtils.rm_f(fn)
+ end
end
diff --git a/tool/rbs_skip_tests_windows b/tool/rbs_skip_tests_windows
index bdf3dddfd7a594..43888c98a71324 100644
--- a/tool/rbs_skip_tests_windows
+++ b/tool/rbs_skip_tests_windows
@@ -1,5 +1,8 @@
ARGFTest Failing on Windows
+RactorSingletonTest Hangs up on Windows
+RactorInstanceTest Hangs up on Windows
+
# NotImplementedError: fileno() function is unimplemented on this machine
test_fileno(DirInstanceTest)
test_fchdir(DirSingletonTest)