Experimental feature in GraalVM

从 JRuby 迁移到 TruffleRuby

在您的 Gem 和应用程序中尝试 TruffleRuby 时,我们鼓励您与 TruffleRuby 团队联系以获取帮助。

部署 #

如果您正在从 JRuby 迁移,使用 TruffleRuby 最简单的方法可能是安装TruffleRuby JVM 独立版

如果您不需要 TruffleRuby 的 Java 互操作功能,那么您可以安装TruffleRuby 原生独立版

从 Java 使用 Ruby #

JRuby 支持多种将 Ruby 嵌入 Java 的方式,包括 JSR 223(也称为 javax.script)、Bean Scripting Framework (BSF)、JRuby Embed(也称为 Red Bridge)和 JRuby 直接嵌入 API。

嵌入 TruffleRuby 的最佳方式是使用多语言 API (Polyglot API)。此 API 不同,因为它旨在支持多种语言,而不仅仅是 Ruby。

TruffleRuby 还支持与 JRuby 兼容的 JSR 223,以便更容易运行旧版 JRuby 代码。有关如何使用它,请参阅此文档

您需要使用 JVM 独立版或依赖 org.graalvm.polyglot:polyglot Maven 包才能使用多语言 API。

有关如何从包括 Java 在内的其他语言使用 Ruby 的更多信息,请参阅多语言文档;本文档仅显示与 JRuby 的比较。

创建上下文 #

在 JRuby 中使用 JSR 223,您会这样写

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

设置选项 #

您可以通过系统属性或通过 .option(name, value) 构建器方法设置 TruffleRuby 选项

评估代码 #

在 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 支持多种语言,因此您每次都需要指定语言。

带参数评估代码 #

在 JRuby 中使用 JSR 223,您可以将参数(称为绑定)传递到脚本中

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

在 TruffleRuby 中,eval 方法不接受参数。相反,您应该返回一个接受参数的 proc,然后在此值上调用 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 类有一个 getMember 方法,用于返回对象上的 Ruby 方法,然后您可以通过调用 execute 来调用这些方法。您无需编组参数

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

要在原始类型上调用方法,请使用 lambda

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

传递块 #

块是 Ruby 特有的语言特性,因此它们不会出现在与语言无关的 API(如 JSR 223 和 BSF)中。JRuby Embed API 和直接嵌入确实允许将 Block 参数传递给 callMethod 方法,但尚不清楚如何创建 Block 对象来使用它。

在 TruffleRuby 中,您应该返回一个执行调用的 Ruby lambda,并传递一个执行您传入的 Java lambda 的块

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 中,您可以使用 getArrayElementsetArrayElementgetArraySize,或者您可以使用 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 中,目前没有统一的方式访问哈希或字典式数据结构。目前我们建议使用 lambda 访问器

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 方法的名称为 get_fluid_force(使用 Ruby 约定),而不是 getFluidForce(使用 Java 约定)。TruffleRuby 目前不支持此功能。

实现 Lambda #

据我们所知,JSR 223、BSF、JRuby Embed 和直接嵌入都没有方便的方法从 Ruby lambda 获取 Java lambda。

在 TruffleRuby 中,您可以通过使用 as(FunctionalInterface.class) 从 Ruby lambda 获取 Java lambda(实际上是函数式接口的实现)

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

一次解析多次运行 #

一些 JRuby 嵌入 API 允许脚本编译一次然后多次执行

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

在 TruffleRuby 中,您可以简单地从解析中返回一个 lambda,并多次执行它。它将像任何其他 Ruby 代码一样进行优化

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

从 Ruby 使用 Java #

TruffleRuby 提供自己的 Java 互操作方案,该方案适用于从任何 GraalVM 语言到任何其他 GraalVM 语言的使用。这与现有的 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 World")

# 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.WindowConstants::EXIT_ON_CLOSE)
frame.pack
frame.setVisible(true)
sleep

在 TruffleRuby 中,我们将这样写

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

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

frame.add(label)
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.pack
frame.setVisible(true)
sleep

我们明确导入类,而不是使用 Ruby 元编程来模拟 Java 包名。Java.import 类似于 JRuby 的 java_import,并执行 ClassName = Java.type('package.ClassName')

常量通过读取类的属性而不是使用 Ruby 符号来读取。

要求 Java #

请勿在 TruffleRuby 中 require 'java'。但是,您确实需要在 --jvm 模式下运行。这在原生独立版中不可用。

引用类 #

在 JRuby 中,Java 类可以在 Java 模块中引用,例如 Java::ComFoo::Bar,或者如果它们具有共同的 TLD,则可以引用为 com.foo.Barjava_import com.foo.BarBar 定义为顶级常量。

在 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)重写为 Java 约定中的 myMethod,并将 getFoo 转换为 foo,将 setFoo 转换为 foo=。TruffleRuby 不执行这些转换。

调用重载方法 #

当存在多个重载时,需要明确选择。例如,java.util.concurrent.ExecutorService 既有 submit(Runnable) 也有 submit(Callable<T> task)。在不指定哪个重载的情况下调用 submit 会显示可能的重载

$ ruby -e 'Java.type("java.util.concurrent.Executors").newFixedThreadPool(1).submit {}'
-e:1:in `main': Multiple applicable overloads found for method name submit (candidates: [
  Method[public java.util.concurrent.Future java.util.concurrent.AbstractExecutorService.submit(java.util.concurrent.Callable)],
  Method[public java.util.concurrent.Future java.util.concurrent.AbstractExecutorService.submit(java.lang.Runnable)]],
  arguments: [RubyProc@4893b344 (RubyProc)]) (TypeError)

您可以使用以下方式选择特定的重载

executor = Java.type("java.util.concurrent.Executors").newFixedThreadPool(1)
executor['submit(java.lang.Runnable)'].call(-> { 1 })
# or
executor.send("submit(java.lang.Runnable)") { 1 }

引用常量 #

在 JRuby 中,Java 常量被建模为 Ruby 常量,例如 MyClass::FOO。在 TruffleRuby 中,您使用读取符号将其作为属性读取,例如 MyClass.FOOMyClass[: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 按钮添加动作监听器,我们可以执行以下三种操作中的任何一种

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 接口作为 Java 和 Ruby 之间的接口。

重新打开 Java 类 #

Java 类不能在 TruffleRuby 中重新打开。

子类化 Java 类 #

Java 类不能在 TruffleRuby 中进行子类化。请改用组合或接口。

使用 Java 扩展 TruffleRuby #

JRuby 支持用 Java 编写的扩展。这些扩展是针对一个非正式接口编写的,该接口就是 JRuby 的整个内部,类似于 MRI C 扩展接口的工作方式。

TruffleRuby 目前不支持编写此类 Java 扩展。我们建议如上所述使用 Java 互操作。

工具 #

独立类和 JAR #

JRuby 支持使用 jrubyc 将 Ruby 编译为独立的源类和已编译的 JAR。

TruffleRuby 不支持将 Ruby 代码编译为 Java。我们建议使用 Polyglot API 作为从 Java 到 Ruby 的入口点。

Warbler #

JRuby 支持构建 WAR 文件以加载到企业级 Java Web 服务器中。

TruffleRuby 目前不支持此功能。

VisualVM #

VisualVM 对 TruffleRuby 的工作方式与对 JRuby 相同。

此外,当您使用堆转储工具时,VisualVM 理解 Ruby 对象,而不是 Java 对象。

联系我们