Rubyでバイナリデータを読み書きする


Rubyでバイナリデータを読み書きするのには、pack, unpack使うじゃないですか。

僕もねぇ、Ruby使い始めてもう何年も経つんですけど、pack, unpackっていつまでたっても覚えられないし、
使うとなんか汚い煩雑なコードになっていくんですね。そんなことってないですか?


なんかライブラリないかなーとググりまして、こちらで紹介されているライブラリを一通り見てみました。
http://d.hatena.ne.jp/kenhys/20070522/1179848262
・binaryparser
calibre-binaryreader
・bindata
・BitStructEx
しかし、なんかどれも複雑というか、いまいち直感的じゃないという気がします。


自分の理想の使い方の形は次のような感じです。
include(BinaryData)


# float3要素のベクトル構造体定義
Vector = struct{
  # Vectorの最初の要素はxで、float 32 bitの型という意味
  var x: F32

  # Vectorの二つ目の要素はyで、float 32 bitの型という意味
  var y: F32
  
  # まぁ!みて!このz要素の定義を!まるでJavaScriptの定義みたいだわ!
  var z: F32
}

# 3x3のマトリックス構造体定義
Matrix = struct{
  # Matrixの最初の要素はrowsで、ユーザーが定義したVector構造体の三つの要素を持つ配列という意味
  var rows: Vector[3]

  # ユーザーが定義した構造体をネストして、しかも配列として使えるということね!
}

#
Model = struct{
  # 頂点の数をinteger 32 bit型として持つ
  var vertex_count: I32

  # 上のvertex_countを長さとして使えるのね!
  var vertices: Vector[:vertex_count]

  var matrix: Matrix
}

# モデル構造体のインスタンス生成
model = Model.new

# 値を格納するのも、直感的な記法なのね!
model.vertex_count = 2;
model.vertices[0].x = 1
model.vertices[0].y = 2
model.vertices[0].z = 3
model.vertices[1].x = 4
model.vertices[1].y = 5
model.vertices[1].z = 6

model.matrix.rows[0].x = 10
model.matrix.rows[1].y = 20
model.matrix.rows[2].z = 30


sio = StringIO.new

# バイナリとして書き込む
Model.write_to_stream(sio, model)

#これでsioにはC言語で次のような感じで書いて出力したバイナリと同じフォーマットで入ってる
# struct Vector{ float x, y, z; };
# struct Matrix{ Vector rows[3]; };
# struct Model{ int vertex_count; Vector vertices[vertex_count]; Matrix matrix; };
# Model model = {...};
# fwrite(fp, &model, 1, sizeof(model));

# ストリームを一番最初に戻す
sio.pos = 0

# バイナリから読み込んで復元する
model_deserialized = Model.read_from_stream(sio)

# 文字列化して出力
print model_deserialized
文字列化されたプリント結果
{:vertex_count=>2,
 :vertices=>[{:x=>1.0, :y=>2.0, :z=>3.0}, {:x=>4.0, :y=>5.0, :z=>6.0}],
 :matrix=>
  {:rows=>
    [{:x=>10.0, :y=>0.0, :z=>0.0},
     {:x=>0.0, :y=>20.0, :z=>0.0},
     {:x=>0.0, :y=>0.0, :z=>30.0}]}}
で、そんな理想なライブラリの下地を作ってみました。
定義がいろいろと足りないんですけど、上のサンプルは動かせます。
Ruby1.9.1以上で動きます
require "stringio"
require "pp"

module BinaryData

class StructType
	def [](key)
		return StructArrayType.new(self, key)
	end
end

class StructArrayData
	def initialize(type, data)
		@type = type
		@data = data
	end
	
	def data()
		@data
	end
		
	def fillup(i)
		while i>=@data.length
			@data.push(@type.new)
		end
	end
	
	def [](i)
		fillup(i)
		return @data[i]
	end
	
	def []=(i, v)
		fillup(i)
		@data[i] = v
	end
end

class StructArrayType < StructType
	
	def initialize(type, length)
		@type = type
		@length = length
	end
	
	def write_to_stream(stream, data, parent = nil)
		length = @length
		if(length.is_a?(Symbol))
			length = parent[length];
		end
		
		length.times{ |i|
			@type.write_to_stream(stream, data[i], parent)
		}
		
	end
	
	def read_from_stream(stream, parent = nil)
		length = @length;
		if(length.is_a?(Symbol))
			length = parent[length];
		end

		data = Array.new(length)
		length.times{ |i|
			data[i] = @type.read_from_stream(stream, parent)
		}
		return StructArrayData.new(@type, data);
	end
	
	
	def new
		return StructArrayData.new(@type, [])
	end
end

class PrimaryType < StructType
	def initialize(packformat, bytenum, defaultvalue)
		@packformat = packformat
		@bytenum = bytenum
		@defaultvalue = defaultvalue
	end
	
	def write_to_stream(stream, value, parent = nil) 
		stream.write([value].pack(@packformat))
	end

	def read_from_stream(stream, parent = nil)
		return stream.read(@bytenum).unpack(@packformat)[0]
	end

	def new() 
		return @defaultvalue
	end
end

class StructMapData
	def initialize(type, data)
		@type = type
		@data = data
	end
	
	def data()
		return @data
	end
	
	def [](i)
		return @data[i]
	end
	
	def []=(i, v)
		@data[i] = v
	end
	
	def method_missing(name, *args)
		if name.to_s =~ /(\w+)=/
			@data[$1.to_sym] = args[0]
		else
			return @data[name]
		end
	end
end

class StructMapType < StructType
	
	def initialize()
		@members = {}
	end
	
	def define(key, val)
		@members[key] = val
	end
	
	def write_to_stream(stream, data, parent = nil)
		@members.each_pair{ |key, type|
			type.write_to_stream(stream, data[key], data)
		}
	end
	
	def read_from_stream(stream, parent = nil)
		data = StructMapData.new(self, {})
		@members.each_pair{ |key, type|
			data[key] = type.read_from_stream(stream, data)
		}
		return data
	end
			
	def new()
		data = StructMapData.new(self, {})
		@members.each_pair{ |key, type|
			data[key] = type.new
		}
		return data;
	end
end

def extract(value)
	if(value.is_a?(StructMapData))
		ret = {}
		value.data.each_pair{ |key, val|
			ret[key] = extract(val) 
		}
		return ret
	end

	if(value.is_a?(StructArrayData))
		return value.data.map{|val| extract(val) }
	end

	return value;
end

def struct(&block)
	ret = StructMapType.new();

	def ret.var(arg)
		arg.each_pair{ |key, type|
			define(key, type)
			break
		}
	end
	
	ret.instance_eval(&block)

	return ret;
end

def print(v)
	pp extract(v)
end

I32 = PrimaryType.new("N", 4, 0)
I8 = PrimaryType.new("c", 1, 0)
F32 = PrimaryType.new("g", 4, 0.0)

end
>