Ruby で構造体もどき その3
前回までと違って unpack フォーマットを直接書くのではなく型を定義して BinData のような形式にしてみました。unpack のフォーマット文字列を生成して一回の unpack 実行で済ませる方針は変わっていないので、配列の添字にシンボルは使えません。(そのうちするかもですが)
# sample class Binary < BinStruct::Base class Vector < BinStruct::Base var Float, :x, :y end var Long, :version var String[10], :name # String の配列は文字列の長さを意味する。 var Vector[100], :pos end stream = BinStruct::Stream.new("data.bin") bin = stream.read(Binary) bin.version bin.name bin.pos[0]
実行速度はその1、その2と同じ条件で0.06秒程度ですので速度低下はありません。生成したフォーマットの文字数が19万文字を超えているので、つまり BinData は19万回 read して unpack を繰り返してるわけでそりゃ遅いわ〜。
プリミティブの作成は
Float = Primitive.create("f", "f", 4)
のように作ります。リトルエンディアン、ビッグエンディアン、バイト数の順に指定しますが、現在ネイティブな型のエンディアンを指定する方法がないので同じままです。そのうち拡張される予定っぽいのでそれに期待。
# 2/26 更新 # ・構造体の配列を指定したときにバグってたのを修正(BinStruct::Base#var_base_array) # ・BinStruct::Stream#read に構造体以外にも、プリミティブ(Floatとか)や配列、構造体の配列も渡せるようにした # # example include BinStruct::BuiltinPrimitive bin = BinStruct::Stream.new("normal.pmd") bin.read(String[3]) #=> "Pmd" bin.read(Float) #=> 1.0 # ... bin.pos = 0 header = bin.read(Header) # 配列にすることで、ループで取得するより圧倒的に速い処理が可能 vertices = bin.read(Vertex[header.vert_count])
module BinStruct class UnknownTypeError < ArgumentError; end class Primitive @@endian = :little class << self def create(format_le, format_be, size) klass = Class.new(self) klass.class_variable_set("@@format_le", format_le) klass.class_variable_set("@@format_be", format_be) klass.class_variable_set("@@size", size) return klass end def [](param) return PrimitiveArray.new(self, param) end def pattern if @@endian == :little return class_variable_get("@@format_le") else return class_variable_get("@@format_be") end end def size return class_variable_get("@@size") end def endian=(endian) @@endian = endian end end end module ArrayCommon attr_reader :type, :param def initialize(type, param) @type = type @param = param end def size @size ||= @type.size * @param return @size end end class PrimitiveArray include ArrayCommon def pattern @pattern ||= @type.pattern + @param.to_s return @pattern end end class BaseArray include ArrayCommon def pattern @pattern ||= @type.pattern * @param return @pattern end end module BuiltinPrimitive Float = Primitive.create("f", "f", 4) Long = Primitive.create("L", "L", 4) String = Primitive.create("Z", "Z", 1) UInt16 = Primitive.create("S", "S", 2) UInt8 = Primitive.create("C", "C", 1) class String class << self def pattern return "#{super}#{@@length}" end def [](size) @@length = size return self end def size return @@size * @@length end end end end class Base @@pattern = {} @@size = {} @@index = {} @@variable_num = {} @@endian = :little include BuiltinPrimitive #attr_reader :values def [](index) return @values[index] end def __values return @values end class << self def inherited(klass) @@pattern[klass] = "" @@size[klass] = 0 @@index[klass] = 0 @@variable_num[klass] = 0 end def var(type, *variables) variables.each do |name| case type when BaseArray var_base_array(type, name) when PrimitiveArray var_primitive_array(type, name) when Class if type.ancestors.include?(Primitive) var_primitive(type, name) elsif type.ancestors.include?(Base) var_base(type, name) else raise UnknownTypeError, "unknown type `#{type}`" end else raise UnknownTypeError, "unknown type `#{type}`" end end end def endian(e) @@endian = e Primitive.endian = e end def pattern return @@pattern[self] end def size return @@size[self] end def variable_num return @@variable_num[self] end def [](param) return BaseArray.new(self, param) end private def var_primitive(type, name) @@pattern[self] << type.pattern @@size[self] += type.size module_eval <<-EOD def #{name} @values[#{@@index[self]}] end EOD @@index[self] += 1 @@variable_num[self] += 1 end def var_primitive_array(p_array, name) @@pattern[self] << p_array.pattern @@size[self] += p_array.size module_eval <<-EOD def #{name} @values[#{@@index[self]}, #{p_array.param}] end EOD @@index[self] += p_array.param @@variable_num[self] += p_array.param end def var_base(type, name) @@pattern[self] << type.pattern @@size[self] += type.size module_eval <<-EOD def #{name} @base_#{name} ||= #{type}.new(@values[#{@@index[self]}, #{type.variable_num}]) end EOD @@index[self] += type.variable_num @@variable_num[self] += type.variable_num end def var_base_array(b_array, name) array_type = b_array.type @@pattern[self] << b_array.pattern @@size[self] += b_array.size module_eval <<-EOD def #{name} unless @base_array_#{name} @base_array_#{name} = [] #{b_array.size}.times do |i| @base_array_#{name} << #{array_type}.new(@values[#{@@index[self]} + i * #{array_type.variable_num}, #{array_type.variable_num}]) end end @base_array_#{name} end EOD @@index[self] += array_type.variable_num * b_array.param @@variable_num[self] += array_type.variable_num * b_array.param end end def initialize(values) @values = values end end class Stream class InvalidStructError < StandardError; end attr_accessor :pos def initialize(file) @pos = 0 case file when String open(file) else @buffer = file.read end end def open(file_name) @buffer = File.read(file_name, File.size(file_name)) end def read(type) data = @buffer.unpack("@#{@pos}#{type.pattern}") @pos += type.size if type.kind_of?(Class) && type.ancestors.include?(Base) return type.new(data) end if type.kind_of?(BaseArray) base_array = [] type.param.times do |i| base_array << type.type.new(data[i * type.type.variable_num, type.type.variable_num]) end return base_array end return data.length == 1 ? data[0] : data end end end class PMDBinary < BinStruct::Base endian :little class Header < BinStruct::Base var String[3], :magic var Float, :version var String[20], :model_name var String[256], :comment var Long, :vert_count end class Vector < BinStruct::Base var Float, :x, :y, :z end class Vertex < BinStruct::Base var Vector, :pos var Vector, :normal var Float, :u, :v var UInt16[2], :bone_num var UInt8, :bone_weight var UInt8, :edge_flag end var Header, :header var Vertex[15847], :vertex end bin = BinStruct::Stream.new("normal.pmd") pmd = bin.read(PMDBinary) p pmd.header p pmd.vertex[0]