Crystal でバイナリデータを扱う

前回も書いたように Crystal の String のエンコーディングは UTF-8 固定です。なので Ruby のようにバイナリデータを String オブジェクトで扱うことはできません。

バイナリデータは Pointer, Slice, MemoryIO で扱うことができるようです。 自分でもよくわかってなかったので、自分用のメモとしてまとめておきます。

Pointer

最も低レイヤーのクラスです。C のポインタと同じです。

p = Pointer(UInt8).malloc(10)  # 10バイト獲得
p[0] = 0xAAu8
p[1] = 'X'.ord.to_u8
x = 123
p = pointerof(x) # x のアドレス
p.value          #=> 123
p.value = 456
x                #=> 456

獲得したメモリ領域を超えてアクセスできてしまうため、簡単にメモリを破壊できるのも C と同じです。

p = Pointer(UInt8).malloc(10)  # 10バイト獲得
p[999999]
#=> Segmentation fault
p = Pointer(UInt8).null  # NULLポインタ
p[0]
#=> Segmentation fault

Slice

固定のサイズを持ったメモリ領域です。オブジェクト生成時に型とサイズが決まります。 領域を超えたアクセスはできません。Pointer に比べると安全です。

s = Slice(UInt8).new(10)  # 10バイト獲得
s[0] = 0xAAu8
s[1] = 'X'.ord.to_u8
p s    #=> [170, 88, 0, 0, 0, 0, 0, 0, 0, 0]
s[10]  #=> Index out of bounds (IndexError)

IO#read, #write でバイナリの読み書きをする場合は Slice を介して行います。

IO#read は最大 Slice 領域分の大きさを読み込もうとし、実際に読み込んだバイト数を返します。 ちょっと C っぽいです。

File.open("somefile") do |f|
  s = Slice(UInt8).new(100)
  len = f.read(s)  # ファイルから100バイト読み込み
  len              # 実際に読み込んだバイト数
end

IO#write は Slice 領域の全データを書き込みます。Slice の一部を書き込むには、その一部を取り出した Slice オブジェクトを作って渡します。

File.open("somefile", "w") do |f|
  s = Slice(UInt8).new(100)
  f.write(s)         # 100バイト書き込み
  f.write(s[20, 10]) # 先頭から20バイト目から10バイト分だけ書き込み
end

MemoryIO

IO じゃなくてメモリ上にバイナリデータを作成したい場合には MemoryIO を使用します。 IO と同じようなメソッドが使えますが、データはメモリ上にあります。 Slice は色んな型を持てますが、MemoryIO は UInt8 固定です。

m = MemoryIO.new
s = Slice(UInt8).new(100)
m.write(s)         # 100バイト書き込み
m.write(s[20, 10]) # 10バイト書き込み
m.size             #=> 110

数値の内部表現

IO#read_bytes, IO#write_bytes を使って、整数や浮動小数点数の内部表現を IO に読み書きすることができます。

m = MemoryIO.new
m.write_bytes(123456)
m.write_bytes(123.456)
m.rewind
s = Slice(UInt8).new(100)
m.read(s)             #=> 12
s[0, 12]              #=> [64, 226, 1, 0, 119, 190, 159, 26, 47, 221, 94, 64]
m.rewind
m.read_bytes(UInt32)  #=> 123456
m.read_bytes(Float64) #=> 123.456

相互変換

# Pointer -> Slice
pointer.to_slice(size)
Slice.new(pointer, size)

# Pointer -> String
String.new(pointer)   # NUL文字終わりのポインタ

# Slice -> Pointer
slice.pointer(size)   # サイズはチェック用
slice.to_unsafe

# Slice -> MemoryIO
MemoryIO.new(slice)

# Slice -> String
String.new(slice)

# MemoryIO -> Pointer
memoryio.buffer

# MemoryIO -> Slice
memoryio.to_slice

# MemoryIO -> String
memoryio.to_s

# String -> Pointer
string.to_unsafe

# String -> Slice
string.to_slice

# String -> MemoryIO
MemoryIO.new(string)