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メソッドから見ていくことにしましょう。
def YAML.dump( obj, io = nil ) obj.to_yaml( io || io2 = StringIO.new ) io || ( io2.rewind; io2.read ) end
第2引数は指定していないので、StringIOオブジェクトに出力されて文字列として返されています。
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クラスはどこで定義されているかというと、拡張ライブラリで定義されています。というわけでここからはCを読むことになります:-) 拡張ライブラリのプログラミングはここら辺を参照してください。
Emitter#resetメソッドを実装しているのはsyck_emitter_reset関数です。大まかにやっていることを書くと、
ということをしています。
次に、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関数が呼ばれています。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オブジェクトに出力を書き込んでいます。