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には本記事よりは詳しい解説を載せているので、そちらを参照して頂きたい。