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では、getArrayElement
、setArrayElement
および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の規則に書き換えられ、getFoo
がfoo
に、setFoo
がfoo=
に変換されます。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オブジェクトを認識します。