Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GR-59866] Add missing io-related functions #3723

Merged
merged 2 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

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.4"
#define TRUFFLERUBY_ABI_VERSION "3.3.5.6"

#endif
14 changes: 14 additions & 0 deletions lib/truffle/truffle/cext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions spec/ruby/optional/capi/ext/io_spec.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}

Expand Down
134 changes: 134 additions & 0 deletions spec/ruby/optional/capi/io_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
31 changes: 31 additions & 0 deletions src/main/c/cext/io.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
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
Loading