diff --git a/CHANGELOG.md b/CHANGELOG.md index 133b164ab47f..b13d84934e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ Compatibility: * Add `rb_gc_mark_locations()` (#3704, @andrykonchin). * 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 ae700777b0cc..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.4" +#define TRUFFLERUBY_ABI_VERSION "3.3.5.6" #endif diff --git a/lib/truffle/truffle/cext.rb b/lib/truffle/truffle/cext.rb index e337ceb3914e..fbd443f0f2dc 100644 --- a/lib/truffle/truffle/cext.rb +++ b/lib/truffle/truffle/cext.rb @@ -2367,6 +2367,20 @@ 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 + def rb_tr_io_pointer(io) Primitive.object_hidden_var_get(io, RB_IO_STRUCT) end diff --git a/spec/ruby/optional/capi/ext/io_spec.c b/spec/ruby/optional/capi/ext/io_spec.c index e0b1df0de7a2..d1cc861e2170 100644 --- a/spec/ruby/optional/capi/ext/io_spec.c +++ b/spec/ruby/optional/capi/ext/io_spec.c @@ -371,6 +371,27 @@ static VALUE io_spec_rb_io_mode(VALUE self, VALUE io) { static VALUE io_spec_rb_io_path(VALUE self, VALUE io) { return rb_io_path(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) { @@ -409,6 +430,14 @@ void Init_io_spec(void) { #if defined(RUBY_VERSION_IS_3_3) || defined(TRUFFLERUBY) 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 01588408e10c..a34b3a4ff051 100644 --- a/spec/ruby/optional/capi/io_spec.rb +++ b/spec/ruby/optional/capi/io_spec.rb @@ -516,6 +516,140 @@ @o.rb_io_path(@rw_io).should == @name end end + + describe "rb_io_closed_p" do + it "returns false when io is not closed" do + @o.rb_io_closed_p(@r_io).should == false + @r_io.closed?.should == false + end + + it "returns true when io is closed" do + @r_io.close + + @o.rb_io_closed_p(@r_io).should == true + @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 e0d03a6ae0cd..f99b3ccd16fc 100644 --- a/src/main/c/cext/io.c +++ b/src/main/c/cext/io.c @@ -43,6 +43,37 @@ 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); +} + static RFile_and_rb_io_t* get_RFile_and_rb_io_t(VALUE io) { RFile_and_rb_io_t* pointer = RUBY_CEXT_INVOKE_NO_WRAP("rb_tr_io_pointer", io); if (!polyglot_is_null(pointer)) { diff --git a/src/main/ruby/truffleruby/core/io.rb b/src/main/ruby/truffleruby/core/io.rb index 0c51f86b0163..05bd3c70b110 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]) # :skip_mode_enforcing is a TruffleRuby specific option used internally only binmode if binary set_encoding external, internal