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

WHITELEAF:Kindle応援サイト

KindleでWEB小説を読もう! Narou.rb 公開中

Ruby で構造体もどき その3

Ruby

その1 | その2

前回までと違って 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]