Hex Structの解説

はじめに

HexStruct Ver1.1は「るびま 0011号」の 「あなたの Ruby コードを添削します 【第 2 回】 HexStruct.rb」における に掲載されている青木さんの添削結果を参考にして作ったものだ。

実際には、青木さんの添削結果とはかなり違った作りになっている。

ここでいくつかのテクニックを解説してみる。

構造定義の方法

HexStructでは、バイナリデータの構造定義を以下のような方法で定義でいるようにしている。

  class MsgFrame < HexStruct
    define_struct [
      [:from_add, 3]
      [:to_add, 3],
      [:len, 2],
      [:msgcode, 2],
      [:body, nil]
    ]
  end
  

これは、define_struct()というクラスメソッドを呼び出している。そのメソッドの引数に構造定義のArrayが指定されている わけだ。るびまの添削記事でクラスにメソッドを動的に追加する方法を教えてもらったので、そのテクニックを使っている。 このおかげで、method_missingもつかわずに済むようになった。

クラスメソッドの定義方法

"オブジェクト.メソッド名"の形式で呼び出すメソッドは単に「メソッド」または「インスタンスメソッド」と呼ばれる。良く使うメソッドは たいていこれだ。メソッドには、「クラスメソッド」と呼ばれるものがある。"File.basename()"など、わざわざインスタンスを作らなくても クラスが提供してくれるサービスを使えるのがクラスメソッドだ。

HexStructで使用しているdefine_structはクラスメソッドだが、外部にサービスを提供するものではない。このクラスメソッドは、構造情報の 記録と、アクセサメソッドを定義するのに使っている。使用するのもクラス定義の内部だけである。

最初、まさかクラス定義の中でメソッドが呼べるとは思ってなかった。でも、クラス定義や定数定義も実行文の一種なので、ここでメソッド 呼び出しもできるのだろう。 define_structクラスメソッドをクラスの外部から呼べないようにするには、プライベートメソッドとして定義しておけばいいみたいだ。

msgcode_0001のフォーマット

このおかげで、添削記事で使わないほうがいいと指摘をうけたmethod_missingを使わなくてもよくなった。

具体的なコード

構造定義を処理する部分を抜き出すと、以下のようになる。

  class HexStruct
    class << HexStruct
      def size()
        total_size = 0
        @struct.each {|sym, elem_size|
          total_size += elem_size if elem_size
        }
        total_size
      end
  
      attr_reader :struct
  
      private
  
      def define_struct(struct_arr)
        @struct = []
        struct_arr.each {|sym, elem_size|
          if (sym == nil) or (elem_size and (elem_size <= 0))
            raise FormatError, "Illegal STRUCT Format."
          end
          @struct.push [sym, elem_size]
          define_method(sym) {
            self[sym].value
          }
          define_method("#{sym}=".to_sym) {|obj|
            self[sym].value = obj
          }
        }
      end
    end
  end
  

ポイントをいくつか

  • クラスメソッドをいくつも定義したり、プライベートなクラスメソッドを定義するときは、class << HexStruct〜end の 記述が有効。この表記は「特異クラス定義」と呼ばれるみたいだ。
  • define_structをプライベートメソッドとすることで、HexStruct.define_struct()のような呼び出しを禁止できる。 クラス定義内から呼ぶしかなくなる。
  • 以下のような記載で、クラスに"hogehoge"というインスタンスメソッドを追加できる。 これもプライベートなクラスメソッドなので、self.class.define_method(:name) {...} のようなことはできない。
  define_method(:hogehoge) {
    # 処理
  }
  
  • 構造情報は@structというインスタンス変数に保存している。インスタンス変数なんだけど、保持しているのはクラス。 「クラスもオブジェクトである」という"?"となる説明はよく目にするが、クラスもオブジェクトなので、 インスタンス変数をもてるのだろう。attr_reader :struct というアクセサメソッドをつけておけばクラスの@structに インスタンスからアクセスできる。
  • HexStruct.sizeは、@structの情報を使ってバイト長を求めている

構造体の生成方法

添削記事ではHexStruct構造体を生成する時、new(),for_io(),parse()などを状況に応じて使い分ける方法が推奨されていた。

実際のバイナリデータ解析では、同じフォーマットのデータがきれいに並んでいるということはあまりない。 たいていは、ヘッダのあるコードによって、データのフォーマットが変化するケースが多いため、for_io()のような IOストリームから次々にデータをパースしていくという状況はあまりない。

そのため、HEX表記文字列を与えてnewするか、引数無しで全フィールドを"00"で埋めた構造体を生成する方法しか ないので、インスタンスを生成するのはnewだけにしておいた。

コード

構造体を生成しているのは、以下の処理。 HEX表記文字列が渡されない場合は、自クラスのsizeクラスメソッドを使って規定のバイト長を求めて、"00"×データ長の HEX表記文字列を作っている。

これをHEX文字列のパース処理に渡して、パースした結果を@field_listに組み込んでもらっている。

  class HexStruct
    def initialize(hex_string=nil)
      hex_string = "00" * self.class.size() unless hex_string
      @field_list = []
      parse_hexstring(hex_string)
    end
  end
  

ItemFieldによるフィールドの管理

インスタンス生成時に、HEX表記文字列をパースするのがparse_hexstring()である。

青木さんの添削記事での指摘にしたがって、渡された文字列を構造定義配列のデータ長情報に基づいてフィールド毎に 分割して、各フィールドをItemFieldオブジェクトで管理するようにしている。

HexStructのように、フィールドがいろんな形式のデータを管理する場合、「フィールド」を管理するクラスを導入 すると、データの種別を意識しなくて済むようになって、処理が簡単になる。

フィールド名とItemFieldオブジェクトの対応を管理しているのが、@field_listだ。[フィールド名, ItemFieldオブジェクト] のペアを管理する。配列なので、フィールドの順序も管理できる。

フィールドの管理

フィールド名からフィールドオブジェクトを得るのにはHashを使わないとだめだろうと思っていたが、 青木さんの記事から、Array#assocというメソッドがあることを知った。

このメソッドを使えば、[[name1,obj1], [name2,obj2],....]という配列の中から、名前を使って対応するオブジェクト を手に入れることもできる。この構造は今までも使ったことはあったが、「alist」という名前が付いているとは知らな かった(assocも)。

  # 名前からフィールドを探す処理
  def [](field_name)
    name, field = @field_list.assoc(field_name.to_s)
    raise NoMemberError, "'#{field_name}' is not member" unless field
    field
  end
  

ItemFieldを使ったフィールド値の管理

フィールドが保持する値の管理は、ItemFieldが行っている。 フィールドには数値/HEX表記文字列/配列/構造体を代入できる。

  frame.data = 0x1122334455                 # 数値で代入
  frame.data = "1122334455"                 # HEX表記文字列の代入
  frame.data = ["1122", "334455"]           # 配列で代入
  frame.data = DataFrame.new("1122334455")  # 別の構造体を代入
  

フィールドへの数値の代入処理は、以下のように記述している。 数値を文字列に変換しないといけないので、Rubyではご法度のタイプわけが必要になってしまっている。

  class ItemField
    def value=(val)
      if val.kind_of?(Integer)
        keta = self.byte_size * 2
        keta = 2 if keta == 0
        val = sprintf("%0#{keta}X", val)
      end
      if @byte_size and (calc_byte_size(val) != @byte_size)
        raise ArgumentError, "unmatch data size"
      end
      if val.respond_to?(:upcase)
        @value = val.upcase
      else
        @value = val
      end
    end
  end
  

そのかわり、そのような 汚いタイプわけはItemFieldクラス内だけ押し込んだ。ItemFieldを使う時にはタイプの違いは意識しなくて すむようにしたので、許してもらおう。

データ長の計算

可変長フィールドを含む構造体のデータ長を求める処理はそんなに難しくない。 HexStructのレベルでは、各フィールドのバイト長の合計を求めるだけでよい。

ItemFieldのレベルでは、固定長フィールドなら@byte_sizeを返し(@byte_sizeに値がセットしてあるなら固定長)、 可変長フィールドであれば、@valueを文字列に変換し、そのサイズを1/2にすればいい。 @valueが配列やHexStructを保持していたとしても、to_sを作用させれば、1つの文字列になるので、ここではタイプ分けは 不要になっている。

  class HexStruct
    def byte_size
      sum = 0
      @field_list.each {|field_name, field|
        sum += field.byte_size
      }
      sum
    end
  
    class ItemField
      def byte_size
        (@byte_size || calc_byte_size(@value) )
      end
  
      private
  
      def calc_byte_size(val)
        val.to_s.size / 2
      end
    end
  end
  

配列化

構造体オブジェクトにto_aryを作用させると、配列に変換するのだが、ここがちょっと汚い。

HexStructのレベルでは、各フィールドにto_aryしたものをArrayに組み込めばいいわけだが、 ItemFieldのレベルでは、@valueのタイプによって、何を返すかを変えないといけない。

@valueがArrayを保持している場合、Array内の要素を順番に配列化してから渡す必要がある。 「要素が一つなら、配列から外に出す」といった処理をしているのは、テストケースの"test_構造体のフィールドに配列が入っている場合の配列化" のテストを通すため。

  class HexStruct
    def to_ary
      @field_list.map {|field_name, field|
        field.parse
      }
    end
  
    class ItemField
      def parse
        parse_val(@value)
      end
  
      private
  
      def parse_val(val)
        arr = 
          case
          when val.kind_of?(Array)
            val.map {|item| 
              parse_val(item)
            }
          else
            val.to_a
          end
        arr.size == 1 ? arr[0] : arr
      end
    end
  end
  


戻る