よたらぼ
自分の興味の赴くままにIT技術系のネタを取りとめもなくメモっています。
Ruby言語やLinuxのネタが多いです。

August 08, 2005 [おもひで]

[Ruby-GNOME2] WikiRPC Client by ZnZさん

今のところHiki専用だそうです。Ruby/GTK2, Ruby/Libglade2を使って頂いてます。

[Ruby-GNOME2] 大バグ発見

元ネタはこちら。実はこれ、昔から気づいてたんだけど今まで放置しちゃったんだよなぁ。てへっ。
とまぁ、ちょっと頭を整理するためにここにメモしてみる。

まず、以下のPureRubyWindow, GtkWindowという仮想的なクラスを例としてあげる。

(1)
class PureRubyWindow
   def child
      @child
   end
   def set_child(value)
      @child = value
      self
   end
end
 
(2)
class GtkWindow
   def child
       gtk_widget_get_child(self)      #実際のCのコードを呼び出すイメージ
    end
    def set_child(val)
       gtk_widget_set_child(self, val) 
       #=> valはGTKの内部でCのポインタとしてchildが管理されている。
       self
    end
end

この例で、以下のようなコードを書いたとする。Ruby-GNOME2ではよく見るコードだと思う。

def create_window
   child = Button.new
   window = PureRubyWindow.new
#   window = GtkWindow.new
   window.set_child(child)
end
 
window = create_widget

このコードではchildの有効な生存期間はwindowの生存期間と同じ、となるのが直感的だ。実際にウインドウが表示されているときに、自分が生成したボタンのインスタンスが実は無くなっているっていうのは気持ち悪いよね。つまり使用者はPureRubyWindowみたいな動作を期待するはず。

が、しかし。

GtkWindow版では、それぞれCのAPIを呼び出すだけになっており、実体としてCのライブラリ側ではchildは生き続けるが、Rubyオブジェクトとしてcreate_windowが呼び出された後はGCの対象になってしまう(はず)。

で、実は現在のRuby-GNOME2の実装は上記のGtkWindowになっている。

GtkWindow#childが返す値はC言語のポインタとしては同じものを指しているものの、それをラップするRubyオブジェクトが毎回違う、ということが発生する(GCのタイミングにもよるみたい)。

もうちょっと厳密に言うと、一度Rubyオブジェクト→Gtkオブジェクトに変換される際にGtkオブジェクト自身にRubyオブジェクトのポインタを覚えさせることをしているので上記のcreate_windowのような順番では問題ないのけど、それは話の本筋と異なるので割愛する。

これにより問題となるのは、GtkWindow自体を自分で拡張したりサブクラスを作成したときにRuby側で定義したオブジェクト内の情報(インスタンス変数や特殊メソッド等)が正しく引き継げないことだ(Rubyとしては新しいオブジェクトが生成・返されてしまう)。

それでは、GtkWindowをどのような実装にすればよいか。

以下のようなソースを見てみる。

class GtkWindow
   def initialize
      @store = {}
   end
   def child
       val = gtk_widget_get_child(self)      #実際のCのコードを呼び出すイメージ
       @store["child"] << val
       val
    end
    def set_child(val)
       gtk_widget_set_child(self, val) 
       @store["child"] << val
       self
    end
end

つまり、GTKのAPIに渡した情報であったとしても、PureRubyWindowと同様な動きが期待されるメソッドでは、GtkWindowのインスタンス変数としてcreate_windowで渡されるchildを保存しておくということだ(childそれ自身に覚えさせておくのではない、というのがポイント)。

こうしておくことで、少なくともGtkWindowの生存期間とchildの生存期間を合わせることができる....はず。

実はRuby-GNOME2では、これの局所的な問題解決用にG_RELATIVE2というマクロが定義されている(正直忘れてた....(^^;))。

で、それを使って上記のGtkWindowをもうちょっとCっぽく書いてみる。

#ただ、今見直すと、第3引数が不要と思われるのでここでは省略。

class GtkWindow
   def child
       val = gtk_widget_get_child(self)      #実際のCのコードを呼び出すイメージ
       G_RELATIVE2(self, val, "child");  //"child"はHashのkey。
                      //この"child"は文字列ではない方が良いかも。
       val
    end
    def set_child(val)
       gtk_widget_set_child(self, val) 
       G_REMOVE_RELATIVE(self, "child"); 
       G_RELATIVE2(self, val, "child"); 
       self
    end
end

要はこれをRuby-GNOME2全体に適用すればよいのだけど...。もうちょっと整理しないとしないとだなぁ。ということで以下にTODOをあげておく。

  1. まず、どういった基準で上記を適用するか、全てのCのWrapperメソッドで適用していいのか。
  2. 上記のマクロは実装も含めもう少し整理した方が良いな。
    GOBJ2RVAL3(key, self, gobj)/RVAL2GOBJ3(key, self, val)みたいなのを作っても良いかもしれない。
  3. G_RELATIVE/G_RELATIVE2の実装は見直したほうがいいなぁ...。
  4. 上記はsetter/getterの組み合わせで成立する。addとかinsertとか、同じ変数(らしきもの)に値がどんどん追加されるようなものはもう少し検討が必要そう。
  5. プロパティやシグナルなど、自動的に生成されるRubyオブジェクトにも同様な仕掛けを入れ込むが必要あるかも。

さてさて。どうしたもんかなぁ。まぁ、問題自体が今になって顕在化するようなものだから今さら焦ってもしょうがないよね(?)。ゆっくりやるとするか...。ってかホントにこれで大丈夫なのか不安だ(苦笑)。

しかし、このTomaszって人すごいなぁ。こんな短期間によくここまで考察できたなぁ。いっそのこと彼に書き直してもらえないかな(^^;)。


編集