TracePointの弱点
Ruby組み込みのクラスTracePoint
は非常に便利ですが、
Cで定義されたメソッドはフックできないという弱点があります。
target = ::Class.instance_method(:attr_accessor)
TracePoint.new(:call) do |tp|
p tp
end.enable(target: target) do
class Foo
attr_accessor :foo
end
class Bar
attr_accessor :bar
end
end
#=> <internal:trace_point>:212:in `enable': specified target is not supported (ArgumentError)
これはドキュメントにも記載があります。
target
should be a code object for whichRubyVM::InstructionSequence.of
will return an instruction sequence.
対応できないメソッドは、正確には"RubyVM::InstructionSequence.of
でnil
が返るメソッド"ということですね。
target = ::Class.instance_method(:attr_accessor)
p RubyVM::InstructionSequence.of(target)
#=> nil
Cで定義されたメソッドはTracePointでフックすることは不可能なのでしょうか?
今回、この不可能を可能にする方法を発見したので紹介します。
薄いmoduleをprependで挟む
基本となるアイデアは「薄いmoduleをprependで挟む」これだけです。
薄いmoduleとは、super
を呼び出すだけのメソッドを定義したmoduleです。
module Hook
def attr_accessor(*name)
super
end
end
これをClass
classにprependします。
::Class.prepend(Hook)
これまでのことをまとめて1つのコードにしてみます。
module Hook
def attr_accessor(*name)
super
end
end
::Class.prepend(Hook)
target = ::Class.instance_method(:attr_accessor)
p RubyVM::InstructionSequence.of(target)
#=> <RubyVM::InstructionSequence:attr_accessor@t.rb:4>
TracePoint.new(:call) do |tp|
p [tp, tp.self, tp.method_id, tp.binding.local_variable_get(:name)]
#=> [#<TracePoint:call `attr_accessor' t.rb:4>, Foo, :attr_accessor, [:foo]]
#=> [#<TracePoint:call `attr_accessor' t.rb:4>, Bar, :attr_accessor, [:bar]]
end.enable(target: target) do
class Foo
attr_accessor :foo
end
class Bar
attr_accessor :bar
end
end
エラーが発生していたコードが動くようになりました!
どのclassからattr_accessor
メソッドが何の引数と共に呼び出されたことが完璧に分かります。
仕組みとしては、元のCで定義されたメソッドを呼び出す前に、Rubyで定義されたメソッドが呼ばれてフックが効くようですね。
この方法の注意点
- どこから呼ばれたかわからない
-
TracePoint#lineno
はRubyで書いた薄いmoduleのメソッド定義のある行を取るので、「どこから呼ばれたのか」には使えません……。 -
caller
とかを使えばもしかしたらできるかも。
-
- コードをちょっと破壊している
- 薄いmoduleをprependしているので完全に非破壊とはいっていません。。。
- いちいち指定がいる
- 「全部のメソッドに一括で対応!」とはいきません。。。
まとめ
TracePoint
は便利。