Skip to content

Commit

Permalink
Add rb_io_open_descriptor function
Browse files Browse the repository at this point in the history
  • Loading branch information
andrykonchin committed Nov 22, 2024
1 parent 05baac1 commit efef0ca
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion lib/cext/include/truffleruby/truffleruby-abi-version.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions lib/truffle/truffle/cext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions spec/ruby/optional/capi/ext/io_spec.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}

Expand Down
120 changes: 120 additions & 0 deletions spec/ruby/optional/capi/io_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions spec/tags/optional/capi/io_tags.txt
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions src/main/c/cext/io.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
8 changes: 5 additions & 3 deletions src/main/ruby/truffleruby/core/io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit efef0ca

Please sign in to comment.