From dc6ee1b7a2b1a5b13b1a81a3e373f6263f2ae7de Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Mon, 18 Nov 2024 19:17:37 +0200 Subject: [PATCH] Add rb_io_open_descriptor function --- CHANGELOG.md | 1 + .../truffleruby/truffleruby-abi-version.h | 2 +- lib/truffle/truffle/cext.rb | 10 ++ spec/ruby/optional/capi/ext/io_spec.c | 24 ++++ spec/ruby/optional/capi/io_spec.rb | 120 ++++++++++++++++++ spec/tags/optional/capi/io_tags.txt | 2 + src/main/c/cext/io.c | 27 ++++ src/main/ruby/truffleruby/core/io.rb | 8 +- 8 files changed, 190 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d957e7db4045..b13d84934e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Compatibility: * Implement `rb_str_format()` (#3716, @andrykonchin). * Add `IO#{pread, pwrite}` methods (#3718, @andrykonchin). * Add `rb_io_closed_p()` (#3681, @andrykonchin). +* Add `rb_io_open_descriptor()` (#3681, @andrykonchin). Performance: diff --git a/lib/cext/include/truffleruby/truffleruby-abi-version.h b/lib/cext/include/truffleruby/truffleruby-abi-version.h index 31e3d90b0718..9a19f5e409de 100644 --- a/lib/cext/include/truffleruby/truffleruby-abi-version.h +++ b/lib/cext/include/truffleruby/truffleruby-abi-version.h @@ -20,6 +20,6 @@ // $RUBY_VERSION must be the same as TruffleRuby.LANGUAGE_VERSION. // $ABI_NUMBER starts at 1 and is incremented for every ABI-incompatible change. -#define TRUFFLERUBY_ABI_VERSION "3.3.5.5" +#define TRUFFLERUBY_ABI_VERSION "3.3.5.6" #endif diff --git a/lib/truffle/truffle/cext.rb b/lib/truffle/truffle/cext.rb index fa8829dd683e..fbd443f0f2dc 100644 --- a/lib/truffle/truffle/cext.rb +++ b/lib/truffle/truffle/cext.rb @@ -2367,6 +2367,16 @@ def rb_io_path(io) io.instance_variable_get(:@path) end + def rb_io_open_descriptor(klass, fd, mode, path, timeout, internal_encoding, external_encoding, flags, options) + return klass.allocate if klass != IO and klass != File + + # Translate Ruby-specific modes (`FMODE_`) to corresponding platform-specific file open flags (`O_`). + # Ruby interface accepts `FMODE_` flags, but C API functions accept `O_` flags. + mode = Truffle::IOOperations.translate_omode_to_fmode(mode) + + klass.new(fd, mode, **options, internal_encoding: internal_encoding, external_encoding: external_encoding, path: path, flags: flags, skip_mode_enforcing: true) + end + def rb_io_closed_p(io) io.closed? end diff --git a/spec/ruby/optional/capi/ext/io_spec.c b/spec/ruby/optional/capi/ext/io_spec.c index a8b95dbabcb5..d1cc861e2170 100644 --- a/spec/ruby/optional/capi/ext/io_spec.c +++ b/spec/ruby/optional/capi/ext/io_spec.c @@ -375,6 +375,23 @@ static VALUE io_spec_rb_io_path(VALUE self, VALUE io) { static VALUE io_spec_rb_io_closed_p(VALUE self, VALUE io) { return rb_io_closed_p(io); } + +static VALUE io_spec_rb_io_open_descriptor(VALUE self, VALUE klass, VALUE descriptor, VALUE mode, VALUE path, VALUE timeout, VALUE internal_encoding, VALUE external_encoding, VALUE ecflags, VALUE ecopts) { + struct rb_io_encoding *io_encoding; + + io_encoding = (struct rb_io_encoding *) malloc(sizeof(struct rb_io_encoding)); + + io_encoding->enc = rb_to_encoding(internal_encoding); + io_encoding->enc2 = rb_to_encoding(external_encoding); + io_encoding->ecflags = FIX2INT(ecflags); + io_encoding->ecopts = ecopts; + + return rb_io_open_descriptor(klass, FIX2INT(descriptor), FIX2INT(mode), path, timeout, io_encoding); +} + +static VALUE io_spec_rb_io_open_descriptor_without_encoding(VALUE self, VALUE klass, VALUE descriptor, VALUE mode, VALUE path, VALUE timeout) { + return rb_io_open_descriptor(klass, FIX2INT(descriptor), FIX2INT(mode), path, timeout, NULL); +} #endif void Init_io_spec(void) { @@ -414,6 +431,13 @@ void Init_io_spec(void) { rb_define_method(cls, "rb_io_mode", io_spec_rb_io_mode, 1); rb_define_method(cls, "rb_io_path", io_spec_rb_io_path, 1); rb_define_method(cls, "rb_io_closed_p", io_spec_rb_io_closed_p, 1); + rb_define_method(cls, "rb_io_open_descriptor", io_spec_rb_io_open_descriptor, 9); + rb_define_method(cls, "rb_io_open_descriptor_without_encoding", io_spec_rb_io_open_descriptor_without_encoding, 5); + rb_define_const(cls, "FMODE_READABLE", INT2FIX(FMODE_READABLE)); + rb_define_const(cls, "FMODE_WRITABLE", INT2FIX(FMODE_WRITABLE)); + rb_define_const(cls, "FMODE_BINMODE", INT2FIX(FMODE_BINMODE)); + rb_define_const(cls, "FMODE_TEXTMODE", INT2FIX(FMODE_TEXTMODE)); + rb_define_const(cls, "ECONV_UNIVERSAL_NEWLINE_DECORATOR", INT2FIX(ECONV_UNIVERSAL_NEWLINE_DECORATOR)); #endif } diff --git a/spec/ruby/optional/capi/io_spec.rb b/spec/ruby/optional/capi/io_spec.rb index 85c47d4dd374..2095f4decaad 100644 --- a/spec/ruby/optional/capi/io_spec.rb +++ b/spec/ruby/optional/capi/io_spec.rb @@ -530,6 +530,126 @@ @r_io.closed?.should == true end end + + describe "rb_io_open_descriptor" do + it "creates a new IO instance" do + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.should.is_a?(IO) + end + + it "return an instance of the specified class" do + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.class.should == File + + io = @o.rb_io_open_descriptor(IO, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.class.should == IO + end + + it "sets the specified file descriptor" do + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.fileno.should == @r_io.fileno + end + + it "sets the specified path" do + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.path.should == "a.txt" + end + + it "sets the specified mode" do + io = @o.rb_io_open_descriptor(File, @r_io.fileno, CApiIOSpecs::FMODE_BINMODE, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.should.binmode? + + io = @o.rb_io_open_descriptor(File, @r_io.fileno, CApiIOSpecs::FMODE_TEXTMODE, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.should_not.binmode? + end + + it "sets the specified timeout" do + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.timeout.should == 60 + end + + it "sets the specified internal encoding" do + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.internal_encoding.should == Encoding::US_ASCII + end + + it "sets the specified external encoding" do + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.external_encoding.should == Encoding::UTF_8 + end + + it "does not apply the specified encoding flags" do + File.write("a.txt", "123\r\n456\n89") + file = File.open("a.txt", "r") + + io = @o.rb_io_open_descriptor(File, file.fileno, CApiIOSpecs::FMODE_READABLE, "a.txt", 60, "US-ASCII", "UTF-8", CApiIOSpecs::ECONV_UNIVERSAL_NEWLINE_DECORATOR, {}) + io.read_nonblock(20).should == "123\r\n456\n89" + end + + it "ignores the IO open options" do + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {external_encoding: "windows-1251"}) + io.external_encoding.should == Encoding::UTF_8 + + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {internal_encoding: "windows-1251"}) + io.internal_encoding.should == Encoding::US_ASCII + + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {encoding: "windows-1251:binary"}) + io.external_encoding.should == Encoding::UTF_8 + io.internal_encoding.should == Encoding::US_ASCII + + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {textmode: false}) + io.should_not.binmode? + + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {binmode: true}) + io.should_not.binmode? + + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {autoclose: false}) + io.should.autoclose? + + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, "a.txt", 60, "US-ASCII", "UTF-8", 0, {path: "a.txt"}) + io.path.should == "a.txt" + end + + it "ignores the IO encoding options" do + io = @o.rb_io_open_descriptor(File, @w_io.fileno, CApiIOSpecs::FMODE_WRITABLE, "a.txt", 60, "US-ASCII", "UTF-8", 0, {crlf_newline: true}) + + io.write("123\r\n456\n89") + io.flush + + @r_io.read_nonblock(20).should == "123\r\n456\n89" + end + + it "allows wrong mode" do + io = @o.rb_io_open_descriptor(File, @w_io.fileno, CApiIOSpecs::FMODE_READABLE, "a.txt", 60, "US-ASCII", "UTF-8", 0, {}) + io.should.is_a?(File) + + -> { io.read_nonblock(1) }.should raise_error(Errno::EBADF) + end + + it "tolerates NULL as rb_io_encoding *encoding parameter" do + io = @o.rb_io_open_descriptor_without_encoding(File, @r_io.fileno, 0, "a.txt", 60) + io.should.is_a?(File) + end + + it "deduplicates path String" do + path = "a.txt".dup + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, path, 60, "US-ASCII", "UTF-8", 0, {}) + io.path.should_not equal(path) + + path = "a.txt".freeze + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, path, 60, "US-ASCII", "UTF-8", 0, {}) + io.path.should_not equal(path) + end + + it "calls #to_str to convert a path to a String" do + path = Object.new + def path.to_str; "a.txt"; end + + io = @o.rb_io_open_descriptor(File, @r_io.fileno, 0, path, 60, "US-ASCII", "UTF-8", 0, {}) + + io.path.should == "a.txt" + end + end end ruby_version_is "3.4" do diff --git a/spec/tags/optional/capi/io_tags.txt b/spec/tags/optional/capi/io_tags.txt index d03f3b887009..623a0443b35d 100644 --- a/spec/tags/optional/capi/io_tags.txt +++ b/spec/tags/optional/capi/io_tags.txt @@ -1,3 +1,5 @@ fails:C-API IO function rb_io_maybe_wait_writable raises an IOError if the IO is not initialized fails:C-API IO function rb_io_maybe_wait_readable raises an IOError if the IO is not initialized fails:C-API IO function rb_io_maybe_wait raises an IOError if the IO is not initialized +fails:C-API IO function rb_io_open_descriptor sets the specified timeout +fails:C-API IO function rb_io_open_descriptor ignores the IO open options diff --git a/src/main/c/cext/io.c b/src/main/c/cext/io.c index c833ef9fd25f..f99b3ccd16fc 100644 --- a/src/main/c/cext/io.c +++ b/src/main/c/cext/io.c @@ -43,6 +43,33 @@ VALUE rb_io_path(VALUE io) { return RUBY_CEXT_INVOKE("rb_io_path", io); } +VALUE rb_io_open_descriptor(VALUE klass, int descriptor, int mode, VALUE path, VALUE timeout, struct rb_io_encoding *encoding) { + VALUE internal_encoding, external_encoding, ecflags, ecopts; + + if (encoding) { + internal_encoding = rb_enc_from_encoding(encoding->enc); + external_encoding = rb_enc_from_encoding(encoding->enc2); + ecflags = INT2FIX(encoding->ecflags); + ecopts = encoding->ecopts; + } else { + internal_encoding = Qnil; + external_encoding = Qnil; + ecflags = INT2FIX(0); + ecopts = rb_hash_new(); + } + + return RUBY_CEXT_INVOKE("rb_io_open_descriptor", + klass, + INT2FIX(descriptor), + INT2FIX(mode), + path, + timeout, + internal_encoding, + external_encoding, + ecflags, + ecopts); +} + VALUE rb_io_closed_p(VALUE io) { return RUBY_CEXT_INVOKE("rb_io_closed_p", io); } diff --git a/src/main/ruby/truffleruby/core/io.rb b/src/main/ruby/truffleruby/core/io.rb index 0c51f86b0163..af18edb4c9b4 100644 --- a/src/main/ruby/truffleruby/core/io.rb +++ b/src/main/ruby/truffleruby/core/io.rb @@ -817,7 +817,9 @@ def self.sysopen(path, mode = nil, perm = nil) # # The +sync+ attribute will also be set. # - def self.setup(io, fd, mode, sync) + # The +skip_mode_enforcing+ parameter is needed for implementing some C-functions and allows + # to bypass enforcing a specified file mode to be the same as current mode of a file. + def self.setup(io, fd, mode, sync, skip_mode_enforcing = false) if !Truffle::Boot.preinitializing? && Truffle::POSIX::NATIVE cur_mode = Truffle::POSIX.fcntl(fd, F_GETFL, 0) Errno.handle if cur_mode < 0 @@ -828,7 +830,7 @@ def self.setup(io, fd, mode, sync) mode = Truffle::IOOperations.parse_mode(mode) mode &= ACCMODE - if cur_mode and (cur_mode == RDONLY or cur_mode == WRONLY) and mode != cur_mode + if cur_mode and (cur_mode == RDONLY or cur_mode == WRONLY) and mode != cur_mode && !skip_mode_enforcing raise Errno::EINVAL, "Invalid mode #{cur_mode} for existing descriptor #{fd} (expected #{mode})" end else @@ -863,7 +865,7 @@ def initialize(fd, mode = nil, **options) fd = Truffle::Type.coerce_to(fd, Integer, :to_int) sync = fd == 2 # stderr is always unbuffered, see setvbuf(3) - IO.setup(self, fd, mode, sync) + IO.setup(self, fd, mode, sync, options[:skip_mode_enforcing]) binmode if binary set_encoding external, internal