JRubyをJavaから使う方法を調べた

2015-03-08
blog

JRubyをコマンドライン上ではなく、Javaのプログラム中から呼び出して使う方法を調べた。

  • 使用したJRubyのバージョン: 9.1.2.0

JRubyを使うJavaコードのコンパイル、実行

JRubyを使ったJavaコードをコンパイルするには、-cp(クラスパス)オプションで.jarファイルを指定してコンパイルする:

javac -cp .:jruby.jar Foo.java

実行にも同じ.jarファイルを指定して

java -cp .:jruby.jar Foo

などとする。

文字列をeval

Javaの文字列として持っているRubyコードを実行するには Ruby.evalScriptlet) が使える:

import org.jruby.Ruby;
import org.jruby.runtime.builtin.IRubyObject;

public class EvalString {
public static void main(String[] args) {
Ruby ruby = Ruby.newInstance();
IRubyObject result = ruby.evalScriptlet("(1 + 2) * 3");
System.out.println(result);
}
}

Rubyのコードをファイルから読み込んで実行

適当なRubyのコードが書かれたファイル

puts "foobar"

を読み込んで実行するには

import org.jruby.Ruby;
import org.jruby.embed.ScriptingContainer;

public class RunFile {
public static void main(String[] args) {
Ruby ruby = Ruby.newInstance();
ScriptingContainer container = new ScriptingContainer();
container.runScriptlet(org.jruby.embed.PathType.RELATIVE, "foo.rb");
}
}

JRubyからJavaの静的メソッドを呼び出す

import org.jruby.Ruby;

public class CallStaticMethod {
public static int staticMethod(int x) { // このメソッドをJRubyから呼び出す
return x * x;
}

public static void main(String[] args) {
Ruby ruby = Ruby.newInstance();
ruby.evalScriptlet("puts Java::CallStaticMethod.staticMethod(111)"); //=> 12321
}
}
  • Java::クラス名 でJavaのクラスにアクセス
  • なにもしなくても透過的に呼び出せるし、引数や戻り値も自動変換、超楽チン

JRubyからJavaのインスタンスを生成してメソッド呼び出し

適当なJavaのクラスがあったとして

public class Foo {
private int foo;

public Foo(int foo) {
this.foo = foo;
}

public int bar(int x) {
return foo + x;
}
}

それをRubyから生成しメソッドを呼び出す:

import org.jruby.Ruby;

public class CallInstanceMethod {
public static void main(String[] args) {
Ruby ruby = Ruby.newInstance();
ruby.evalScriptlet("foo = Java::Foo.new(1)\n" +
"puts foo.bar(2)"); //=> 3
}
}
  • 普通にRubyで定義したクラスと同様にnewして呼び出せる、すげぇ

JavaからJRubyのメソッドを呼び出す

import org.jruby.Ruby;
import org.jruby.embed.ScriptingContainer;

public class CallJRubyMethod {
public static void main(String[] args) {
Ruby ruby = Ruby.newInstance();

ScriptingContainer container = new ScriptingContainer();
container.runScriptlet("def add(x, y)\n" +
" x + y\n" +
"end");
Object result = container.callMethod(ruby.getCurrentContext(), "add", 123, 456);
System.out.println(result); //=> 579
}
}

JRubyからブロックを渡して、Javaから呼び出す

import org.jruby.Ruby;
import org.jruby.RubyFixnum;
import org.jruby.RubyProc;
import org.jruby.runtime.builtin.IRubyObject;

public class CallBlock {
public static void main(String[] args) {
Ruby ruby = Ruby.newInstance();
ruby.evalScriptlet("Java::CallBlock.times(5) {|i| p i}");
ruby.evalScriptlet("Java::CallBlock.times(5).each {|i| p i}");
}

public static void times(int n, RubyProc proc) {
Ruby runtime = proc.getRuntime();
for (int i = 0; i < n; ++i) {
IRubyObject[] args = new IRubyObject[] {new RubyFixnum(runtime, i)};
proc.call(runtime.getCurrentContext(), args);
}
}

public static int[] times(int n) {
int[] result = new int[n];
for (int i = 0; i < n; ++i)
result[i] = i;
return result;
}
}
  • RubyProc型の引数を指定するとブロックが受け取れる
  • getRuntime)でRubyインスタンスが取り出せるので、それを使ってあれこれする
    • getCurrentContext)のコンテキストをそのまま使っていいのかどうかは不明…
  • ブロックが省略されてもいい場合には、Javaのオーバーロードを使うのか?

Javaの配列やリストを受け渡す

複雑なオブジェクトの受け渡し:

import java.util.ArrayList;
import java.util.List;
import org.jruby.Ruby;
import org.jruby.embed.ScriptingContainer;

public class HandleArray {
public static void main(String[] args) {
Ruby ruby = Ruby.newInstance();

ScriptingContainer container = new ScriptingContainer();
container.runScriptlet("def handle_array(array)\n" +
" p array\n" +
" array.each do |elem|\n" +
" p elem\n" +
" end\n" +
"end");

Object[] array = {(Integer)123, "foobar"};
container.callMethod(ruby.getCurrentContext(), "handle_array", new Object[]{array});

List<Object> list = new ArrayList<Object>();
list.add(987);
list.add("hogefuga");
container.callMethod(ruby.getCurrentContext(), "handle_array", new Object[]{list});
}
}

// 結果:
// java.lang.Object[123, foobar]@6b68cb27
// 123
// "foobar"
// #<Java::JavaUtil::ArrayList:0x62e6a3ec>
// 987
// "hogefuga"
  • Rubyの配列のように扱える、ダックタイピングとはいえ便利すぐる…
  • 同様にJavaのMapもRubyのHashとして扱える

パース

import org.jruby.Ruby;
import org.jruby.ast.Node;

public class Parse {
public static void main(String[] args) {
Ruby ruby = Ruby.newInstance();
Node node = ruby.parseEval("1 + 2 * 3", "", null, 0);
System.out.println(node);
//(RootNode 0, (CallNode:+ 0, (FixnumNode 0), (ArrayNode 0, (CallNode:* 0, (FixnumNode 0), (ArrayNode 0, (FixnumNode 0)), null)), null))

try {
Node node2 = ruby.parseEval("1 + 2 *", "", null, 0);
System.out.println(node2);
} catch (Exception e) {
System.err.println("\nException:");
System.err.println(e);
}
//:1: warning: `*' after local variable or literal is interpreted as binary operator
//:1: warning: even though it seems like argument prefix
//
//Exception:
//org.jruby.exceptions.RaiseException: (SyntaxError) :1: syntax error, unexpected end-of-file
//1 + 2 *
}
}

コンパイル・実行

import org.jruby.Ruby;
import org.jruby.ast.Node;
import org.jruby.ast.executable.Script;
import org.jruby.runtime.builtin.IRubyObject;

public class CompileRun {
public static void main(String[] args) {
Ruby ruby = Ruby.newInstance();
Node node = ruby.parseEval("1 + 2 * 3", "", null, 0);

Script script = ruby.tryCompile(node);
System.out.println(script);

IRubyObject result = ruby.runScript(script);
System.out.println("result = " + result);
}
}

起動時間

  • JRubyの起動にはそこそこ時間がかかる
  • Ruby.newInstance()を呼び出すだけのコードでも、6秒前後の時間がかかる