JRubyからTruffleRubyへの移行

gemおよびアプリケーションでTruffleRubyを試す場合は、TruffleRubyチームに連絡することをお薦めします。

デプロイメント

JRubyから移行する場合、TruffleRubyを使用する最も簡単な方法は、TruffleRuby JVMスタンドアロンをインストールすることです。

TruffleRubyのJava相互運用性機能が不要な場合は、TruffleRubyネイティブ・スタンドアロンをインストールできます。

JavaからのRubyの使用

JRubyでは、JSR 223 (javax.scriptとも呼ばれる)、Beanスクリプト・フレームワーク(BSF)、JRuby Embed (Red Bridgeとも呼ばれる)、JRuby直接埋込みAPIなど、RubyをJavaに埋め込むための様々な方法がサポートされています。

TruffleRubyを埋め込む最善の方法は、ポリグロットAPIを使用することです。このAPIの特徴は、Rubyだけでなく、多くの言語をサポートするように設計されていることにあります。

また、TruffleRubyでは、レガシーJRubyコードを容易に実行できるように、JRubyと互換性があるJSR 223もサポートされています。使用方法については、このドキュメントを参照してください。

ポリグロットAPIを使用するには、JVMスタンドアロンを使用するか、org.graalvm.polyglot:polyglot Mavenパッケージに依存する必要があります。

Javaを含む他の言語からRubyを使用する方法の詳細は、ポリグロットのドキュメントを参照してください。このドキュメントでは、JRubyとの比較のみを示します。

コンテキストの作成

JSR 223を使用するJRubyでは、次のように記述していました:

ScriptEngineManager m = new ScriptEngineManager();
ScriptEngine scriptEngine = m.getEngineByName("ruby");

BSFを使用する場合は、次のように記述していました:

BSFManager.registerScriptingEngine("jruby", "org.jruby.embed.bsf.JRubyEngine", null);
BSFManager bsfManager = new BSFManager();

JRuby Embedを使用する場合は、次のように記述していました:

ScriptingContainer container = new ScriptingContainer();

直接埋込みAPIを使用する場合は、次のように記述していました:

Ruby ruby = Ruby.newInstance(new RubyInstanceConfig());

TruffleRubyでは、次のように記述します:

Context polyglot = Context.newBuilder().allowAllAccess(true).build();

allowAllAccess(true)メソッドにより、Rubyが完全に機能するために必要となる緩やかなアクセス権限が許可されます。GraalVMでは、ネイティブ・ファイル・アクセスなど、安全でない可能性がある多くの権限がデフォルトで禁止されていますが、通常のRubyインストールではこれらが使用されるため、それらを有効にします。それらの権限を付与しないことを決定することもできますが、これにより、Rubyの機能の一部が制限されます。

// No privileges granted, restricts functionality
Context polyglot = Context.newBuilder().build();

通常は、tryブロック内でコンテキストを作成して、それが適切に破棄されるようにします:

try (Context polyglot = Context.newBuilder().allowAllAccess(true).build()) {
}

Contextの詳細は、Context APIを参照してください。

オプションの設定

TruffleRubyのオプションは、システム・プロパティまたは.option(name, value)ビルダー・メソッドを使用して設定できます。

コードの評価

次に示すJRubyの例のいずれかを記述したJRubyでは、使用可能なオプションが指定されます:

scriptEngine.eval("puts 'hello'");
bsfManager.exec("jruby", "<script>", 1, 0, "puts 'hello'");
container.runScriptlet("puts 'hello'");
ruby.evalScriptlet("puts 'hello'");

TruffleRubyでは、次のように記述します:

polyglot.eval("ruby", "puts 'hello'");

evalでは複数の言語がサポートされているため、言語を毎回指定する必要があります。

パラメータを使用したコードの評価

JSR 223を使用するJRubyでは、バインディングと呼ばれるパラメータをスクリプトに渡すことができます:

Bindings bindings = scriptEngine.createBindings();
bindings.put("a", 14);
bindings.put("b", 2);
scriptEngine.eval("puts a + b", bindings);

TruffleRubyでは、evalメソッドはパラメータを使用しません。かわりに、パラメータを使用するプロシージャを返し、この値に対してexecuteをコールする必要があります:

polyglot.eval("ruby", "-> a, b { puts a + b }").execute(14, 2);

プリミティブ値

それぞれの埋込みAPIでは、異なる方法でプリミティブ値が処理されます。JSR 223、BSFおよびJRuby Embedでは、戻り型はObjectであり、longなどのプリミティブにキャストし、instanceofで確認できます。直接埋込みAPIでは、戻り値はルートIRubyObjectインタフェースであり、プリミティブをIntegerに変換し、そこからJava longに変換する必要があります:

(long) scriptEngine.eval("14 + 2");
(long) bsfManager.eval("jruby", "<script>", 1, 0, "14 + 2");
(long) container.runScriptlet("14 + 2");
ruby.evalScriptlet("14 + 2").convertToInteger().getLongValue();

TruffleRubyでは、戻り値は常に、カプセル化されたValueオブジェクトであり、オブジェクトで可能であればlongとしてこれにアクセスできます。fitsInLong()により、このことをテストできます:

polyglot.eval("ruby", "14 + 2").asLong();

メソッドのコール

evalから取得したオブジェクトまたは他のオブジェクトでメソッドをコールするには、JRuby埋込みAPIでは、メソッドを起動するようにコンテキストに指示する必要があり、直接埋込みの場合は、レシーバでメソッドをコールし、引数をJRuby型に自分でマーシャリングする必要があります。BSFには、メソッドをコールする方法がないようです:

((Invocable) scriptEngine).invokeMethod(scriptEngine.eval("Math"), "sin", 2);
container.callMethod(container.runScriptlet("Math"), "sin", 2);
ruby.evalScriptlet("Math").callMethod(ruby.getCurrentContext(), "sin", new IRubyObject[]{ruby.newFixnum(2)})

TruffleRubyでは、Valueクラスに、オブジェクトのRubyメソッドを返すgetMemberメソッドがあります。その後、executeをコールすることにより、それらのメソッドをコールできます。引数をマーシャリングする必要はありません:

polyglot.eval("ruby", "Math").getMember("sin").execute(2);

プリミティブでメソッドをコールするには、ラムダを使用します:

polyglot.eval("ruby", "-> x { x.succ }").execute(2).asInt();

ブロックの受渡し

ブロックはRuby固有の言語機能であるため、JSR 223やBSFなど、言語に依存しないAPIには出現しません。JRuby Embed APIおよび直接埋込みでは、BlockパラメータをcallMethodメソッドに渡すことはできますが、これを使用するためのBlockオブジェクトを作成する方法が明確ではありません。

TruffleRubyでは、コールを実行するRubyラムダを返し、渡したJavaラムダを実行するブロックを渡す必要があります:

polyglot.eval("ruby", "-> block { (1..3).each { |n| block.call n } }")
  .execute(polyglot.asValue((IntConsumer) n -> System.out.println(n)));

オブジェクトの作成

JRuby埋込みAPIでは、新しいオブジェクトの作成はサポートされていませんが、newメソッドを自分でコールすることはできます:

((Invocable) scriptEngine).invokeMethod(scriptEngine.eval("Time"), "new", 2021, 3, 18);
container.callMethod(container.runScriptlet("Time"), "new", 2021, 3, 18)
ruby.evalScriptlet("Time").callMethod(ruby.getCurrentContext(), "new",
  new IRubyObject[]{ruby.newFixnum(2021), ruby.newFixnum(3), ruby.newFixnum(8)})

TruffleRubyでは、newInstanceを使用してRuby classからオブジェクトを作成できます。canInstantiateを使用して、このことが可能かどうかを確認できます:

polyglot.eval("ruby", "Time").newInstance(2021, 3, 18);

文字列の処理

JRubyの埋込みAPIでは、toStringを使用してJava Stringに変換します。TruffleRubyではasStringを使用します(また、isStringを使用して確認します)。

配列へのアクセス

JRubyの配列はList<Object>を実装しているため、このインタフェースにキャストしてそれらにアクセスできます:

((List) scriptEngine.eval("[3, 4, 5]")).get(1);
((List) container.runScriptlet("[3, 4, 5]")).get(1);
((List) bsfManager.eval("jruby", "<script>", 1, 0, "[3, 4, 5]")).get(1);
((List) ruby.evalScriptlet("[3, 4, 5]")).get(1);

TruffleRubyでは、getArrayElementsetArrayElementおよびgetArraySizeを使用できます。また、as(List.class)を使用してList<Object>を取得することもできます:

polyglot.eval("ruby", "[3, 4, 5]").getArrayElement(1);
polyglot.eval("ruby", "[3, 4, 5]").as(List.class).get(1);

ハッシュへのアクセス

JRubyのハッシュはMap<Object, Object>を実装しているため、このインタフェースにキャストしてそれらにアクセスできます:

((Map) scriptEngine.eval("{'a' => 3, 'b' => 4, 'c' => 5}")).get("b");
((Map) scriptEngine.eval("{3 => 'a', 4 => 'b', 5 => 'c'}")).get(4);

TruffleRubyには、現在、ハッシュや、ディクショナリのようなデータ構造にアクセスするための統一された方法はありません。現時点では、ラムダ・アクセッサを使用することをお薦めします:

Value hash = polyglot.eval("ruby", "{'a' => 3, 'b' => 4, 'c' => 5}");
Value accessor = polyglot.eval("ruby", "-> hash, key { hash[key] }");
accessor.execute(hash, "b");

インタフェースの実装

Rubyオブジェクトを使用してJavaインタフェースを実装することをお薦めします(JRuby Wikiからコピーした例):

interface FluidForce {
  double getFluidForce(double a, double b, double depth);
}
class EthylAlcoholFluidForce
  def getFluidForce(x, y, depth)
    area = Math::PI * x * y
    49.4 * area * depth
  end
end

EthylAlcoholFluidForce.new
String RUBY_SOURCE = "class EthylAlcoholFluidForce\n  def getFluidForce...";

JSR 223では、getInterface(object, Interface.class)を使用できます。JRuby Embedでは、getInstance(object, Interface.class)を使用できます。直接埋込みでは、toJava(Interface.class)を使用できます。BSFでは、インタフェースの実装がサポートされていないようです:

FluidForce fluidForce = ((Invocable) scriptEngine).getInterface(scriptEngine.eval(RUBY_SOURCE), FluidForce.class);
FluidForce fluidForce = container.getInstance(container.runScriptlet(RUBY_SOURCE), FluidForce.class);
FluidForce fluidForce = ruby.evalScriptlet(RUBY_SOURCE).toJava(FluidForce.class);
fluidForce.getFluidForce(2.0, 3.0, 6.0);

TruffleRubyでは、as(Interface.class)を使用して、Rubyオブジェクトによって実装されたインタフェースを取得できます:

FluidForce fluidForce = polyglot.eval("ruby", RUBY_SOURCE).as(FluidForce.class);
fluidForce.getFluidForce(2.0, 3.0, 6.0);

JRubyでは、Rubyメソッドの名前を、Javaの規則を使用するgetFluidForceではなく、Rubyの規則を使用するget_fluid_forceにすることができます。TruffleRubyでは、現時点ではこのようなことはサポートされていません。

ラムダの実装

オラクル社が把握している範囲では、JSR 223、BSF、JRuby Embedおよび直接埋込みには、RubyラムダからJavaラムダを取得するための便利な方法はありません。

TruffleRubyでは、as(FunctionalInterface.class)を使用することにより、RubyラムダからJavaラムダ(実際には関数型インタフェースの実装)を取得できます:

BiFunction<Integer, Integer, Integer> adder = polyglot.eval("ruby", "-> a, b { a + b }").as(BiFunction.class);
adder.apply(14, 2).intValue();

1回解析して何回も実行

一部のJRuby埋込みAPIでは、スクリプトを1回コンパイルした後、複数回評価できます:

CompiledScript compiled = ((Compilable) scriptEngine).compile("puts 'hello'");
compiled.eval();

TruffleRubyでは、単に解析からラムダを返し、これを何回も実行できます。これは、他のRubyコードと同様に最適化の対象になります:

Value parsedOnce = polyglot.eval("ruby", "-> { run many times }");
parsedOnce.execute();

RubyからのJavaの使用

TruffleRubyには、任意のGraalVM言語と他のGraalVM言語の間で一貫して使用できる、Javaの相互運用性のための独自のスキームが用意されています。これは既存のJRubyとJavaの相互運用性とは互換性がないため、移行する必要があります。

ポリグロット・プログラミングの概要は、他の場所に記載されています。この項では、JRubyに関連して説明します。

次の例は、JRuby Wikiからのものです:

require 'java'

# With the 'require' above, you now can refer to things that are part of the
# standard Java platform via their full paths.
frame = javax.swing.JFrame.new("Window") # Creating a Java JFrame
label = javax.swing.JLabel.new("Hello")

# You can transparently call Java methods on Java objects, just as if they were defined in Ruby.
frame.add(label)  # Invoking the Java method 'add'.
frame.setDefaultCloseOperation(javax.swing.JFrame::EXIT_ON_CLOSE)
frame.pack
frame.setVisible(true)

TruffleRubyでは、かわりにこれを次のように記述します:

Java.import 'javax.swing.JFrame'
Java.import 'javax.swing.JLabel'

frame = JFrame.new("Window")
label = JLabel.new("Hello")

frame.add(label)
frame.setDefaultCloseOperation(JFrame[:EXIT_ON_CLOSE])
frame.pack
frame.setVisible(true)

Rubyメタプログラミングを使用してJavaパッケージ名をシミュレートするかわりに、クラスを明示的にインポートします。Java.importはJRubyのjava_importに似ており、ClassName = Java.type('package.ClassName')を実行します。

定数は、Rubyの表記を使用するのではなく、クラスのプロパティを読み取ることによって読み取られます。

javaの要求

TruffleRubyではrequire 'java'を使用しないでください。ただし、--jvmモードで実行する必要があります。これはネイティブ・スタンドアロンでは使用できません。

クラスの参照

JRubyでは、JavaクラスをJava::ComFoo::BarのようにJavaモジュールで参照できます。また、共通のTLDがある場合は、com.foo.Barとして参照することもできます。java_import com.foo.Barでは、Barがトップレベル定数として定義されます。

TruffleRubyにおいては、JavaクラスはJava.type('com.foo.Bar')を使用して参照し、その後、通常は、定数に割り当てます。または、Java.import 'com.foo.Bar'を使用して、Barを包含モジュールに定義することもできます。

ワイルドカード・パッケージのインポート

JRubyでは、include_package 'com.foo'を使用できます。これにより、そのパッケージ内のすべてのクラスを現在のスコープで定数として使用できるようになります。

TruffleRubyでは、クラスを明示的に参照します。

メソッドのコールとインスタンスの作成

JRubyとTruffleRubyの両方で、Rubyメソッドと同様にJavaメソッドをコールします。

JRubyでは、my_methodのようなメソッド名がmyMethodというJavaの規則に書き換えられ、getFoofooに、setFoofoo=に変換されます。TruffleRubyでは、こうした変換は行われません。

定数の参照

JRubyでは、Java定数はRuby定数MyClass::FOOとしてモデル化されます。TruffleRubyでは、読み取った表記を使用して、それらをプロパティMyClass[:FOO]として読み取ります。

JARファイルからのクラスの使用

JRubyでは、requireを使用してクラスおよびJARをクラスパスに追加できます。TruffleRubyでは、現時点では通常どおり-classpath JVMフラグを使用します。

Java固有のその他のメソッド

JRubyでは、Javaオブジェクトに対して次のメソッドが定義されています。かわりに、同等となるこれらのものを使用してください。

java_class - classを使用します。

java_kind_of? - is_a?を使用します

java_object - サポートされていません。

java_send - __send__を使用します。

java_method - サポートされていません。

java_alias - サポートされていません。

Java配列の作成

JRubyでは、Java::byte[1024].newを使用します。

TruffleRubyでは、Java.type('byte[]').new(1024)を使用します。

Javaインタフェースの実装

JRubyには、インタフェースを実装する方法がいくつかあります。たとえば、Swingボタンにアクション・リスナーを追加するには、次の3つのいずれかを実行できます:

class ClickAction
  include java.awt.event.ActionListener

  def actionPerformed(event)
   javax.swing.JOptionPane.showMessageDialog nil, 'hello'
  end
end

button.addActionListener ClickAction.new
button.addActionListener do |event|
  javax.swing.JOptionPane.showMessageDialog nil, 'hello'
end
button.addActionListener -> event {
  javax.swing.JOptionPane.showMessageDialog nil, 'hello'
}

TruffleRubyでは、常に最後のオプションを使用してインタフェースを生成します:

button.addActionListener -> event {
  JOptionPane.showMessageDialog nil, 'hello'
}

実行時におけるJavaクラスの生成

JRubyでは、become_java!を使用したRubyクラスからJava具象クラスへの変換がサポートされています。

TruffleRubyでは、このようなことはサポートされていません。JavaとRubyの間のインタフェースとして、適切なJavaインタフェースを使用することをお薦めします。

Javaクラスを再度開く

TruffleRubyでJavaクラスを再度開くことはできません。

Javaクラスのサブクラス化

TruffleRubyでJavaクラスをサブクラス化することはできません。かわりにコンポジションまたはインタフェースを使用してください。

Javaを使用したTruffleRubyの拡張

JRubyでは、Javaで記述された拡張機能がサポートされています。これらの拡張機能は、MRIのC拡張機能インタフェースの動作と同様に、単にJRubyの内部全体である非公式なインタフェースに対して記述されます。

TruffleRubyでは、現時点ではこのような種類のJava拡張機能の記述はサポートされていません。前述のようにJavaの相互運用性を使用することをお薦めします。

ツール

スタンドアロン・クラスおよびJAR

JRubyでは、jrubycを使用したRubyからのスタンドアロン・ソース・クラスおよびコンパイル済JARへのコンパイルがサポートされています。

TruffleRubyでは、JavaへのRubyコードのコンパイルはサポートされていません。ポリグロットAPIをJavaからRubyへのエントリ・ポイントとして使用することをお薦めします。

Warbler

JRubyでは、エンタープライズJava WebサーバーにロードするためのWARファイルのビルドがサポートされています。

TruffleRubyでは、現時点ではこのようなことはサポートされていません。

VisualVM

VisualVMは、TruffleRubyでもJRubyと同様に機能します。

また、VisualVMは、ヒープ・ダンプ・ツールを使用するときに、JavaオブジェクトではなくRubyオブジェクトを認識します。