読者です 読者をやめる 読者になる 読者になる

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

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)

Crystal に String#scrub と String#valid_encoding? を追加

Crystal

前に次のような記事を書きました。

tmtms.hatenablog.com

Ruby と異なり、エンコーディングを変換したり UTF-8 として正しいバイト列かどうかを判定する方法もありません。つらい。

これを解決するために Crystal に String#scrubString#valid_encoding? を追加するライブラリを作りました。

github.com

これを使えば、UTF-8 かどうか怪しい文字列データを扱う時に実行時エラーを回避することができます。

require "string-scrub"
line = File.open("/dev/urandom").gets.to_s
p line.valid_encoding?  # => false
p line.scrub('')  # => "\u{13}\u{7}u〓^\t〓3\u{15}〓〓\u{0}〓\u{e}〓〓\"〓^2w\e〓〓{\r\u{1d}〓\u{f})\u{f}-〓〓C\u{13}8〓U〓a〓Ye〓'〓f\u{3}〓\\〓Z\u{10}〓〓〓ϸ〓(〓〓〓\u{1c}e〓C`〓〓〓〓\u{1c}〓〓\n"

Crystal の名前付き引数は自然に書けてつらくない

Crystal Ruby

これは「Ruby脳にはCrystalつらい Advent Calendar 2015」の25日目の記事です。

qiita.com

Ruby の名前付き引数の例です。

def hoge(a, b=1, c: 2)
  p [a, b, c]
end

hoge(123)               #=> [123, 1, 2]
hoge(123, 456)          #=> [123, 456, 2]
hoge(123, c: 789)       #=> [123, 1, 789]
hoge(123, 456, c: 789)  #=> [123, 456, 789]

a は通常の引数、b はデフォルト値を持つ引数、c は名前付き引数です。 名前付き引数は専用の記述方法があります。

Crystal の記述は Ruby とは少し異なります。

def hoge(a, b=1, c=2)
  p [a, b, c]
end

hoge(123)                  #=> [123, 1, 2]
hoge(123, b: 456)          #=> [123, 456, 2]
hoge(123, c: 789)          #=> [123, 1, 789]
hoge(123, b: 456, c: 789)  #=> [123, 456, 789]
hoge(123, 456)             #=> [123, 456, 2]
hoge(123, 456, c: 789)     #=> [123, 456, 789]
hoge(123, 456, 789)        #=> [123, 456, 789]

デフォルト値を持つ引数は名前付き引数にもなります。

Crystal の方が自然でいいですね!(個人の感想です)

Crystal はソースコードも Crystal でつらくない!

Crystal Ruby

これは「Ruby脳にはCrystalつらい Advent Calendar 2015」の24日目の記事です。

qiita.com

Crystal のソースコードは Crystal で記述されています。最初は Ruby で書かれていたようですが、現在はすべて Crystal で書かれています。

Ruby は本体と組み込みライブラリは C で書かれているため、Ruby 内部の処理を調べようとすると C の知識が必要になりますが、Crystal は Crystal を理解していれば(Crystal を使う人は普通は理解できているでしょう)、本体のコードを読むことができます。

良いですね!

Crystal はブロックがエラーになることがあってつらい

Crystal Ruby

これは「Ruby脳にはCrystalつらい Advent Calendar 2015」の23日目の記事です。

qiita.com

メソッドに渡されたブロックを実行するには次の2つの方法があります。

def hoge
  yield
end
hoge{ p 123 }
def hoge(&block)
  block.call
end
hoge{ p 123 }

ですが、引数があるブロックの場合は後者はエラーになります。

def hoge
  yield 123
end
hoge{ |x| p x }  # これはOK
def hoge(&block)
  block.call 123
end
hoge{ |x| p x }  #=> wrong number of block arguments (1 for 0)

次のように引数の型を指定すると動作します。

def hoge(&block : Int32 -> _)
  block.call 123
end
hoge{ |x| p x }

あと、block.call の場合はブロック内で break するとエラーになります。

def hoge(&block)
  block.call
end
hoge{ break }  #=> Invalid break

つらい…

Crystal には Thread がなくてつらい

Crystal Ruby

これは「Ruby脳にはCrystalつらい Advent Calendar 2015」の22日目の記事です。

qiita.com

タイトルのまんまですが、Crystal は Thread がなくてつらい。

Thread というクラスはありますが、ソースを見ると、

  # Don't use this class, it is used internally by the event scheduler.
  # Use spawn and channels instead.

と書いてあって気軽に使える雰囲気ではありません。

spawn や channel を使えと書かれてるので spawn を使ってみます。

spawn do
  10.times do |i|
    print 'a'
    sleep 1
  end
end

spawn do
  10.times do |i|
    print 'b'
    sleep 1
  end
end

gets

このプログラムを動かすと次のようになります。

% crystal hoge.cr 
bababababaababababab

a と b が交互に出力されて、ちゃんと並列動作しているようです。

なお、sleep がないと交互にはなりません。spawn は Fiber で実装されていて、sleep によってコンテキストを切り替えているようです。 つまりスレッドと違い、複数 CPU を同時に使うことはできません。

上の例では最後に gets でキー入力を待っていますが、Channel を使えば待ち合わせができるようです。

chann = Channel(Bool).new

spawn do
  10.times do
    print 'a'
    sleep 1
  end
  chann.send true
end

spawn do
  10.times do
    print 'b'
    sleep 1
  end
  chann.send true
end

chann.receive
chann.receive

Channel は Crystal のドキュメントに説明がなくてよくわからないのですが、Ruby の Queue みたいなもんでしょうか。

Channel を各 spawn ブロックの中で使えば sleep を使わなくてもコンテキスト切り替えができるようです。

Crystal のバイナリデータ読み込みは C みたいでつらい

Crystal Ruby

これは「Ruby脳にはCrystalつらい Advent Calendar 2015」の21日目の記事です。

qiita.com

Crystal の String は UTF-8 固定なのでバイナリデータを String で扱うことはできません。

バイナリデータを扱うには String ではなく Slice を使います。

Slice(UInt8).new(1024) とすると 1024バイトのメモリが獲得されます。C の calloc(sizeof(char), 1024) みたいな感じですね。

Slice のインスタンスは配列と同じような感じで使えますが、長さは固定で拡張したり縮小したりはできません。

slice = Slice(UInt8).new(5)
slice[0] = 11u8
slice[1] = 22u8
slice[2] = 33u8
slice   #=> [11, 22, 33, 0, 0]

File や Socket 等の IO からバイナリデータを読み込むには IO#read を使います。

File.open("/etc/hosts") do |f|
  buf = Slice(UInt8).new(1024)
  length = f.read(buf)
  buf[0, length]
end

読み込むためのバッファを用意して、IO#read の戻り値で読み込んだバイト数を調べて…というような感じでやるしかないようです。 まるで C みたいで、ちょっとつらい。