Skip to content

Commit 58aea01

Browse files
committed
[GR-40333] Ruby 3.1 Add rb_io_maybe_wait_readable, rb_io_maybe_wait_writable and rb_io_maybe_wait C-functions
PullRequest: truffleruby/3707
2 parents 55a723d + b973927 commit 58aea01

File tree

8 files changed

+256
-4
lines changed

8 files changed

+256
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Compatibility:
9999
* Deprecate `rb_gc_force_recycle` and make it a no-op function (#2733, @moste00).
100100
* Add `Refinement#import_methods` method and add deprecation warning for `Refinement#include` and `Refinement#prepend` (#2733, @horakivo).
101101
* Upgrading `UNICODE` version to 13.0.0 and `EMOJI` version to 13.1 (#2733, @horakivo).
102+
* Add `rb_io_maybe_wait_readable`, `rb_io_maybe_wait_writable` and `rb_io_maybe_wait` functions (#2733, @andrykonchin).
102103

103104
Performance:
104105

lib/cext/ABI_version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4
1+
5

lib/truffle/truffle/cext_structs.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,11 +365,13 @@ def polyglot_has_members?
365365
end
366366

367367
def polyglot_members(internal)
368-
['stdio_file', 'fd', 'mode', 'pathv', 'pid', 'lineno', 'tied_io_for_writing']
368+
['self', 'stdio_file', 'fd', 'mode', 'pathv', 'pid', 'lineno', 'tied_io_for_writing']
369369
end
370370

371371
def polyglot_read_member(name)
372372
case name
373+
when 'self'
374+
Primitive.cext_wrap(@io)
373375
when 'fd'
374376
Primitive.io_fd(@io)
375377
when 'mode'
@@ -405,11 +407,11 @@ def polyglot_invoke_member(name, *args)
405407
end
406408

407409
def polyglot_member_readable?(name)
408-
name == 'fd' || name == 'mode' || name == 'pathv' || 'tied_io_for_writing'
410+
name == 'self' || name == 'fd' || name == 'mode' || name == 'pathv' || name == 'tied_io_for_writing'
409411
end
410412

411413
def polyglot_member_modifiable?(name)
412-
name == 'mode' || name == 'pathv' || 'tied_io_for_writing'
414+
name == 'mode' || name == 'pathv' || name == 'tied_io_for_writing'
413415
end
414416

415417
def polyglot_member_removable?(name)

spec/ruby/optional/capi/ext/io_spec.c

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,60 @@ VALUE io_spec_rb_io_wait_writable(VALUE self, VALUE io) {
162162
return ret ? Qtrue : Qfalse;
163163
}
164164

165+
#ifdef RUBY_VERSION_IS_3_1
166+
VALUE io_spec_rb_io_maybe_wait_writable(VALUE self, VALUE error, VALUE io, VALUE timeout) {
167+
int ret = rb_io_maybe_wait_writable(NUM2INT(error), io, timeout);
168+
return INT2NUM(ret);
169+
}
170+
#endif
171+
172+
#ifdef RUBY_VERSION_IS_3_1
173+
VALUE io_spec_rb_io_maybe_wait_readable(VALUE self, VALUE error, VALUE io, VALUE timeout, VALUE read_p) {
174+
int fd = io_spec_get_fd(io);
175+
#ifndef SET_NON_BLOCKING_FAILS_ALWAYS
176+
char buf[RB_IO_WAIT_READABLE_BUF];
177+
int ret, saved_errno;
178+
#endif
179+
180+
if (set_non_blocking(fd) == -1)
181+
rb_sys_fail("set_non_blocking failed");
182+
183+
#ifndef SET_NON_BLOCKING_FAILS_ALWAYS
184+
if(RTEST(read_p)) {
185+
if (read(fd, buf, RB_IO_WAIT_READABLE_BUF) != -1) {
186+
return Qnil;
187+
}
188+
saved_errno = errno;
189+
rb_ivar_set(self, rb_intern("@write_data"), Qtrue);
190+
errno = saved_errno;
191+
}
192+
193+
// main part
194+
ret = rb_io_maybe_wait_readable(NUM2INT(error), io, timeout);
195+
196+
if(RTEST(read_p)) {
197+
ssize_t r = read(fd, buf, RB_IO_WAIT_READABLE_BUF);
198+
if (r != RB_IO_WAIT_READABLE_BUF) {
199+
perror("read");
200+
return SSIZET2NUM(r);
201+
}
202+
rb_ivar_set(self, rb_intern("@read_data"),
203+
rb_str_new(buf, RB_IO_WAIT_READABLE_BUF));
204+
}
205+
206+
return INT2NUM(ret);
207+
#else
208+
UNREACHABLE;
209+
#endif
210+
}
211+
#endif
212+
213+
#ifdef RUBY_VERSION_IS_3_1
214+
VALUE io_spec_rb_io_maybe_wait(VALUE self, VALUE error, VALUE io, VALUE events, VALUE timeout) {
215+
return rb_io_maybe_wait(NUM2INT(error), io, events, timeout);
216+
}
217+
#endif
218+
165219
VALUE io_spec_rb_thread_wait_fd(VALUE self, VALUE io) {
166220
rb_thread_wait_fd(io_spec_get_fd(io));
167221
return Qnil;
@@ -294,6 +348,11 @@ void Init_io_spec(void) {
294348
rb_define_method(cls, "rb_io_taint_check", io_spec_rb_io_taint_check, 1);
295349
rb_define_method(cls, "rb_io_wait_readable", io_spec_rb_io_wait_readable, 2);
296350
rb_define_method(cls, "rb_io_wait_writable", io_spec_rb_io_wait_writable, 1);
351+
#ifdef RUBY_VERSION_IS_3_1
352+
rb_define_method(cls, "rb_io_maybe_wait_writable", io_spec_rb_io_maybe_wait_writable, 3);
353+
rb_define_method(cls, "rb_io_maybe_wait_readable", io_spec_rb_io_maybe_wait_readable, 4);
354+
rb_define_method(cls, "rb_io_maybe_wait", io_spec_rb_io_maybe_wait, 4);
355+
#endif
297356
rb_define_method(cls, "rb_thread_wait_fd", io_spec_rb_thread_wait_fd, 1);
298357
rb_define_method(cls, "rb_thread_fd_writable", io_spec_rb_thread_fd_writable, 1);
299358
rb_define_method(cls, "rb_thread_fd_select_read", io_spec_rb_thread_fd_select_read, 1);

spec/ruby/optional/capi/io_spec.rb

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,27 @@
256256
end
257257
end
258258

259+
ruby_version_is "3.1" do
260+
describe "rb_io_maybe_wait_writable" do
261+
it "returns mask for events if operation was interrupted" do
262+
@o.rb_io_maybe_wait_writable(Errno::EINTR::Errno, @w_io, nil).should == IO::WRITABLE
263+
end
264+
265+
it "returns 0 if there is no error condition" do
266+
@o.rb_io_maybe_wait_writable(0, @w_io, nil).should == 0
267+
end
268+
269+
it "raises an IOError if the IO is closed" do
270+
@w_io.close
271+
-> { @o.rb_io_maybe_wait_writable(0, @w_io, nil) }.should raise_error(IOError, "closed stream")
272+
end
273+
274+
it "raises an IOError if the IO is not initialized" do
275+
-> { @o.rb_io_maybe_wait_writable(0, IO.allocate, nil) }.should raise_error(IOError, "uninitialized stream")
276+
end
277+
end
278+
end
279+
259280
describe "rb_thread_fd_writable" do
260281
it "waits til an fd is ready for writing" do
261282
@o.rb_thread_fd_writable(@w_io).should be_nil
@@ -305,6 +326,40 @@
305326
thr.join
306327
end
307328
end
329+
330+
ruby_version_is "3.1" do
331+
describe "rb_io_maybe_wait_readable" do
332+
it "returns mask for events if operation was interrupted" do
333+
@o.rb_io_maybe_wait_readable(Errno::EINTR::Errno, @r_io, nil, false).should == IO::READABLE
334+
end
335+
336+
it "returns 0 if there is no error condition" do
337+
@o.rb_io_maybe_wait_readable(0, @r_io, nil, false).should == 0
338+
end
339+
340+
it "blocks until the io is readable and returns events that actually occurred" do
341+
@o.instance_variable_set :@write_data, false
342+
thr = Thread.new do
343+
Thread.pass until @o.instance_variable_get(:@write_data)
344+
@w_io.write "rb_io_wait_readable"
345+
end
346+
347+
@o.rb_io_maybe_wait_readable(Errno::EAGAIN::Errno, @r_io, IO::READABLE, true).should == IO::READABLE
348+
@o.instance_variable_get(:@read_data).should == "rb_io_wait_re"
349+
350+
thr.join
351+
end
352+
353+
it "raises an IOError if the IO is closed" do
354+
@r_io.close
355+
-> { @o.rb_io_maybe_wait_readable(0, @r_io, nil, false) }.should raise_error(IOError, "closed stream")
356+
end
357+
358+
it "raises an IOError if the IO is not initialized" do
359+
-> { @o.rb_io_maybe_wait_readable(0, IO.allocate, nil, false) }.should raise_error(IOError, "uninitialized stream")
360+
end
361+
end
362+
end
308363
end
309364

310365
describe "rb_thread_wait_fd" do
@@ -344,6 +399,42 @@
344399
@o.rb_wait_for_single_fd(@r_io, 1, 0, 0).should == 0
345400
end
346401
end
402+
403+
ruby_version_is "3.1" do
404+
describe "rb_io_maybe_wait" do
405+
it "waits til an fd is ready for reading" do
406+
start = false
407+
thr = Thread.new do
408+
start = true
409+
sleep 0.05
410+
@w_io.write "rb_io_maybe_wait"
411+
end
412+
413+
Thread.pass until start
414+
415+
@o.rb_io_maybe_wait(Errno::EAGAIN::Errno, @r_io, IO::READABLE, nil).should == IO::READABLE
416+
417+
thr.join
418+
end
419+
420+
it "returns mask for events if operation was interrupted" do
421+
@o.rb_io_maybe_wait(Errno::EINTR::Errno, @w_io, IO::WRITABLE, nil).should == IO::WRITABLE
422+
end
423+
424+
it "returns false if there is no error condition" do
425+
@o.rb_io_maybe_wait(0, @w_io, IO::WRITABLE, nil).should == false
426+
end
427+
428+
it "raises an IOError if the IO is closed" do
429+
@w_io.close
430+
-> { @o.rb_io_maybe_wait(0, @w_io, IO::WRITABLE, nil) }.should raise_error(IOError, "closed stream")
431+
end
432+
433+
it "raises an IOError if the IO is not initialized" do
434+
-> { @o.rb_io_maybe_wait(0, IO.allocate, IO::WRITABLE, nil) }.should raise_error(IOError, "uninitialized stream")
435+
end
436+
end
437+
end
347438
end
348439

349440
describe "rb_fd_fix_cloexec" do

spec/tags/optional/capi/io_tags.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fails:C-API IO function rb_io_maybe_wait_writable raises an IOError if the IO is not initialized
2+
fails:C-API IO function rb_io_maybe_wait_readable raises an IOError if the IO is not initialized
3+
fails:C-API IO function rb_io_maybe_wait raises an IOError if the IO is not initialized

src/main/c/cext/io.c

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,98 @@ int rb_wait_for_single_fd(int fd, int events, struct timeval *tv) {
8383
return polyglot_as_i32(polyglot_invoke(RUBY_CEXT, "rb_wait_for_single_fd", fd, events, tv_sec, tv_usec));
8484
}
8585

86+
// The only difference between this implementation and CRuby's one
87+
// is that a fiber scheduler isn't supported here
88+
VALUE rb_io_wait(VALUE io, VALUE events, VALUE timeout) {
89+
rb_io_t * fptr = NULL;
90+
RB_IO_POINTER(io, fptr);
91+
92+
struct timeval tv_storage;
93+
struct timeval *tv = NULL;
94+
95+
if (timeout != Qnil) {
96+
tv_storage = rb_time_interval(timeout);
97+
tv = &tv_storage;
98+
}
99+
100+
int ready = rb_wait_for_single_fd(fptr->fd, RB_NUM2INT(events), tv);
101+
102+
if (ready < 0) {
103+
rb_sys_fail(0);
104+
}
105+
106+
// Not sure if this is necessary:
107+
rb_io_check_closed(fptr);
108+
109+
if (ready) {
110+
return RB_INT2NUM(ready);
111+
}
112+
else {
113+
return Qfalse;
114+
}
115+
}
116+
117+
VALUE rb_io_maybe_wait(int error, VALUE io, VALUE events, VALUE timeout) {
118+
// fptr->fd can be set to -1 at any time by another thread when the GVL is
119+
// released. Many code, e.g. `io_bufread` didn't check this correctly and
120+
// instead relies on `read(-1) -> -1` which causes this code path. We then
121+
// check here whether the IO was in fact closed. Probably it's better to
122+
// check that `fptr->fd != -1` before using it in syscall.
123+
rb_io_check_closed(RFILE(io)->fptr);
124+
125+
switch (error) {
126+
// In old Linux, several special files under /proc and /sys don't handle
127+
// select properly. Thus we need avoid to call if don't use O_NONBLOCK.
128+
// Otherwise, we face nasty hang up. Sigh.
129+
// e.g. http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commit;h=31b07093c44a7a442394d44423e21d783f5523b8
130+
// http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commit;h=31b07093c44a7a442394d44423e21d783f5523b8
131+
// In EINTR case, we only need to call RUBY_VM_CHECK_INTS_BLOCKING().
132+
// Then rb_thread_check_ints() is enough.
133+
case EINTR:
134+
#if defined(ERESTART)
135+
case ERESTART:
136+
#endif
137+
// We might have pending interrupts since the previous syscall was interrupted:
138+
rb_thread_check_ints();
139+
140+
// The operation was interrupted, so retry it immediately:
141+
return events;
142+
143+
case EAGAIN:
144+
#if EWOULDBLOCK != EAGAIN
145+
case EWOULDBLOCK:
146+
#endif
147+
// The operation would block, so wait for the specified events:
148+
return rb_io_wait(io, events, timeout);
149+
150+
default:
151+
// Non-specific error, no event is ready:
152+
return Qfalse;
153+
}
154+
}
155+
156+
int rb_io_maybe_wait_readable(int error, VALUE io, VALUE timeout) {
157+
VALUE result = rb_io_maybe_wait(error, io, RB_INT2NUM(RUBY_IO_READABLE), timeout);
158+
159+
if (RTEST(result)) {
160+
return RB_NUM2INT(result);
161+
}
162+
else {
163+
return 0;
164+
}
165+
}
166+
167+
int rb_io_maybe_wait_writable(int error, VALUE io, VALUE timeout) {
168+
VALUE result = rb_io_maybe_wait(error, io, RB_INT2NUM(RUBY_IO_WRITABLE), timeout);
169+
170+
if (RTEST(result)) {
171+
return RB_NUM2INT(result);
172+
}
173+
else {
174+
return 0;
175+
}
176+
}
177+
86178
VALUE rb_io_addstr(VALUE io, VALUE str) {
87179
// use write instead of just #<<, it's closer to what MRI does
88180
// and avoids stack-overflow in zlib where #<< is defined with this method

src/main/ruby/truffleruby/core/io.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ class EINPROGRESSWaitWritable < Errno::EINPROGRESS
7575
end
7676
end
7777

78+
READABLE = Truffle::Config['platform.poll.POLLIN']
79+
WRITABLE = Truffle::Config['platform.poll.POLLOUT']
80+
PRIORITY = Truffle::Config['platform.poll.POLLPRI']
81+
7882
# InternalBuffer provides a sliding window into a region of bytes.
7983
# The buffer is filled to the +used+ indicator, which is
8084
# always less than or equal to +total+. As bytes are taken

0 commit comments

Comments
 (0)