はじめに

YAMLの説明はここら辺を見てください。オブジェクトのシリアライズ・デシリアライズ方法に興味を引かれたので見てみることにします。

まず適当に定義したクラスをYAMLライブラリでシリアライズしてみます。

require 'yaml'

class Bar
  def initialize
    @array = [1, 2, 3]
    @hash = {'a' => 123, 'b' => 'abc'}
  end
end

class Foo
  def initialize
    @int = 1
    @string = 'str'
    @bar = Bar.new
  end
end

foo = Foo.new
puts YAML.dump(foo)

上記のスクリプトを実行すると以下の出力が得られます。

--- !ruby/object:Foo 
bar: !ruby/object:Bar 
  array: 
  - 1
  - 2
  - 3
  hash: 
    a: 123
    b: abc
int: 1
string: str

今回読んだバージョンはruby-1.8.5-p12添付のYAMLライブラリです。

YAML.dump(yaml.rb)

では、YAML.dumpメソッドから見ていくことにしましょう。

def YAML.dump( obj, io = nil )
  obj.to_yaml( io || io2 = StringIO.new )
  io || ( io2.rewind; io2.read )
end

第2引数は指定していないので、StringIOオブジェクトに出力されて文字列として返されています。

Object#to_yaml(yaml/rubytypes.rb)

Object#to_yamlメソッドは以下のように定義されています。

def to_yaml( opts = {} )
  YAML::quick_emit( object_id, opts ) do |out|
    out.map( taguri, to_yaml_style ) do |map|
      to_yaml_properties.each do |m|
        map.add( m[1..-1], instance_variable_get( m ) )
      end
    end
  end
end

YAML.quick_emitメソッドです。optsはStringIOオブジェクトが渡っているのでYAML::Syck::Emitter#resetメソッドが呼ばれます。

def YAML.quick_emit( oid, opts = {}, &e )
  out = 
    if opts.is_a? YAML::Emitter
      opts
    else
      emitter.reset( opts )
    end
  out.emit( oid, &e )
end

YAML::Syck::Emitter(ext/syck/rubyext.c)

内部表現の構築

YAML::Syck::Emitterクラスはどこで定義されているかというと、拡張ライブラリで定義されています。というわけでここからはCを読むことになります:-) 拡張ライブラリのプログラミングはここら辺を参照してください。

Emitter#resetメソッドを実装しているのはsyck_emitter_reset関数です。大まかにやっていることを書くと、

  • 引数が1個でwriteメソッドが定義されているので出力先としてそれを設定

ということをしています。

次に、Emitter#emitメソッドを実装しているsyck_emitter_emit関数です。オブジェクトがすでに登録されている(同じオブジェクトが複数の場所で参照されている)場合の処理が行われています。今回はそちらは実行されずelseのブロック呼び出しが行われます。これで一旦Rubyスクリプトに戻ってきます。

ブロック中のout.mapメソッドが呼ばれるとまたCに戻ります。outはYAML::Syck::Outオブジェクトです。taguriはメソッドでyaml_asメソッドを呼ぶことにより定義されます。

def taguri
  if respond_to? :to_yaml_type
    YAML::tagurize( to_yaml_type[1..-1] )
  else
    return @taguri if defined?(@taguri) and @taguri
    tag = #{ tag.dump }
    if self.class.yaml_tag_subclasses? and self.class != YAML::tagged_classes[tag]
      tag = "\#{ tag }:\#{ self.class.yaml_tag_class_name }"
    end
    tag
  end
end

冒頭のFooメソッドの場合、"tag:ruby.yaml.org,2002:object:Foo"という文字列が返るはずです。

to_yaml_styleはnilが返るはずなので、syck_out_mapメソッドに移りましょう。YAML::Syck::Mapオブジェクトを構築して再びブロック呼び出しを行っています。

Objectのto_yaml_propertiesメソッドはインスタンス変数の一覧を返します。それらをMapオブジェクトのaddメソッドを呼び出して登録しています。YAML::Syck::Map#addメソッドを実装しているsyck_map_add_mメソッドに移りましょう。

if ( rb_respond_to( emitter, s_node_export ) ) {
    key = rb_funcall( emitter, s_node_export, 1, key );
    val = rb_funcall( emitter, s_node_export, 1, val );
}

emitterはYAML::Syck::Emitterオブジェクトです。node_exportメソッドはto_yamlメソッドを引数なしで呼び出すのでこれで再帰的にシリアライズが行われていると推察されます。

出力の構築

さて、syck_emitter_emit関数に戻って、次は出力です。emitter.cで定義されているsyck_emit関数が呼ばれています。なお、rubyext.cに定義されている関数はRuby用ですがそれ以外のファイルに定義されている関数は汎用のライブラリです。

syck_emit関数はアンカー処理などをやっていますが無視して、ハンドラが呼ばれてrubyext.cで定義されているrb_syck_emitter_handler関数に戻ってきます。

Objectの場合、以下の部分が実行されるはずです。

case syck_map_kind:
    {
        int i;
        syck_emit_map( e, n->type_id, n->data.pairs->style );
        for ( i = 0; i < n->data.pairs->idx; i++ )
        {
            syck_emit_item( e, syck_map_read( n, map_key, i ) );
            syck_emit_item( e, syck_map_read( n, map_value, i ) );
        }
        syck_emit_end( e );
    }
break;

syck_emit_map関数(emitter.c)内で呼ばれているsyck_emit_tag関数を見てみましょう。いろいろ分岐していますが最終的に"tag:ruby.yaml.org,2002:object:Foo"という文字列から"!ruby/object:Foo"という文字列が書き込まれています。

次にsyck_emit_item関数ではHashやArrayのためのマーキング文字列を出力しています。その後、syck_emit関数が再帰呼び出しされています。

最後にsyck_emit_flush関数を見てみましょう。ハンドラが呼ばれrubyext.cで定義されているrb_syck_output_handler関数が呼ばれます。rb_syck_output_handlerではStringIOオブジェクトに出力を書き込んでいます。

おわりに

今回はYAMLライブラリのオブジェクトシリアライズ処理を読みました。再帰してるだろうな〜とかリフレクションしてるだろうな〜というのは想像通りだったので学んだこととすると

  • 汎用的に書かれたライブラリのRubyインターフェースの書き方

といったところでしょうか。

それではみなさんもよいコードリーディングを。


トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2007-08-14 (火) 06:20:07 (6260d)