# The `IO::Buffered` mixin enhances an `IO` with input/output buffering. # # The buffering behaviour can be turned on/off with the `#sync=` and # `#read_buffering=` methods. # # Additionally, several methods, like `#gets`, are implemented in a more # efficient way. module IO::Buffered @in_buffer = Pointer(UInt8).null @out_buffer = Pointer(UInt8).null @in_buffer_rem = Bytes.empty @out_count = 0 @sync = false @read_buffering = true @flush_on_newline = false @buffer_size = IO::DEFAULT_BUFFER_SIZE # Reads at most *slice.size* bytes from the wrapped `IO` into *slice*. # Returns the number of bytes read. # # TODO: Add return type restriction `Int32` abstract def unbuffered_read(slice : Bytes) # Writes *slice* entirely into the wrapped `IO`. # # TODO: Add return type restriction `Nil` abstract def unbuffered_write(slice : Bytes) # Flushes the wrapped `IO`. # # TODO: Add return type restriction `Nil` abstract def unbuffered_flush # Closes the wrapped `IO`. # # TODO: Add return type restriction `Nil` abstract def unbuffered_close # Rewinds the wrapped `IO`. # # TODO: Add return type restriction `Nil` abstract def unbuffered_rewind # Return the buffer size used def buffer_size : Int32 @buffer_size end # Set the buffer size of both the read and write buffer # Cannot be changed after any of the buffers have been allocated def buffer_size=(value) if (@in_buffer || @out_buffer) && (buffer_size != value) raise ArgumentError.new("Cannot change buffer_size after buffers have been allocated") end @buffer_size = value end # :nodoc: def read_byte : UInt8? check_open fill_buffer if read_buffering? && @in_buffer_rem.empty? if @in_buffer_rem.empty? return nil if read_buffering? byte = uninitialized UInt8 if read(Slice.new(pointerof(byte), 1)) == 1 byte else nil end else b = @in_buffer_rem[0] @in_buffer_rem += 1 b end end # Buffered implementation of `IO#read(slice)`. def read(slice : Bytes) : Int32 check_open count = slice.size return 0 if count == 0 if @in_buffer_rem.empty? # If we are asked to read more than half the buffer's size, # read directly into the slice, as it's not worth the extra # memory copy. if !read_buffering? || count >= @buffer_size // 2 return unbuffered_read(slice[0, count]).to_i else fill_buffer return 0 if @in_buffer_rem.empty? end end to_read = Math.min(count, @in_buffer_rem.size) slice.copy_from(@in_buffer_rem.to_unsafe, to_read) @in_buffer_rem += to_read to_read end # Returns the bytes hold in the read buffer. # # This method only performs a read to return # peek data if the current buffer is empty: # otherwise no read is performed and whatever # is in the buffer is returned. def peek : Bytes check_open if @in_buffer_rem.empty? fill_buffer if @in_buffer_rem.empty? return Bytes.empty # EOF end end @in_buffer_rem end # :nodoc: def skip(bytes_count : Int) : Nil check_open if bytes_count <= @in_buffer_rem.size @in_buffer_rem += bytes_count return end bytes_count -= @in_buffer_rem.size @in_buffer_rem = Bytes.empty super(bytes_count) end # Buffered implementation of `IO#write(slice)`. def write(slice : Bytes) : Nil check_open return if slice.empty? count = slice.size if sync? return unbuffered_write(slice) end if flush_on_newline? index = slice[0, count.to_i32].rindex('\n'.ord.to_u8) if index flush index += 1 unbuffered_write slice[0, index] slice += index count -= index end end if count >= @buffer_size flush return unbuffered_write slice[0, count] end if count > @buffer_size - @out_count flush end slice.copy_to(out_buffer + @out_count, count) @out_count += count end # :nodoc: def write_byte(byte : UInt8) : Nil check_open if sync? return super end if @out_count >= @buffer_size flush end out_buffer[@out_count] = byte @out_count += 1 if flush_on_newline? && byte === '\n' flush end end # Returns the current position (in bytes) in this `IO`. # # ``` # File.write("testfile", "hello") # # file = File.new("testfile") # file.pos # => 0 # file.gets(2) # => "he" # file.pos # => 2 # ``` def pos : Int64 flush in_rem = @in_buffer_rem.size # TODO In 2.0 we should make `unbuffered_pos` an abstract method of Buffered if self.responds_to?(:unbuffered_pos) self.unbuffered_pos - in_rem else super - in_rem end end # Turns on/off `IO` **write** buffering. When *sync* is set to `true`, no buffering # will be done (that is, writing to this `IO` is immediately synced to the # underlying `IO`). def sync=(sync : Bool) : Bool flush if sync && !@sync @sync = sync end # Determines if this `IO` does write buffering. If `true`, no buffering is done. def sync? : Bool @sync end # Turns on/off `IO` **read** buffering. def read_buffering=(read_buffering : Bool) : Bool @read_buffering = read_buffering end # Determines whether this `IO` buffers reads. def read_buffering? : Bool @read_buffering end # Turns on/off flushing the underlying `IO` when a newline is written. def flush_on_newline=(flush_on_newline : Bool) : Bool @flush_on_newline = flush_on_newline end # Determines if this `IO` flushes automatically when a newline is written. def flush_on_newline? : Bool @flush_on_newline end # Flushes any buffered data and the underlying `IO`. Returns `self`. def flush : self unbuffered_write(Slice.new(out_buffer, @out_count)) if @out_count > 0 unbuffered_flush @out_count = 0 self end # Flushes and closes the underlying `IO`. def close : Nil flush if @out_count > 0 ensure unbuffered_close end # Rewinds the underlying `IO`. Returns `self`. def rewind : self unbuffered_rewind @in_buffer_rem = Bytes.empty self end private def fill_buffer in_buffer = in_buffer() size = unbuffered_read(Slice.new(in_buffer, @buffer_size)).to_i @in_buffer_rem = Slice.new(in_buffer, size) end private def in_buffer @in_buffer ||= GC.malloc_atomic(@buffer_size.to_u32).as(UInt8*) end private def out_buffer @out_buffer ||= GC.malloc_atomic(@buffer_size.to_u32).as(UInt8*) end end