嵌入语言

The GraalVM Polyglot API 允许您在 Java 主机应用程序中嵌入和运行来自客户语言的代码。

在本节中,您将学习如何创建在 GraalVM 上运行并直接调用客户语言的 Java 主机应用程序。您可以使用每个代码示例下方的选项卡来选择 JavaScript、R、Ruby 和 Python。

注意:针对 JDK 21 的 GraalVM 和 Polyglot API 23.1.0 版本对多语言嵌入的使用说明进行了修订。如果您仍在使用早于 23.1.0 版本的 Polyglot API,请确保显示正确的文档版本。有关此更改的更多信息,请参阅发行说明

依赖设置 #

自 Polyglot API 23.1.0 版本起,所有必要的构件都可以直接从 Maven Central 下载。与嵌入器相关的构件可在 Maven 依赖组 org.graalvm.polyglot 中找到。有关完整的可运行示例,请参阅 GitHub 上的多语言嵌入演示

以下是一个可以添加到您的项目中的 Maven 依赖设置示例

<dependency> 
	<groupId>org.graalvm.polyglot</groupId> 
	<artifactId>polyglot</artifactId> 
	<version>${graalvm.polyglot.version}</version>
</dependency>
<dependency> 
	<groupId>org.graalvm.polyglot</groupId>
	<!-- Select a language: js, ruby, python, java, llvm, wasm, languages-->
	<artifactId>js</artifactId> 
	<version>${graalvm.polyglot.version}</version>
	<type>pom</type>
</dependency>
<!-- Add additional languages if needed -->
<dependency> 
	<groupId>org.graalvm.polyglot</groupId> 
    <!-- Select a tool: profiler, inspect, coverage, dap, tools -->
	<artifactId>profiler</artifactId> 
	<version>${graalvm.polyglot.version}</version>
	<type>pom</type>
</dependency>

pom 类型是语言或工具依赖项的要求。

语言和工具依赖项使用 GraalVM 免费条款和条件 (GFTC) 许可证。若要使用社区许可版本,请为每个构件添加 -community 后缀(例如,js-community)。若要访问多语言隔离构件,请改用 -isolate 后缀(例如,js-isolate)。

构件 languagestools 包含所有可用语言和工具作为依赖项。此构件可能会在主要版本之间增加或减少。我们建议生产部署只选择所需的语言。

此外,在使用 Java 模块时,您的 module-info.java 文件应要求 org.graalvm.polyglot

module com.mycompany.app {
  requires org.graalvm.polyglot;
}

您的配置是否可以使用 Truffle 运行时优化运行取决于您使用的 GraalVM JDK。有关更多详细信息,请参阅运行时编译部分

我们建议尽可能使用模块和模块路径配置多语言嵌入。请注意,如果改为从类路径使用 org.graalvm.polyglot,将允许类路径上的所有库访问不安全的 API。如果应用程序尚未模块化,则可以混合使用类路径和模块路径。例如

$java -classpath=lib --module-path=lib/polyglot --add-modules=org.graalvm.polyglot ...

在此示例中,lib/polyglot 目录应包含所有多语言和语言 JAR 文件。要从类路径访问多语言类,您还必须指定 --add-modules=org.graalvm.polyglot JVM 选项。如果您使用GraalVM Native Image,类路径上的多语言模块将自动升级到模块路径。

虽然我们支持从多语言库创建单个 uber JAR 文件(例如,使用 Maven Assembly 插件),但我们不建议这样做。另请注意,使用 GraalVM Native Image 创建本机二进制文件时不支持 uber JAR 文件。

编译并运行多语言应用 #

GraalVM 可以运行使用 Truffle 语言实现框架实现的任何语言编写的多语言应用程序。这些语言此后被称为 客户语言

完成本节中的步骤,以创建在 GraalVM 上运行并演示编程语言互操作性的示例多语言应用程序。

  1. 使用 Maven 创建新的 Java 项目。

  2. 克隆 polyglot-embedding-demo 存储库。
     git clone https://github.com/graalvm/polyglot-embedding-demo.git
    
  3. 将示例代码插入到 Main 类中。

  4. 按照上一节的描述,更新 Maven pom.xml 依赖项配置以包含要运行的语言。

  5. 通过将 JAVA_HOME 环境变量的值设置为 GraalVM JDK 的位置来下载并安装 GraalVM

  6. 运行 mvn package exec:exec 以构建并执行示例代码。

您现在拥有一个多语言应用程序,它由一个 Java 主机应用程序和客户语言代码组成,在 GraalVM 上运行。您可以将此应用程序与其他代码示例一起使用,以演示 GraalVM Polyglot API 的更高级功能。

将客体语言函数定义为 Java 值 #

多语言应用程序允许您从一种编程语言中获取值,并将其与其他语言一起使用。

将本节中的代码示例与您的多语言应用程序一起使用,以展示 Polyglot API 如何将 JavaScript、Python 或 Ruby 函数作为 Java 值返回。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class function_js {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value function = context.eval("js", "x => x+1");
    assert function.canExecute();
    int x = function.execute(41).asInt();
    assert x == 42;
}
// END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class function_python {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value function = context.eval("python", "lambda x: x + 1");
    assert function.canExecute();
    int x = function.execute(41).asInt();
    assert x == 42;
}
 // END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class function_ruby {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value function = context.eval("ruby", "proc { |x| x + 1 }");
    assert function.canExecute();
    int x = function.execute(41).asInt();
    assert x == 42;
}
 // END-SNIPPET
    }
}

  

在此代码中

  • Value function 是一个引用函数的 Java 值。
  • eval 调用解析脚本并返回客户语言函数。
  • 第一个断言检查代码片段返回的值是否可以执行。
  • execute 调用以参数 41 执行函数。
  • asInt 调用将结果转换为 Java int
  • 第二个断言验证结果是否按预期递增了一。

直接从 Java 访问客体语言 #

多语言应用程序可以轻松访问大多数语言类型,并且不限于函数。主机语言(如 Java)可以直接访问嵌入在多语言应用程序中的客户语言值。

将本节中的代码示例与您的多语言应用程序一起使用,以展示 Polyglot API 如何访问对象、数字、字符串和数组。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class access_js_from_java {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value result = context.eval("js", 
                    "({ "                   +
                        "id   : 42, "       +
                        "text : '42', "     +
                        "arr  : [1,42,3] "  +
                    "})");
    assert result.hasMembers();

    int id = result.getMember("id").asInt();
    assert id == 42;

    String text = result.getMember("text").asString();
    assert text.equals("42");

    Value array = result.getMember("arr");
    assert array.hasArrayElements();
    assert array.getArraySize() == 3;
    assert array.getArrayElement(1).asInt() == 42;
}
// END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class access_python_from_java {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value result = context.eval("python", 
                    "type('obj', (object,), {" +
                        "'id'  : 42, "         +
                        "'text': '42', "       +
                        "'arr' : [1,42,3]"     +
                    "})()");
    assert result.hasMembers();
    
    int id = result.getMember("id").asInt();
    assert id == 42;
    
    String text = result.getMember("text").asString();
    assert text.equals("42");
    
    Value array = result.getMember("arr");
    assert array.hasArrayElements();
    assert array.getArraySize() == 3;
    assert array.getArrayElement(1).asInt() == 42;
}
// END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class access_ruby_from_java {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value result = context.eval("ruby", 
                    "o = Struct.new(:id, :text, :arr).new(" +
                        "42, "       +
                        "'42', "     +
                        "[1,42,3] "  +
                    ")");
    assert result.hasMembers();
    
    int id = result.getMember("id").asInt();
    assert id == 42;
    
    String text = result.getMember("text").asString();
    assert text.equals("42");
    
    Value array = result.getMember("arr");
    assert array.hasArrayElements();
    assert array.getArraySize() == 3;
    assert array.getArrayElement(1).asInt() == 42;
}
// END-SNIPPET
    }
}

  

在此代码中

  • Value result 是一个包含三个成员的对象:一个名为 id 的数字、一个名为 text 的字符串和一个名为 arr 的数组。
  • 第一个断言验证返回值可以包含成员,这表明该值是一个类对象结构。
  • id 变量通过从结果对象中读取名为 id 的成员来初始化。然后使用 asInt() 将结果转换为 Java int
  • 下一个断言验证结果值为 42
  • text 变量使用成员 text 的值进行初始化,该值也使用 asString() 转换为 Java String
  • 以下断言验证结果值等于 Java String "42"
  • 接下来读取持有数组的 arr 成员。
  • 数组对于 hasArrayElements 返回 true
  • 下一个断言验证数组的大小等于三。Polyglot API 支持大数组,因此数组长度为 long 类型。
  • 最后,我们验证索引 1 处的数组元素等于 42。多语言值数组索引始终是基于零的,即使对于索引从一开始的语言也是如此。

从客体语言访问 Java #

多语言应用程序提供客户语言和主机语言之间的双向访问。因此,您可以将 Java 对象传递给客户语言。

由于 Polyglot API 默认是安全的,因此在默认配置中访问受到限制。要允许客户语言访问 Java 对象的任何公共方法或字段,您必须在构建上下文时显式指定 allowAllAccess(true)。在此模式下,客户语言代码可以访问主机 Java 代码可访问的任何资源。

将本节中的代码示例与您的多语言应用程序一起使用,以展示客户语言如何访问 Java 原始值、对象、数组和函数式接口。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import java.util.concurrent.Callable;
import org.graalvm.polyglot.*;

public class access_java_from_js {

// BEGIN-SNIPPET
public static class MyClass {
    public int               id    = 42;
    public String            text  = "42";
    public int[]             arr   = new int[]{1, 42, 3};
    public Callable<Integer> ret42 = () -> 42;
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        context.getBindings("js").putMember("javaObj", new MyClass());
        boolean valid = context.eval("js",
               "    javaObj.id         == 42"          +
               " && javaObj.text       == '42'"        +
               " && javaObj.arr[1]     == 42"          +
               " && javaObj.ret42()    == 42")
           .asBoolean();
        assert valid == true;
    }
}
// END-SNIPPET
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import java.util.concurrent.Callable;
import org.graalvm.polyglot.*;

public class access_java_from_python {

// BEGIN-SNIPPET
public static class MyClass {
    public int               id    = 42;
    public String            text  = "42";
    public int[]             arr   = new int[]{1, 42, 3};
    public Callable<Integer> ret42 = () -> 42;
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        context.getPolyglotBindings().putMember("javaObj", new MyClass());
        boolean valid = context.eval("python",
               "import polyglot \n"                            +
               "javaObj =  polyglot.import_value('javaObj')\n" +
               "javaObj.id                   == 42"            +
               " and javaObj.text            == '42'"          +
               " and javaObj.arr[1]          == 42"            +
               " and javaObj.ret42() == 42")
           .asBoolean();
        assert valid == true;
    }
}
// END-SNIPPET
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import java.util.concurrent.Callable;
import org.graalvm.polyglot.*;

public class access_java_from_ruby {

// BEGIN-SNIPPET
public static class MyClass {
    public int               id    = 42;
    public String            text  = "42";
    public int[]             arr   = new int[]{1, 42, 3};
    public Callable<Integer> ret42 = () -> 42;
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        context.getPolyglotBindings().putMember("javaObj", new MyClass());
        boolean valid = context.eval("ruby",
               "javaObj = Polyglot.import('javaObj')\n" +
               "    javaObj[:id]         == 42"         +
               " && javaObj[:text]       == '42'"       +
               " && javaObj[:arr][1]     == 42"         +
               " && javaObj[:ret42].call == 42")
           .asBoolean();
        assert valid == true;
    }
}
// END-SNIPPET
}
 
  

在此代码中

  • Java 类 MyClass 有四个公共字段 idtextarrret42。这些字段分别用 42"42"new int[]{1, 42, 3} 和始终返回 int42 的 lambda () -> 42 进行初始化。
  • Java 类 MyClass 被实例化并以名称 javaObj 导出到多语言作用域中,这允许主机和客户语言交换符号。
  • 评估一个客户语言脚本,该脚本导入 javaObj 符号并将其分配给也名为 javaObj 的局部变量。为避免与变量冲突,多语言作用域中的每个值都必须在语言的最顶层作用域中显式导入和导出。
  • 接下来的两行通过将 Java 对象的内容与数字 42 和字符串 '42' 进行比较来验证。
  • 第三次验证从数组的第二个位置读取并将其与数字 42 进行比较。数组是使用基于 0 的索引还是基于 1 的索引访问取决于客户语言。无论语言如何,存储在 arr 字段中的 Java 数组始终使用转换后的基于 0 的索引进行访问。例如,在 JavaScript 和 Ruby 语言中,第二个数组元素位于索引 1。在所有语言示例中,Java 数组都使用相同的索引 1 进行读取。
  • 最后一行调用字段 ret42 中包含的 Java lambda,并将结果与数值 42 进行比较。
  • 客户语言脚本执行后,进行验证以确保脚本返回布尔值 true 作为结果。

从客体语言查找 Java 类型 #

除了将 Java 对象传递给客户语言之外,还可以允许在客户语言中查找 Java 类型。

将本节中的代码示例与您的多语言应用程序一起使用,以展示客户语言如何查找 Java 类型并实例化它们。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;

public class lookup_java_from_js {


public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.newBuilder()
                           .allowAllAccess(true)
                       .build()) {
    java.math.BigDecimal v = context.eval("js",
            "var BigDecimal = Java.type('java.math.BigDecimal');" +
            "BigDecimal.valueOf(10).pow(20)")
        .asHostObject();
    assert v.toString().equals("100000000000000000000");
}
// END-SNIPPET
}
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;

public class lookup_java_from_python {


public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.newBuilder()
                           .allowAllAccess(true)
                       .build()) {
    java.math.BigDecimal v = context.eval("python",
            "import java\n" +
            "BigDecimal = java.type('java.math.BigDecimal')\n" + 
            "BigDecimal.valueOf(10).pow(20)")
        .asHostObject();
    assert v.toString().equals("100000000000000000000");
}
// END-SNIPPET
}
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;

public class lookup_java_from_ruby {

public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.newBuilder()
                           .allowAllAccess(true)
                       .build()) {
    java.math.BigDecimal v = context.eval("ruby",
            "BigDecimal = Java.type('java.math.BigDecimal')\n" + 
            "BigDecimal.valueOf(10).pow(20)")
        .asHostObject();
    assert v.toString().equals("100000000000000000000");
}
// END-SNIPPET
}
}
 
  

在此代码中

  • 创建一个新上下文,并启用所有访问权限 (allowAllAccess(true))。
  • 评估一个客体语言脚本。
  • 脚本查找 Java 类型 java.math.BigDecimal 并将其存储在名为 BigDecimal 的变量中。
  • 调用静态方法 BigDecimal.valueOf(long) 以创建值为 10 的新 BigDecimal。除了查找 Java 静态方法之外,还可以直接实例化返回的 Java 类型,例如,在 JavaScript 中使用 new 关键字。
  • 新的十进制数用于调用 pow 实例方法,参数为 20,计算 10^20
  • 脚本的结果通过调用 asHostObject() 转换为主机对象。返回值自动转换为 BigDecimal 类型。
  • 断言结果十进制字符串等于 "100000000000000000000"

使用多语言代理计算数组 #

Polyglot API 包含多语言代理接口,允许您通过模仿客户语言类型(如对象、数组、本机对象或原始类型)来自定义 Java 互操作性。

将本节中的代码示例与您的多语言应用程序一起使用,以了解如何实现延迟计算其值的数组。

注意:Polyglot API 支持在 JVM 或 Native Image 上使用多语言代理。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;
import org.graalvm.polyglot.proxy.*;

public class proxy_js {

// BEGIN-SNIPPET
static class ComputedArray implements ProxyArray {
    public Object get(long index) {
        return index * 2;
    }
    public void set(long index, Value value) {
        throw new UnsupportedOperationException();
    }
    public long getSize() {
        return Long.MAX_VALUE;
    }
}

public static void main(String[] args) {
    try (Context context = Context.create()) {
        ComputedArray arr = new ComputedArray();
        context.getBindings("js").putMember("arr", arr);
        long result = context.eval("js",
                    "arr[1] + arr[1000000000]")
                .asLong();
        assert result == 2000000002L;
    }
}
// END-SNIPPET
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.proxy.ProxyArray;

public class proxy_python {

// BEGIN-SNIPPET
static class ComputedArray implements ProxyArray {
    public Object get(long index) {
        return index * 2;
    }
    public void set(long index, Value value) {
        throw new UnsupportedOperationException();
    }
    public long getSize() {
        return Long.MAX_VALUE;
    }
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        ComputedArray arr = new ComputedArray();
        context.getPolyglotBindings().putMember("arr", arr);
        long result = context.eval("python",
               "import polyglot\n" +
               "arr = polyglot.import_value('arr') \n" +
               "arr[1] + arr[1000000000]")
           .asLong();
        assert result == 2000000002L;
    }
}
// END-SNIPPET
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.proxy.ProxyArray;

public class proxy_ruby {

// BEGIN-SNIPPET
static class ComputedArray implements ProxyArray {
    public Object get(long index) {
        return index * 2;
    }
    public void set(long index, Value value) {
        throw new UnsupportedOperationException();
    }
    public long getSize() {
        return Long.MAX_VALUE;
    }
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        ComputedArray arr = new ComputedArray();
        context.getPolyglotBindings().putMember("arr", arr);
        long result = context.eval("ruby",
               "arr = Polyglot.import('arr') \n" +
               "arr[1] + arr[1000000000]")
           .asLong();
        assert result == 2000000002L;
    }
}
// END-SNIPPET
}

  

在此代码中

  • Java 类 ComputedArray 实现了代理接口 ProxyArray,以便客户语言将 Java 类的实例视为类数组。
  • ComputedArray 数组重写 get 方法并使用算术表达式计算值。
  • 数组代理不支持写入访问。因此,它在 set 的实现中抛出 UnsupportedOperationException
  • getSize 的实现为其长度返回 Long.MAX_VALUE
  • 主方法创建一个新的多语言执行上下文。
  • 然后使用名称 arr 导出 ComputedArray 类的新实例。
  • 客户语言脚本导入 arr 符号,该符号返回导出的代理。
  • 访问第二个元素和第 1000000000 个元素,求和,然后返回。请注意,来自基于 1 的语言的数组索引会转换为代理数组的基于 0 的索引。
  • 语言脚本的结果作为长整型值返回并经过验证。

有关多语言代理接口的更多信息,请参阅 Polyglot API JavaDoc

主机访问 #

Polyglot API 默认限制对某些关键功能(如文件 I/O)的访问。通过将 allowAllAccess 设置为 true 可以完全解除这些限制。

注意:访问限制目前仅支持 JavaScript。

控制主机函数访问 #

限制客户应用程序对主机的访问可能是可取的。例如,如果暴露了一个调用 System.exit 的 Java 方法,那么客户应用程序将能够退出主机进程。为了避免意外暴露方法,默认情况下不允许主机访问,并且每个公共方法或字段都需要使用 @HostAccess.Export 显式注解。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.PolyglotException;

public class explicit_access_java_from_js {

static
// BEGIN-SNIPPET
public class Employee {
    private final String name;
    Employee(String name) {this.name = name;}

    @HostAccess.Export
    public String getName() {
        return name;
    }
}
//END-SNIPPET
static
//BEGIN-SNIPPET
public class Services {
    @HostAccess.Export
    public Employee createEmployee(String name) {
        return new Employee(name);
    }
    
    public void exitVM() {
        System.exit(1);
    }
}

public static void main(String[] args) {
    try (Context context = Context.create()) {
        Services services = new Services();
        context.getBindings("js").putMember("services", services);
        String name = context.eval("js",
                "let emp = services.createEmployee('John Doe');" + 
                "emp.getName()").asString();
        assert name.equals("John Doe");
        
        try {
            context.eval("js", "services.exitVM()");
            assert false;
        } catch (PolyglotException e) {
            assert e.getMessage().endsWith(
                    "Unknown identifier: exitVM");
        }
    }
}
// END-SNIPPET
}

  

在此代码中

  • Employee 声明了一个类型为 String 的字段 name。通过使用 @HostAccess.Export 注解 getName 方法,显式允许访问该方法。
  • Services 类公开了两个方法:createEmployeeexitVMcreateEmployee 方法接受员工姓名作为参数并创建一个新的 Employee 实例。createEmployee 方法使用 @HostAccess.Export 注解,因此客户应用程序可以访问。exitVM 方法未显式导出,因此不可访问。
  • main 方法首先在默认配置中创建一个新的多语言上下文,除了用 @HostAccess.Export 注解的方法外,禁止主机访问。
  • 创建一个新的 Services 实例并作为全局变量 services 放入上下文中。
  • 第一个评估的脚本使用服务对象创建一名新员工并返回其姓名。
  • 断言返回的名称等于预期的名称 John Doe
  • 评估第二个脚本,该脚本在服务对象上调用 exitVM 方法。这会因为 exitVM 方法未向客户应用程序公开而导致 PolyglotException 失败。

通过创建自定义 HostAccess 策略,主机访问是完全可定制的。

控制主机回调参数作用域 #

默认情况下,Value 的生命周期与相应的 Context 相同。但是,可能需要更改此默认行为并将值绑定到某个作用域,以便当执行离开该作用域时,该值将失效。此类作用域的一个示例是客户到主机的回调,其中 Value 可以作为回调参数传递。我们已经看到上面默认的 HostAccess.EXPLICIT 如何传递回调参数。

public class Services {
    Value lastResult;

    @HostAccess.Export
    public void callback(Value result) {
        this.lastResult = result;
    }

    String getResult() {
        return this.lastResult.asString();
    }
}

public static void main(String[] args) {
    Services s = new Services()
    try (Context context = Context.newBuilder().allowHostAccess(HostAccess.EXPLICIT).build()) {
        context.getBindings("js").putMember("services", s);
        context.eval("js", "services.callback('Hello from JS');");
        System.out.println(s.getResult());
    }
}

在此示例中,lastResult 维护对存储在主机上的客户值的引用,并且在 callback() 作用域结束之后仍然可访问。

然而,这并非总是可取的,因为保持值处于活动状态可能会不必要地阻塞资源或无法正确反映临时值的行为。对于这些情况,HostAccess.SCOPED 可以使用,它会更改所有回调的默认行为,使得作为回调参数传递的值仅在回调期间有效。

为了使上述代码与 HostAccess.SCOPED 一起工作,作为回调参数传递的单个值可以被固定,以延长其有效期直到回调返回之后。

public class Services {
    Value lastResult;

    @HostAccess.Export
    void callback(Value result, Value notneeded) {
        this.lastResult = result;
        this.lastResult.pin();
    }

    String getResult() {
        return this.lastResult.asString();
    }
}

public static void main(String[] args) {
    Services s = new Services()
    try (Context context = Context.newBuilder().allowHostAccess(HostAccess.SCOPED).build()) {
        context.getBindings("js").putMember("services", s);
        context.eval("js", "services.callback('Hello from JS', 'foobar');");
        System.out.println(services.getResult());
    }
}

或者,如果用 @HostAccess.DisableMethodScope 注解,整个回调方法可以退出作用域,从而为回调的所有参数保持常规语义。

public class Services {
    Value lastResult;
    Value metaInfo;

    @HostAccess.Export
    @HostAccess.DisableMethodScope
    void callback(Value result, Value metaInfo) {
        this.lastResult = result;
        this.metaInfo = metaInfo;
    }

    String getResult() {
        return this.lastResult.asString() + this.metaInfo.asString();
    }
}

public static void main(String[] args) {
    Services s = new Services()
    try (Context context = Context.newBuilder().allowHostAccess(HostAccess.SCOPED).build()) {
        context.getBindings("js").putMember("services", s);
        context.eval("js", "services.callback('Hello from JS', 'foobar');");
        System.out.println(services.getResult());
    }
}

访问权限配置 #

可以为客户应用程序配置细粒度的访问权限。在构建新上下文时,可以使用 Context.Builder 类提供配置。可以配置以下访问参数:

  • 使用 allowPolyglotAccess 允许访问其他语言。
  • 使用 allowHostAccess 允许和自定义对主机对象的访问。
  • 使用 allowHostClassLookup 允许和自定义主机对主机类型的查找。允许客户应用程序查找查找谓词所允许的主机应用程序类。例如,JavaScript 上下文可以创建一个 Java ArrayList,前提是 ArrayList 被 classFilter 允许列表,并且访问被主机访问策略允许:context.eval("js", "var array = Java.type('java.util.ArrayList')")
  • 使用 allowHostClassLoading 允许主机类加载。只有在主机访问策略授予访问权限时,类才可访问。
  • 使用 allowCreateThread 允许创建线程。
  • 使用 allowNativeAccess 允许访问本机 API。
  • 使用 allowIO 允许访问 I/O,并使用 fileSystem 代理文件访问。

注意:授予对类加载、本机 API 或主机 I/O 的访问权限实际上等同于授予所有访问权限,因为这些权限可用于绕过其他访问限制。

运行时优化支持 #

多语言 Truffle 运行时可以在多个主机虚拟机上使用,它们对运行时优化的支持程度各不相同。客户应用程序代码的运行时优化对于嵌入式客户应用程序的高效执行至关重要。此表显示了 Java 运行时当前提供的优化级别:

Java 运行时 运行时优化级别
Oracle GraalVM 经过额外编译器优化
GraalVM 社区版 已优化
Oracle JDK 通过实验性 VM 选项启用后优化
OpenJDK 通过实验性 VM 选项启用后优化
不具备 JVMCI 功能的 JDK 无运行时优化(仅解释器模式)

解释 #

  • 已优化: 执行的客户应用程序代码可以在运行时编译并作为高效的机器代码执行。
  • 通过额外编译器传递优化: Oracle GraalVM 实现了在运行时编译期间执行的额外优化。例如,它使用更高级的内联启发式。这通常会带来更好的运行时性能和内存消耗。
  • 通过实验性 VM 选项启用时优化: 优化默认情况下不启用,必须使用 -XX:+EnableJVMCI 虚拟机选项启用。此外,为了支持编译,必须将 Graal 编译器作为 JAR 文件下载并放置在 --upgrade-module-path 上。在此模式下,编译器作为 Java 应用程序运行,并可能对主机应用程序的执行性能产生负面影响。
  • 无运行时优化: 在没有运行时优化或未启用 JVMCI 的情况下,客户应用程序代码仅在解释器模式下执行。
  • JVMCI: 指的是大多数 Java 运行时支持的Java 级别 JVM 编译器接口

已创建一个项目,用于默认启用 Oracle JDK 和 OpenJDK 的运行时优化。有关更多详细信息,请参阅 Galahad 项目

在 OpenJDK 和 Oracle JDK 上启用优化 #

在默认启用 JDK 运行时优化(例如 OpenJDK)的环境中运行时,您可能会看到如下警告:

[engine] WARNING: The polyglot engine uses a fallback runtime that does not support runtime compilation to machine code.
Execution without runtime compilation will negatively impact the guest application performance.

这表示客户应用程序在未启用运行时优化的模式下执行。可以通过使用 --engine.WarnInterpreterOnly=false 选项或 -Dpolyglot.engine.WarnInterpreterOnly=false 系统属性来抑制此警告。此外,compiler.jar 文件及其依赖项必须从 Maven Central 下载,并使用 --upgrade-module-path 选项引用。请注意,compiler.jar **不得**放置在模块或类路径上。有关使用 Maven 或 Gradle 的配置示例,请参阅多语言嵌入演示

切换到回退引擎 #

如果有需要,例如,仅运行简单的脚本或在资源受限的系统中,您可能希望切换到没有运行时优化的备用引擎。自 Polyglot 23.1 版本起,可以通过从类或模块路径中删除 truffle-runtimetruffle-enterprise 模块来激活备用引擎。

使用 Maven 可以这样实现:

<dependencies>
  <dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>js</artifactId>
    <version>$graalvm-version</version>
    <exclusions>
      <exclusion>
        <groupId>org.graalvm.truffle</groupId>
        <artifactId>truffle-runtime</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.graalvm.truffle</groupId>
        <artifactId>truffle-enterprise</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
</dependencies>

如果您只使用 -community 依赖项,则 truffle-enterprise 的排除规则是不必要的。由于 truffle-enterprise 被排除,备用引擎不支持高级扩展,例如沙盒限制或多语言隔离。使用 mvn dependency:tree 再次检查这两个依赖项是否未包含在其他地方可能会很有用。

如果运行时已成功排除,您应该会看到以下日志消息:

[engine] WARNING: The polyglot engine uses a fallback runtime that does not support runtime compilation to native code.
Execution without runtime compilation will negatively impact the guest application performance.
The following cause was found: No optimizing Truffle runtime found on the module or class path.
For more information see: https://graalvm.java.net.cn/latest/reference-manual/embed-languages/.
To disable this warning use the '--engine.WarnInterpreterOnly=false' option or the '-Dpolyglot.engine.WarnInterpreterOnly=false' system property.

您可以使用指示的选项作为附加步骤来禁用此消息。

删除这些依赖项也会在 Native Image 构建中自动切换到备用引擎。

从多语言应用构建原生可执行文件 #

对于 GraalVM for JDK 21 及更高版本上的 Polyglot 23.1 版本,使用 Native Image 构建包含嵌入式多语言运行时的映像无需特殊配置。像任何其他 Java 依赖项一样,在构建本机可执行文件时,多语言 JAR 文件必须位于类或模块路径上。我们建议使用 MavenGradle Native Image 插件来配置您的 native-image 构建。有关 Native Image 的 Maven 和 Gradle 配置示例,请参阅 多语言嵌入演示存储库

以下是 Maven 配置文件配置示例:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <version>0.10.1</version>
                    <extensions>true</extensions>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>compile-no-fork</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                    <configuration>
                        <imageName>${project.artifactId}</imageName>
                        <mainClass>org.example.embedding.Main</mainClass>
                        <buildArgs>
                            <buildArg>--no-fallback</buildArg>
                            <buildArg>-J-Xmx20g</buildArg>
                        </buildArgs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

要使用上述配置构建原生可执行文件,请运行:

mvn -Pnative package

从多语言应用程序(例如,嵌入 Python 的 Java 主机应用程序)构建本机可执行文件时,会自动捕获所有包含的语言和工具所需的内部资源。默认情况下,这些资源包含在本机可执行文件本身中。可以通过 -H:-IncludeLanguageResources 禁用本机可执行文件中包含资源。另一个选项是使用一个单独的 *resources* 目录,其中包含所有必需的文件。要切换到此选项,请使用 -H:+CopyLanguageResources。当不支持 -H:+IncludeLanguageResources 时,这是默认行为,即对于早于 24.2.x 的 Graal 语言(请参阅版本路线图)。当使用 -H:+CopyLanguageResources 时,语言运行时将查找相对于本机可执行文件或共享库的资源目录。在运行时,可以使用 -Dpolyglot.engine.resourcePath=path/to/resources 选项自定义查找位置。要完全禁用资源捕获,请将 -H:-IncludeLanguageResources-H:-CopyLanguageResources 都添加到构建时选项中。请注意,某些语言可能不支持在没有其资源的情况下运行。

对于 Graal Languages 23.1 及更高版本,像 -Dorg.graalvm.home 这样的语言主目录选项不应再使用,已被资源目录选项取代。语言主目录选项出于兼容性原因仍可使用,但可能会在未来版本中移除。

配置原生主机反射 #

从客户应用程序访问主机 Java 代码需要 Java 反射才能工作。当在本机可执行文件中使用反射时,需要反射配置文件

对于此示例,我们使用 JavaScript 展示本机可执行文件的主机访问。将以下代码复制到一个名为 AccessJavaFromJS.java 的新文件中。

import org.graalvm.polyglot.*;
import org.graalvm.polyglot.proxy.*;
import java.util.concurrent.*;

public class AccessJavaFromJS {

    public static class MyClass {
        public int               id    = 42;
        public String            text  = "42";
        public int[]             arr   = new int[]{1, 42, 3};
        public Callable<Integer> ret42 = () -> 42;
    }

    public static void main(String[] args) {
        try (Context context = Context.newBuilder()
                                   .allowAllAccess(true)
                               .build()) {
            context.getBindings("js").putMember("javaObj", new MyClass());
            boolean valid = context.eval("js",
                   "    javaObj.id         == 42"          +
                   " && javaObj.text       == '42'"        +
                   " && javaObj.arr[1]     == 42"          +
                   " && javaObj.ret42()    == 42")
               .asBoolean();
            System.out.println("Valid " + valid);
        }
    }
}

将以下代码复制到 reachability-metadata.json 中:

{
  "reflection": [
     { "type": "AccessJavaFromJS$MyClass", "allPublicFields": true },
     { "type": "java.util.concurrent.Callable", "allPublicMethods": true }
  ]
}

现在,您可以将 reachability-metadata.json 添加到您项目的 META-INF/native-image/<group-id>/ 中。

跨多个上下文的代码缓存 #

GraalVM Polyglot API 允许跨多个上下文进行代码缓存。代码缓存允许重用编译后的代码,并允许源文件只解析一次。代码缓存通常可以减少应用程序的内存消耗和预热时间。

默认情况下,代码仅在单个上下文实例中缓存。需要指定一个显式引擎才能在多个上下文之间启用代码缓存。在创建上下文时,使用上下文构建器指定引擎。引擎实例决定了代码共享的范围。代码仅在与同一引擎实例关联的上下文之间共享。

默认情况下,所有源都已缓存。可以通过将 cached(boolean cached) 设置为 false 来显式禁用缓存。如果已知源只评估一次,禁用缓存可能很有用。

请考虑以下代码片段作为示例:

public class Main {
    public static void main(String[] args) {
        try (Engine engine = Engine.create()) {
            Source source = Source.create("js", "21 + 21");
            try (Context context = Context.newBuilder()
                .engine(engine)
                .build()) {
                    int v = context.eval(source).asInt();
                    assert v == 42;
            }
            try (Context context = Context.newBuilder()
                .engine(engine)
                .build()) {
                    int v = context.eval(source).asInt();
                    assert v == 42;
            }
        }
    }
}

在此代码中

  • import org.graalvm.polyglot.* 导入 Polyglot API 的基础 API。
  • Engine.create() 创建一个新的默认配置的引擎实例。
  • Source.create() 为表达式“21 + 21”创建一个源对象。我们使用一个显式的 Source 对象来确保代码缓存不会在上下文之间被垃圾回收。使用“js”语言,它是 JavaScript 的语言标识符。
  • Context.newBuilder().engine(engine).build() 使用为其分配的显式引擎构建一个新上下文。所有与引擎关联的上下文共享代码。
  • context.eval(source).asInt() 评估源并以 Value 实例的形式返回结果。

重要: 为了使缓存源的代码缓存能够在执行上下文之间保持活动状态,应用程序必须确保 Source 对象持续被引用。多语言运行时可能会在下一个 GC 周期中回收不再被引用的源的缓存代码。

管理代码缓存 #

代码缓存的数据作为 Engine 实例的一部分存储。两个独立的引擎实例之间永远不会发生代码共享。因此,如果需要全局代码缓存,我们建议使用单例 Engine 实例。与上下文不同,引擎始终可以在多个线程之间共享。上下文是否可以在多个线程之间共享取决于所使用的语言。

没有显式方法来清除代码缓存。我们依赖垃圾收集器在下一次收集时自动执行此操作。只要引擎仍然被强引用且未关闭,引擎的代码缓存就不会被收集。此外,必须保持 Source 实例的活动状态,以确保关联的代码不会被收集。如果源实例不再被引用,但引擎仍被引用,则与源对象关联的代码缓存可能会被 GC 收集。因此,我们建议在 Source 应该保持缓存的情况下,对其保持强引用。

总之,可以通过保持和维护对 EngineSource 对象的强引用来控制代码缓存。

多语言隔离区 #

在 Oracle GraalVM 上,多语言引擎可以配置为在专用的 Native Image 隔离区中运行。在此模式下,多语言引擎在具有专用垃圾收集器和 JIT 编译器的 VM 级故障域中执行。多语言隔离区对于沙盒化很有用。在隔离区中运行语言适用于 HotSpot 和 Native Image 主机虚拟机。

用作多语言隔离的语言可以使用 -isolate 后缀从 Maven Central 下载。例如,可以通过添加如下 Maven 依赖项来配置对隔离 JavaScript 的依赖:

<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>polyglot</artifactId>
    <version>${graalvm.polyglot.version}</version>
    <type>jar</type>
</dependency>
<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>js-isolate</artifactId>
    <version>${graalvm.polyglot.version}</version>
    <type>pom</type>
</dependency>

从 Polyglot API 24.1.0 版本开始,多语言引擎支持针对各个平台的多语言隔离。要下载特定平台的多语言隔离,请将操作系统和 CPU 架构分类器附加到多语言隔离 Maven artifactId。例如,要为 Linux amd64 配置对隔离 Python 的依赖项,请添加以下 Maven 依赖项:

<dependency>
	<groupId>org.graalvm.polyglot</groupId>
	<artifactId>polyglot</artifactId>
	<version>${graalvm.polyglot.version}</version>
	<type>jar</type>
</dependency>
<dependency>
	<groupId>org.graalvm.polyglot</groupId>
	<artifactId>python-isolate-linux-amd64</artifactId>
	<version>${graalvm.polyglot.version}</version>
	<type>pom</type>
</dependency>

支持的平台分类器有:

  • linux-amd64
  • linux-aarch64
  • darwin-amd64
  • darwin-aarch64
  • windows-amd64

有关为当前平台添加多语言隔离 Native Image 依赖项的完整 Maven POM 文件,请参阅 GitHub 上的多语言嵌入演示

要在 Polyglot API 中启用隔离使用,在构造 EngineContext 时必须传递 --engine.SpawnIsolate=true 选项。如果在 Oracle GraalVM 之外的任何 JDK 上使用,engine.SpawnIsolate 选项可能不可用。

import org.graalvm.polyglot.*;

public class PolyglotIsolate {
	public static void main(String[] args) {
		try (Context context = Context.newBuilder("js")
			  .allowHostAccess(HostAccess.SCOPED)
			  .option("engine.SpawnIsolate", "true").build()) {
			  
			Value function = context.eval("js", "x => x+1");
			assert function.canExecute();
			int x = function.execute(41).asInt();
			assert x == 42;
		}
	}
}

目前,以下语言可用作多语言隔离区:

语言 可用版本
JavaScript (js-isolate) 23.1
Python (python-isolate) 24.1

我们计划在未来版本中增加对更多语言的支持。

在前面的示例中,我们使用 HostAccess.SCOPED 启用作用域引用。这是必要的,因为主机 GC 和客户 GC 相互之间不知道,所以对象之间的循环引用无法自动解决。因此,我们强烈建议对主机回调使用作用域参数,以完全避免循环引用。

通过共享引擎,可以在同一个隔离引擎中生成多个上下文。

public class PolyglotIsolateMultipleContexts {
    public static void main(String[] args) {
        try (Engine engine = Engine.newBuilder("js")
                .option("engine.SpawnIsolate", "true").build()) {
            Source source = Source.create("js", "21 + 21");
            try (Context context = Context.newBuilder()
                .engine(engine)
                .build()) {
                    int v = context.eval(source).asInt();
                    assert v == 42;
            }
            try (Context context = Context.newBuilder()
                .engine(engine)
                .build()) {
                    int v = context.eval(source).asInt();
                    assert v == 42;
            }
        }
    }
}

传递原生镜像运行时选项 #

在隔离区中运行的引擎可以通过将 --engine.IsolateOption.<option> 传递给引擎构建器来使用Native Image 运行时选项。例如,这可以通过 --engine.IsolateOption.MaxHeapSize=128m 设置隔离区的最大堆大小来限制引擎使用的最大堆内存。

import org.graalvm.polyglot.*;

public class PolyglotIsolateMaxHeap {
  public static void main(String[] args) {
    try {
      Context context = Context.newBuilder("js")
        .allowHostAccess(HostAccess.SCOPED)
        .option("engine.SpawnIsolate", "true")
        .option("engine.IsolateOption.MaxHeapSize", "64m").build()
      context.eval("js", "var a = [];while (true) {a.push('foobar');}");
    } catch (PolyglotException ex) {
      if (ex.isResourceExhausted()) {
        System.out.println("Resource exhausted");
      }
    }
  }
}

超过最大堆大小将自动关闭上下文并引发 PolyglotException

确保主机回调栈余量 #

对于 Polyglot Isolates,--engine.HostCallStackHeadRoom 确保在执行主机回调时有最小的可用堆栈空间。如果可用堆栈大小低于指定阈值,主机回调将失败。

内存保护 #

在支持内存保护键的 Linux 环境中,可以使用 --engine.MemoryProtection=true 选项在硬件级别隔离 Polyglot Isolates 的堆。如果使用此选项创建引擎,将为隔离引擎的堆分配一个专用的保护键。GraalVM 仅在执行 Polyglot Isolate 的代码时才允许访问引擎的堆。

在 Java 中嵌入客体语言 #

GraalVM Polyglot API 可以通过 Java 互操作性在客户语言中使用。如果脚本需要与父上下文隔离运行,这会很有用。在 Java 作为主机语言时,调用 Context.eval(Source) 会返回一个 Value 实例,但由于我们正在将此代码作为客户语言的一部分执行,我们可以改用特定于语言的互操作性 API。因此,可以使用在语言内部创建的上下文返回的值,就像该语言的常规值一样。在下面的示例中,我们可以方便地编写 value.data 而不是 value.getMember("data")。有关如何与外部值互操作的详细信息,请参阅各个语言文档。有关多个上下文之间值共享的更多信息,请参阅此处

请考虑以下代码片段作为示例:

import org.graalvm.polyglot.*;

public class Main {
    public static void main(String[] args) {
        try (Context outer = Context.newBuilder()
                                   .allowAllAccess(true)
                               .build()) {
            outer.eval("js", "inner = Java.type('org.graalvm.polyglot.Context').create()");
            outer.eval("js", "value = inner.eval('js', '({data:42})')");
            int result = outer.eval("js", "value.data").asInt();
            outer.eval("js", "inner.close()");

            System.out.println("Valid " + (result == 42));
        }
    }
}

在此代码中

  • Context.newBuilder().allowAllAccess(true).build() 构建一个新的具有所有权限的外部上下文。
  • outer.eval 在外部上下文中评估 JavaScript 代码片段。
  • inner = Java.type('org.graalvm.polyglot.Context').create() 第一个 JS 脚本行查找 Java 主机类型 Context 并创建一个没有权限(默认)的新内部上下文实例。
  • inner.eval('js', '({data:42})'); 在内部上下文中评估 JavaScript 代码 ({data:42}) 并返回存储结果。
  • "value.data" 此行从内部上下文的结果中读取成员 data。请注意,只有在内部上下文尚未关闭的情况下才能读取此结果。
  • context.eval("js", "c.close()") 此代码片段关闭内部上下文。内部上下文需要手动关闭,不会随父上下文自动关闭。
  • 最后,该示例预计将在控制台打印 Valid true

为多种语言构建 Shell #

只需几行代码,GraalVM Polyglot API 就可以让您构建与 GraalVM 支持的任何客户语言集成的应用程序。

此 shell 实现与任何特定的客体语言无关。

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
PrintStream output = System.out;
Context context = Context.newBuilder().allowAllAccess(true).build();
Set<String> languages = context.getEngine().getLanguages().keySet();
output.println("Shell for " + languages + ":");
String language = languages.iterator().next();
for (;;) {
    try {
        output.print(language + "> ");
        String line = input.readLine();
        if (line == null) {
            break;
        } else if (languages.contains(line)) {
            language = line;
            continue;
        }
        Source source = Source.newBuilder(language, line, "<shell>")
                        .interactive(true).buildLiteral();
        context.eval(source);
    } catch (PolyglotException t) {
        if(t.isExit()) {
            break;
        }
        t.printStackTrace();
    }
}

使用执行监听器进行单步调试 #

GraalVM Polyglot API 允许用户通过 ExecutionListener 类检测客户语言的执行。例如,它允许您附加一个执行监听器,该监听器将针对客户语言程序的每个语句进行调用。执行监听器被设计为多语言嵌入器的一个简单 API,并且在例如单步调试程序时可能会派上用场。

import org.graalvm.polyglot.*;
import org.graalvm.polyglot.management.*;

public class ExecutionListenerTest {
    public static void main(String[] args) {
        try (Context context = Context.create("js")) {
            ExecutionListener listener = ExecutionListener.newBuilder()
                      .onEnter((e) -> System.out.println(
                              e.getLocation().getCharacters()))
                      .statements(true)
                      .attach(context.getEngine());
            context.eval("js", "for (var i = 0; i < 2; i++);");
            listener.close();
        }
    }
}

在此代码中

  • Context.create() 调用为客户语言创建一个新上下文。
  • 通过调用 ExecutionListeners.newBuilder() 创建一个执行监听器构建器。
  • 设置 onEnter 事件,以便在进入并消费元素执行时通知。至少需要启用一个事件消费者和一个过滤后的源元素。
  • 要完成监听器附件,需要调用 attach()
  • statements(true) 将执行监听器仅过滤到语句。
  • context.eval() 调用评估指定的一段客户语言代码。
  • listener.close() 会更早地关闭监听器,但执行监听器会随引擎自动关闭。

与 JSR-223 ScriptEngine 的兼容性 #

Truffle 语言实现框架不提供 JSR-223 ScriptEngine 实现。Polyglot API 提供了对 Truffle 功能更细粒度的控制,我们强烈建议用户使用 org.graalvm.polyglot.Context 接口,以便直接控制许多设置并受益于 GraalVM 中更细粒度的安全设置。

然而,为了方便地将 Truffle 语言作为使用 ScriptEngine API 集成的其他脚本语言的替代品进行评估,我们提供了以下单文件脚本引擎。该文件可以放入源代码树中,并直接用于通过 ScriptEngine API 评估 Truffle 语言。您的项目只需要调整两行:

public final class CHANGE_NAME_EngineFactory implements ScriptEngineFactory {
    private static final String LANGUAGE_ID = "<<INSERT LANGUAGE ID HERE>>";

根据需要重命名类,并将 LANGUAGE_ID 更改为所需的 Truffle 语言(例如,GraalPy 为“python”,TruffleRuby 为“ruby”)。要使用它,请在您的资源中包含一个名为 META-INF/services/javax.script.ScriptEngineFactory 的文件,其中包含所选的类名。这将允许默认的 javax.script.ScriptEngineManager 自动发现该语言。或者,可以通过 javax.script.ScriptEngineManager#registerEngineName 注册工厂或直接实例化和使用。

最佳实践是在不再使用 ScriptEngine 时关闭它,而不是依赖终结器。要关闭它,请使用 ((AutoCloseable) scriptEngine).close();,因为 ScriptEngine 没有 close() 方法。

请注意,GraalJS 为从 JDK 11 中已弃用的 Nashorn JavaScript 引擎迁移的用户提供了ScriptEngine 实现,因此此处不需要此方法。

展开以查看单文件中的 Truffle 语言的 ScriptEngineFactory 实现。

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;

import org.graalvm.home.Version;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Language;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;

public final class CHANGE_NAME_EngineFactory implements ScriptEngineFactory {
    private static final String LANGUAGE_ID = "<>";

    /***********************************************************/
    /* Everything below is generic and does not need to change */
    /***********************************************************/

    private final Engine polyglotEngine = Engine.newBuilder().build();
    private final Language language = polyglotEngine.getLanguages().get(LANGUAGE_ID);

    @Override
    public String getEngineName() {
        return language.getImplementationName();
    }

    @Override
    public String getEngineVersion() {
        return Version.getCurrent().toString();
    }

    @Override
    public List getExtensions() {
        return List.of(LANGUAGE_ID);
    }

    @Override
    public List getMimeTypes() {
        return List.copyOf(language.getMimeTypes());
    }

    @Override
    public List getNames() {
        return List.of(language.getName(), LANGUAGE_ID, language.getImplementationName());
    }

    @Override
    public String getLanguageName() {
        return language.getName();
    }

    @Override
    public String getLanguageVersion() {
        return language.getVersion();
    }

    @Override
    public Object getParameter(final String key) {
        switch (key) {
            case ScriptEngine.ENGINE:
                return getEngineName();
            case ScriptEngine.ENGINE_VERSION:
                return getEngineVersion();
            case ScriptEngine.LANGUAGE:
                return getLanguageName();
            case ScriptEngine.LANGUAGE_VERSION:
                return getLanguageVersion();
            case ScriptEngine.NAME:
                return LANGUAGE_ID;
        }
        return null;
    }

    @Override
    public String getMethodCallSyntax(final String obj, final String m, final String... args) {
        throw new UnsupportedOperationException("Unimplemented method 'getMethodCallSyntax'");
    }

    @Override
    public String getOutputStatement(final String toDisplay) {
        throw new UnsupportedOperationException("Unimplemented method 'getOutputStatement'");
    }

    @Override
    public String getProgram(final String... statements) {
        throw new UnsupportedOperationException("Unimplemented method 'getProgram'");
    }

    @Override
    public ScriptEngine getScriptEngine() {
        return new PolyglotEngine(this);
    }

    private static final class PolyglotEngine implements ScriptEngine, Compilable, Invocable, AutoCloseable {
        private final ScriptEngineFactory factory;
        private PolyglotContext defaultContext;

        PolyglotEngine(ScriptEngineFactory factory) {
            this.factory = factory;
            this.defaultContext = new PolyglotContext(factory);
        }

        @Override
        public void close() {
            defaultContext.getContext().close();
        }

        @Override
        public CompiledScript compile(String script) throws ScriptException {
            Source src = Source.create(LANGUAGE_ID, script);
            try {
                defaultContext.getContext().parse(src); // only for the side-effect of validating the source
            } catch (PolyglotException e) {
                throw new ScriptException(e);
            }
            return new PolyglotCompiledScript(src, this);
        }

        @Override
        public CompiledScript compile(Reader script) throws ScriptException {
            Source src;
            try {
                src = Source.newBuilder(LANGUAGE_ID, script, "sourcefromreader").build();
                defaultContext.getContext().parse(src); // only for the side-effect of validating the source
            } catch (PolyglotException | IOException e) {
                throw new ScriptException(e);
            }
            return new PolyglotCompiledScript(src, this);
        }

        @Override
        public Object eval(String script, ScriptContext context) throws ScriptException {
            if (context instanceof PolyglotContext) {
                PolyglotContext c = (PolyglotContext) context;
                try {
                    return c.getContext().eval(LANGUAGE_ID, script).as(Object.class);
                } catch (PolyglotException e) {
                    throw new ScriptException(e);
                }
            } else {
                throw new ClassCastException("invalid context");
            }
        }

        @Override
        public Object eval(Reader reader, ScriptContext context) throws ScriptException {
            Source src;
            try {
                src = Source.newBuilder(LANGUAGE_ID, reader, "sourcefromreader").build();
            } catch (IOException e) {
                throw new ScriptException(e);
            }
            if (context instanceof PolyglotContext) {
                PolyglotContext c = (PolyglotContext) context;
                try {
                    return c.getContext().eval(src).as(Object.class);
                } catch (PolyglotException e) {
                    throw new ScriptException(e);
                }
            } else {
                throw new ScriptException("invalid context");
            }
        }

        @Override
        public Object eval(String script) throws ScriptException {
            return eval(script, defaultContext);
        }

        @Override
        public Object eval(Reader reader) throws ScriptException {
            return eval(reader, defaultContext);
        }

        @Override
        public Object eval(String script, Bindings n) throws ScriptException {
            throw new UnsupportedOperationException("Bindings for Polyglot language cannot be created explicitly");
        }

        @Override
        public Object eval(Reader reader, Bindings n) throws ScriptException {
            throw new UnsupportedOperationException("Bindings for Polyglot language cannot be created explicitly");
        }

        @Override
        public void put(String key, Object value) {
            defaultContext.getBindings(ScriptContext.ENGINE_SCOPE).put(key, value);
        }

        @Override
        public Object get(String key) {
            return defaultContext.getBindings(ScriptContext.ENGINE_SCOPE).get(key);
        }

        @Override
        public Bindings getBindings(int scope) {
            return defaultContext.getBindings(scope);
        }

        @Override
        public void setBindings(Bindings bindings, int scope) {
            defaultContext.setBindings(bindings, scope);
        }

        @Override
        public Bindings createBindings() {
            throw new UnsupportedOperationException("Bindings for Polyglot language cannot be created explicitly");
        }

        @Override
        public ScriptContext getContext() {
            return defaultContext;
        }

        @Override
        public void setContext(ScriptContext context) {
            throw new UnsupportedOperationException("The context of a Polyglot ScriptEngine cannot be modified.");
        }

        @Override
        public ScriptEngineFactory getFactory() {
            return factory;
        }

        @Override
        public Object invokeMethod(Object thiz, String name, Object... args)
                throws ScriptException, NoSuchMethodException {
            try {
                Value receiver = defaultContext.getContext().asValue(thiz);
                if (receiver.canInvokeMember(name)) {
                    return receiver.invokeMember(name, args).as(Object.class);
                } else {
                    throw new NoSuchMethodException(name);
                }
            } catch (PolyglotException e) {
                throw new ScriptException(e);
            }
        }

        @Override
        public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
            throw new UnsupportedOperationException();
        }

        @Override
        public  T getInterface(Class interfaceClass) {
            throw new UnsupportedOperationException();
        }

        @Override
        public  T getInterface(Object thiz, Class interfaceClass) {
            return defaultContext.getContext().asValue(thiz).as(interfaceClass);
        }
    }

    private static final class PolyglotContext implements ScriptContext {
        private Context context;
        private final ScriptEngineFactory factory;
        private final PolyglotReader in;
        private final PolyglotWriter out;
        private final PolyglotWriter err;
        private Bindings globalBindings;

        PolyglotContext(ScriptEngineFactory factory) {
            this.factory = factory;
            this.in = new PolyglotReader(new InputStreamReader(System.in));
            this.out = new PolyglotWriter(new OutputStreamWriter(System.out));
            this.err = new PolyglotWriter(new OutputStreamWriter(System.err));
        }

        Context getContext() {
            if (context == null) {
                Context.Builder builder = Context.newBuilder(LANGUAGE_ID)
                        .in(this.in)
                        .out(this.out)
                        .err(this.err)
                        .allowAllAccess(true);
                Bindings globalBindings = getBindings(ScriptContext.GLOBAL_SCOPE);
                if (globalBindings != null) {
                    for (Entry<String, Object> entry : globalBindings.entrySet()) {
                        Object value = entry.getValue();
                        if (value instanceof String) {
                            builder.option(entry.getKey(), (String) value);
                        }
                    }
                }
                context = builder.build();
            }
            return context;
        }

        @Override
        public void setBindings(Bindings bindings, int scope) {
            if (scope == ScriptContext.GLOBAL_SCOPE) {
                if (context == null) {
                    globalBindings = bindings;
                } else {
                    throw new UnsupportedOperationException(
                            "Global bindings for Polyglot language can only be set before the context is initialized.");
                }
            } else {
                throw new UnsupportedOperationException("Bindings objects for Polyglot language is final.");
            }
        }

        @Override
        public Bindings getBindings(int scope) {
            if (scope == ScriptContext.ENGINE_SCOPE) {
                return new PolyglotBindings(getContext().getBindings(LANGUAGE_ID));
            } else if (scope == ScriptContext.GLOBAL_SCOPE) {
                return globalBindings;
            } else {
                return null;
            }
        }

        @Override
        public void setAttribute(String name, Object value, int scope) {
            if (scope == ScriptContext.ENGINE_SCOPE) {
                getBindings(scope).put(name, value);
            } else if (scope == ScriptContext.GLOBAL_SCOPE) {
                if (context == null) {
                    globalBindings.put(name, value);
                } else {
                    throw new IllegalStateException("Cannot modify global bindings after context creation.");
                }
            }
        }

        @Override
        public Object getAttribute(String name, int scope) {
            if (scope == ScriptContext.ENGINE_SCOPE) {
                return getBindings(scope).get(name);
            } else if (scope == ScriptContext.GLOBAL_SCOPE) {
                return globalBindings.get(name);
            }
            return null;
        }

        @Override
        public Object removeAttribute(String name, int scope) {
            Object prev = getAttribute(name, scope);
            if (prev != null) {
                if (scope == ScriptContext.ENGINE_SCOPE) {
                    getBindings(scope).remove(name);
                } else if (scope == ScriptContext.GLOBAL_SCOPE) {
                    if (context == null) {
                        globalBindings.remove(name);
                    } else {
                        throw new IllegalStateException("Cannot modify global bindings after context creation.");
                    }
                }
            }
            return prev;
        }

        @Override
        public Object getAttribute(String name) {
            return getAttribute(name, ScriptContext.ENGINE_SCOPE);
        }

        @Override
        public int getAttributesScope(String name) {
            if (getAttribute(name, ScriptContext.ENGINE_SCOPE) != null) {
                return ScriptContext.ENGINE_SCOPE;
            } else if (getAttribute(name, ScriptContext.GLOBAL_SCOPE) != null) {
                return ScriptContext.GLOBAL_SCOPE;
            }
            return -1;
        }

        @Override
        public Writer getWriter() {
            return this.out.writer;
        }

        @Override
        public Writer getErrorWriter() {
            return this.err.writer;
        }

        @Override
        public void setWriter(Writer writer) {
            this.out.writer = writer;
        }

        @Override
        public void setErrorWriter(Writer writer) {
            this.err.writer = writer;
        }

        @Override
        public Reader getReader() {
            return this.in.reader;
        }

        @Override
        public void setReader(Reader reader) {
            this.in.reader = reader;
        }

        @Override
        public List getScopes() {
            return List.of(ScriptContext.ENGINE_SCOPE, ScriptContext.GLOBAL_SCOPE);
        }

        private static final class PolyglotReader extends InputStream {
            private volatile Reader reader;

            public PolyglotReader(InputStreamReader inputStreamReader) {
                this.reader = inputStreamReader;
            }

            @Override
            public int read() throws IOException {
                return reader.read();
            }
        }

        private static final class PolyglotWriter extends OutputStream {
            private volatile Writer writer;

            public PolyglotWriter(OutputStreamWriter outputStreamWriter) {
                this.writer = outputStreamWriter;
            }

            @Override
            public void write(int b) throws IOException {
                writer.write(b);
            }
        }
    }

    private static final class PolyglotCompiledScript extends CompiledScript {
        private final Source source;
        private final ScriptEngine engine;

        public PolyglotCompiledScript(Source src, ScriptEngine engine) {
            this.source = src;
            this.engine = engine;
        }

        @Override
        public Object eval(ScriptContext context) throws ScriptException {
            if (context instanceof PolyglotContext) {
                return ((PolyglotContext) context).getContext().eval(source).as(Object.class);
            }
            throw new UnsupportedOperationException(
                    "Polyglot CompiledScript instances can only be evaluated in Polyglot.");
        }

        @Override
        public ScriptEngine getEngine() {
            return engine;
        }
    }

    private static final class PolyglotBindings implements Bindings {
        private Value languageBindings;

        PolyglotBindings(Value languageBindings) {
            this.languageBindings = languageBindings;
        }

        @Override
        public int size() {
            return keySet().size();
        }

        @Override
        public boolean isEmpty() {
            return size() == 0;
        }

        @Override
        public boolean containsValue(Object value) {
            for (String s : keySet()) {
                if (get(s) == value) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void clear() {
            for (String s : keySet()) {
                remove(s);
            }
        }

        @Override
        public Set keySet() {
            return languageBindings.getMemberKeys();
        }

        @Override
        public Collection values() {
            List values = new ArrayList<>();
            for (String s : keySet()) {
                values.add(get(s));
            }
            return values;
        }

        @Override
        public Set<Entry<String, Object>> entrySet() {
            Set<Entry<String, Object>> values = new HashSet<>();
            for (String s : keySet()) {
                values.add(new Entry<String, Object>() {
                    @Override
                    public String getKey() {
                        return s;
                    }

                    @Override
                    public Object getValue() {
                        return get(s);
                    }

                    @Override
                    public Object setValue(Object value) {
                        return put(s, value);
                    }
                });
            }
            return values;
        }

        @Override
        public Object put(String name, Object value) {
            Object previous = get(name);
            languageBindings.putMember(name, value);
            return previous;
        }

        @Override
        public void putAll(Map<? extends String, ? extends Object> toMerge) {
            for (Entry<? extends String, ? extends Object> e : toMerge.entrySet()) {
                put(e.getKey(), e.getValue());
            }
        }

        @Override
        public boolean containsKey(Object key) {
            if (key instanceof String) {
                return languageBindings.hasMember((String) key);
            } else {
                return false;
            }
        }

        @Override
        public Object get(Object key) {
            if (key instanceof String) {
                Value value = languageBindings.getMember((String) key);
                if (value != null) {
                    return value.as(Object.class);
                }
            }
            return null;
        }

        @Override
        public Object remove(Object key) {
            Object prev = get(key);
            if (prev != null) {
                languageBindings.removeMember((String) key);
                return prev;
            } else {
                return null;
            }
        }
    }
}
</code></pre>
</details>

联系我们