#contents

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関数に戻ってきます。

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]


トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS