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型の引数を指定するとブロックが受け取れる
  • getRuntimeRubyインスタンスが取り出せるので、それを使ってあれこれする
    • 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秒前後の時間がかかる