RubyでバイナリデータをパースするGemライブラリ(フレームワーク)を製作した話

Ruby-Binary-Parser (今回製作したライブラリ)

インストールはgemから可能。

gem install binary_parser

ソースはgithubに上げてある。簡単な使い方の説明もgithub上のREADMEに掲載。
https://github.com/sawaken/ruby-binary-parser

成り立ち (製作の動機)

個人的な興味(必要?)のため、MPEG-2/TS形式のファイルをパースするRubyのライブラリを作成しようと思い立ち、TSデータの構造を色々と調べ、大まかな構造は把握することが出来た。
しかしながら、TSデータの構造は多種多様なバイナリ構造の入れ子になっており、それらの構造をパースするプログラムを逐一実装していては、とても骨が折れるだろうことも同時に判明した。
そこで、TSデータのパーサーを作る前に、汎用的なバイナリデータ解析のフレームワークを作り、その上でTSパーサーを実装した方が効率的に(かつ美しく)目的を達成できると考えた。

※バイナリデータとは、広義にはビットで表現されたデータ全般を差すが、ここでは「特定の構造を持った、(画像や音声、あるいはプログラム等の)情報を表現するバイト列」をバイナリデータと称する。

Rubyでのバイナリデータ解析の実情

当然(?)のことながら、Rubyでバイナリデータを扱いたいと考える人間はあまり多くはないようで、少なくとも日本語での紹介記事などは殆ど無く、Rubyではバイナリデータを特別な文字コードの文字列として扱うこと、Array#packで数列をバイナリデータに変換出来る(その逆はString#unpackで出来る)こと等を紹介する記事がちらほら見られる程度であった。

また、似たようなGemライブラリ(バイナリ解析のフレームワーク)が既に存在しないか調べてみた所、それっぽい用途のライブラリは何件か見つかったものの、どうにもショボイというか、コレジャナイ感が強く、元々自分で作るつもりだったので探索は早々に打ち切り、製作に乗り出すことにした。

どんな「フレームワーク」が欲しいのか

理想的には、「フレームワークを利用する側の人間」が自らの手で行う必要のある唯一の仕事は、「自分がやりたいことを指示すること」であって、「それをどう実現するか」を考えるのは「フレームワークを作る側の人間」が行うべき仕事である。
このことを踏まえると、良いフレームワークの条件は主に以下の3つであると考えることができる。

  • 利用者が指示できる「やりたいこと」に制限が少ないこと
  • 利用者が余計な指示をしなくても済むこと
  • 指示された「やりたいこと」をそつなくこなせること

Rubyフレームワークが得意

Rubyがバイナリデータを扱うことに向いているかどうかはともかくとして、Rubyフレームワークの記述(というよりかはDSLの構築)を得意とする言語であるというのは周知の事実である。
ピンと来ない方にとっては言葉で説明されるより現物を見た方が早い気がするので、そろそろ御託は止めにして、次項からはさっそく現物の紹介を始めたいと思う。

簡単な紹介 (github上のREADMEからの抜粋)

例えば、以下の定義を持つ画像データ形式(MyImage)を考える(説明のための仮想的なデータ形式)。

MyImage (non-fixed length)
Data Name Type Bit Length Number Of Replications
height UInt 8 1
width UInt 8 1
RGB color bit-map UInt 8 * 3 'height' * 'width'
has date? Flag 1 1
date MyDate 31 'has date?' is 1 => 1
else => 0

MyDate (31 bit)
Data Name Type Bit Length Number Of Replications
year UInt 13 1
month UInt 9 1
day UInt 9 1


以上のバイナリデータ構造をフレームワーク上に定義すると、以下の通りとなる。

require 'binary_parser'

class MyDate < BinaryParser::TemplateBase
  require 'date'

  Def do
    data :year,  UInt, 13
    data :month, UInt, 9
    data :day,   UInt, 9
  end

  def to_date
    return Date.new(year.to_i, month.to_i, day.to_i)
  end
end

class MyImage < BinaryParser::TemplateBase
  Def do
    data :height, UInt, 8
    data :width,  UInt, 8

    TIMES var(:height), :i do
      TIMES var(:width), :j do
        data :R, UInt, 8
        data :G, UInt, 8
        data :B, UInt, 8
      end
    end

    data :has_date, Flag, 1
    IF cond(:has_date){|v| v.flagged?} do
      data :date, MyDate, 31
    end
  end
end

この定義クラスを用いて画像データの情報を読み込んで表示するプログラムは、例えば以下の様に書ける。

File.open('my_image.bin', 'rb') do |f|
  image = MyImage.new(f.read)
  print "Image size: #{image.height.to_i}x#{image.width.to_i}\n"
  pix = image.i[0].j[0]
  print "RGB color at the first is (#{pix.R.to_i}, #{pix.G.to_i}, #{pix.B.to_i})\n"
  print "Image date: #{image.date.to_date}\n"
end


ファイル 'my_image.bin' にバイナリデータ[0x02, 0x02, 0xe7,0x39,0x62, 0x00,0x00,0x00, 0xe7,0x39,0x62, 0x00,0x00,0x00, 0x9f, 0x78, 0x08, 0x03]を格納し、上記のプログラムを実行した結果が以下。

Image size: 2x2
RGB color at the first is (231, 57, 98)
Image date: 2014-04-03

詳細なドキュメント

残念ながら全てを網羅したドキュメントは未だ用意できていない。
github上のREADMEには本記事よりは詳しい解説を載せているので、そちらを参照して頂きたい。


本ライブラリの特徴

  • 全ての解析を遅延評価にて行う

実際に参照されるまでバイナリデータの操作は一切行わないため、大体の場合において非常に高速に動作する。

  • 無駄の無い美しい定義構文(DSL)

おそらくこれ以上簡潔な構文は実現できないのではないだろうか。

構造内にてパースされるデータはそれ自体がまた別の構造であるという点。

  • おおよそ考え得る全てのバイナリデータ構造に対応できる(?)

構造定義に独自の制御構文であるIF文, TIMES文, SPEND文を導入したことによる。