JRuby1.0 で Ruby から Java にオブジェクトを渡す際の細かい挙動あれこれ

前回の記事 http://d.hatena.ne.jp/n_shuyo/20070627/jruby では、既存の Java アプリケーションを JRuby(jirb) の中から起動すれば、ちょうど Ruby コンソール付きで起動した状態となり、稼働中の Java アプリケーションの内部を対話的に操作できることを紹介させてもらった。
ただし、JRuby の方に Java アプリケーション内の各種インスタンスが自動的に渡ってくるわけではないので、無改造で操作できる範囲は限られる可能性が高い。まあでもほとんどのケースでは、比較的少量の改修で Ruby から存分に本体をいじくることができるんじゃないかと見込んでいる。


というわけでそれを実践するべく、先の Blokus アプリケーションにて Ruby で思考ルーチンを書くというのに挑戦してみたところ、確かにこの場合については少量の改修で成功した。
が、Blokus についてはひとまず稿を改めるとして、ここでは「 JRuby において、Ruby のオブジェクトを Java に渡す」際、かなりよくわからん挙動をするので、そこをわかっている範囲でまとめてみる。っていうか、正直まだ結構あれこれよくわかってない。へるぷみー。


早速だが、次のプログラムを動かしてみる。
本当は、たかが動作確認サンプルのくせにこんなに長いコードを blog に貼り付けたくはないのだが、かなり直感的でない結果になので、証拠がないときっと信じてもらえまい……

/* JRubyTrial.java */
import java.util.List;
public class JRubyTrial {
  public String testToString(List a) {
    return a.toString();
  }
  public int testSize(List a) {
    return a.size();
  }

  private List _list = null;
  public void setMember(List list) {
    _list = list;
  }
  public List getMember() {
    return _list;
  }
  public int getMemberSize() {
  	return _list.size();
  }
}
require 'java'

include_class "JRubyTrial"
t = JRubyTrial.new

include_class 'java.util.ArrayList'
class MyList < ArrayList
  def toString
    "MYLIST"
  end
  def size
    111
  end
end

include_class 'java.util.List'
class MyList2
  include List
  def toString
    "RUBYLIST"
  end
  def size
    111
  end
end

puts "== (A) ArrayList.new"
p list0 = ArrayList.new
puts "toString: #{t.testToString(list0)}"
puts "size[default]: #{t.testSize(list0)}"
def list0.size; 222; end
puts "size[override]: #{t.testSize(list0)}"

puts "\n== (B) MyList.new (< ArrayList)"
p list1 = MyList.new
t.member = list1
puts "toString: #{t.testToString(list1)}"
puts "size: #{t.testSize(list1)}"
puts "size[member]: #{t.memberSize}"
puts "size[return member]: #{t.member.size}"
def list1.size; 222; end
puts "size[override]: #{t.testSize(list1)}"
puts "size[member]: #{t.memberSize}"
puts "size[return member]: #{t.member.size}"

puts "\n== (C) MyList2.new (include List)"
p list2 = MyList2.new
t.member = list2
puts "toString: #{t.testToString(list2)}"
puts "size: #{t.testSize(list2)}"
puts "size[member]: #{t.memberSize}"
puts "size[return member]: #{t.member.size}"
def list2.size; 222; end
puts "size[override]: #{t.testSize(list2)}"
puts "size[member]: #{t.memberSize}"
puts "size[return member]: #{t.member.size}"

puts "\n== (D) List.new"
p list3 = List.new
t.member = list3
puts "size: #{t.testSize(list3)}" rescue puts "size[undefined]: #{$!}"
def list3.toString; "List.new"; end
def list3.size; 222; end
puts "toString[ruby]: #{list3.toString}"
puts "toString[java]: #{t.testToString(list3)}"
puts "size[defined]: #{t.testSize(list3)}"
puts "size[member]: #{t.memberSize}"
puts "size[return member]: #{t.member.size}"

やっていることはシンプル。
java.util.List インターフェースを持つ Ruby オブジェクトを4通りの方法で生成、Java に渡して期待される動作をするかを確認している。
生成する4つの方法とは以下の通り。

また、Java 側では toString() と size() について動作を確認できるようになっている。また、渡されたオブジェクトを一度インスタンス変数に格納し、Java 側で size() を呼んだ場合と、オブジェクトを Ruby に返してから size() を呼んだ場合も検証できるようにしてある。
これを実行した結果が以下の通り。

== (A) ArrayList.new
#<Java::JavaUtil::ArrayList:0x183e7de @java_object=[]>
toString: []
size[default]: 0
size[override]: 0

== (B) MyList.new (< ArrayList)
#<MyList:0xa522a6 @java_object=MYLIST>
toString: MYLIST
size: 111
size[member]: 111
size[return member]: 0
size[override]: 222
size[member]: 222
size[return member]: 0

== (C) MyList2.new (include List)
#<MyList2:0x1c9e67a @java_object=$Proxy0>
toString: $Proxy0
size: 111
size[member]: 111
size[return member]: 111
size[override]: 222
size[member]: 222
size[return member]: 222

== (D) List.new
#<#<Class:01x7cd37a>:0xa89ce3 @java_object=$Proxy0>
size[undefined]: undefined method `size' for #<#<Class:01x7cd37a>:0xa89ce3 @java_object=$Proxy0>:#<Class:01x7cd37a>
toString[ruby]: List.new
toString[java]: $Proxy0
size[defined]: 222
size[member]: 222
size[return member]: 222

結果をまとめると、こうだ。

(1) は納得できるし、実際のプログラミングにおいて障害になることもないだろう。問題は (2) と (3) だ。


(2) は Java のクラスを継承した Ruby クラスを Java とやりとりするのは結構危なっかしいという事を示している。
実は、JRuby の Limitations ( http://jruby.codehaus.org/Limitations ) に "Extended Java Classes in Ruby will not be visible to Java consumers" と書かれている。で、そこに例が記されているのだが、でもそれはちゃんと動いてしまうのだ……。
でもわざわざ limitations にそう書かれるということは何か不具合があるに違いないということで色々試して見つけたのがこの (2) の現象である。
このサンプルではこれ以上触れていないが、Java から返ってきたオブジェクトは Ruby 側で定義した内容が全て無くなっているかと思えば必ずしもそうではない。いっそ Java のベースクラスのインスタンスになってしまってくれていた方が挙動としてはわかりやすいくらいなのだが。
上記 Limitations には一応 "This will be fixed once we have a compiler." とも書いてある。が、そこに書いてある例は動くわけで、fix されるのが (2) の現象なのかどうかはわからない……


(3) も「 implements できないメソッドは toString だけなのか?」というあたりに確証が無くて困っている。
ともあれ、問題の範囲が予測できるという点で、Ruby から Java に渡すオブジェクトは (C) か (D) の方法で作った方が問題が起こりにくそうだということがわかるだろう。


これがわかってしまえば(そのために盛大な試行錯誤をしたのだが……)、Blokus アプリを JRuby に対応させるのは簡単だった。というわけで、それらは次回。


追記
はっ。もしや「Ruby で定義できるのはインターフェースに明記されているメソッドのみ」なのか!? とひらめく。
早速確認。

/* ToString.java */
public interface ToString {
  public String toString();
}

あとは jirb で……

irb(main):001:0> require 'java'
=> true
irb(main):002:0> include_class 'ToString'
=> ["ToString"]
irb(main):003:0> t = ToString.new
=> #<#<Class:01x15983b7>:0x1d0d124 @java_object=$Proxy0>
                                                ^^^^^^^ <= ここに Java で toString した値が出る
irb(main):004:0> def t.toString; "TEST" ; end
=> nil
irb(main):005:0> t
=> #<#<Class:01x15983b7>:0x1d0d124 @java_object=$Proxy0>
                                                ^^^^^^^

だめか orz