irb を使えばオブジェクトと対話するだけでなく、オブジェクトの中に入ることもできます。
irb(main):002:0> class Foo irb(main):003:1> def initialize irb(main):004:2> @a = "I am an instance var." irb(main):005:2> end irb(main):006:1> end => nil
とクラス Foo を定義して以下のように実行します。
irb(main):007:0> irb Foo.new
すると以後 irb は Foo オブジェクトの中で実行されます。
irb#1(#<Foo:0x40444b28>):001:0> @a => "I am an instance var."
この機能と drb を一緒に使えば、リモートのオブジェクトの中にだって入れるかもしれません。ナイスなアイディアです。
以下のようにコマンドラインから実行して、
$ ruby -r drb/drb -e 'class Foo def initialize @a = "I am an instance var." end def foo "foo foo bar" end end DRb.start_service("druby://:12250", Foo.new) sleep '
別の端末から以下のように実行すると
$ irb -rdrb/drb irb(main):001:0> irb DRbObject.new(nil, "druby://:12250") irb#1(#<Foo:0x4022d5dc>):001:0> @a => nil
あら nil しか返ってきませんね。これは @a が Foo オブジェクトのインスタンス変数ではなく、DRbObject オブジェクトのインスタンス変数として実行されちゃうからです。でもメソッドなら呼び出せます。
irb#1(#<Foo:0x4022d5dc>):002:0> foo => "foo foo bar"
どうせならインスタンス変数も直接呼べるようにしたいですよね。
irb を適当に変更すれば、リモートのインスタンス変数を直接触ったり、メソッドを定義したり、実行したりする事は簡単にできます。ただ問題がひとつ出てきます。ローカルの irb のプロセスで定義されていないクラスのインスタンスを返り値として渡された時どうするかという問題です。これに関して『dRubyによる分散オブジェクトプログラミング』では次のように書かれています。
値渡しには大きな問題があります。オブジェクトを渡される側、つまり読み手の知らないクラスのオブジェクトは渡せません。読み手が Marshal.load できないためです。dRuby ではこれを解決する決め手が見つからず、あきらめています。値渡しをするアプリケーション間では、そのオブジェクトのクラスをお互いに知っているように注意してください。
『dRubyによる分散オブジェクトプログラミング』 P40-41
しかし、今回の場合はローカルの irb のプロセスにリモートのオブジェクトのコピーが必要というわけではありません。ローカルの irb のプロセスが知っておく必要があるのはオブジェクトを inspect した文字列だけです。これさえあれば、irb を操作している人間は、リモートのオブジェクトを本当に直接触っている感を得ることができます。
ここからが本題です。もし irb でリモートのオブジェクトを操作できるようになれば、それはデバッグに使えるはずです。ruby にも debug.rb というデバッガが付属しています。が、僕が愛用しているデバッガは print だとか p とかだったりするわけです。debug.rb はどうも難しくて使えません。たぶんそれは以下のような理由なんじゃないかなと思います。
ブレークポイントを設定するのにどうして直接ソースに書かないのか前から不思議に思っていました。コンパイルの必要な言語ならまだしも、コンパイルの必要のないスクリプト言語なわけですから、ソースなんて必要になったら書き直して良いはずです。この方法だと、やっていることは print デバッグと同じですから、人間に分かりやすいはずです。
というわけで、
というような仕組みを作ってみました。デバッガに drb を組み合わせるのは yard の真似です。
class Hoge attr_accessor :somevar end Thread.new{ loop do i = 'foo' j = 'another thread' IRBDB::SetBreakePoint(binding) if $DEBUG end } loop do i = rand() j = Hoge.new IRBDB::SetBreakePoint(binding) if $DEBUG i = 'hoge' IRBDB::SetBreakePoint(binding) if $DEBUG end
みたいなデバッグ対象のソース sample.rb を用意します。コマンドラインから以下のように実行します。
$ ruby -rdebuggee -d sample.rb
別の端末から以下のように実行します。
$ irb -r irbdb irb(main):001:0> irb DRbObject.new(nil, "druby://:12250") irb#1("sample.rb:8"):001:0>
sample.rb の8行目で止まっていることが分かります。例えばローカル変数を参照できます。
irb#1("sample.rb:8"):001:0> j => "another thread"
irbdb_c (or irbdb_continue) でスクリプトの実行を継続します。
irb#1("sample.rb:8"):002:0> irbdb_c continue ... => reach another break point irb#1("sample.rb:15"):003:0> j => #<Hoge:0x402186c8> irb#1("sample.rb:15"):004:0> j.somevar = "foo foo bar" => "foo foo bar" irb#1("sample.rb:15"):005:0> j.somevar => "foo foo bar"
ローカルの irb プロセスが知らない Hoge クラスのインスタンスも扱えます。メソッドも動的に定義できます。
irb#1("sample.rb:15"):006:0> def hoge irb#1("sample.rb:15"):007:1> 'hoge' irb#1("sample.rb:15"):008:1> end => nil irb#1("sample.rb:15"):009:0> hoge => 'hoge'
irbdb からゲットできるので試してみてください。一応デフォルトで drb/acl を使ってアクセス制御はしていますが、他の悪意あるマシンから drb サーバに接続された場合なんでも実行可能なので注意してください。参考。
この90行程度のスクリプトを作成するのに2回ほど print デバッグを行ないました。
binding_of_caller.rb すげえ。callcc と set_trace_func を使って、メソッドを呼び出した側の binding を取得する。some_method(binding) じゃ駄目ですかね。
最近のコメント