#contents *はじめに [#g43b2130] YAMLの説明は[[ここら辺>http://jp.rubyist.net/magazine/?0009-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) [#m483bd78] では、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) [#k4f96f76] 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) [#f6f8854a] **内部表現の構築 [#dd54f22d] YAML::Syck::Emitterクラスはどこで定義されているかというと、拡張ライブラリで定義されています。というわけでここからはCを読むことになります:-) 拡張ライブラリのプログラミングは[[ここら辺>http://i.loveruby.net/w/RubyExtensionProgrammingGuide.html]]を参照してください。 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メソッドを引数なしで呼び出すのでこれで再帰的にシリアライズが行われていると推察されます。 **出力の構築 [#e11f4f18] さて、syck_emitter_emit関数に戻って、次は出力です。emitter.cで定義されているsyck_emit関数が呼ばれています。syck_emit関数はアンカー処理などをやっていますが無視して、ハンドラが呼ばれてrubyext.cで定義されているrb_syck_emitter_handler関数に戻ってきます。 さて、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オブジェクトに出力を書き込んでいます。 *おわりに [#x5dd1a8d] 今回はYAMLライブラリのオブジェクトシリアライズ処理を読みました。再帰してるだろうな〜とかリフレクションしてるだろうな〜というのは想像通りだったので学んだこととすると -汎用的に書かれたライブラリのRubyインターフェースの書き方 といったところでしょうか。 それではみなさんもよいコードリーディングを。