Ruby Library Report 【第 2 回】 Java との連携

著者:立石 孝彰 編集・校正:馬場 道明、青木 峰郎

はじめに

今回は Ruby と Java をつなぐライブラリとして、 rjb, rjni, RubyJDWP という三つのライブラリを紹介します。 rjb と rjni は Ruby から Java のクラスファイルにアクセスするためのライブラリです。 RubyJDWP は実行中の Java プロセスにアクセスするためのライブラリです。

近年、Java は普及が進み、 名前だけであれば一般的な PC ユーザですらも知っているようになりました。 それだけにライブラリの種類も Ruby と比べると遥かに多く、特殊なサービスへの アクセス用ライブラリが Java でしか提供されていない場合もあります。 また、Java を多く使用するユーザの中には Java 用に作成した様々なリソース を活用したいこともあるでしょう。 このような場合に rjb や rjni を利用すると Java のリソースをそのまま再利用でき、 類似ライブラリや特殊なラッパーライブラリなどを開発する手間が省くことができます。

一方で、頻度はそれほど多くないかもしれませんが、実行中の Java プログラムに アクセスして動的にクラスファイルを入れ替えたり、プログラムを解析したい場合が あります。 このようなことは、JDWP [1] というデバッグ用のプロトコルを使うと 実現できます。このプロトコルを Ruby から使えるようにしたのが RubyJDWP です。 JDWP は低レベルなプロトコルですので、より使いやすくしたものに JDI という ライブラリがあります。JDI には、Java のメソッドコールを行うことができる程度*1 の機能があります。 RubyJDWP を利用して JDI 相当のライブラリを作成すれば、 Ruby プロセスから Java のメソッドを呼ぶことができます。 本レポートの作成当初はこのような機能を利用して rjb や rjni と同様に Java のメソッドを呼ぶ機能を実装しようと考えていたのですが、 RubyJDWP で JDI に相当する部分の実装が不十分なため断念しました。 そのため RubyJDWP の例では JVM へアクセスするだけに留めます。

試用レポート -- rjb

登録データ

概要

rjb は JNI (Java Native Interface) を利用して Java VM (JVM) を 操作するライブラリです。具体的には、Ruby プロセス内に JVM を起動し Java クラスファイルをロードしてメソッドを呼ぶことができます。

作者からの声

作者の arton 氏より以下のコメントを頂きました。*2

rjb の 3 つの目的と 3 つの誤算

 ここ数年、サーバーサイド Java の開発を仕事にしているのですが幾つか気に食わな い点がある、というのが rjb の開発動機です。

目的 1  JUnitでテストを作るのが (特にモックを作るのが) 面倒くさくなったこと。開発す るターゲットとテストプログラムを同一言語で作るという意義は認めます。でもヒアド キュメントはないし (結構長い文字列を使うテストが多い)、Proxy の生成には interface 縛りがあるといった点です。

誤算 1  Ruby ならメソッド名と引数の数が合っていればいくらでもモックできますが、残念、 JNI 経由で呼ぶためには結局 Proxy を利用しなければなりませんでした。でも、まあ 使えるからこれは良しとします。

目的 2  サーバーサイドのビジネス処理用クラスはそのインスタンスだけでは (少なくても僕 が作ってるのは) 動けません。誰か制御するプログラムが必要です。だから時々シェル から使いたくなっても、そのためにグルーとなるプログラムを Java で作らなければな りません。これってどこかで経験したな、ああ ActiveX コントロールと ActiveScript みたいだ、というわけで Ruby がインスタンス化とメソッド呼び出しをすれば良いとい うのも目的です。で、一応 RAA を探したところ JNI でささっと呼べるブリッジはなか ったので作り始めました。

誤算 2  rjni みたいに RAA に登録していない先達の存在に気付きませんでした。とは言えこ の目的としては幸せに使っています。元々使うつもりのクラスはシングルスレッドでし か動作しないし Ruby 自身は単に Java のクラスを操作するだけの処理しか想定してい ないので問題がないからです。

目的 3  僕は現在、Win32, Solaris, OSX, Linux を使っています。全部で共用できる GUI ラ イブラリでかつ Win32 にも標準で乗っているものを Ruby から呼べれば使いまわしが 楽になるでしょう。となると Tk でも Gtk でもなく Swing ですなぁ、というのも目的 でした。

誤算 3  OSX では Java の GUI ライブラリ(AWT)はメインスレッドからは呼び出せないという 制限が rjb が動き出してからわかりました。目的 2 から Ruby はシェルそのものとし て動かしたいのでこれはあんまり嬉しくはありません、というか事実上使えません。も ちろん、Ruby を組み込みで使うプログラムを作りワーカスレッド上で Ruby を呼べば 良さそうですが、それを作っても Win32 とは共用できません。また Linux についても 描画のあたりでうまく動作しないという問題があって (これは原因は追求していません) 結局第 3 の目的は頓挫しました。なお、Win32 や Solaris では一応動くとは言え、 rjni の開発者が [ruby-talk:115504] で語っているように、Ruby はネイティブスレッ ドや再入をうまく処理できなないので Java のイベントリスナからの呼び出しは綱渡り となります。もっともこれはデッドロックを覚悟すればどうにかならないわけではない のですが (ASR で無理矢理実装している) 既に OSX と Linux の挫折があるのでそれほ ど乗り気にはならない部分です。

 というわけで、誤算はあったものの、当初の目的のうち 1 と 2 については使えてい るのでこんなものかなぁというところで小休止しています。

 ちなみに、誤算 2 の補足ですが、実際には誰かが作っていても結局自分で作ったか も。既存の車輪を無視してガンガン何度でも作れるところが NPD(非営利開発)の良いと ころかも知れません。使いたければ使うし作りたければ作る、そんな感じです。

 最後になりましたが、文字列の変換処理を追加してくださった桑島さんと、バグ報告 をくださった立石さん、ありがとうございます (現時点では更新が止まっていますがそ ろそろ反映させますのでもうちょっとお待ちください) 。

rjb は趣味・実験目的で作られた物と思っていましたが、実用目的だったのですね。目的が合致する方は、使ってみると良いのではないのでしょうか。なお、コメントにあるバグですが、今回のサンプルや通常の使用ではほとんど問題ありません。*3

サンプル

まず最初は Java の System.out.println メソッドを利用して 文字列を出力する簡単なスクリプトを紹介します。

 require 'rjb'

 Rjb::load    # (a1)

 System = Rjb::import("java.lang.System")   # (a2)
 System.out.println("Hello World!")         # (a3)

(a1) 最初に Rjb::load で JVM を起動します。 rjb 0.1 以降ではこの文は省略可能です。

(a2) Rjb.import で java.lang.System クラスをロードし、 Ruby の定数 System に保持します。 Rjb.import は Java で言う import に相当します。 このように、Java 上のクラスなどの要素を Ruby から扱えるようにすることを、 このレポートでは「インポート」と呼ぶことにします。 なお、素の Java では java.lang パッケージの import は省略できますが、 rjb を使う場合は省略できません。

(a2) Java の System.out.println メソッドを使って "Hello World!" という文字列を出力させています。 System.out はメソッドではなく public 変数ですが、 rjb では、違いを意識することなくアクセスできます。 また、整数などの Java の基本データ型の値と文字列は、 対応する Ruby のオブジェクトから自動変換されるようです。 このため、"Hello World!" という Ruby の String オブジェクトは 明示的に変換せずとも渡せます。

もう少し実用的なサンプルとして、FreeMarker [3] という Java の HTML/XML テンプレートエンジンを Ruby から使ってみましょう。 ここでは以下のテンプレート (ファイル名 sample.flt) を使います。

 <html>
 <head>
   <title>RLR</title>
 </head>
 <body>
   ${user}'s latest product:
   <a href="${latestProduct.url}">${latestProduct.name}</a>!
 </body>
 </html>

${user}, ${latestProduct.url}, ${latestProduct.name} という箇所に プログラムから与えたパラメータが埋め込まれ、 最終的な HTML ドキュメントを生成します。 埋め込むデータは Java の Map オブジェクトとして与えます。

まずネイティブの Java ならこのテンプレートを使うコードは 次のようになります。

 import java.util.*;
 import java.io.*;
 import freemarker.template.*;

 class FillTemplate
 {
     static public void main(String[] args) {
         // データを作る
         Map root = new HashMap();
         root.put("user", "Takaaki Tateishi");
         Map latest = new HashMap();
         latest.put("url", "products/RLR.html");
         latest.put("name", "Ruby Library Report");
         root.put("latestProduct", latest);

         // テンプレートを埋める
         try {
             Configuration conf = Configuration.getDefaultConfiguration();
             conf.setDirectoryForTemplateLoading(new File("."));
             Template template = conf.getTemplate("sample.ftl");

             OutputStreamWriter out = new OutputStreamWriter(System.out);
             template.process(root, out);
         }
         catch (Exception ex) {
             // 例外処理
         }
     }
 }

これと同じ処理を rjb を使って Ruby から実行すると、 以下のようになります。

 require 'rjb'

 # (a1)
 Rjb::load
 # (a2)
 module Java
   extend Rjb
   System = import("java.lang.System")
   String = import("java.lang.String")
   Number = import("java.lang.Number")
   Boolean = import("java.lang.Boolean")
   List = import("java.util.List")
   Map = import("java.util.Map")
   HashMap = import("java.util.HashMap")
   File = import("java.io.File")
   OutputStreamWriter = import("java.io.OutputStreamWriter")
   IOException = import("java.io.IOException")
 end
 module FreeMarker
   extend Rjb
   Configuration = import("freemarker.template.Configuration")
   Template = import("freemarker.template.Template")
 end

 # (b1)
 cfg = FreeMarker::Configuration.getDefaultConfiguration()
 cfg.setDirectoryForTemplateLoading(Java::File.new("."))
 template = cfg.getTemplate("sample.ftl")

 # (b2)
 root = Java::HashMap.new()
 root.put("user", "Takaaki Tateishi")
 latest = Java::HashMap.new()
 latest.put("url", "products/RLR.html")
 latest.put("name", "Ruby Library Report")
 root.put("latestProduct", latest)

 # (b3)
 out = Java::OutputStreamWriter.new(Java::System.out)
 template.process(root, out)

(a1) 最初のサンプルと同じく Rjb::load で JVM を起動します。省略しても構いません。

(a2) 必要な Java のクラスをインポートします。 Java 標準クラスと FreeMarker のクラスを Ruby 上でも区別するため、 別々のモジュールに Java のクラスを集めておきます。

(b1) FreeMarker の設定を行い、 テンプレート用のオブジェクトを作成しています。

(b2) テンプレートに与えるデータを作成します。 先の Java におけるデータ作成とほとんど同じ要領で作成できています。

(b3) テンプレートにデータを埋め込み標準出力へ結果を出力します。 出力結果は以下の通りです。

 <html>
 <head>
   <title>RLR</title>
 </head>
 <body>
   Takaaki Tateishi's latest product:
   <a href="products/RLR.html">Ruby Library Report</a>!
 </body>
 </html>

感想

rjb を用いると Java のクラスをあたかも Ruby のクラスであるかのように利用することができます。 つまり Ruby と Java のデータの受け渡しなどのために 特別な処理を行う必要がないということです。

ただ、rjb 0.1.6 で試した限りでは環境変数 CLASSPATH が効かなくなるという問題に遭遇しました。 そのため FreeMarker のライブラリ本体である freemarker.jar を JVM デフォルトのロードパスに置いてテストしています。 デフォルトのロードパスは Linux などでは $JAVA_HOME/jre/lib/ext、 Windows では %JAVA_HOME%\jre\lib\ext です。

また、サンプルでは紹介しませんでしたが、rjb では Ruby のオブジェクトを Java のインターフェイスに関連付け、Java オブジェクトとして扱うこともできます。 この機能は rjb のドキュメント で Comparable インターフェイスの例が紹介されています。 この仕組みを利用すると、Map インターフェイスを実装した Java のクラスを Ruby の Hash にマップする、といったことが実現できそうです。

ところで、サンプルに似たプログラム例が、 @IT の記事 にあります。 この記事では、Groovy を用いて、(b1) と (b2) に相当することを行っています。 比べて見ると、インポート方法が若干異なりますが、 似た雰囲気を感じることができるでしょう。

試用レポート -- rjni

登録データ

概要

rjni も rjb と同様に JNI を利用して Java のメソッドコールなどを実現しています。

作者からの声

作者の Mauricio Julio Fernandez Pradier 氏より 以下のコメントを頂きました。

Regarding rjni, it was originally just a straightforward mapping of the JNI API to Ruby, with some minimal wrapping for Java's primitives:

jobject -> RJNI::Object

jclass -> RJNI::jclass

jmethodID -> RJNI::MethodID

etc

After a while, I realized it would be easy to implement transparent access to Java objects/classes by using Java's introspection capabilities. I built it all on top of the RJNI layer, for quicker prototyping: it would be rewritten as C eventually. The higher-level layer featured:

  • Java object instantiation from Ruby
  • Java method dispatching from Ruby with overloading
  • metaclass objects representing Java classes
  • static methods becomed class singleton methods
  • some basic type conversions (String <=> java.lang.String, etc)
  • automatic accessors for fields:

 * readers: javaobject.field?

 * setters: javaobject.field = value

  • conversion of Java exceptions to Ruby
  • implementing Java interfaces in Ruby

...

I got to the point where I could implement Java interfaces in Ruby, which I wanted to use for GUI programming, but ruby's lack of reentrancy and some undesired interaction with the JVM got in the way. Eventually, the project was put to sleep.

I have looked at rjb and it seems very promising. As I said above, I had originally planned to rewrite rjni's higher-level API (which was the only interesting part after all) in C, and I have to say that rjb looks very much like what I had planned (only so much better, of course). I'm glad arton undertook the tiresome task of creating something like rjb :)

rjni もいくつか改善予定はあったようですが、結局開発は休止したままのようです。 rjb には、rjni で実装予定だった機能も入っているようです.

サンプル

まずは rjni でも "Hello, World!" を書いてみます。

 require 'rjni'
 require 'rjni/base'
 require 'rjni/reflect'

 RJNI::JVM.new

 System = RJNI::Reflect.new["java.lang.System"]
 System.out?.println("Hello World!")   # (c1)

rjni のコードは rjb とかなり似ています。 JVM を起動するメソッドは RJNI::JVM.new、 Java のクラスをインポートするメソッドは RJNI::Reflect#[] です。 RJNI::Reflect オブジェクトをリフレクターと呼びます。

(c1) に注目してください。rjni ではメソッドと public 変数アクセスを 区別するので、public 変数にアクセスする場合は '?' を最後につける必要が あります。

また rjb と同じように FreeMarker を利用する Ruby プログラムを 書いてみました。しかし現在の rjni では Java オブジェクトを 引数から渡す機能が実装されていないようで、HashMap を構築する ところでエラーになってしまいました。従って以下のプログラムは動作しません。

 require 'rjni'
 require 'rjni/base'
 require 'rjni/reflect'

 RJNI::JVM.new

 module Java
   Ref = RJNI::Reflect.new
   System = Ref["java.lang.System"]
   String = Ref["java.lang.String"]
   Number = Ref["java.lang.Number"]
   Boolean = Ref["java.lang.Boolean"]
   List = Ref["java.util.List"]
   Map = Ref["java.util.Map"]
   HashMap = Ref["java.util.HashMap"]
   File = Ref["java.io.File"]
   OutputStreamWriter = Ref["java.io.OutputStreamWriter"]
   IOException = Ref["java.io.IOException"]
 end
 module FreeMarker
   include Java
   Configuration = Ref["freemarker.template.Configuration"]
   Template = Ref["freemarker.template.Template"]
 end

 cfg = FreeMarker::Configuration.getDefaultConfiguration() # (d1)
 cfg.setDirectoryForTemplateLoading(Java::File.new("."))
 template = cfg.getTemplate("sample.ftl")

 root = Java::HashMap.new()
 root.put("user", "Takaaki Tateishi")
 latest = Java::HashMap.new()
 latest.put("url", "products/RLR.html")
 latest.put("name", "Ruby Library Report")
 root.put("latestProduct", latest)

 out = Java::OutputStreamWriter.new(Java::System.out?)
 template.process(root, out)

感想

rjni はまだ正式にリリースされていないためか、 実用できるほどの完成度には達していません。 しかし環境変数 CLASSPATH に関しては rjb よりもうまく扱えているようです。 他にもよい点があるかもしれません。

試用レポート -- RubyJDWP

登録データ

概要

先に紹介した通り、RubyJDWP は JDWP というプロトコルを扱うためのライブラリ です。また JDI に相当するライブラリも、まだ不完全ではありますが、 含まれています。今後の展開が楽しみなライブラリです。

作者からの声

作者の Chad Fowler 氏より以下のコメントを頂きました。

RubyJDWP is our attempt to extend the simplicity and flexibility of Ruby as deeply into our professional lives as possible. As with many Rubyists before and after us, Rich and I both came to Ruby from backgrounds in other languages. We both specifically had a lot of exposure to Java, and have found ourselves continuing to work with Java as part of our jobs. Also a pattern with Rubyists, we found that Ruby spoiled us. Sure, we still know Java, but after having programmed in Ruby, going back to Java is a painful experience.

RubyJDWP enables us to do more than just debug Java virtual machines. For many applications, running the JVM in debug mode is acceptable even in production environments. Given the deep level to which you can control a running JVM using JDWP, it enables us to freeze running JVM instances, insert bytecode at runtime, and inspect the JVM's loaded classes and objects in a way that extends beyond what traditional Java programming allows--much more reminscent of what's possible with Ruby.

The most interesting implementation detail of RubyJDWP is how we chose to create the code for the wire protocol itself. Instead of hand coding the entire spec, we chose to generate the code. Rich converted Sun's JDWP specification document into a very readable Ruby metalanguage and then wrote a generator that implements the protocol directly from the specification. We know of at least one alternative JDWP implementation that took advantage of the Ruby generator to create a Scheme implementation. Now if only we could get Sun to document all of their protocols in Ruby... :)

Chad Fowler 氏は JDWP を単なるデバッグ用のプロトコルではなく、 JVM の停止・実行時におけるバイトコードレベルでの編集・ クラスやオブジェクトの内部表現の調査など、 通常行われている Java プログラミング以上の機能を実現するものと捉えているようです。

実装上の自慢は、JDWP の仕様を Ruby を利用したメタ言語で記述し、 JDWP のプロトコル実装部分を自動化したところのようです。 [5] *4

サンプル

JDWP は別プロセスから JVM にアクセスするプロトコルなので、 まずアクセス対象にする Java プロセスを起動しておく必要があります。 この点は RubyJDWP でも同様です。 そこで、アクセス対象として、 以下の 2 つのプログラム Main.java と Person.java を動かしておくことにします。

 // Main.java
 import java.lang.*;
 public class Main {
   public static void main(String[] args) {
     Person man = new Person();
     for( int i = 0; i < 10; i++ ){
       System.out.println("My name is " + man.getName() + ".");
       System.out.println("I am " + man.getAge() + " years old.");
       try{
         Thread.sleep(1000);
       } catch (InterruptedException e){
         e.printStackTrace();
       }
     }
   }
 }

 // Person.java
 public class Person {
   private String name;
   private int    age;
   public Person(){
     setName("<noname>");
     setAge(0);
   }
   public int getAge() {
     return age;
   }
   public String getName() {
     return name;
   }
   public void setAge(int i) {
     age = i;
   }
   public void setName(String string) {
     name = string;
   }
 }

見てわかる通り、main() では Person オブジェクトを表示し 1 秒 sleep、を 10 回繰り返します。 つまりこのプログラムは最短でも 10 秒以上動作しているはずです。 このようにゆっくり処理することで Ruby から JVM プロセスにアクセスする時間を稼ぎます。

この 2 つのソースコードをコンパイルし、クラスファイルを作成します。 通常は「java Main」として実行しますが、JDWP によるアクセスを許可する場合は 次のように起動しておく必要があります。

 java -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8081,server=y,suspend=n Main

ここで address=8081 は JDWP での接続を許可するポート番号です。 このようにして起動した JVM に Ruby からアクセするためには 次のような Ruby スクリプトを使います。

 require 'jdi'
 jvm = JDI::JavaVirtualMachine.connect_via_tcpip('localhost', 8081)

connect_via_tcpip には JDWP でアクセスするホストとポート番号を指定します。 今回は単一ホスト上で JVM プロセスと Ruby プロセスを実行するので ホストは localhost、ポート番号はさきほど指定した 8081 を使います。

接続が済んだら JVM にアクセスできるようになります。 試しに接続先の JVM で動作しているスレッドの一覧を表示してみましょう。

 jvm.each_thread{|thread|
   puts thread.name
 }

さきほど起動した JVM プロセスがちゃんと動作していれば スレッドの一覧が表示されるはずです。 もしすでに実行が終了していたら、再度 JVM を起動してから試してください。

もう少し他のこともやってみましょう。 以下のコードは JVM 上で起こるメソッドコールとスレッドの停止を監視する Ruby プログラムです。JDWP では JVM 上で起こる様々な動作をイベントとして 監視できます。監視したいイベントを add_event_listner メソッドで 登録しておくと、そのイベントが起きたときにブロックを実行してくれます。

 req = JDWP::Packets::EventRequest::Set.new(
   JDWP::EventKind::METHOD_ENTRY,
   JDWP::SuspendPolicy::NONE,
   [])
 jvm.session.add_event_listener(req){|suspend_plicy, event_kind, event|
   p event
 }

 req = JDWP::Packets::EventRequest::Set.new(
   JDWP::EventKind::THREAD_END,
   JDWP::SuspendPolicy::NONE,
   [])
 jvm.session.add_event_listener(req){|suspend_plicy, event_kind, event|
   p event
   exit
 }

 sleep   # イベントが起こるまで停止する

感想

現在の RubyJDWP では、JDI に相当する機能は一部しか提供されていないため、 実際に利用できるのは、本来の用途である Java プログラムの監視やデバッグ などに限定されるでしょう。もちろん、このライブラリはまだ正式にリリース されていないので、これは仕方のないことです。開発が進み、JDI などの 高レベルなライブラリの整備が整ってくれば、rjb と同様に、 Java のメソッドを呼ぶなどの高級な作業も行えるようになるでしょう。 また、実行時にクラスファイルを入れ替えたり、 メソッドコールをフックすることもできますから、 柔軟に Java のクラスファイルを利用することができるようになるはずです。

まとめ

今回の RLR 第 2 回では Ruby から Java のリソースへアクセスするための ライブラリとして、rjb, rjni, RubyJDWP の 3 つを紹介しました。

rjb と rjni は JNI を用いて JVM にアクセスし、 メソッドコールなどを行うことを目的としている点は同じです。 rjb は、FreeMarker のサンプルを見ての通り、ほとんど Java と同等のコード で Ruby から JVM の機能を利用することができました。 一方、rjni はまだ実用レベルには至っておらず 今後様々な点を改善する必要があるように感じました。

RubyJDWP は、より使いやすくするためにも、 Java の JDI 相当の高レベルなライブラリが必要です。 そのようなライブラリも部分的には実装されていますが、 まだ用途が限られてしまっています。 今後実装が進めば、rjb と同じように、Ruby スクリプトから Java の機能を自在に利用することができるようになると思います。

参考リソース

[1] JDWP は、Java において、リモートデバッグを行うためのプロトコル仕様です。

[2] JPDA は、Java 用のデバッガのためのアーキテクチャです。

[3] FreeMarker は、Java 用の HTML/XML テンプレートエンジンです。

[4] FreeMarker Manual における Designer's Guide と Programmer's Guide それぞれの章の Quick Start の例 を参考に本レポートのサンプルを作成しました。

[5] http://www.zenspider.com/dl/rubyconf2003/RubyAndJDWP.pdf は、RubyJDWP の Ruby Conference 2003 における発表資料です。

著者について

著者 (立石) は、ソフトウェアに関する研究開発職に従事しています。

Last modified:2005/10/02 22:53:47
Keyword(s):
References:[Rubyist Magazine 0003 号] [各号目次] [pre-Checking0003] [pre-RLRPlan]

*1 com.sun.jdi.ObjectReference における、invokeMethod を利用します。

*2 arton 氏ご自身のページに、コメントに対する補足説明があります。[11/25 追記]

*3 Rjb::load メソッドの第 1 引数に JNI のバージョン番号を渡せるのですが、現状、1 引いて渡さなければなりません。例えば、バージョン 1.3 を指定する場合、0.3 を渡す必要があります。デフォルト値で使用する分には問題ありません。

*4 Rich Kilmer さんが中心となり実装したようです。該当部分は、RubyJDWP ソースツリーの lib/jdwp/spec/ 以下になります。