vlambda博客
学习文章列表

读书笔记《supercharge-your-applications-with-graalvm》第 6 章 Truffle 支持多语言多语言

Chapter 7: GraalVM Polyglot – JavaScript and Node.js

在上一章中,我们了解了 Truffle 如何提供一个层来集成其他语言程序以在 GraalVM 上运行。在本章中,我们将重点介绍 JavaScript 和 Node.js 解释器,在下一章中,我们将介绍其他运行时,例如 Java/Truffle、Python、R 和 WebAssembly。我们将介绍 Truffle 的多语言互操作性特性的各个方面,并探索 JavaScript 解释器。我们将通过编写代码亲自探索这些功能。

在本章中,我们将介绍以下主题:

  • 了解如何在 Graal 上运行非 JVM 语言应用程序,特别是 JavaScript 和 Node
  • 学习如何在用不同语言编写的应用程序之间传递对象/值
  • 了解如何使用优化技术来微调代码

在本章结束时,您将对如何在 GraalVM 上构建多语言应用程序有一个非常清晰的了解。

Technical requirements

在本章中,我们将进行大量实践编码,以探索 GraalVM 支持的各种来宾语言。要尝试代码,您将需要以下内容:

Understanding the JavaScript (including Node.js) Truffle interpreter

GraalVM 版本的 JavaScript 是兼容 ECMAScript 的运行时,适用于 JavaScript,js 和 Node.js,node。在撰写本书时,它支持 ECMAScript 2021 的所有功能。它还兼容 Nashorn 和 Rhino,并为 Node.js 提供完整支持。

GraalVM Node.js 使用 原始 Node.js 源代码,并将 V8 JavaScript 引擎替换为 GraalVM JavaScript 引擎。替换是无缝的,应用程序开发人员无需修改任何大量代码或配置即可使用 GraalVM Node.js 运行现有的 Node.js 应用程序。 GraalVM Node.js 提供了更多功能,用于嵌入其他语言的代码,访问数据和代码,以及互操作其他语言的代码。 节点包管理器(NPM)也包含在npm中。

在本节中,除了使用 JavaScript 和 Node 作为运行应用程序的替代运行时之外,我们还将探索它们的多语言互操作性特性。我们将通过大量 JavaScript 和 Node.js 示例代码来探索 GraalVM JavaScript 运行时的多语言功能。

Verifying JavaScript, Node, and npm installation and versions

随着 GraalVM 的安装提供 JavaScript 和 Node.js;您将在 <GraalHome>/bin 目录中找到它们。我们可以通过检查版本号来检查JavaScript运行时是否配置正确。

要检查版本,请执行 js --version 命令。在撰写本书时,GraalVM JavaScript 21.0.0.2 是最新的。以下是输出(注意是 GraalVM JavaScript):

GraalVM JavaScript (GraalVM EE Native 21.0.0.2)

我们还可以通过执行 node --version 命令调用特定的 node.js 版本来确保我们正在运行正确版本的 Node.js。在以下命令中,我们显式调用了正确的版本。请注意,您的 GraalVM 主位置可能会有所不同:

/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/node --version
v12.20.1

我们还通过执行npm --version确保NPM正常工作命令。 下面是命令和输出:

/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/npm --version
6.14.10

现在我们已经验证了 JavaScript、Node.js 和 npm 的安装,让我们创建一个简单的 Node.js 应用程序。

转到应用程序文件夹,然后执行 npm init。这将为 Node.js 应用程序设置样板配置。我们将应用程序命名为 graal-node-app。下面显示了控制台输出:

/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/npm init     
package name: (npm) graal-node-app
version: (1.0.0) 1.0.0
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /chapter7/js/npm/package.json:
{
  "name": "graal-node-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" andand exit 1"
  },
  "author": "",
  "license": "ISC"
}
Is this OK? (yes)

根据我们选择的选项,这将创建一个 Node.js 应用程序和一个带有样板配置的 package.json 文件。让我们通过执行 npm install --save express 来安装 express 包。这会在应用程序文件夹中安装 express 包并更新 package.json 文件(因为 --save 参数)。这是输出:

/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/npm install --save express
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN [email protected] No description
npm WARN [email protected] No repository field.
+ [email protected]
added 50 packages from 37 contributors and audited 50 packages in 6.277s
found 0 vulnerabilities

您将找到 node_modules 目录,其中 包含运行我们的应用程序所需的所有包。让我们使用以下代码创建一个 index.js 文件:

var express = require('express');
var app = express();
app.get('/', function(request, response) {
    var responseString = "<h1>Hello Graal Node </h1>";
    response.send(responseString);
});
app.listen(8080, function() {
    console.log('Started the server at 8080')
});

如您所见,这是一个非常简单的应用程序,当在根目录调用时,它以 HTML Hello Graal Node 作为标题 1 进行响应。应用程序将侦听端口号 8080

让我们使用以下命令运行此应用程序:

/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/node index.js 
Started the server at 8080

现在我们可以看到输出,我们知道应用程序正在监听 8080。让我们尝试从网络浏览器 http://localhost:8080/ 调用它。以下是网页浏览器上应用响应的截图:

读书笔记《supercharge-your-applications-with-graalvm》第 6 章 Truffle 支持多语言多语言

图 7.1 – Hello Graal 节点截图

现在我们知道 GraalVM 上的 Node.js 运行良好,让我们了解多语言互操作性。

JavaScript interoperability

第 6 章中,Truffle 支持多语言(多语言),我们详细介绍了 Truffle 如何启用多语言支持并提供多语言互操作性和多语言嵌入的基础设施。在本节中,我们将通过示例代码探索这些功能。

让我们以我们在上一节中创建的 Node.js 应用程序为例,在我们的 index.js /poly代码>文件。让我们创建一个简单的 Python 数组对象,并在对象中存储一些 数字。然后,我们将在 Node.js 中遍历这个 Python 对象,以列出这些数字。这显示了我们如何在 JavaScript 中嵌入 Python 代码片段。

以下源代码显示了这个新端点 /poly

var express = require('express');
var app = express();
app.get('/', function(request, response) {
    var responseString = "<h1>Hello Graal Node </h1>";
    response.send(responseString);
});
app.get('/poly', function(request, response) {
    var responseString = "<h1>Hello Graal Polyglot </h1>";
    var array = Polyglot.eval("python", "[1,2,3,4, 100,                              200, 300, 400]")
    responseString = responseString + "<ul>";
    for (let index = 0; index < array.length; index++) {
        responseString = responseString + "<li>";
        responseString = responseString + array[index];
        responseString = responseString + "</li>";
    }
    responseString = responseString + "</ul>";
    response.send(responseString);
});

现在让我们的代码监听端口 8080 并在收到请求时调用前面的函数:

app.listen(8080, function() {
    console.log('Started the server at 8080')
});

正如您在代码中所见,我们使用 Polyglot.eval() 方法来运行 Python 代码。为了让 polyglot 对象知道它是 Python 代码,我们将 python 作为参数传递,并传递数组的 Python 表示形式。现在让我们用 node 运行这段代码:

/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/node --jvm --polyglot index.js
Started the server at 8080

请注意,我们必须将 --jvm--polyglot 参数传递给节点。传递这些参数非常重要。 --jvm 告诉节点Java虚拟机上运行(JVM) 和 --polyglot,顾名思义,告诉节点支持 polyglot 。由于 Truffle 和 Graal 在 JVM 上运行,因此使用 jvm 参数很重要,即使我们可能没有在代码中直接使用 Java。

现在让我们从浏览器访问这个新端点。以下屏幕截图显示了预期的输出:

读书笔记《supercharge-your-applications-with-graalvm》第 6 章 Truffle 支持多语言多语言

图 7.2 – /poly 端点结果截图

您可能已经注意到,我们第一次调用时,加载页面需要时间,但随后的调用是即时的。让我们用 curl 计时 (curl 是一个调用任何 URL 的命令行实用程序。请参考 https://curl.se/ 了解有关 curl 以及如何在您的机器上安装 curl 的更多详细信息)。以下是一系列 curl 命令的屏幕截图:

读书笔记《supercharge-your-applications-with-graalvm》第 6 章 Truffle 支持多语言多语言

图 7.3 – Node.js 在后续调用后的性能

我们可以看到 初始负载在 CPU 上,但随后的调用很快,没有额外的 CPU 负载。

现在让我们探索多语言互操作性的更高级特性。 JavaScript 和 Java 的互操作性非常复杂。让我们用比列表更复杂的实现来探索这些概念。

JavaScript embedded code in Java

让我们回顾一下我们在前几章中使用的 FibonaaciCalculator.java 文件。让我们修改 FibonacciCalculator.java 以使用 JavaScript 片段并在 Java 中执行该 JavaScript 片段。

这是 FibonacciCalculator 的修改版本,带有嵌入的 JavaScript 片段。 Java 文件名为 FibonacciCalculatorPolyglot.java。您可以在 Git 存储库中找到完整的代码:

import org.graalvm.polyglot.*;
import org.graalvm.polyglot.proxy.*;

我们必须导入 polyglot 类。这实现了 Truffle 互操作性:

public class FibonacciCalculatorPolyglot{
    static String JS_SNIPPET =     "(function logTotalTime(param){console.log('total(from         JS) : '+param);})";
    public int[] findFibonacci(int count) {
        int fib1 = 0;
        int fib2 = 1;
        int currentFib, index;
        int [] fibNumbersArray = new int[count];
        for(index=2; index < count; ++index ) {    
            currentFib = fib1 + fib2;    
            fib1 = fib2;    
            fib2 = currentFib;    
            fibNumbersArray[index - 1] = currentFib;
        }
        return fibNumbersArray;
    }

现在让我们定义main()函数,它将调用findFibonacci() 几次达到编译器阈值:

    public static void main(String args[]){    
        FibonacciCalculatorPolyglot fibCal             = new FibonacciCalculatorPolyglot();
        long startTime = System.currentTimeMillis();
        long now = 0;
        long last = startTime;
        for (int i = 1000000000; i < 1000000010; i++) {
            int[] fibs = fibCal.findFibonacci(i);
            
            long total = 0;
            for (int j=0; j<fibs.length; j++) {
                total += fibs[j];
            }
            now = System.currentTimeMillis();
            System.out.printf("%d (%d ms)%n", i , now – last);
            last = now;
        }
        long endTime = System.currentTimeMillis();
        long totalTime =             System.currentTimeMillis() - startTime;
        System.out.printf("total (from Java): (%d ms)%n",                          totalTime);  
        try (Context context = Context.create()) {
            Value value = context.eval("js", JS_SNIPPET);
            value.execute(totalTime);
        }
    }
}

让我们探索这段代码。我们定义了一个静态 String 变量来保存 JavaScript 片段,如下所示:

static String JS_SNIPPET = "(function logTotalTime(param){console.log('total(from JS) : '+param);})";

我们用一个简单的 JavaScript 函数定义了一个静态 String,该函数打印传递给它的任何参数。要在 Java 中调用此 JavaScript 代码,我们首先需要通过导入以下包来导入 Polyglot 库:

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

要调用 JavaScript 代码,我们首先需要创建一个 org.graalvm.polyglot.Context 类的实例。 Context 对象提供多语种上下文以允许来宾语言代码以宿主语言运行。多语言上下文表示所有已安装和允许的语言的全局运行时状态。

使用 Context 对象的最简单方法是创建 Context 对象并使用 eval Context 对象中的 () 函数,执行其他语言代码。以下是代码片段 ,我们在其中执行 Java 中的 JavaScript 代码片段。在这种情况下,来宾语言是 JavaScript,它作为参数 "js" 在中的 eval 方法中传递宿主语言,Java:

try (Context context = Context.create()) {
    Value value = context.eval("js", JS_SNIPPET);
    value.execute(totalTime);
}

现在让我们执行这段代码。以下是执行后的输出截图:

读书笔记《supercharge-your-applications-with-graalvm》第 6 章 Truffle 支持多语言多语言

图 7.4 – 显示 Java 和 JavaScript 输出的 FibonacciCalculatorPolyglot 的输出

正如您在输出中看到的,我们打印了两个总数,一个是用 Java 代码打印的,另一个是从 JavaScript 代码打印的。

这开辟了很多可能性——想象一下在 Node.js Web 应用程序中运行用 Python 或 R 编写的机器学习代码。我们将各种语言的最佳功能集中在一个 VM 中。

Context 对象有ContextBuilder,可以用来设置具体的环境属性。以下是它可以设置的一些属性,相关的Context创建代码。此可用于控制访客语言对主机的访问。控制访问的代码是Context.newBuilder().allowXXX().build()。以下是可用于更精细访问控制的各种 allowXXX 方法:

  • allowAllAccess(boolean):这是默认值。它提供对客人语言的所有访问。
  • allowCreateProcess(boolean):为来宾语言提供控制访问以创建新进程。
  • allowCreateThread(boolean):为来宾语言提供控制访问以创建新线程。
  • allowEnvironmentAccess(EnvironmentAccess):允许使用提供的策略控制对环境的访问。
  • allowHostClassLoading(boolean):这允许来宾语言通过 JAR 或类文件加载新的宿主类。
  • allowIO(boolean):控制访问以执行 I/O 操作。如果为 true,则来宾语言可以在主机系统上执行不受限制的 I/O 操作。
  • allowNativeAccess(boolean):控制来宾语言访问本机界面。
  • allowPolyglotAccess(PolyglotAccess):使用提供的策略控制多语言访问。 PolyglotAccess 可用于定义自定义多语言访问策略,以更好地控制数据、绑定和代码执行。这是一个自定义实现,来宾语言可以使用 PolyglotAccess 构建器构建它。

参考 Javadoc (https://www.graalvm.org/ truffle/javadoc/org/graalvm/polyglot/Context.html)了解有关其他方法的更多详细信息。给来宾语言所有访问权限是有风险的;根据要求提供精细和特定的访问总是更好。

以下是我们如何构建具有特定访问权限的 Context 对象的示例:

Context context = Context.newBuilder().allowIO(true).build();

我们还可以使用以下代码片段加载外部文件,这是嵌入代码的推荐方式。将其他语言代码作为字符串复制粘贴到宿主语言中并不是一个好习惯。使代码保持最新和无错误是配置管理的噩梦,因为其他语言的代码可能由不同的开发人员开发。

以下代码片段显示了如何将源代码作为文件加载,而不是在主机源代码中嵌入来宾语言代码:

Context ctx =    Context.newBuilder().allowAllAccess(true).build();
    File path = new File("/path/to/scriptfile");
    Source pythonScript =         Source.newBuilder("python", new File(path,            "pythonScript.py")).build();
    ctx.eval(pythonScript)

在本节中,我们看到了如何从 Java 调用 JavaScript 代码。现在让我们尝试从 JavaScript 调用 Java 类。

Calling a Java class from JavaScript/Node.js

现在我们已经了解了 Java 代码如何运行 JavaScript 代码,让我们尝试从 JavaScript 调用 Java 代码。这是一个非常简单的 Java 应用程序,它在控制台上打印传递给它的参数。 Java 文件的名称是 HelloGraalPolyglot.java

public class HelloGraalPolyglot {
    public static void main(String[] args) {
        System.out.println(args[0]);
    }
}

让我们用 javac HelloGraalPolyglot.java 编译这个应用程序。

现在让我们尝试从 JavaScript 调用这个 应用程序。以下是 JavaScript 代码 hellograalpolyglot.js

var hello = Java.type('HelloGraalPolyglot');
hello.main(["Hello from JavaScript"]);

这是非常简单的 JavaScript 代码。我们正在使用 JavaScript 中的 Java.type() 方法加载 Java 类,并使用 main() 方法调用String 参数,并传递字符串 "Hello from JavaScript"

要执行这个 JavaScript,我们必须传递 --jvm 参数和 --vm.cp 来设置类路径。这是命令:

js --jvm --vm.cp=. hellograalpolyglot.js

下面显示了执行此命令的输出:

js --jvm --vm.cp=. hellograalpolyglot.js
Hello from JavaScript

这是一个非常简单的例子。为了了解如何传递参数以及如何在 JavaScript 中捕获和使用方法返回数据,让我们尝试调用 findFibonacci() 方法>FibonacciCalculator.java 代码,来自 Node.js 应用程序。我们将传递一个参数并从该方法中获取一个数组,我们将其呈现为网页。

让我们修改 index.js 并添加另一个端点 /fibonacci。这是完整的源代码:

app.get('/fibonacci', function(request, response) {
    var fibonacciCalculatorClass =        Java.type("FibonacciCalculatorPolyglot");
    var fibonacciCalculatorObject = new         fibonacciCalculatorClass();
    //fibonacciCalculatorClass.class.static.main([""]);
    var array =        fibonacciCalculatorObject.findFibonacci(10);
    var responseString =         "<h1>Hello Graal Polyglot - Fibonacci numbers </h1>";
    responseString = responseString + "<ul>";
    for (let index = 0; index < array.length; index++) {
        responseString = responseString + "<li>";
        responseString = responseString + array[index];
        responseString = responseString + "</li>";
    }
    responseString = responseString + "</ul>";
    response.send(responseString);
});

在这个node.js代码中,我们首先加载了Java类FibonacciCalculatorPolyglot 使用 Java.Type() 方法。然后我们正在创建这个类的一个实例并直接调用该方法。我们知道,输出是一个数组。我们正在遍历数组并将结果打印为 HTML 列表。

让我们使用以下命令运行此代码:

/Library/Java/JavaVirtualMachines/graalvm-ee-java11-21.0.0.2/Contents/Home/bin/node --jvm --polyglot index.js
Started the server at 8080

现在让我们访问 http://localhost:8080/fibonacci。这是输出的屏幕截图:

读书笔记《supercharge-your-applications-with-graalvm》第 6 章 Truffle 支持多语言多语言

图 7.5 – Node.js 应用程序调用 FibonacciCalculator 方法的输出截图

前面的 屏幕截图显示了 Node.js/Fibonacci 端点的工作情况,它将前 10 个斐波那契数列为 HTML 列表。

在本节中,我们了解了如何在 Java 中运行 JavaScript 片段、从 JavaScript 调用 Java 并调用 Java 方法、传递参数以及从 Node.js 应用程序的 Java 方法中获取结果。让我们快速总结一下各种 JavaScript 互操作性 特性:

  • 当我们想从 JavaScript 调用 Java 代码时,我们传递 --jvm 参数并设置 CLASSPATH 以使用--vm.cp
  • 我们使用 Java 中的 polyglot Context 对象来运行其他语言代码。有一个特殊的 ScriptEngine 对象用于在 Java 中运行 JavaScript。 Context 对象包装了它并且是推荐的运行方式。
  • 我们使用 Java.type() 从 JavaScript/Node.js 加载 Java 类。
  • 我们可以使用 new 来创建类的实例。
  • GraalVM 负责 Java 和 JavaScript 之间的类型转换。在可能丢失数据的情况下(例如,从 long 转换为 intTypeError
  • Java 包解析可以通过在调用 Java.type() 时提供完整的包路径来完成。
  • 异常处理可以自然地使用 Java 和 JavaScript 中的 try{}catch 块来完成。 GraalVM 负责转换异常。
  • 在前面的示例中,我们研究了 Java 数组如何被 JavaScript 迭代。同样,Hashmap 也可以使用 put()get()< /code> 方法。
  • Java 代码可以将 JavaScript 对象作为 com.oracle.truffle.api.interop.java.TruffleMap 类的实例进行访问。

在本节中,我们研究了 我们如何在 Java 和 JavaScript 之间进行互操作。现在让我们探索如何构建多语言原生图像。

Polyglot native images

Graal 还支持创建多语言应用程序的原生图像。要创建这个 Java 类的原生镜像,我们必须使用 --language 参数来构建原生镜像。以下是我们可以传递给 native-image(Native Image builder)的各种语言标志。在 第 5 章中,Graal Ahead-of-Time Compiler and Native Image ,我们详细介绍了 Native Image builder:

--language:nfi
--language:python
--language:regex
--language:wasm
--language:java
--language:llvm
--language:js
--language:ruby

在我们的示例中,我们必须通过 --language:js 让 Native Image builder 知道我们在 Java 代码中使用 JavaScript。所以,我们需要执行以下命令:

native-image --language:js FibonacciCalculatorPolyglot

以下是执行命令后输出的截图:

读书笔记《supercharge-your-applications-with-graalvm》第 6 章 Truffle 支持多语言多语言

图 7.6 – Polyglot Native Image 构建输出截图

Native Image builder 执行静态代码分析并构建我们的多语言应用程序的最佳图像。我们应该能够在目录中找到可执行的 fibonaccicalculatorpolyglot 文件。让我们使用以下命令执行本机映像:

./fibonaccicalculatorpolyglot

下图是我们运行原生镜像时输出的截图:

读书笔记《supercharge-your-applications-with-graalvm》第 6 章 Truffle 支持多语言多语言

图 7.7 – Polyglot Native Image 执行结果截图

(在本例中,您可能会发现代码的执行速度比 JIT 模式下要慢。请参阅 第 4 章 Graal Just-In-Time Compiler,详细了解为什么会发生这种情况。)

Bindings

绑定对象充当 Java 和 JavaScript 之间的 中间层,以访问语言之间的方法、变量和对象。为了理解绑定是如何工作的,让我们编写一个非常简单的 JavaScript 文件,它包含三个方法 - add()subtract()、和 multiply()。这三个方法都访问两个数字并返回一个数字。我们还有一个包含简单字符串的变量。这是 JavaScript 代码,Math.js

var helloMathMessage = " Hello Math.js Variable";
function add(a, b) {
    return a+b;
}
function subtract(a, b) {
    return a-b;
}
function multiply(a, b) {
    return a*b;
}

这段 JavaScript 代码非常简单明了。

现在让我们编写一个 简单的 Java 类,它加载这个 JavaScript 文件并通过传递整数参数调用方法,并打印 JavaScript 方法返回的结果。此类还访问变量 helloMathMessage 并打印它。

让我们通过代码来了解它是如何工作的。这是代码,MathJSCaller.java

import java.io.File;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;

我们正在导入所有实现 Truffle 互操作性的多语言类:

public void runMathJS() {
  Context ctx = Context.create("js");
  try {
    File mathJSFile = new File("./math.js");
    ctx.eval(Source.newBuilder("js", mathJSFile).build());

在前面的代码中,我们正在创建 Context 对象并加载 JavaScript 文件并构建它。加载 JavaScript 后,要访问 JavaScript 文件中的方法成员和变量成员,我们使用 Context.getBindings()。绑定提供了一个允许多语言访问数据和方法成员的层:

    Value addFunction =        ctx.getBindings("js").getMember("add");
    Value subtractFunction =        ctx.getBindings("js").getMember("subtract");
    Value multiplyFunction =         ctx.getBindings("js").getMember("multiply");
    Value helloMathMessage =        ctx.getBindings("js").getMember("helloMathMessage");
    System.out.println("Binding Keys :" +        ctx.getBindings("js").getMemberKeys());

我们只是打印 绑定键以查看所有成员都暴露于什么。现在,让我们通过调用方法和访问变量来访问成员:

    Integer addResult = addFunction.execute(30, 20).asInt();
    Integer subtractResult = subtractFunction.execute(30,            20).asInt();
    Integer multiplyResult = multiplyFunction.execute(30,            20).asInt();
    System.out.println(("Add Result "+ addResult+ "        Subtract Result "+ subtractResult+ " Multiply         Result "+ multiplyResult));
    System.out.println("helloMathMessage : " +        helloMathMessage.toString());
}

最后,我们打印所有结果。 技术要求部分提供的 Git 存储库链接提供了完整的源代码。

现在,让我们运行这个应用程序。以下屏幕截图显示了输出:

读书笔记《supercharge-your-applications-with-graalvm》第 6 章 Truffle 支持多语言多语言

图 7.8 MathJSCaller 执行结果

我们可以看到我们的 程序正在运行。它可以加载 JavaScript math.js 文件并调用所有方法。我们还看到了绑定键列表,我们通过调用 System.out.println("Binding Keys :" + ctx.getBindings("js").getMemberKeys());System.out.println("Binding Keys :" + ctx.getBindings("js").getMemberKeys()); 。我们可以看到列表有四个键,它们与我们在 math.js 文件中的内容相匹配。

在此示例中,我们看到了绑定对象如何充当从 Java 访问 JavaScript 成员的接口。

Multithreading

GraalVM 上的 JavaScript 支持多线程。在本节中,我们将探讨在 Java 和 JavaScript 之间的多语言上下文中支持的各种模式。

在线程中创建的 JavaScript 对象只能在该线程中使用,不能从另一个线程访问。例如,在我们的示例中,Value 对象如 addFunctionsubtractFunction ,等等只能与该线程一起使用。

让我们修改我们的 MathJSCaller 类的 runMathJS() 方法来无限期地运行一个线程,来模拟一个并发访问的情况。让我们修改前面的代码,在单独的线程中调用成员函数。这是代码片段:

    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                Integer addResult =                     addFunction.execute(30, 20).asInt();
                Integer subtractResult =                    subtractFunction.execute(30, 20).asInt();
                Integer multiplyResult =                    multiplyFunction.execute(30, 20).asInt();
            }
        }
    });
    thread.start();

我们在单独的线程中复制了对 成员方法的访问。现在让我们在一个循环中调用它,以模拟并发访问,在线程内和线程外使用相同的 Context 对象。以下代码片段显示了使用相同 Context 对象的线程外调用:

    while (true) {
        Integer addResult =             addFunction.execute(30, 20).asInt();
        Integer subtractResult =            subtractFunction.execute(30, 20).asInt();
        Integer multiplyResult =            multiplyFunction.execute(30, 20).asInt();
        }
        }  catch (Exception e) {
            System.out.println("Exception : " );
            e.printStackTrace();
        }
    }

当我们运行这段代码时,在某个时刻,当两个线程同时访问对象时,我们应该得到以下异常:

$ java MathJSCallerThreaded (docker-desktop/bozo-book-library-dev)
Binding Keys :[helloMathMessage, add, subtract, multiply]
java.lang.IllegalStateException: Multi threaded access requested by thread Thread[Thread-3,5,main] but is not allowed for language(s) js.
…..

为了克服这个问题,建议使用隔离的运行时。我们可以为每个线程创建单独的 Context 对象,并创建这些对象的新实例并在该线程中使用它们。这是固定代码:

    public void runMathJS() {
        Context ctx = Context.create("js");
        try {
            File mathJSFile = new File("./math.js");
            ctx.eval(Source.newBuilder                ("js", mathJSFile).build());
            Value addFunction =               ctx.getBindings("js").getMember("add");
            Value subtractFunction =               ctx.getBindings("js").getMember("subtract");
            Value multiplyFunction =               ctx.getBindings("js").getMember("multiply");
            Value helloMathMessage =               ctx.getBindings("js")               .getMember("helloMathMessage");
            System.out.println("Binding Keys :" + ctx.            getBindings("js").getMemberKeys());
            while (true) {
                Integer addResult =                     addFunction.execute(30, 20).asInt();
                Integer subtractResult =                    subtractFunction.execute(30, 20).asInt();
                Integer multiplyResult =                    multiplyFunction.execute(30, 20).asInt();
}

现在,在线程中,我们正在 创建一个单独的 Context 对象。以下代码片段显示了更新后的代码:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            Context ctx = Context.create("js");
            ctx.eval(Source.newBuilder("js",                mathJSFile).build());
            Value addFunction =               ctx.getBindings("js").getMember("add");
            Value subtractFunction =               ctx.getBindings("js").getMember("subtract");
            Value multiplyFunction =               ctx.getBindings("js").getMember("multiply");
            Value helloMathMessage =               ctx.getBindings("js")               .getMember("helloMathMessage");
            while (true) {
                Integer addResult =                addFunction.execute(30, 20).asInt();
                Integer subtractResult =                  subtractFunction.execute(30, 20).asInt();
                Integer multiplyResult =                  multiplyFunction.execute(30, 20).asInt();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});
thread.start();

如我们所见,在这段代码中,我们在线程内创建了一个单独的 context 对象,该对象是线程本地的。这不会产生异常。

this 的另一个解决方案是在正确的 同步 中访问 context 对象块或方法,以便不会同时访问运行时。这是更新后的代码,带有 synchronized 块:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            // Solution 2
            while (true) {
                synchronized(ctx) {
                Integer addResult =                     addFunction.execute(30, 20).asInt();
                Integer subtractResult =                    subtractFunction.execute(30, 20).asInt();
                Integer multiplyResult =                    multiplyFunction.execute(30, 20).asInt();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});
thread.start();

我们还可以将整个 块包含为同步块,仍然使用相同的 Context 对象:

while (true) {
    synchronized(ctx) {
        Integer addResult =             addFunction.execute(30, 20).asInt();
        Integer subtractResult =            subtractFunction.execute(30, 20).asInt();
        Integer multiplyResult =            multiplyFunction.execute(30, 20).asInt();
    }
}

这也可以正常运行,但可能比以前的解决方案运行得慢,因为 Context 对象上可能有很多锁。

Java 对象是线程安全的,因此可以在运行不同线程的 JavaScript 运行时之间访问 Java 对象。

Asynchronous programming – Promise and await

异步编程在现代分布式应用程序中非常突出。 JavaScript 使用 PromisePromise 对象 表示异步活动的完成以及最终值。 Promise 对象具有三种状态:

  • Pending:这个状态是初始状态。
  • 已完成:此状态表示操作已成功执行。
  • Rejected:此状态表示操作失败。

有时,我们可能有 让 JavaScript 创建一个 promise 并且逻辑可能在 Java 代码中运行,并且当 Java 代码完成后,它可能必须履行或拒绝承诺。为了处理这个问题,Graal 提供了一个 PromiseExecuter 接口。 Java 类必须实现这个接口方法,void onPromiseCreation(Value onResolve, Value onReject);。 JavaScript 可以使用实现此接口的 Java 类来创建 Promise 对象。 JavaScript 可以在实现 void 的 Java 对象上调用 await 然后 (Value onResolve, Value onReject ); 来实现 JavaScript 和 Java 之间的异步编程。

Summary

在本章中,我们详细介绍了 GraalVM/Truffle for JavaScript 和 Node.js 的各种多语言互操作性和嵌入特性。我们通过一些真实的代码示例探索了所有关键概念,以清楚地了解 JavaScript 和 Node.js 如何调用、传递数据以及与其他语言代码互操作。这是 GraalVM 的显着特点之一。

本章中给出的示例将帮助您在同一运行时构建和运行使用 Java 和 JavaScript 语言编写的多语言应用程序。

在下一章中,我们将继续探索 R、Python 和最新的 Java on Truffle。

Questions

  1. 什么 JavaScript 对象和方法用于运行其他语言代码?
  2. Java 中的 Context 对象是什么?
  3. 您如何控制访客语言对主机的访问?
  4. 您如何构建多语言应用程序的本机映像?
  5. 什么是绑定?

Further reading