gogyou (ゴギョウ)

The gogyou is a library that provides auxiliary features of binary data operation for ruby.

The C-liked struct, union and multidimensional array definition are posible in ruby syntax.

"gogyou" is means "Gnaphalium affine" in japanese.


(in Japanese)

gogyou は、バイナリデータ操作の補助機能を提供する、ruby 拡張ライブラリです。

ruby 構文による、C 言語の構造体・共用体・多次元配列 (もどき) の定義が可能です。

名称は春の七種の一つである『ゴギョウ』から取りました。


  • Product Name (名称): gogyou (ゴギョウ / 御形 / Gnaphalium affine)
  • Author (制作者): dearblue <[email protected]>
  • Distribute License (頒布ライセンス): 2-clause BSD License (二条項 BSD ライセンス)
  • Software Quarity (ソフトウェア品質): alpha
  • User (想定利用者): Rubyist
  • Release Number (リリースナンバー): 0.2
  • Memory Usage (使用メモリ量): 2 MB +
  • Installed Size (インストール容量): under 1 MB
  • Project Page: <http://sourceforge.jp/projects/rutsubo/>
  • Support Ruby: ruby-2.0+ <http://www.ruby-lang.org/>

Example

ruby/ruby.h の struct RBasic と struct RObject を gogyou を用いて次のように記述出来ます:

require "gogyou"

module MyRuby
  extend Gogyou

  ROBJECT_EMBED_LEN_MAX = 3

  typedef :uintptr_t, :VALUE

  RBasic = struct {
    VALUE :flags
    union {
      const VALUE :klass
      struct -> {
        VALUE :klass
      }, :force_modify
    }
  }

  RObject = struct {
    RBasic :basic
    union -> {
      struct -> {
        long :numiv
        uintptr_t :ivptr
        uintptr_t :iv_index_tbl
      }, :heap
      VALUE :ary, ROBJECT_EMBED_LEN_MAX
    }, :as
  }
end

extend Gogyou して呼ばれた struct は、構築した構造体の無名クラスを返します。

この無名クラスを定数に代入すれば、ruby の一般的なクラスと同様に扱うことが出来ます。

RObject のインスタンスは次のように (C 言語のそれや、ruby の他のオブジェクトと相違無く) 扱うことが出来ます。

obj = MyRuby::RObject.new
# or obj = MyRuby::RObject.new(File.read("sample.bin", MyRuby::RObject.size, mode: "rb"))
obj.basic.flags = 0x12345678
(obj.basic.klass = 0xaaaaaaaa) rescue p $!  # => exception! klass field is immutable type
obj.basic.force_modify.klass = 0xaaaaaaaa
obj.as.heap.numiv = 0x55555555
p obj.as.ary[0]  # => 0x55555555
tmp = obj.as.heap
tmp.ivptr = 0x44444444
p obj.as.ary[1]  # => 0x44444444

# 以下の結果は 64ビット環境によるものです
p obj.bytesize  # => 40
p obj.to_buffer  # => "xV4\x12\0\0\0\0\xaa\xaa\xaa\xaa\0\0\0\0UUUU\0\0\0\0DDDD\0\0\0\0\0\0\0\0\0\0\0\0"

About features (機能について)

  • Support A C-liked struct and union (with nested containers)

    C に似た構造体・共用体に対応 (入れ子構造も可能)

    X = Gogyou.struct {
      int :a
      float :b
      double :c
      union {
        struct -> {
          float :x, y, z
        }, :d
        const struct -> {
          int :x, :y, :z
        }, :e
      }
    }
    
  • Support multidimensional arrays

    多次元配列に対応

    Gogyou.struct {
      char :name, 64, 4  # => char name[64][4];
    }
    
  • Alias types by typedef (with array)

    typedef による型の別名定義 (配列も可能)

    module MyModule
      extend Gogyou
    
      typedef :float, :vector3f, 3  # => C: typedef float vector3f[3];
    
      X = struct {                  #       struct X {
        vector3f :a                 #           vector3f a;
        vector3f :b, 4              #           vector3f b[4];
      }                             #       };
    end
    
  • Support packed struct liked GCC __attribute__((packed))

    GCC の __attribute__((packed)) に似た、パックされた構造体に対応

    C 言語での記述

    struct X
    {
        char a;
        int b;
    } __attribute__((packed));
    

    ruby による記述

    X = Gogyou.struct {
      packed {
        char :a
        int :b
      }
    }
    
  • Appended bit operation for Integer

    Integer に対する追加のビット操作

  • Appended binary operation for String

    String に対する追加のバイナリ操作

How to usage (使い方)

最初に gogyou を読み込みます。

require "gogyou"

次に、クラスやモジュールの中で extend Gogyou します。

module MyModule
  extend Gogyou
end

Define struct (構造体の定義)

構造体(もどき)を構築するには、struct をブロック付きで呼び出します。

このブロックは struct 内部で生成されるオブジェクトが instance_exec するときにそのまま渡されます。self が切り替わることに注意して下さい。

フィールド名はシンボル (または文字列) で与えます。

module MyModule
  TypeA = struct {
    int :a
  }
end

これで int 型一つのフィールドからなる構造体(もどき)のクラスが TypeA として定義されました。

struct は定義した無名クラスを返すため、利用者側で定数に代入することで構造体名が定義されるわけです。

Define array in struct (配列の定義)

配列として定義する場合は、フィールド名に続く引数として整数値を与えます。

module MyModule
  TypeA1 = struct {
    int :b, 4  #  => C: int b[4];
  }
end

多次元配列の場合は、連続して整数値を与えます。

module MyModule
  TypeA2 = struct {
    int :c, 8, 4, 2  #  => C: int c[8][4][2];
  }
end

配列の場合でも、複数のフィールドを連続してまとめることが出来ます。

module MyModule
  TypeA3 = struct {
    int :a, :b, 4, :c, 8, 4, 2  #  => C: int a, b[4], c[8][4][2];
  }
end

Define nested struct (入れ子になった構造体の定義)

入れ子構造体を定義するには、struct の内部で struct を使えばいいだけです。

struct の最初の引数にブロックを与えること以外は、先に述べた通常のフィールド定義と同じです。

コメントは C 言語で記述した場合の対比としてあります。

module MyModule
  TypeB = struct {      #  struct TypeB {
    struct -> {         #      struct {
      int :a, :b        #          int a, b;
    }, :n, 2, 4, 8, :m  #      } n[2][4][8], m;
  }                     #  };
end

最初の引数にブロックではなく、型情報を持つオブジェクトを与えることも出来ます。

module MyModule
  TypeC = struct {                 #  struct TypeC {
    struct TypeA, :n, 2, 4, 8, :m  #      struct TypeA n[2][4][8], m;
  }                                #  };
end

無名構造体の場合、引数は渡さずにブロックを渡すだけです。

module MyModule
  TypeD = struct {  #  struct TypeD {
    struct {        #      struct {
      int :a, :b    #          int a, b;
    }               #      };
  }                 # };
end

struct 内の struct の呼び出し方法を示します。union も同様に利用できます。

  • struct { ... } -> nil
  • struct proc_object, field_name, *array_elements
  • struct user_type_info, field_name, *array_elements

  • union { ... } -> nil

  • union proc_object, field_name, *array_elements

  • union user_type_info, field_name, *array_elements

引数なしのブロック付きで呼ぶと、無名構造体 (union であれば無名共用体) を定義します。

proc_object を与えて呼ぶことで、続く field_name によってその内部を参照することが出来ます。

array_elements は任意個の整数値で、直前のフィールド名を配列として定義します。

field_namearray_elements を組にして複数個並べることが出来ます。

user_type_info は、任意のオブジェクト (クラスやオブジェクトを含む) を型として用いる場合に利用できます。 詳細は『About user typed info』を見て下さい。

About user typed info (利用者定義の型情報について)

構造体・共用体の内部フィールドには、利用者が定義した型情報を用いることが出来ます。

この型情報は次のメソッドを持ったあらゆるオブジェクト (クラスでもモジュールでも、インスタンスでも構いません) のことです。

  • .bytesize
  • .bytealign
  • .extensible?
  • .aref(buffer, offset)
  • .aset(buffer, offset, data)

例として、MD5 を定義する場合の型情報は次のようになります。

class MD5
  def self.bytesize
    16
  end

  def self.bytealign
    1
  end

  def self.extensible?
    false
  end

  def self.aref(buffer, offset)
    ... snip ...
  end

  def self.aset(buffer, offset, data)
    ... snip ...
  end
end

これらのメソッドを一つ一つ定義する代わりに、任意のクラス・モジュールの中で Gogyou.define_typeinfo を用いることでまとめて定義することも出来ます。

class MD5
  Gogyou.define_typeinfo(16,    # bytesize
                         1,     # bytealign
                         false, # extensible?
                         ->(buffer, offset) { ... snip ... },       # aref
                         ->(buffer, offset, data) { ... snip ... }) # aset
end

順を追って説明していきます。

.bytesize

このメソッドはその型が必要とする領域のバイト数を正の整数値で返します。

型が拡張構造になっている場合は、最小値となる値を返します。

MD5 を定義する場合、16バイトなので 16 を返します。

.bytealign

このメソッドはその型のアライメントサイズを正の整数値で返します。

MD5 を定義する場合、内部表現は1バイトの塊なので 1 を返します (MD5 の実装によっては 4 だったり 8 だったり、はたまた 16 になるかもしれません)。

.extensible?

このメソッドはその型が常に固定長か、任意に拡張する構造になっているかどうかを返します。

true であれば拡張構造であることを意味し、false であれば固定長であることを意味します。

MD5 は固定長なので、false を返します。

.aref(buffer, offset)

このメソッドは構造体のフィールドを参照した場合に呼ばれます。

buffer は上位構造のバイナリデータとしての String インスタンスです。

offset は上位構造から見た、フィールドの絶対位置をバイト値で表した整数値です。

戻り値はその構造体のフィールドに対するインスタンスを返します。

MD5 の場合、buffer からデータを切り出して MD5 のインスタンスを返すべきです。

class MD5
  def self.aref(buffer, offset)
    new(buffer.byteslice(offset, 16))
  end
end

もしもインスタンスの変更を反映させる MD5#[]= のようなメソッドが必要であるならば、上述したメソッドではうまく行きません。 理由は buffer.byteslice によって buffer オブジェクトが切り離されてしまっているからです。

クラスを MD5 のデータが保持されるだけの構造から、bufferoffset を保持する構造に変更する必要があります。 その上でメソッドの定義を変更します。

class MD5
  def self.aref(buffer, offset)
    new(buffer, offset)
  end

  def [](index)
    raise IndexError unless index >= 0 && index < 16
    @buffer.getbyte(@offset + index)
  end

  def []=(index, num)
    raise IndexError unless index >= 0 && index < 16
    @buffer.setbyte(@offset + index, num)
  end
end

.aset(buffer, offset, data)

このメソッドは構造体のフィールドへデータを代入した時に呼ばれます。

例えば、structobj.field = data のような場合です。

buffer は上位構造のバイナリデータとしての String インスタンスです。

offset は上位構造から見た、フィールドの絶対位置をバイト値で表した整数値です。

data は代入する値です。

戻り値は無視されます。

data に対してどのような値 (オブジェクト) を受け入れるのかを決定するのは、型情報を定義する側の問題となります。

MD5 の場合、最低でも MD5 インスタンスを受け入れるようにするべきです。

今回は MD5 インスタンスだけではなく 16バイトの文字列、そして nil を受け取れるようにしてみます。

class MD5
  def self.aset(buffer, offset, data)
    case data
    when MD5
      buffer.setbinary(offset, data.to_binary)
    when String
      buffer.setbinary(offset, data.byteslice(0, 16))
    when nil
      buffer[offset, 16] = ?0 * 16
    else
      raise ArgumentError, "data is not a MD5, String or nil"
    end
  end
end

Define packed struct

GCC の __attribute__((packed)) に似たパックされた構造体のフィールドを定義するには、そのフィールドを packed メソッドのブロックとして囲うことで行います。

構造体自体をパックするには、その構造体のフィールド全体を packed することで行います。

また、packed の中に structunion を含めることも出来、その入れ子内部で packed を行うことも出来ます。

ただし packed を直接入れ子にして呼び出すことは出来ません。

構造体全体を packed する場合:

X = Gogyou.struct {
  packed {
    char :a
    int :b
    int :c
  }
}

p X.bytesize # => 9

packed された入れ子構造体の内部でさらに packed する場合:

Y = Gogyou.struct {
  char :a
  packed {
    struct {
      int :b
    }
  }
  char :c, 3
  int :d
}

p Y.bytesize # => 12

直接入れ子にして packed して例外が発生する場合:

Z = Gogyou.struct {
  packed {
    packed {  ## => EXCEPTION!
      char :a
      int :b
    }
  }
}

Demerit (短所)

  • Can't be handled pointer

    ポインタが扱えない

  • The cost is high for reference/asignment from/to fields

    フィールドに対する参照・代入のコストが高い