Ruby IO::Buffer

これは Ruby/Rails Advent Calendar 2025 の 4日目の記事です。

qiita.com

Ruby 3.1 から IO::Buffer というのが増えたんだけど、よく知らなかったので調べてみた。

docs.ruby-lang.org

もともとは Fiber Scheduler のために導入されたものっぽいけどよくわかってない。

文字列やファイル内のデータから直接バイナリデータとして読んだり書き換えたりするためのものっぽい。 experimental(実験的) みたいなのでそのつもりで使おう。

以下はドキュメントやソースコードを読みながら Linux で実際に試してみたりしたもの。 調べてる中で each_byte のバグをみつけたので直したりした

インスタンス作成

サイズを指定して作成 - IO::Buffer.new(size=IO::Buffer::DEFAULT_SIZE, flags=0)

flags は IO::Buffer::INTERNAL, IO::Buffer::MAPPED を論理和で指定可能。

IO::Buffer::INTERNAL は malloc(3) でメモリを確保する。 IO::Buffer::MAPPED は mmap(2) を使う。

どちらも指定しない場合は、size が IO::Buffer::PAGE_SIZE(4096)未満の場合は IO::Buffer::INTERNAL、それ以上の場合は IO::Buffer::MAPPED になる。

IO::Buffer::INTERNAL | IO::Buffer::MAPPED という指定をすると IO::Buffer::INTERNAL が優先される。 けど、flags はそのまま保持されるので、internal? も真、mapped? も真という状態になってその後の操作に悪影響を及ぼす(イマイチな気がする)。

文字列から作成(ブロックなし) - IO::Buffer.for(string)

string が freeze されている場合はそのまま、freeze されていない場合はコピーされて IO::Buffer インスタンス内部に保持される。インスタンスは読み込み専用になる。 string を変更してもバッファは変更されない。

str = 'abcdefg'
b = IO::Buffer.for(str)
b.readonly?  #=> true
str[2,1] = 'x'
str  #=> "abxdefg"
b.get_string  #=> "abcdefg"

文字列から作成(ブロックつき) - IO::Buffer.for(string) {|io_buffer| ...}

string はコピーされない。なので、ブロック内で IO::Buffer インスタンスを変更すると string も変わる。 string が freeze されている場合は、インスタンスは読み込み専用になる。 戻り値はブロックの評価結果。 ブロック内で string を変更しようとするとエラーになる。

str = 'abcdefg'
IO::Buffer.for(str) do |b|
  b.set_string('hoge')
  b.get_string  #=> "hogeefg"
  str  #=> 'hogeefg"
  str[0,4] = '0123'  #=> can't modify string; temporarily locked (RuntimeError)
end

ブロック外でインスタンスを参照すると空になる。

b = IO::Buffer.for('abcdefg'){ _1 }
b.get_string  #=> ""
b.null?  #=> true

ファイルから作成 - IO::Buffer.map(file, size=nil, offset=0, flags=0)

ファイルの内容がメモリにマップされてそれを読み書きできる。Linux の場合は内部的に mmap(2) を使ってるっぽい。

File.write('/tmp/hoge', 'abcdefg')
f = File.open('/tmp/hoge', 'r+')
b = IO::Buffer.map(f)
b.set_string('hoge')
File.read('/tmp/hoge')  #=> "hogeefg"

size を指定した場合はそのサイズだけマップする。 offset を指定した場合は、マップするファイルの位置を指定する。 flags には IO::Buffer::READONLY、IO::Buffer::PRIVATE を論理和で指定できる。 ファイルが読み込み専用でオープンされている場合は、flags に IO::Buffer::READONLY か IO::Buffer::PRIVATE を指定する必要がある。 IO::Buffer::READONLY が指定されたら作成されたオブジェクトは書き込み不可になる。 IO::Buffer::PRIVATE は mmap(2) の MAP_PRIVATE と同じ。ファイルとマッピングするんだけど書き込みはプライベートメモリに保持されファイルには反映されない。

f = File.open('/tmp/hoge', 'r')
IO::Buffer.map(f)  #=> Permission denied - io_buffer_map_file:mmap (Errno::EACCES)
IO::Buffer.map(f, nil, 0, IO::Buffer::READONLY)  #=> OK

mmap(2) の制約な気はするけど 0バイトのファイルを指定するとエラーになる。

File.write('/tmp/empty', '')
f = File.open('/tmp/empty', 'r+')
IO::Buffer.map(f)  #=> Invalid argument - io_buffer_map_file:mmap (Errno::EINVAL)

size にファイルサイズ以上のサイズ(ページ単位で超過するようなサイズ)を指定して作成したインスタンスでデータを読むと Ruby が Bus Error で落ちる。イマイチ。

File.write('/tmp/hoge', 'abcdefg')  #=> 7
IO::Buffer::PAGE_SIZE  #=> 4096
b = IO::Buffer.map(File.open('/tmp/hoge', 'r+'), 4097)
b.get_string
#=> [BUG] Bus Error at 0x000073c1bd0ca000
...

これも mmap(2) の制約な気がするけど offset はOSのページサイズ単位で指定しないとエラーになる。

File.write('/tmp/hoge', 'abcdefg'*1000) #=> 7000
f = File.open('/tmp/hoge', 'r+')
b = IO::Buffer.map(f, nil, 1024)  #=> Invalid argument - io_buffer_map_file:mmap (Errno::EINVAL)
b = IO::Buffer.map(f, nil, 4096)  #=> OK

map は面白そうなんだけど色々気をつけて使わないといけなそうで厳しい。

サイズを指定してブロックを実行して文字列を返す - IO::Buffer.string(length) {|io_buffer| ...}

バッファのサイズを指定してブロックを実行し、内部的に生成した文字列オブジェクトを返す。

IO::Buffer.string(4){|b| b.set_string('hoge')}  #=> "hoge"

バッファから読み込み

文字列として取り出し - get_string(offset=0, length=nil, encoding=Encoding::BINARY)

バッファデータを文字列として取り出す。 offset はバッファ内の位置、length は長さ。どちらもバイト単位。 length が nil の場合は offset から末尾まで。 encoding は取り出した文字列オブジェクトのエンコーディング。 encoding を指定しても offset と length はバイト単位なので注意。

b = IO::Buffer.for('abcdefg')
b.get_string  #=> "abcdefg"
b.get_string(2, 4)  #=> "cdef"
b = IO::Buffer.for('ほげほげ')
b.get_string  #=> "\xE3\x81\xBB\xE3\x81\x92\xE3\x81\xBB\xE3\x81\x92"
b.get_string(0, nil, 'utf-8')  #=> "ほげほげ"
b.get_string(2, 5, 'utf-8')  #=> "\xBBげ\xE3"

返される文字列はファイルとかにマップされてない普通の文字列オブジェクトなのでその文だけメモリを食うので注意。

system "cat /proc/#$$/status | grep -E 'VmSize|VmData'"
# VmSize:    593408 kB
# VmData:    517288 kB

f = File.open('/tmp/largefile')
b = IO::Buffer.map(f, nil, 0, IO::Buffer::READONLY)
system "cat /proc/#$$/status | grep -E 'VmSize|VmData'"
# VmSize:   1572916 kB  仮想メモリは増えてるけど
# VmData:    520232 kB  データ領域は増えてない

s = b.get_string;
system "cat /proc/#$$/status | grep -E 'VmSize|VmData'"
# VmSize:   2549868 kB
# VmData:   1497184 kB  文字列分のデータ領域が増えた

数値として取り出し - get_value(buffer_type, offset)

形式を指定して数値として取り出す。 buffer_type は形式、offset はバッファ内の位置。

s = "\x01\x02\x03\x04"
b = IO::Buffer.for(s)
b.get_value(:U8, 0)  #=> 0x01
b.get_value(:U16, 0)  #=> 0x102
b.get_value(:u16, 0)  #=> 0x201
b.get_value(:U32, 0)  #=> 0x01020304
b.get_value(:u32, 0)  #=> 0x04030201

buffer_type はシンボルで指定する。U u は符号なし整数、S, s は符号付き整数、F, f は浮動小数点数で、大文字がビッグエンディアン、小文字がリトルエンディアン。続く数字はビット数って感じ。

  • :U8 - 1バイト符号なし整数
  • :S8 - 1バイト符号付き整数
  • :u16 - 2バイト符号なし整数(リトルエンディアン)
  • :U16 - 2バイト符号なし整数(ビッグエンディアン)
  • :s16 - 2バイト符号付き整数(リトルエンディアン)
  • :S16 - 2バイト符号付き整数(ビッグエンディアン)
  • :u32 - 4バイト符号なし整数(リトルエンディアン)
  • :U32 - 4バイト符号なし整数(ビッグエンディアン)
  • :s32 - 4バイト符号付き整数(リトルエンディアン)
  • :S32 - 4バイト符号付き整数(ビッグエンディアン)
  • :u64 - 8バイト符号なし整数(リトルエンディアン)
  • :U64 - 8バイト符号なし整数(ビッグエンディアン)
  • :s64 - 8バイト符号付き整数(リトルエンディアン)
  • :S64 - 8バイト符号付き整数(ビッグエンディアン)
  • :f32 - 4バイト浮動小数点数(リトルエンディアン)
  • :F32 - 4バイト浮動小数点数(ビッグエンディアン)
  • :f64 - 8バイト浮動小数点数(リトルエンディアン)
  • :F64 - 8バイト不動小数点数(ビッグエンディアン)

複数の数値を取り出し - get_values(buffer_types, offset)

形式を配列で指定して数値の配列として取り出す。

buffer_type は形式の配列。offset はバッファ内の位置。

b = IO::Buffer.for("\x01\x02\x03\x04\x05\x06\x07\x08")
b.get_values([:U8, :U16, :U32], 0)  #=> [0x01, 0x0203, 0x04050607]

値を繰り返し取り出し - values(buffer_type=:U8, offset=0, [count])

buffer_type の形式で count 回数繰り返して数値として取り出す。 count を指定しない場合はバッファの最後まで。

b = IO::Buffer.for("\x01\x02\x03\x04\x05")
b.values  #=> [0x01, 0x02, 0x03, 0x04, 0x05]
b.values(:U16)  #=> [0x0102, 0x0304]
b.values(:U8, 2)  #=> [0x03, 0x4, 0x5]
b.values(:U8, 3, 1)  #=> [0x4]

バッファに書き込み

文字列を設定する - set_string(string, offset=0, length=nil, source_offset=0)

string をバッファに書き込む。 offset はバッファ内の位置、length はバイト数、source_offset は string 内の位置。

b = IO::Buffer.new(10)
b.set_string('abcdefg')
b.get_string  #=> "abcdefg\0\0\0"

b = IO::Buffer.new(10)
b.set_string('abcdefg', 4, 3, 2)  # 'abcdefg' の位置2(3バイト目)から3バイト分をバッファの位置4(5バイト目)に書き込む
b.get_string  #=> "\0\0\0\0cde\0\0\0"

数値を設定する - set_value(type, offset, value)

type 形式で数値 value をバッファの offset 位置に書き込む。 type は get_value と同じ。

b = IO::Buffer.new(10)
b.set_value(:u32, 2, 0x12345678)
b.get_string  #=> "\0\0\x78\x56\x34\x12\0\0\0\0"

複数の数値を設定する - set_values(buffer_types, offset, values)

配列で指定した数値 values を配列の buffer_types に対応した形式でバッファの offset 位置に書き込む。

b = IO::Buffer.new(10)
b.set_values([:U8, :U16], 3, [1, 0x203])
b.get_string  #=> "\0\0\0\x01\x02\x03\0\0\0\0"

IO

IOからバッファに読み込む - read(io, length=nil, offset=0)

IO オブジェクト io の read メソッドを使ってバッファにデータを読み込む。 length は指定サイズだけを読み込むという意味ではなくて、指定したサイズ以上になるまで読み込むという意味。 length が nil の場合は、io の末尾またはバッファの末尾まで読み込む。0 を指定すると read が1回だけ発行される。

File.write('/tmp/x', '0123456789')
b = IO::Buffer.new(10)
f = File.open('/tmp/x')
b.read(f, nil, 3)
b.get_string  #=> "\0\0\00123456"

IOの指定位置からバッファに読み込む - pread(io, from, length=nil, offset=0)

IOオブジェクト io の pread メソッドを使ってバッファにデータを読み込む。 from で io の位置を指定する。 length, offset は read と同じ。

File.write('/tmp/x', '0123456789')
b = IO::Buffer.new(10)
f = File.open('/tmp/x')
b.pread(f, 2, nil, 3)
b.get_string  #=> "\0\0\02345678"

バッファからIOに書き込む - write(io, length=nil, offset=0)

IO オブジェクト io にバッファのデータを書き込む。 offset はバッファ内の位置。 read と同じく、length は指定したサイズ以上になるまで書き込むという意味。 length が nil の場合は、オフセット位置からバッファの末尾までのデータを書き込む。0 を指定すると write が1回だけ発行される。

f = File.open('/tmp/x', 'w')
b = IO::Buffer.for('abcdefg')
b.write(f, nil, 2)
File.read('/tmp/x')  #=> "cdefg"

バッファからIOの指定位置に書き込む - pwrite(io, from, length=nil, offset=0)

IOオブジェクト io の pwrite メソッドを使ってバッファのデータを書き込む。 from で io の位置を指定する。 length, offset は write と同じ。

f = File.open('/tmp/x', 'w')
b = IO::Buffer.for('abcdefg')
b.pwrite(f, 3, nil, 2)
File.read('/tmp/x')  #=> "\0\0\0cdefg"

イテレータ

データ形式を指定して繰り返し - each(buffer_type=:U8, offset=0, [count]) {|offset, byte| ...}

バッファの offset の位置から buffer_type の形式でデータを取り出し count 回数分ブロックを繰り返す。 count が指定されていない場合はバッファの最後まで。 ブロックが指定されていない場合は enumerator を返す。 ブロック引数はバッファ内の位置とデータ。

b = IO::Buffer.for("\x00\x01\x02\x03\x04\x05\x06\x07")
b.each{|o, x| pp [o, x]}
#=> [0, 0]
#=> [1, 1]
#=> [2, 2]
#=> [3, 3]
#=> [4, 4]
#=> [5, 5]
#=> [6, 6]
#=> [7, 7]

b.each(:U16, 2, 3){|o, x| pp [o, x]}
#=> [2, 515]
#=> [4, 1029]
#=> [6, 1543]

バイト単位で繰り返し - each_byte(offset=0, [count]) {|byte| ...}

バッファの offset の位置からバイトデータを取り出し count 回数分ブロックを繰り返す。 count が指定されていない場合はバッファの最後まで。 ブロック引数は取り出したデータ。

b = IO::Buffer.for('abcdefg')
b.each_byte{|x| pp x}
#=> 97
#=> 98
#=> 99
#=> 100
#=> 101
#=> 102
#=> 103

b.each_byte(nil, 2, 3){|x| pp x}
#=> 99
#=> 100
#=> 101

状態

empty?

バッファのサイズが 0 なら true, それ以外は false を返す。

IO::Buffer.for('').empty?  #=> true
IO::Buffer.for('a').empty?  #=> false

internal?

バッファが内部のデータを参照している場合(?)は true。

IO::Buffer.new.internal?  #=> false
IO::Buffer.new(10).internal?  #=> true
IO::Buffer.new(4096).internal?  #=> false
IO::Buffer.for('abc').internal?  #=> false
IO::Buffer.for('abc').dup.internal?  #=> true
IO::Buffer.map(file).internal?  #=> false

external?

バッファが外部のデータを参照している場合(?)は true。

IO::Buffer.new.external?  #=> false
IO::Buffer.new(10).external?  #=> false
IO::Buffer.new(4096).external?  #=> false
IO::Buffer.for('abc').external?  #=> true
IO::Buffer.for('abc').dup.external?  #=> false
IO::Buffer.map(file).external?  #=> true

external? と internal? の両方とも false になることがあるのがよくわからない。

locked?

ロックされてれば true。 後述の locked 参照。

mapped?

バッファが mmap(2) で作られた場合に true。

null?

サイズ 0 で初期化されたり free された場合は true。

private?

IO::Buffer::PRIVATE フラグを指定して作られた場合は true。 mmap(2) の MAP_PRIVATE に相当。

shared?

IO::Buffer::SHARED フラグを指定して作られた場合は true。 mmap(2) の MAP_SHARED に相当。

readonly?

読み込み専用の場合は true。

valid?

バッファがアクセス可能であれば true。

内部のバッファが解放されたりアドレスが変更された場合に false になるらしい。 けどどうすればそういう状態になるのかはわからない。

ビット演算

論理積 - &

左辺のバッファを右辺のバッファで論理積した結果の IO::Buffer オブジェクトを返す。 左辺より右辺が短い場合は右辺を繰り返し適用する。

a = IO::Buffer.for('abcdefg')
b = IO::Buffer.for("\xff\x5f")
c = a & b
c.get_string  #=> "aBcDeFg"

論理積(破壊的) - and!

self が書き換わる以外は & と同じ。

a = IO::Buffer.for('abcdefg').dup
b = IO::Buffer.for("\xff\x5f")
a.and!(b)
a.get_string  #=> "aBcDeFg"

論理和 - |

左辺のバッファを右辺のバッファで論理和した結果の IO::Buffer オブジェクトを返す。 左辺より右辺が短い場合は右辺を繰り返し適用する。

a = IO::Buffer.for('ABCDEFG')
b = IO::Buffer.for("\x00\x20")
c = a | b
c.get_string  #=> "AbCdEfG"

論理和(破壊的) - or!

self が書き換わる以外は | と同じ。

a = IO::Buffer.for('ABCDEFG').dup
b = IO::Buffer.for("\x00\x20")
a.or!(b)
a.get_string  #=> "AbCdEfG"

排他的論理和 - ^

左辺のバッファを右辺のバッファで論理和した結果の IO::Buffer オブジェクトを返す。 左辺より右辺が短い場合は右辺を繰り返し適用する。

a = IO::Buffer.for("\x01\x02\x03\x04\x05")
b = IO::Buffer.for("\x00\xff")
c = a ^ b
c.get_string  #=> "\x01\xFD\x03\xFB\x05"

排他的論理和(破壊的) - xor!

self が書き換わる以外は ^ と同じ。

a = IO::Buffer.for("\x01\x02\x03\x04\x05").dup
b = IO::Buffer.for("\x00\xff")
a.xor!(b)
a.get_string  #=> "\x01\xFD\x03\xFB\x05"

否定 - ~

バッファを論理演算否定した結果の IO::Buffer オブジェクトを返す。

a = IO::Buffer.for("\x01\x02\x03\x04\x05")
b = ~a
b.get_string  #=> "\xFE\xFD\xFC\xFB\xFA"

否定(破壊的) - not!

self が書き換わる以外は ~ と同じ。

b = IO::Buffer.for("\x01\x02\x03\x04\x05").dup
b.not!
b.get_string  #=> "\xFE\xFD\xFC\xFB\xFA"

その他

比較 - <=>

左辺と右辺をバイト単位で比較して左辺が小さければ -1、等しければ 0、右辺が小さければ 1 を返す。

a = IO::Buffer.for('0123')
b = IO::Buffer.for('0246')
a <=> b  #=> -1

クリア - clear(value=0, offset=0, length=nil)

offse 位置から length バイト分のデータを value に置き換える。

b = IO::Buffer.for('abcdefg').dup
b.clear
b.get_string  #=> "\x00\x00\x00\x00\x00\x00\x00"
b.clear(1)
b.get_string  #=> "\x01\x01\x01\x01\x01\x01\x01"
b.clear(9, 2)
b.get_string  #=> "\x01\x01\x09\x09\x09\x09\x09"
b.clear(5, 3, 2)
b.get_string  #=> "\x01\x01\x09\x05\x05\x09\x09"

コピー - copy(source, offset=0, length=nil, source_offset=0)

source から offset 位置に length バイト分のデータをコピーする。 source_offset を指定すると source の位置を指定できる。

s = IO::Buffer.for('abcdefg')
d = IO::Buffer.for('0123456789').dup
d.copy(s)
d.get_string  #=> "abcdefg789"

d = IO::Buffer.for('0123456789').dup
d.copy(s, 3, 2)
d.get_string  #=> "012ab56789"

d = IO::Buffer.for('0123456789').dup
d.copy(s, 0, nil, 3)
d.get_string  #=> "defg456789"

同じオブジェクト内のコピーも可能。

b = IO::Buffer.for('0123456789').dup
b.copy(b, 3, 4)
b.get_string  #=> "0120123789"

解放 - free

バッファを解放する。

b = IO::Buffer.for('abcdefg')
b.size  #=> 7
b.free
b.size  #=> 0
b.null?  #=> true

16進ダンプ - hexdump(offset=0, length=nil, width=16)

offset 位置から length バイト分のデータを16進ダンプ形式の文字列で出力する。 length が nil の場合は末尾まで。 width は1行に出力するバイト数。

b = IO::Buffer.for('abcdfeg')
b.hexdump
#=> "0x00000000  61 62 63 64 65 66 67                            abcdefg"

b.hexdump(2, 3, 8)
#=> "0x00000002  63 64 65                cde"

空のオブジェクトだと nil を返すっぽい。空文字列の方が扱いやすいような気はするけども。

IO::Buffer.new(0).hexdump  #=> nil

複製 - dup / clone

オブジェクトを複製する。内部のバッファも複製され、外部オブジェクトとは関係が切れる。

b = IO::Buffer.for('abcdefg')
b.readonly?  #=> true
b.internal?  #=> false
d = b.dup
d.readonly?  #=> false
d.internal?  #=> true

inspect

inspect 用の文字列を返す。先頭 256バイト分の hexdump も含む。

b = IO::Buffer.for('abcdefg')
b.inspect
#=> #<IO::Buffer 0x00007d25797505e0+7 EXTERNAL READONLY SLICE>
#=> 0x00000000  61 62 63 64 65 66 67                            abcdefg

ロック - locked {...}

バッファをロックしてブロックを実行する。

Locking is not thread safe. It is designed as a safety net around non-blocking system calls. You can only share a buffer between threads with appropriate synchronisation techniques.

と書いてあるので、スレッドセーフではないらしい。

サイズ変更 - resize(new_size)

バッファのサイズを new_size で指定したサイズに変更する。 readonly や shared でマップされたバッファはサイズ変更できない。

バッファサイズ - size

バッファサイズを返す。

切り出し - slice(offset=0, length=nil)

バッファの offset の位置から length バイト分を切り出して新しい IO::Buffer オブジェクトを返す。 length が nil の場合は末尾まで。

新しいオブジェクトのバッファは元のオブジェクトと共有される。

a = IO::Buffer.for('abcdefg').dup
b = a.slice(2, 3)
b.get_string  #=> "cde"
a.set_string('0123456')
b.get_string  #=> "234"
b.set_string('xyz')
a.get_string  #=> "'01xyz56'"

to_s

バッファ情報を文字列で返す。inspect から hexdump を除いたものと同じ。

b = IO::Buffer.for('abcdefg')
b.to_s
#=> #<IO::Buffer 0x00007d25797505e0+7 EXTERNAL READONLY SLICE>

移動 - transfer

自身のバッファの所有を移動した新しい IO::Buffer オブジェクトを返す。 自身のバッファは空になる。

b = IO::Buffer.for('abcdefg')
b.get_string  #=> "abcdefg"
t = b.transfer
b.get_string  #=> ""
t.get_string  #=> "abcdefg"

IO::Buffer は experimental というだけあって、ちょっとまだこなれてないところがあるみたいだけど、こういうレイヤーのライブラリは好みなので使ってみよ。