yajbについていろいろ

そろそろyajbのバージョンアップを考えている。内部的にはプログラムは出来ていて、後はリリースノート書いたりという作業などがあって、なかなか進まないでいる。せっかくのRubyForgeなのだけども共同作業などもはじめる余裕がなくて難しい。

とりあえず、yajbについてのメモや記録などをいったん全部出してみて、自分的なプレッシャーをかけてみる。

yajbの設計と実装


以下、開発のメモを手直し。

流れなど

はじまりはこのあたり。

RubyでJava

で、実装出来そうだという手ごたえは、Jlambdaを作ったときの感触から。

Jlambda

その後、1ヶ月ほどで普通に使えるようになって、1ヶ月ぐらいがんばって英語でドキュメントを書いて公開。ぽつぽつと直しながら、ここ1年ぐらいは放置気味。一応、毎月メールでの質問が来るのでそれに答えるくらい。

最近急速にJRubyが良くなってきているので、そろそろ役目は終わりかなとか思っている。

Java-RubyをまたぐGC

Ruby側のGCのタイミングをObjectSpaceで拾うことができるので、そこからJava側にGC対象のオブジェクトを知らせることで何とか実現。単にJavaをラップしている代理オブジェクトはこれでいけた。

ただし、Javaから呼ばれるRubyオブジェクトについては、JavaRubyの両方から要らないと判断されない限り捨てられない。いろいろ考えた結果、これらのオブジェクトは手動で削除することにした。

例えば、java.awt.event.ActionListener を作ってボタンに割り当てた場合、ActionListenerはJavaのオブジェクト同士で結びついているので、Ruby側ではその結びつきを直接知ることができない。なので、Ruby側ではそのブロックが終了するとActionListenerのオブジェクトはGCの対象になる。しかし、ここでGCしてしまうとボタンのイベントが発生したときにRuby側で対応するオブジェクトが居なくなる。

ではと、Java側でSoftReferenceで保持しておいて、Java側でGCされたタイミングでRubyGCするということを考えてみる。そうすれば、Java側のオブジェクト同士のリファレンスの切断をRubyで検出できる。しかし今度は、ActionListenerを作ってボタンに登録する間にGCされる可能性ことがあることが分かった。

確実なのは、JavaからもRubyからも必要ないオブジェクトはGCしてもいいということ。しかし Pure Ruby、Pure Javaで実装すると、必要ないという判断はGCした後でしか分からないため、要するに自動でGCすることは出来ない事がわかった。

HORBとかCORBAとか、既存の分散系の実装を見ておけばもう少し楽に設計できたかもしれない。

オブジェクトの同一性

Ruby側のオブジェクトで比較せずに、Java側のオブジェクトで同一判定するようにした。

今まで、同一のJavaオブジェクトには同一の代理オブジェクトを返すようにしていたのだけども、それをやめて同一のJavaオブジェクトに対してRuby側の代理オブジェクトを必要に応じて複数生成するようにした。

これにより代理オブジェクトの再利用などを考えなくて良くなったのでGCが実装しやすくなったけども、リファレンスカウンタを制御する必要が出てきて、オブジェクト生成系とGC周りがかなり大きくなった。

Javaのクラス・インタフェースをRubyで実装

javassist でインタフェースやクラスを implements, extends したクラスを新規作成して、それらが持っている finalやprivate 以外のメソッドをオーバーライドしてしまうという、非常に強引で普通な方法を用いた。

通常は java.lang.relrect.Proxy クラスを使うのだけども、そうするとインタフェースしか実装できず、任意のクラス(たとえばWindowAdapterとか)を継承できなくて実際には使いにくい。ここはSwingがまともに使えるという要件から外せないポイントとしてがんばってみた。

オーバーライドの中身は、デフォルトでは親クラスの実装を呼ぶか、Rubyへの呼び出しになっている。Rubyの Module#define_method を使ってRuby側で実装した旨をJava側に通知することで、Javaのクラスの動的な継承が出来るようにした。

かなりjavaコードを動的に生成するため、独自のテンプレートエンジンを作った。Velocityなどを使うことも検討したのだけども、Velocityだとオーバースペック過ぎるのと、依存ライブラリを増やしたくないということで自作。なかなかテンプレートコードやテンプレートエンジンはコンパクトに実装できて良かったと思う。

困ったことは、Javassistコンパイルのエラーメッセージがまともに出てこないため、デバッグに非常に苦労したこと。でも、コツをつかめば大丈夫。

これでJavaのコードを1行も書かずにRubyからJavaを制御できるようになった。

マルチスレッド

問題はSwingを使うとデッドロックするというもの。具体的には、JavaからRubyを呼び出すと、呼び出したスレッドはその呼び出しの結果が返ってくるまでブロックされるため、GUIイベントハンドラRubyを呼び出してそこでGUIを更新したりすると、Swingの1スレッドルールによってデッドロックが発生する。

解決法のアイデアは早くから思いついていて、JavaからRubyを呼び出したときに単にスレッドをブロックさせるのではなくて、スレッドを再利用可能な形で休眠させるというもの。休眠中にRubyからJavaの呼び出しが発生した場合は、そのスレッドに仕事をさせることでデッドロックを回避する。いわゆる、スレッドを使った継続の実装。

実装は以下のような感じ。

  • JavaRubyの呼び出す部分を集中管理
  • JavaRubyの呼び出しにセッションIDを割り振る
  • JavaRubyの呼び出しがあった場合、スレッドは以下の要求のために待機する
    • Rubyの呼び出しが終了
    • RubyからJavaの呼び出しが発生
  • RubyJavaの呼び出しがあった場合、セッションIDに対応する休眠中のスレッドに仕事をさせる
    • 仕事が終わったら休眠に戻る
  • Rubyの呼び出しが終わったら、休眠を解除してセッションを終了

これでとりあえずSwingのデッドロックがなくなった。また、 JComponent#paintComponent(Graphics) を実装して絵もかけるようになった。その他、Javaのスレッドやロックに起因していた問題が全て解消した。すばらしい。

Swingが普通に動くようになったことで、RubyクロスプラットフォームGUIツールキットとしての野望に一歩近づいた。

パフォーマンス

明らかに通信が遅いので、通信部分をまるごと取り替えるようにした。もともとJavaBridgeの通信系は通信の実装によらないように設計していたので、XMLRPCの実装をごっそり 俺RPC で取り替えることで高速化を実現した。

俺RPC はデータを俺エンコード方式のバイナリで転送する。そのため、XMLRPCの遅さの原因だったテキスト変換・XMLパースが排除できて高速化できた。また、テキスト変換によって発生していた浮動少数の誤差も排除することが出来た。さらに、独自実装なだけにデータタイプも必要なだけ増やすことが出来たので、効率よく正確にデータを転送できるようになった。

ただし、RPC自体の独自完全実装はなかなか大変だった。データのエンコード・デコードは、今までのいろいろなデコーダーを実装してきた経験から、設計と実装はそれほど苦労はしなかった。一番大変なのは通信の管理で、これはまさにデッドロックとの戦いだった。なんとか効率よくトランザクションを実行して、エラーもちゃんと拾って上位レイヤーに伝えるという目標が達成できた。俺RPC は他にも使えそうなので、これだけ切り出して公開してもいいかもしれない。

通信を高速化した後、やはり通信量を減らすことも必要だということで、プロトコルの改善や重複情報のキャッシュをすることでさらに速くなった。

とりあえず、実用的な速度で動くようになったのだけども、パフォーマンスを改善しだすときりがない。膨らんだコードの半分ぐらいがこの高速化によるもの。

実装して気づいたこと

RubyJavaのメソッドの扱いの違い

Javaは同じ名前でも引数が違えば違うメソッドとして区別されるが、Rubyでは引数で区別するということはない。なので、Ruby側でJavaのメソッドをオーバーライドした場合、同じ名前のメソッドはまとめてオーバーライドされる。

Rubyは、ブロックの最後の値が自動的にそのブロックの帰り値になってしまう。そのため、RubyJavaのメソッドをオーバーライドした場合、意図しない場所で意図しないオブジェクトを帰り値にしてしまってIOErrorが起きてしまうことがある。実装の際には帰り値に気をつける必要がある。

RJBではメソッドシグネチャを指定することで回避しているが、個人的にはこのあたりは限りなく透過的に自動変換出来るべきだと思っている。ただ、やっぱり探索コストや選択に主観が入ってしまうので、必要ならば明示的に指定する方法もあった方がいいのかもしれないとも思っている。

Javaのメタ操作

java.util.Arrays#asList で帰ってくる List は、privateなインナークラスで実装されている。しかし、そのオブジェクトに対して直接 toArray を呼ぶと private だからということで IllegalAccessException が発生する。うまく動かすためには interface の java.util.List までたどって、呼ぶ必要がある。つまり、オブジェクトが public でない限り、public なメソッドであっても呼べないらしい。めんどくさい。別の方法としてMethodオブジェクトをsetAccessible(true)としてもOKなのだけども、それは一般的なセキュリティーマネージャで通るのか自信がない。

この問題も含めて、適切なメソッドを探してくる部分が遅いので、他の pnuts とかの処理系を研究する必要がある。バイトコードで実行してしまえば速いのかな。

さらに発生した問題

静的型 vs DuckTyping

Javaは静的に型が決まり、入れ物である変数に型がある。一方のRubyでは、変数の中身には型があるけども、入れ物である変数には型がない。この違いはかなり大きいけども、ある程度Javaも動的性質があるので乗り越えることが出来なくはない。

一番大きな問題はメソッドの検索方法。Javaはメソッド名と引数の型の情報を使ってメソッドを探す。呼びたいメソッドはある程度コンパイル時に決まる。一方、Rubyは名前だけでメソッドを検索して、引数の数が合わなければエラーになるし、引数の中身が期待したものでなければそこでもエラーになる。

Javaの静的型の性質は、コンパイル時にメソッド呼び出しの問題がほとんど解決されるため、特に大規模開発での開発の安全性が重視されるような場合には有効である。一方、Rubyは実行してみるまでそのような問題が検知できないため、型に頼りたくなるような開発には向かない。

しかし、RubyDuckTyping と呼ばれる柔軟な型チェックというか型認識の考え方がある。特定のメソッドを期待するためにインタフェースや抽象クラスを作らなくてもいきなりオブジェクトを渡せてしまうため、大幅にコード量が減る。特に、デザインパターンの大半が、静的型な世界でどうやって柔軟性を高めるかというアイデアであるので、DuckTypingを使えば回りくどいことをしなくても柔軟性を得られる。コード量や分かりやすさが向上すれば、型に頼らなくてもバグの量を減らすことが出来るという戦略でもある。

話を戻すと、問題はRubyにはメソッドを指定する際に型の情報がないのでJavaのメソッド検索が大変ややこしいというもの。メソッドを名前で検索した後は引数をチェックするのだけども、引数が数値の場合、今渡そうとしている数値が int なのか long なのか、あるいは float なのか double なのか調べる方法がない。そのため、数値自体を調べて必要ならば値を変換してメソッドを探す必要がある。しかも複数の候補が見つかることもある。最終的には、決定的にぴったり合うメソッドが見つからない場合は、とりあえずそれらしいものを全部リストアップして一番もっともらしいメソッドを選ぶ。このため、たまに意図しないメソッドを選んでしまうときもあるし、メソッド検索の速度もかなり遅い。でも、「意図しない」とはいえ、数値であることには変わりないのでそれなりに動くし、Javaで無理やりDuckTypingが行われているみたいで面白い。

参考:http://cappuccino.jp/keisuken/logbook/20070123.html#p03

Javassistのバグ?

Javassist の CtNewMethod で作った Method は、CtClassが違ってもシグニチャが同じであれば hashCode は同じ値を返し、さらに equals は true を返す。ちなみに、 == はfalseを返す。実際違うオブジェクトとして認識されるべきなのだけども、コレクションに入れてしまうと同じものだと認識されてしまう。

最初、Javaで生成されたオブジェクトを java.util.HashMap にキーとして入れていたのだけども、原因が分かるまでは意味不明のエラーが頻発して困った。

原因が分かった後、調べてみるとそういう実装は他にもありそうだったので、結局「== で false を返すオブジェクトを違うキーとして区別する俺HashMap」を作成して解決した。

さらなるパフォーマンス改善

プロファイルを取りながらいろいろやったけども、やっぱり教科書どおりの最適化しか有効ではなかった。

  • 小手先の高速化はあんまり効かない
    • 長いif・case文をtableにしてみる
      • →if文評価よりも、tableから引っ張ってきて評価する時間の方が長いらしい
    • オブジェクトをキャッシュしてみる
      • →キャッシュの検索(キーの比較)に時間がかかってキャンセルされる
  • ストリームのバッファリングはかなり効く
  • ThreadPoolは多少効く。負荷が高くなると良く効く。

他のBridge実装との比較など

同様のRuby/JavaはRAAなどを探すといろいろある。

  • rjava
  • rjb
  • rjni
  • RubyJDWP

このうち、rjavaは独自プロトコルTCPで接続するところが似ている。RubyJDWPもTCPでつなぐのだけども、JDWPというJavaデバッグプロトコルを使う。ただ、これらは単にJavaのクラスを呼び出すだけみたいなので、GUIイベントハンドラRubyで実装するというのは無理っぽい。時間がないので試していない。

rjbとrjniはJNIを使ってつなぐので、パフォーマンスは高い。rjbについては試したことがあるけども、スレッドの問題や、Rubyでオーバーライドできるものがinterfaceのみだったり、いちいちクラスをインポートしなければいけないところが使っていて不満だった。しかしながら、当時も今もちゃんと動くし、yajbを作ることになったきっかけにもなった。

yajbの紹介サイトなど

Bringing Ruby to the Enterprise: Enterprise Ruby Recommendations(PDF)

EnterpriseなJavaシステムとつなぐには硬くて早い rjb と、遅いけど柔軟なyajb がありますよという紹介。

Ruby for Java Programmers, Part III

JavaRubyをつなぐシリーズ。なぜかOSX10.4でクラッシュしてぜんぜん動かなかったらしい。一応、手元の OSX10.4のRuby1.8.4でチェックしてはいるので、多分独自コンパイルRubyとかバージョンが変だったとかそういう問題ではないかと。

JRuby vs yajb

JRuby メイン開発者の Charles Nutter による yajb への感想。JRubyにも不満があって yajb を作ったということで、コメントしてもらえるだけでも大変光栄なのかもしれないが、まったく何にも調べずにあさってなコメントがついているのが悲しい。きっと移籍前で忙しかったのかな。

この当時のJRubyGUIとか遅くてあんまり使えなかったし、普通のクラスがextendできなかったりと、かなり yajb が勝っていたポイントがあったと思う。

今年のRubyKaigi行きたかった。

LRUG Meeting: Java code & Libraries

Integrating Java with Ruby (PDF)

London Ruby User Group 主催のセミナーでの紹介。yajbでApacheFOPを操作するサンプルなど。

August NovaRUG meeting

Ruby and Java(PDF)

Northern Virginia Ruby User's Group 主催のセミナーでの紹介。JRuby, RJB, YAJB の機能の違いだけでなく、各値変換の方法やその他細かい挙動についての詳細な比較。YAJBに至っては、Class図や内部のアーキテクチャまで紹介。さらには自分も知らなかったノウハウやバグまで・・・。

Ruby から JFreeChart

もともと、自分もRubyから自作のJavaかしかツールにアクセスするのが目的だったので、こういう使い方はもっともだと思う。

Plugins - Rails XLS

yajb-POI経由でExcelを扱うRailsプラグイン

Ruby Driver for HSQLDB

RubyからPureJavaなDBであるHSQLDBにアクセスするドライバ。本人も書いているように、遅いので実験的なもの。その後 AcriveRecord のドライバを作るとか作らないとか。

S2用コンソール

昨年のNulab社での勉強会でデモしたもの。コード(単にirb内でS2にアクセスしているだけ)は以下のよう*1irbの補完機能を駆使して、S2内部のオブジェクトをインタラクティブに調べたり、引っ張り出して動作実験したり、いきなりRubyでインタフェースを実装してS2 に登録して実験したりするツール。S2をインタラクティブに触れたり、補完が出来るというところがちょっと受けたみたい。

# s2console.rb
# 
# 適当なところに置いて、以下のコマンドで起動。
# irb -Ku -r s2console.rb 
# 
TOMCAT_PATH = "c:/usr/java/apache-tomcat-5.5.xx"
DICON = "app.dicon"

require 'yajb/jbridge'
require 'yajb/jlambda'
jars = Dir.glob('lib/*.jar')
jars << "classes"
jars << Dir.glob("#{TOMCAT_PATH}/common/lib/*.jar")

JBRIDGE_OPTIONS = {
  :jvm_vm_args => "-Xmx256m",
  :classpath => jars.join(";"),
  :bridge_log => true
}

include JavaBridge

[
  "org.seasar.framework.container.*",
  "org.seasar.framework.container.factory.*",
].each {|i| jimport i}

@s2cutil = :s2console_S2Console.jclass

$s2 = :S2ContainerFactory.jclass.create(DICON)
$s2.init

def comp(name)
  $s2.getComponent(name)
end

def lscompdef(reg = nil)
  comps = @s2cutil.getAllComponentDefs($s2)
  return comps unless reg
  return comps.select {|i| i.getComponentName =~ reg}
end

def lscomp(reg = nil)
  comps = lscompdef(reg)
  return comps.map{|i| i.getComponentName }.compact
end
//S2Console.java
//コンパイルしてどこかクラスパスから見えるところに置いておく
package s2console;

import org.seasar.framework.container.*;
import org.seasar.framework.container.factory.*;
import java.util.ArrayList;

public class S2Console {

    public static ComponentDef[] getAllComponentDefs(S2Container s2) {
        ArrayList<ComponentDef> list = new ArrayList<ComponentDef>();
        __getAllComponentDefs(s2,list);
        return list.toArray(new ComponentDef[list.size()]);
    }

    private static void __getAllComponentDefs(S2Container s2,
                                              ArrayList<ComponentDef> list) {
        if (s2 == null) return;
        for (int i=0;i<s2.getComponentDefSize();i++) {
            list.add(s2.getComponentDef(i));
        }
        for (int i=0;i<s2.getChildSize();i++) {
            __getAllComponentDefs(s2.getChild(i),list);
        }
    }

    public static String[] getAllComponentNames(S2Container s2) {
        ComponentDef[] defs = getAllComponentDefs(s2);
        ArrayList<String> ret = new ArrayList<String>();
        for(int i=0;i<defs.length;i++) {
            String a = defs[i].getComponentName();
            if (a == null) continue;
            ret.add(a);
        }
        return ret.toArray(new String[ret.size()]);
    }

    public static String[] def2str(Object[] list) {
        String[] ret = new String[list.length];
        for (int i=0;i<list.length;i++) {
            ret[i] = ((ComponentDef)(list[i])).getComponentName();
        }
        return ret;
    }
}

yajbの今後

とりあえず、各地から送っていただいたバグ報告やパッチを取り込んで、修正版の 0.9.0 を出したいと思っている。

あと、RubyからJavaのクラスローダーを実装できるようにする拡張や、クラスパスの設定があまり賢くないという部分を直した 0.9.1 も出したい。

手元ではここまで出来ているので、あとはリリース作業だけ。

今現在実験していることは、 RJBを使ったドライバと、yajbのJava側のエンジンに複数接続が出来るようにする拡張。

複数接続については、yajbのJava側のブリッジの多言語化への第一歩で、目下の課題はWebブラウザのJavaScriptShellからサーバー側のJavaオブジェクトをインタラクティブに扱うこと。ブラウザからアクセスしてサーバー側のモニターにSwingが出たら勝ちとか。

コードはダウンロードできるけども、Koderに勝手に登録されて非常に見やすくなっている。すごい便利。

YAJB : Koders - Source Code Search Engine

ところで、Development Cost に $149,740 とか書いてあるけど・・・。


いろいろやりたい。楽しいコードをたくさん書きたい。でも今は出来ない。

*1:これらのコードについて動かし方などは説明してません。雰囲気だけどうぞ。