Class: Ione::ByteBuffer

Inherits:
Object
  • Object
show all
Defined in:
lib/ione/byte_buffer.rb

Overview

A byte buffer is a more efficient way of working with bytes than using a regular Ruby string. It also has convenient methods for reading integers shorts and single bytes that are faster than String#unpack.

When you use a string as a buffer, by adding to the end and taking away from the beginning, Ruby will continue to grow the backing array of characters. This means that the longer you use the string the worse the performance will get and the more memory you waste.

ByteBuffer solves the problem by using two strings: one is the read buffer and one is the write buffer. Writes go to the write buffer only, and reads read from the read buffer until it is empty, then a new write buffer is created and the old write buffer becomes the new read buffer.

Since:

  • v1.0.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(initial_bytes = nil) ⇒ ByteBuffer

Returns a new instance of ByteBuffer.

Since:

  • v1.0.0



20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/ione/byte_buffer.rb', line 20

def initialize(initial_bytes=nil)
  @read_buffer = ''
  @offset = 0
  @length = 0
  if initial_bytes && !initial_bytes.empty?
    @write_buffer = initial_bytes.dup
    @write_buffer.force_encoding(::Encoding::BINARY)
    @length = @write_buffer.bytesize
  else
    @write_buffer = ''
  end
end

Instance Attribute Details

#lengthObject (readonly) Also known as: size, bytesize

Returns the number of bytes in the buffer.

The value is cached so this is a cheap operation.

Since:

  • v1.0.0



36
37
38
# File 'lib/ione/byte_buffer.rb', line 36

def length
  @length
end

Instance Method Details

#append(bytes) ⇒ Ione::ByteBuffer Also known as: <<

Note:

When the bytes are not in an ASCII compatible encoding they are copied and retagged as Encoding::BINARY before they are appended to the buffer – this is required to avoid Ruby retagging the whole buffer with the encoding of the new bytes. If you can, make sure that the data you append is ASCII compatible (i.e. responds true to #ascii_only?), otherwise you will pay a small penalty for each append due to the extra copy that has to be made.

Append the bytes from a string or another byte buffer to this buffer.

Parameters:

Returns:

Since:

  • v1.0.0



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/ione/byte_buffer.rb', line 60

def append(bytes)
  if bytes.is_a?(self.class)
    bytes.append_to(self)
  else
    bytes = bytes.to_s
    unless bytes.ascii_only?
      bytes = bytes.dup.force_encoding(::Encoding::BINARY)
    end
    retag = @write_buffer.empty?
    @write_buffer << bytes
    @write_buffer.force_encoding(::Encoding::BINARY) if retag
    @length += bytes.bytesize
  end
  self
end

#cheap_peekString

Return as much of the buffer as possible without having to concatenate or allocate any unnecessary strings.

If the buffer is not empty this method will return something, but there are no guarantees as to how much it will return. It's primarily useful in situations where a loop wants to offer some bytes but can't be sure how many will be accepted — for example when writing to a socket.

Examples:

feeding bytes to a socket

while true
  _, writables, _ = IO.select(nil, sockets)
  if writables
    writables.each do |io|
      n = io.write_nonblock(buffer.cheap_peek)
      buffer.discard(n)
    end
  end

Returns:

  • (String)

    some bytes from the start of the buffer

Since:

  • v1.0.0



255
256
257
258
259
260
# File 'lib/ione/byte_buffer.rb', line 255

def cheap_peek
  if @offset >= @read_buffer.bytesize
    swap_buffers
  end
  @read_buffer[@offset, @read_buffer.bytesize - @offset]
end

#discard(n) ⇒ Ione::ByteBuffer

Remove the first N bytes from the buffer.

Parameters:

  • n (Integer)

    the number of bytes to remove from the buffer

Returns:

Raises:

  • RangeError when there are not enough bytes in the buffer

Since:

  • v1.0.0



82
83
84
85
86
87
88
# File 'lib/ione/byte_buffer.rb', line 82

def discard(n)
  raise RangeError, 'Cannot discard a negative number of bytes' if n < 0
  raise RangeError, "#{n} bytes to discard but only #{@length} available" if @length < n
  @offset += n
  @length -= n
  self
end

#dupObject

Since:

  • v1.0.0



271
272
273
# File 'lib/ione/byte_buffer.rb', line 271

def dup
  self.class.new(to_str)
end

#empty?Boolean

Returns true when the number of bytes in the buffer is zero.

The length is cached so this is a cheap operation.

Returns:

  • (Boolean)

Since:

  • v1.0.0



43
44
45
# File 'lib/ione/byte_buffer.rb', line 43

def empty?
  length == 0
end

#eql?(other) ⇒ Boolean Also known as: ==

Returns:

  • (Boolean)

Since:

  • v1.0.0



262
263
264
# File 'lib/ione/byte_buffer.rb', line 262

def eql?(other)
  self.to_str.eql?(other.to_str)
end

#hashObject

Since:

  • v1.0.0



267
268
269
# File 'lib/ione/byte_buffer.rb', line 267

def hash
  to_str.hash
end

#index(substring, start_index = 0) ⇒ Object

Since:

  • v1.0.0



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ione/byte_buffer.rb', line 177

def index(substring, start_index=0)
  if @offset >= @read_buffer.bytesize
    swap_buffers
  end
  read_buffer_length = @read_buffer.bytesize
  if start_index + substring.bytesize <= read_buffer_length - @offset && (index = @read_buffer.index(substring, @offset + start_index))
    index - @offset
  elsif start_index + substring.bytesize <= read_buffer_length - @offset + @write_buffer.bytesize
    merge_read_buffer
    start_index = read_buffer_length - substring.bytesize if read_buffer_length - substring.bytesize > start_index
    @read_buffer.index(substring, start_index)
  else
    nil
  end
end

#inspectObject

Since:

  • v1.0.0



280
281
282
# File 'lib/ione/byte_buffer.rb', line 280

def inspect
  %(#<#{self.class.name}: #{to_str.inspect}>)
end

#read(n) ⇒ String

Remove and return the first N bytes from the buffer.

Parameters:

  • n (Integer)

    the number of bytes to remove and return from the buffer

Returns:

  • (String)

    a string with the bytes, the string will be tagged with Encoding::BINARY.

Raises:

  • RangeError when there are not enough bytes in the buffer

Since:

  • v1.0.0



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/ione/byte_buffer.rb', line 96

def read(n)
  raise RangeError, 'Cannot read a negative number of bytes' if n < 0
  raise RangeError, "#{n} bytes required but only #{@length} available" if @length < n
  if @offset >= @read_buffer.bytesize
    swap_buffers
  end
  if @offset + n > @read_buffer.bytesize
    s = read(@read_buffer.bytesize - @offset)
    s << read(n - s.bytesize)
    s
  else
    s = @read_buffer[@offset, n]
    @offset += n
    @length -= n
    s
  end
end

#read_byte(signed = false) ⇒ Integer

Remove and return the first byte from the buffer and decode it as a signed or unsigned integer.

Parameters:

  • signed (Boolean) (defaults to: false)

    whether or not to interpret the byte as a signed number of not

Returns:

  • (Integer)

    the integer interpretation of the byte

Raises:

  • RangeError when the buffer is empty

Since:

  • v1.0.0



165
166
167
168
169
170
171
172
173
174
175
# File 'lib/ione/byte_buffer.rb', line 165

def read_byte(signed=false)
  raise RangeError, "No bytes available to read byte" if empty?
  if @offset >= @read_buffer.bytesize
    swap_buffers
  end
  b = @read_buffer.getbyte(@offset)
  b = (b & 0x7f) - (b & 0x80) if signed
  @offset += 1
  @length -= 1
  b
end

#read_intInteger

Remove and return the first four bytes from the buffer and decode them as an unsigned integer.

Returns:

  • (Integer)

    the big-endian integer interpretation of the four bytes

Raises:

  • RangeError when there are not enough bytes in the buffer

Since:

  • v1.0.0



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ione/byte_buffer.rb', line 118

def read_int
  raise RangeError, "4 bytes required to read an int, but only #{@length} available" if @length < 4
  if @offset >= @read_buffer.bytesize
    swap_buffers
  end
  if @read_buffer.bytesize >= @offset + 4
    i0 = @read_buffer.getbyte(@offset + 0)
    i1 = @read_buffer.getbyte(@offset + 1)
    i2 = @read_buffer.getbyte(@offset + 2)
    i3 = @read_buffer.getbyte(@offset + 3)
    @offset += 4
    @length -= 4
  else
    i0 = read_byte
    i1 = read_byte
    i2 = read_byte
    i3 = read_byte
  end
  (i0 << 24) | (i1 << 16) | (i2 << 8) | i3
end

#read_shortInteger

Remove and return the first two bytes from the buffer and decode them as an unsigned integer.

Returns:

  • (Integer)

    the big-endian integer interpretation of the two bytes

Raises:

  • RangeError when there are not enough bytes in the buffer

Since:

  • v1.0.0



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/ione/byte_buffer.rb', line 143

def read_short
  raise RangeError, "2 bytes required to read a short, but only #{@length} available" if @length < 2
  if @offset >= @read_buffer.bytesize
    swap_buffers
  end
  if @read_buffer.bytesize >= @offset + 2
    i0 = @read_buffer.getbyte(@offset + 0)
    i1 = @read_buffer.getbyte(@offset + 1)
    @offset += 2
    @length -= 2
  else
    i0 = read_byte
    i1 = read_byte
  end
  (i0 << 8) | i1
end

#to_strObject Also known as: to_s

Since:

  • v1.0.0



275
276
277
# File 'lib/ione/byte_buffer.rb', line 275

def to_str
  (@read_buffer + @write_buffer)[@offset, @length]
end

#update(location, bytes) ⇒ Ione::ByteBuffer

Overwrite a portion of the buffer with new bytes.

The number of bytes that will be replaced depend on the size of the replacement string. If you pass a five byte string the five bytes starting at the location will be replaced.

When you pass more bytes than the size of the buffer after the location only as many as needed to replace the remaining bytes of the buffer will actually be used.

Make sure that you get your location right, if you have discarded bytes from the buffer all of the offsets will have changed.

Examples:

replacing bytes in the middle of a buffer

buffer = ByteBuffer.new("hello world!")
bufferupdate(6, "fnord")
buffer # => "hello fnord!"

replacing bytes at the end of the buffer

buffer = ByteBuffer.new("my name is Jim")
buffer.update(11, "Sammy")
buffer # => "my name is Sam"

Parameters:

  • location (Integer)

    the starting location where the new bytes should be inserted

  • bytes (String)

    the replacement bytes

Returns:

Since:

  • v1.0.0



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/ione/byte_buffer.rb', line 220

def update(location, bytes)
  absolute_offset = @offset + location
  bytes_length = bytes.bytesize
  if absolute_offset >= @read_buffer.bytesize
    @write_buffer[absolute_offset - @read_buffer.bytesize, bytes_length] = bytes
  else
    overflow = absolute_offset + bytes_length - @read_buffer.bytesize
    read_buffer_portion = bytes_length - overflow
    @read_buffer[absolute_offset, read_buffer_portion] = bytes[0, read_buffer_portion]
    if overflow > 0
      @write_buffer[0, overflow] = bytes[read_buffer_portion, bytes_length - 1]
    end
  end
  self
end