既存の Java アプリに JRuby コンソールをつけてみる

いろいろ目移りしてなかなか進んでいないブロックス・デュオ
とりあえずの目標は西尾さんとそれぞれが作った思考ルーチンで対決することなのだが、自動対戦させようと思ったらなにがしかのプロトコルを決めないといけない。
言語や環境に依存してしまえれば多少楽だが、お互い rubypython で開発するつもりなので、そうもいかないところ。


でもふと横を見ると JavaBlokus のサーバ&クライアントを開発されている方がいるではないか。しかも思考ルーチンを別途作り足すことも出来るようになっている。
一方、西尾さんは Jython の第一人者。今も Java アプリに Jython コンソールを組み込んでごにょごにょむにゅむにゅしている。
そういえば JRuby 1.0 が出たばかり。
これは天の配剤ってやつですか?


というわけで上記 Blokus のクライアントに JRuby のコンソールを組み込んでみるところから始める。
などというと難しげだが、単純に jirb から Blokus クライアントを立ち上げてしまおうというだけだ。
Blokus のソースを subversion で引っ張ってきて、そのままビルド(今回は Web Start で配布することが目的ではないので署名は略)。生成された jar ファイルをクラスパスに指定して jirb を起動。

$ CLASSPATH=ch.jpn.taoe.blokus.jar jirb
irb(main):001:0>


あとは起動クラスの main メソッド(この Blokus クライアントの場合は ch.jpn.taoe.blokus.StartUpClient )を呼んであげればいい。
注意点は StartUpClient.main をそのまま呼んじゃうと帰ってこなくなるので、スレッド内で呼んでおくことくらいしか。

require 'java'
include_class 'ch.jpn.taoe.blokus.StartUpClient'
arg = java.lang.String[0].new  # 引数を指定したければ、arg に値をセット
thread = Thread.new { StartUpClient.main arg }


これで JRuby コンソールの付いた状態で Java アプリケーションが起動した。まあかんたん。
一般的な情報(VM のステータス等)を取ってくることとかは通過儀礼的にやっておこうっと。

irb(main):1:0> java.lang.Runtime.getRuntime.freeMemory
=> 4398872


ここから稼働中のアプリケーションをいじることも出来る(というかそれが目的)が、それにはアプリケーション内部の知識が必要となる。
とりあえずどのあたりが触れるか手っ取り早く調べるには、非 void な "public static" メソッドがないか探すといい。
ソースを Grep すると ch.jpn.taoe.blokus.BlokusManager に getSettings というメソッドがあった。こいつの返すインスタンスで遊んでみる。

include_class 'ch.jpn.taoe.blokus.BlokusManager'
setting = BlokusManager.getSettings

setting.getComDataKeys.map{|i| i } # 登録されている AI のリスト
 => ["Hostile", "Random"]
setting.getComData("Random").getType
=> "Random"
setting.getComData("Random").getDescription
=> "Randomer"
setting.getComData("Random").getComClass  # 思考ルーチンを実装したクラス
=> #<Java::JavaLang::Class:0x1338be1 @java_object=ch.jpn.taoe.blokus.client.com.RandomMoveComputer>


おっと、一つ書き忘れていたので追記
こうやって取得した Javaインスタンスにどんなメソッドがあるのか、もちろんソースを見ればわかるわけだが、とりあえず手っ取り早く知りたいという場合は、JRuby コンソールで ".methods.sort" を付けて叩いてあげればいい。

setting.methods.sort
=> ["==", "===", "=~", "__id__", ……, "getComData", "getComDataKeys", "getGameData", ……]

setting.getComDataKeys
=> #<#<Class:01x4d41e2>:0x1d6bcf5 @java_object=[Hostile, Random]>

setting.getComDataKeys.methods.sort
=> ["+", "-", "<<", "==", ……, "each", "each_with_index", ……, "length", "map", ……]

setting.getComDataKeys は inspect ではよくわからないが、実は Enumerable であることが methods の出力からわかる。これは Java 側のソースを見てもわからないのでちょっと嬉しい。


とば口ができたところで、改めてやりたいことを整理。

  • 思考ルーチンを Ruby/Python で書きたい
  • 思考ルーチンを後から付け足したり、入れ替えたい
  • ゲームの開始・終了を自動化させたい
  • 棋譜」を残したい

これができれば、あこがれの「 JRuby vs. Jython 」が実現する*1
が、さすがにそれは本体無改造でというわけにはいかなさそうだ。
でも、おそらくいくつかのインスタンスを外部(といっても同じVM内)から取得する方法と、いくつかのリスナーを登録する方法を用意するだけで実現できるだろう。


逆に、今後 Java でアプリケーションを開発する場合に、JRuby/Jython コンソールを付加することを見越して、あらかじめ内部へのアクセス手段を用意しておくというのが流行ると楽しそうだ。
XML Web Services の口を用意するなどというのと比べても、十分低いコストでアプリケーションの多様性を増す効果がある。どうだろう?

*1:目的と手段が入れ替わってるような気もしないでもない