From 2a07541de1eb8d8f933c25873736a6fbd157b734 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Mon, 18 Nov 2024 13:26:56 +0200 Subject: [PATCH] Add IO#pread and IO#pwrite methods --- CHANGELOG.md | 1 + .../truffleruby/truffleruby-abi-version.h | 2 +- spec/ruby/core/io/pread_spec.rb | 6 +++ spec/tags/core/io/pread_tags.txt | 21 -------- spec/tags/core/io/pwrite_tags.txt | 9 ---- src/main/ruby/truffleruby/core/io.rb | 35 +++++++++++++ src/main/ruby/truffleruby/core/posix.rb | 50 +++++++++++++++++++ 7 files changed, 93 insertions(+), 31 deletions(-) delete mode 100644 spec/tags/core/io/pread_tags.txt delete mode 100644 spec/tags/core/io/pwrite_tags.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f91ca99dbb5..133b164ab47f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Compatibility: * Add `rb_category_warn` function (#3710, @andrykonchin). * Add `rb_gc_mark_locations()` (#3704, @andrykonchin). * Implement `rb_str_format()` (#3716, @andrykonchin). +* Add `IO#{pread, pwrite}` methods (#3718, @andrykonchin). Performance: diff --git a/lib/cext/include/truffleruby/truffleruby-abi-version.h b/lib/cext/include/truffleruby/truffleruby-abi-version.h index 755035a3f380..ae700777b0cc 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.3" +#define TRUFFLERUBY_ABI_VERSION "3.3.5.4" #endif diff --git a/spec/ruby/core/io/pread_spec.rb b/spec/ruby/core/io/pread_spec.rb index 6d93b432c263..dc7bcedf3e5c 100644 --- a/spec/ruby/core/io/pread_spec.rb +++ b/spec/ruby/core/io/pread_spec.rb @@ -59,6 +59,12 @@ @file.pread(0, 4).should == "" end + it "returns a buffer for maxlen = 0 when buffer specified" do + buffer = +"foo" + @file.pread(0, 4, buffer).should.equal?(buffer) + buffer.should == "foo" + end + it "ignores the offset for maxlen = 0, even if it is out of file bounds" do @file.pread(0, 400).should == "" end diff --git a/spec/tags/core/io/pread_tags.txt b/spec/tags/core/io/pread_tags.txt deleted file mode 100644 index 84560c9368be..000000000000 --- a/spec/tags/core/io/pread_tags.txt +++ /dev/null @@ -1,21 +0,0 @@ -fails:IO#pread accepts a length, and an offset -fails:IO#pread accepts a length, an offset, and an output buffer -fails:IO#pread does not advance the file pointer -fails:IO#pread raises EOFError if end-of-file is reached -fails:IO#pread raises IOError when file is not open in read mode -fails:IO#pread raises IOError when file is closed -fails:IO#pread shrinks the buffer in case of less bytes read -fails:IO#pread grows the buffer in case of more bytes read -fails:IO#pread ignores the current offset -fails:IO#pread returns an empty string for maxlen = 0 -fails:IO#pread ignores the offset for maxlen = 0, even if it is out of file bounds -fails:IO#pread does not reset the buffer when reading with maxlen = 0 -fails:IO#pread converts maxlen to Integer using #to_int -fails:IO#pread converts offset to Integer using #to_int -fails:IO#pread converts a buffer to String using to_str -fails:IO#pread raises TypeError if maxlen is not an Integer and cannot be coerced into Integer -fails:IO#pread raises TypeError if offset is not an Integer and cannot be coerced into Integer -fails:IO#pread raises ArgumentError for negative values of maxlen -fails:IO#pread raised Errno::EINVAL for negative values of offset -fails:IO#pread raises TypeError if the buffer is not a String and cannot be coerced into String -fails:IO#pread preserves the encoding of the given buffer diff --git a/spec/tags/core/io/pwrite_tags.txt b/spec/tags/core/io/pwrite_tags.txt deleted file mode 100644 index ee8093f8abe5..000000000000 --- a/spec/tags/core/io/pwrite_tags.txt +++ /dev/null @@ -1,9 +0,0 @@ -fails:IO#pwrite returns the number of bytes written -fails:IO#pwrite accepts a string and an offset -fails:IO#pwrite does not advance the pointer in the file -fails:IO#pwrite raises IOError when file is not open in write mode -fails:IO#pwrite raises IOError when file is closed -fails:IO#pwrite calls #to_s on the object to be written -fails:IO#pwrite calls #to_int on the offset -fails:IO#pwrite raises a NoMethodError if object does not respond to #to_s -fails:IO#pwrite raises a TypeError if the offset cannot be converted to an Integer diff --git a/src/main/ruby/truffleruby/core/io.rb b/src/main/ruby/truffleruby/core/io.rb index 2dfc50b0c33f..0c51f86b0163 100644 --- a/src/main/ruby/truffleruby/core/io.rb +++ b/src/main/ruby/truffleruby/core/io.rb @@ -1614,6 +1614,32 @@ def pos=(offset) seek Primitive.rb_num2long(offset), SEEK_SET end + def pread(length, offset, buffer = nil) + ensure_open_and_readable + + length = Primitive.rb_to_int(length) + offset = Primitive.rb_to_int(offset) + + raise ArgumentError, 'negative string size (or size too big)' if length < 0 + raise Errno::EINVAL, 'offset must not be negative' if offset < 0 + + if length == 0 + return buffer ? buffer : +'' + end + + str, errno = Truffle::POSIX.pread_string(self, length, offset) + Errno.handle_errno(errno) unless errno == 0 + + raise EOFError if Primitive.nil? str + + if buffer + buffer = StringValue(buffer) + buffer.replace str.force_encoding(buffer.encoding) + else + str + end + end + ## # Writes each given argument.to_s to the stream or $_ (the result of last # IO#gets) if called without arguments. Appends $\.to_s to output. Returns @@ -1668,6 +1694,15 @@ def printf(fmt, *args) end Truffle::Graal.always_split(instance_method(:printf)) + def pwrite(object, offset) + string = Truffle::Type.rb_obj_as_string(object) + offset = Primitive.rb_to_int(offset) + + ensure_open_and_writable + + Truffle::POSIX.pwrite_string(self, string, offset) + end + def read(length = nil, buffer = nil) ensure_open_and_readable buffer = StringValue(buffer) if buffer diff --git a/src/main/ruby/truffleruby/core/posix.rb b/src/main/ruby/truffleruby/core/posix.rb index 55362e4a9ed5..093eb855d31c 100644 --- a/src/main/ruby/truffleruby/core/posix.rb +++ b/src/main/ruby/truffleruby/core/posix.rb @@ -218,6 +218,7 @@ def self.attach_function_eagerly(native_name, argument_types, return_type, attach_function :truffleposix_poll_single_fd, [:int, :int, :int], :int, LIBTRUFFLEPOSIX attach_function :poll, [:pointer, :nfds_t, :int], :int attach_function :read, [:int, :pointer, :size_t], :ssize_t, LIBC, true + attach_function :pread, [:int, :pointer, :size_t, :off_t], :ssize_t, LIBC, true attach_function :readlink, [:string, :pointer, :size_t], :ssize_t attach_function :realpath, [:string, :pointer], :pointer attach_function :truffleposix_readdir_multiple, [:pointer, :int, :int, :int, :pointer], :int, LIBTRUFFLEPOSIX @@ -236,6 +237,7 @@ def self.attach_function_eagerly(native_name, argument_types, return_type, attach_function :unlink, [:string], :int attach_function :truffleposix_utimes, [:string, :long, :int, :long, :int], :int, LIBTRUFFLEPOSIX attach_function :write, [:int, :pointer, :size_t], :ssize_t, LIBC, true + attach_function :pwrite, [:int, :pointer, :size_t, :off_t], :ssize_t, LIBC, true Truffle::Boot.delay do if NATIVE @@ -485,6 +487,29 @@ def self.read_string_polyglot(io, length) end end + def self.pread_string_native(io, length, offset) + fd = io.fileno + buffer = Primitive.io_thread_buffer_allocate(length) + + begin + bytes_read = Truffle::POSIX.pread(fd, buffer, length, offset) + + if bytes_read < 0 # error + [nil, Errno.errno] + elsif bytes_read == 0 # EOF + [nil, 0] + else + [buffer.read_string(bytes_read), 0] + end + ensure + Primitive.io_thread_buffer_free(buffer) + end + end + + def self.pread_string_polyglot(io, length, offset) + raise 'Not implemented' # there is not way to read starting from a specific position + end + # #write_string (either #write_string_native or #write_string_polyglot) is # called by IO#syswrite, IO#write, and IO::InternalBuffer#empty_to @@ -577,6 +602,27 @@ def self.write_string_nonblock_polyglot(io, string) end end + def self.pwrite_string_native(io, string, offset) + fd = io.fileno + length = string.bytesize + buffer = Primitive.io_thread_buffer_allocate(length) + + begin + buffer.write_bytes string + + written = Truffle::POSIX.pwrite(fd, buffer, length, offset) + Errno.handle_errno(Errno.errno) if written < 0 + + written + ensure + Primitive.io_thread_buffer_free(buffer) + end + end + + def self.pwrite_string_polyglot(io, length, offset) + raise 'Not implemented' # there is not way to write starting from a specific position + end + # Select between native and polyglot variants Truffle::Boot.delay do @@ -584,15 +630,19 @@ def self.write_string_nonblock_polyglot(io, string) class << self alias_method :read_string, :read_string_polyglot alias_method :read_to_buffer, :read_to_buffer_polyglot + alias_method :pread_string, :pread_string_polyglot alias_method :write_string, :write_string_polyglot alias_method :write_string_nonblock, :write_string_nonblock_polyglot + alias_method :pwrite_string, :pwrite_string_polyglot end else class << self alias_method :read_string, :read_string_native alias_method :read_to_buffer, :read_to_buffer_native + alias_method :pread_string, :pread_string_native alias_method :write_string, :write_string_native alias_method :write_string_nonblock, :write_string_nonblock_native + alias_method :pwrite_string, :pwrite_string_native end end end