TL;DR
- json module読み込み時にC言語の拡張メソッドを呼び出して関数の追加とmodule追加して、組み込みのクラスに後からMixinしているから。
疑問
Rubyでjsonに変換する場合to_json
を使用をする場合が多いと思います。
Hash
型やArray
型のオブジェクトから変換する場合は、以下のようなコードを書きますが、require 'json'
をしないとNoMethodError
でエラーが発生します。
-
require 'json'
しない
irb(main):002:0> some_hash = {a: 1,b: 2}
irb(main):004:0> some_hash.to_json()
Traceback (most recent call last):
5: from /usr/local/opt/ruby/bin/irb:23:in `<main>'
4: from /usr/local/opt/ruby/bin/irb:23:in `load'
3: from /usr/local/Cellar/ruby/2.7.1_2/lib/ruby/gems/2.7.0/gems/irb-1.2.3/exe/irb:11:in `<top (required)>'
2: from (irb):3
1: from (irb):4:in `rescue in irb_binding'
NoMethodError (undefined method `to_json' for {:a=>1, :b=>2}:Hash)
Did you mean? to_s
-
require 'json'
する
irb(main):005:0> require 'json'
=> true
irb(main):002:0> some_hash = {a: 1,b: 2}
irb(main):006:0> some_hash.to_json()
=> "{\"a\":1,\"b\":2}"
組み込みのclassに対して後からインスタンスメソッドを追加するのは珍しいなと感じたので、内部の実装を追って見ます。
ソースコード確認
Rubyのドキュメントによると JSON::Generator::GeneratorMethods::Hash
というmoduleが存在するようです。
Rubyソースコードの該当部分を検索してみるとおそらくここから各generatorクラス(json/ext/generator
)の呼び出しをしているみたいです。
module Ext
require 'json/ext/parser'
require 'json/ext/generator'
$DEBUG and warn "Using Ext extension for JSON."
JSON.parser = Parser
JSON.generator = Generator
end
さらに以下を確認するとjson/ext/generator
は.rb
ではなくc言語から拡張ライブラリとしてコンパイルされていることが分かります。
require 'mkmf'
$defs << "-DJSON_GENERATOR"
create_makefile 'json/ext/generator'
該当のc言語のファイルを確認するとrb_define_method
によってHash
クラスに対してto_json
のメソッドが登録及びmoduleの登録が行われていることが分かりました。
mHash = rb_define_module_under(mGeneratorMethods, "Hash");
rb_define_method(mHash, "to_json", mHash_to_json, -1);
そして上記のmoduleは以下でMixinすることで後からインスタンスメソッドの追加を行なっているようです。
klass.class_eval do
instance_methods(false).each do |m|
m.to_s == 'to_json' and remove_method m
end
include modul
end
まとめ
json module読み込み時にC言語の拡張メソッドを呼び出して関数の追加とmodule追加して、組み込みのクラスに後からMixinしているから、というのが分かりました。
言語によっては(Pythonとか)組み込みクラスに対してメソッドの追加を許可していない言語もあるので少し新鮮でした。良くも悪くも組み込みクラスに対して柔軟にインジェクションできるんですね。
これからも勉強していきたいと思います。