Migration from JRuby to TruffleRuby

When trying TruffleRuby on your gems and applications, you are encouraged to get in touch with the TruffleRuby team for help.

Deployment #

If you are migrating from JRuby probably the easiest way to use TruffleRuby is via GraalVM, which gives you a JVM, JavaScript, Ruby and other languages in one package.

If you do not need the Java interoperability capabilities of TruffleRuby, then you could also install via your Ruby manager/installer as any other implementation of Ruby.

You can also use the standalone distribution as a simple tarball. The standalone distribution does not allow for Java interoperability.

Using Ruby from Java #

JRuby supports many different ways to embed Ruby in Java, including JSR 223 (also know as javax.script), the Bean Scripting Framework (BSF), JRuby Embed (also known as Red Bridge), and the JRuby direct embedding API.

Thes best way to embed TruffleRuby is to use the Polyglot API, which is part of GraalVM. The API is different because it is designed to support many languages, not just Ruby.

TruffleRuby also supports JSR 223, compatible with JRuby, to make it easier to run legacy JRuby code.

You will need to use the GraalVM to use both of these APIs.

See the polyglot documentation for more information about how to use Ruby from other languages including Java - this document only shows the comparison to JRuby.

Creating a Context #

In JRuby with JSR 223 where you would have written:

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

Or with BSF where you would have written:

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

Or with JRuby Embed where you would have written:

ScriptingContainer container = new ScriptingContainer();

Or with the direct embedding API where you would have written:

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

In TruffleRuby you now write:

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

allowAllAccess(true) allows the permissive access privileges that Ruby needs for full functionality. GraalVM by default disallows many privileges which may not be safe, such as native file access, but a normal Ruby installation uses these so we enable them. You can decide to not grant those privileges, but this will restrict some of Ruby’s functionality.

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

You would normally create your context inside a try block to ensure it is properly disposed.

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

See the Context API for detailed documentation about Context.

Setting Options #

You can set TruffleRuby options via system properties, or via the .option(name, value) builder method.

Evaluating Code #

In JRuby where you would have written one of these JRuby examples, the options available are given:

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

In TruffleRuby you now write this:

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

Note that eval supports multiple languages, so you need to specify the language each time.

Evaluating Code with Parameters #

In JRuby with JSR 223 you can pass parameters, called bindings, into a script.

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

In TruffleRuby the eval method does not take parameters. Instead you should return a proc which does take parameters, and then call execute on this value.

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

Primitive Values #

The different embedding APIs handle primitive values in different ways. In JSR 223, BSF, and JRuby Embed, the return type is Object and can be cast to a primitive like long and checked with instanceof. In the direct embedding API the return is the root IRubyObject interface and you will need to convert a primitive to an Integer, and from there to a 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();

In TruffleRuby the return value is always an encapsulated Value object, which can be accessed as a long if that is possible for the object. fitsInLong() can test this.

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

Calling Methods #

To call a method on an object you get from an eval, or any other object, in the JRuby embedding APIs you either need to ask the context to invoke the method, or in the case of direct embedding you need to call a method on the receiver and marshal the arguments into JRuby types yourself. The BSF does not appear to have a way to call methods.

((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)})

In TruffleRuby the Value class has a getMember method to return Ruby methods on an object, which you can then call by calling execute. You don’t need to marshal the arguments.

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

To call methods on a primitive, use a lambda:

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

Passing Blocks #

Blocks are a Ruby-specific language feature, so they don’t appear in language agnostic APIs like JSR 223 and BSF. The JRuby Embed API and direct embedding do allow passing a Block parameter to the callMethod method, but it’s not clear how you would create a Block object to use this.

In TruffleRuby you should return a Ruby lambda that performs your call, passing a block that executes a Java lambda that you pass in.

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

Creating Objects #

JRuby embedding APIs don’t have support for creating new objects, but you can just call the new method yourself.

((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)})

In TruffleRuby you can create an object from a Ruby class using newInstance. You can use canInstantiate to see if this will be possible.

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

Handling Strings #

In JRuby’s embedding APIs you would use toString to convert to a Java String. Use asString in TruffleRuby (and isString to check).

Accessing Arrays #

JRuby’s arrays implement List<Object>, so you can cast to this interface to access them.

((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);

In TruffleRuby you can use getArrayElement, setArrayElement, and getArraySize, or you can use as(List.class) to get a List<Object>.

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

Accessing Hashes #

JRuby’s hashes implement Map<Object, Object>, so you can cast to this interface to access them.

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

In TruffleRuby there is currently no uniform way to access hashes or dictionary-like data structures. At the moment we recommend using a lambda accessor.

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

Implementing Interfaces #

You may want to implement a Java interface using a Ruby object (example copied from the 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...";

In JSR 223 you can use getInterface(object, Interface.class), in JRuby Embed you can use getInstance(object, Interface.class), and in direct embedding you can use toJava(Interface.class). BSF does not appear to support implementing interfaces.

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);

In TruffleRuby you can get an interface implemented by your Ruby object by using as(Interface.class).

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

JRuby allows the name of the Ruby method to be get_fluid_force, using Ruby conventions, instead of getFluidForce, using Java conventions. TruffleRuby does not support this at the moment.

Implementing Lambdas #

As far as we know, JSR 223, BSF, JRuby Embed and direct embedding do not have a convenient way to get a Java lambda from a Ruby lambda.

In TruffleRuby you can get a Java lambda (really an implementation of a functional interface) from a Ruby lambda by using as(FunctionalInterface.class).

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

Parse Once Run Many Times #

Some of the JRuby embedding APIs allow a script to be compiled once and then eval’d several times.

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

In TruffleRuby you can simply return a lambda from parsing and execute this many times. It will be subject to optimisation like any other Ruby code.

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

Using Java from Ruby #

TruffleRuby provides its own scheme for Java interoperability that is consistent for use from any GraalVM language, to any other GraalVM language. This isn’t compatible with existing JRuby Java interoperability, so you will need to migrate.

Polyglot programming in general is documented elsewhere - this section describes it relative to JRuby.

This example is from the 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)

In TruffleRuby we would write that this way instead:

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)

Instead of using Ruby metaprogramming to simulate a Java package name, we explicitly import classes. Java.import is similar to JRuby’s java_import, and does ::ClassName = Java.type('package.ClassName').

Constants are read by reading properties of the class rather than using Ruby notation.

Require Java #

Do not require 'java' in TruffleRuby. However, you do need to run in --jvm mode. This is only available in GraalVM - not in the standalone distribution installed by Ruby version managers and installers.

Referring to Classes #

In JRuby Java classes can either be referenced in the Java module, such as Java::ComFoo::Bar, or if they have a common TLD they can be referenced as com.foo.Bar. java_import com.foo.Bar will define Bar as a top-level constant.

In TruffleRuby Java classes are referred to using either Java.type('com.foo.Bar'), which you would then normally assign to a constant, or you can use Java.import 'com.foo.Bar' to have Bar defined as a top-level constant.

Wildcard Package Imports #

JRuby lets you include_package 'com.foo' which will make all classes in that package available as constants in the current scope.

In TruffleRuby you refer to classes explicitly.

Calling Methods and Creating Instances #

In both JRuby and TruffleRuby you call Java methods as you would a Ruby method.

JRuby will rewrite method names such as my_method to the Java convention of myMethod, and converts getFoo to foo, and setFoo to foo=. TruffleRuby does not do these conversions.

Referring to Constants #

In JRuby, Java constants are modelled as Ruby constants, MyClass::FOO. In TruffleRuby you use the read notation to read them as a property, MyClass[:FOO].

Using Classes from JAR files #

In JRuby you can add classes and jars to the classpath using require. In TruffleRuby at the moment you use the -classpath JVM flag as normal.

Additional Java-Specific Methods #

JRuby defines these methods on Java objects, use these equivalents instead.

java_class - use class.

java_kind_of? - use is_a?

java_object - not supported.

java_send - use __send__.

java_method - not supported.

java_alias - not supported.

Creating Java Arrays #

In JRuby you use Java::byte[1024].new.

In TruffleRuby you would use Java.type('byte[]').new(1024)

Implementing Java Interfaces #

JRuby has several ways to implement an interface. For example to add an action listener to a Swing button we could do any of these three things.

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'
}

In TruffleRuby we’d always use the last option to generate an interface.

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

Generating Java Classes at Runtime #

JRuby supports converting a Ruby class to a concrete Java class using become_java!.

TruffleRuby does not support this. We recommend using a proper Java interface as your interface between Java and Ruby.

Reopening Java Classes #

Java classes cannot be re-opened in TruffleRuby.

Subclassing Java Classes #

Java classes cannot be subclassed in TruffleRuby. Use composition or interfaces instead.

Extending TruffleRuby Using Java #

JRuby supports extensions written in Java. These extensions are written against an informal interface that is simply the entire internals of JRuby, similar to how the MRI C extension interface works.

TruffleRuby does not support writing these kind of Java extensions at the moment. We recommend using Java interop as described above.

Tooling #

Standalone Classes and JARs #

JRuby supports compiling to standalone source classes and compiled jars from Ruby using jrubyc.

TruffleRuby does not support compiling Ruby code to Java. We recommend using the Polyglot API as your entry point from Java to Ruby.

Warbler #

JRuby supports building war files for loading into enterprise Java web servers.

TruffleRuby does not support this at the moment.

VisualVM #

VisualVM works for TruffleRuby as for JRuby.

Additionally, the VisualVM included in GraalVM understands Ruby objects, rather than Java objects, when you use the heap dump tool.