读书笔记《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 支持的各种来宾语言。要尝试代码,您将需要以下内容:
- 各种语言的 Graal 运行时:我们将在本章中介绍如何安装和运行这些运行时。
- 访问 GitHub:有一些示例代码片段,可在 Git 存储库中找到。代码可以从以下链接下载。您将在
chapter7
目录下找到特定章节的代码:https://github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM/tree/main/Chapter07/js。 - 本章的 Code in Action 视频可在 https://bit.ly/3yqu4ui 找到。
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):
我们还可以通过执行 node --version
命令调用特定的 node.js 版本来确保我们正在运行正确版本的 Node.js。在以下命令中,我们显式调用了正确的版本。请注意,您的 GraalVM 主位置可能会有所不同:
我们还通过执行npm --version
确保NPM正常工作命令。 下面是命令和输出:
现在我们已经验证了 JavaScript、Node.js 和 npm 的安装,让我们创建一个简单的 Node.js 应用程序。
转到应用程序文件夹,然后执行 npm init
。这将为 Node.js 应用程序设置样板配置。我们将应用程序命名为 graal-node-app
。下面显示了控制台输出:
根据我们选择的选项,这将创建一个 Node.js 应用程序和一个带有样板配置的 package.json
文件。让我们通过执行 npm install --save express
来安装 express
包。这会在应用程序文件夹中安装 express
包并更新 package.json
文件(因为 --save
参数)。这是输出:
您将找到 node_modules
目录,其中 包含运行我们的应用程序所需的所有包。让我们使用以下代码创建一个 index.js
文件:
如您所见,这是一个非常简单的应用程序,当在根目录调用时,它以 HTML Hello Graal Node
作为标题 1 进行响应。应用程序将侦听端口号 8080
。
让我们使用以下命令运行此应用程序:
现在我们可以看到输出,我们知道应用程序正在监听 8080
。让我们尝试从网络浏览器 http://localhost:8080/ 调用它。以下是网页浏览器上应用响应的截图:
现在我们知道 GraalVM 上的 Node.js 运行良好,让我们了解多语言互操作性。
JavaScript interoperability
在第 6 章中,Truffle 支持多语言(多语言),我们详细介绍了 Truffle 如何启用多语言支持并提供多语言互操作性和多语言嵌入的基础设施。在本节中,我们将通过示例代码探索这些功能。
让我们以我们在上一节中创建的 Node.js 应用程序为例,在我们的 index.js
/poly
代码>文件。让我们创建一个简单的 Python 数组对象,并在对象中存储一些 数字。然后,我们将在 Node.js 中遍历这个 Python 对象,以列出这些数字。这显示了我们如何在 JavaScript 中嵌入 Python 代码片段。
以下源代码显示了这个新端点 /poly
:
现在让我们的代码监听端口 8080
并在收到请求时调用前面的函数:
正如您在代码中所见,我们使用 Polyglot.eval()
方法来运行 Python 代码。为了让 polyglot 对象知道它是 Python 代码,我们将 python
作为参数传递,并传递数组的 Python 表示形式。现在让我们用 node
运行这段代码:
请注意,我们必须将 --jvm
和 --polyglot
参数传递给节点。传递这些参数非常重要。 --jvm
告诉节点在Java虚拟机上运行(JVM) 和 --polyglot
,顾名思义,告诉节点支持 polyglot
。由于 Truffle 和 Graal 在 JVM 上运行,因此使用
jvm
参数很重要,即使我们可能没有在代码中直接使用 Java。
现在让我们从浏览器访问这个新端点。以下屏幕截图显示了预期的输出:
您可能已经注意到,我们第一次调用时,加载页面需要时间,但随后的调用是即时的。让我们用 curl 计时 (curl 是一个调用任何 URL 的命令行实用程序。请参考 https://curl.se/ 了解有关 curl 以及如何在您的机器上安装 curl 的更多详细信息)。以下是一系列 curl 命令的屏幕截图:
我们可以看到 初始负载在 CPU 上,但随后的调用很快,没有额外的 CPU 负载。
现在让我们探索多语言互操作性的更高级特性。 JavaScript 和 Java 的互操作性非常复杂。让我们用比列表更复杂的实现来探索这些概念。
JavaScript embedded code in Java
让我们回顾一下我们在前几章中使用的 FibonaaciCalculator.java
文件。让我们修改 FibonacciCalculator.java
以使用 JavaScript 片段并在 Java 中执行该 JavaScript 片段。
这是 FibonacciCalculator
的修改版本,带有嵌入的 JavaScript 片段。 Java 文件名为 FibonacciCalculatorPolyglot.java
。您可以在 Git 存储库中找到完整的代码:
我们必须导入 polyglot
类。这实现了 Truffle 互操作性:
现在让我们定义main()
函数,它将调用findFibonacci()
几次达到编译器阈值:
让我们探索这段代码。我们定义了一个静态 String
变量来保存 JavaScript 片段,如下所示:
我们用一个简单的 JavaScript 函数定义了一个静态 String
,该函数打印传递给它的任何参数。要在 Java 中调用此 JavaScript 代码,我们首先需要通过导入以下包来导入 Polyglot
库:
要调用 JavaScript 代码,我们首先需要创建一个 org.graalvm.polyglot.Context
类的实例。 Context
对象提供多语种上下文以允许来宾语言代码以宿主语言运行。多语言上下文表示所有已安装和允许的语言的全局运行时状态。
使用 Context
对象的最简单方法是创建 Context
对象并使用 eval
函数,执行其他语言代码。以下是代码片段 ,我们在其中执行 Java 中的 JavaScript 代码片段。在这种情况下,来宾语言是 JavaScript,它作为参数 Context
对象中的 ()"js"
在中的 eval
方法中传递宿主语言,Java:
现在让我们执行这段代码。以下是执行后的输出截图:
正如您在输出中看到的,我们打印了两个总数,一个是用 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
对象的示例:
我们还可以使用以下代码片段加载外部文件,这是嵌入代码的推荐方式。将其他语言代码作为字符串复制粘贴到宿主语言中并不是一个好习惯。使代码保持最新和无错误是配置管理的噩梦,因为其他语言的代码可能由不同的开发人员开发。
以下代码片段显示了如何将源代码作为文件加载,而不是在主机源代码中嵌入来宾语言代码:
在本节中,我们看到了如何从 Java 调用 JavaScript 代码。现在让我们尝试从 JavaScript 调用 Java 类。
Calling a Java class from JavaScript/Node.js
现在我们已经了解了 Java 代码如何运行 JavaScript 代码,让我们尝试从 JavaScript 调用 Java 代码。这是一个非常简单的 Java 应用程序,它在控制台上打印传递给它的参数。 Java 文件的名称是 HelloGraalPolyglot.java
:
让我们用 javac HelloGraalPolyglot.java
编译这个应用程序。
现在让我们尝试从 JavaScript 调用这个 应用程序。以下是 JavaScript 代码 hellograalpolyglot.js
:
这是非常简单的 JavaScript 代码。我们正在使用 JavaScript 中的 Java.type()
方法加载 Java 类,并使用 main()
方法调用String
参数,并传递字符串 "Hello from JavaScript"
。
要执行这个 JavaScript,我们必须传递 --jvm
参数和 --vm.cp
来设置类路径。这是命令:
下面显示了执行此命令的输出:
这是一个非常简单的例子。为了了解如何传递参数以及如何在 JavaScript 中捕获和使用方法返回数据,让我们尝试调用 findFibonacci()
方法>FibonacciCalculator.java 代码,来自 Node.js 应用程序。我们将传递一个参数并从该方法中获取一个数组,我们将其呈现为网页。
让我们修改 index.js
并添加另一个端点 /fibonacci
。这是完整的源代码:
在这个node.js
代码中,我们首先加载了Java类FibonacciCalculatorPolyglot
使用 Java.Type()
方法。然后我们正在创建这个类的一个实例并直接调用该方法。我们知道,输出是一个数组。我们正在遍历数组并将结果打印为 HTML 列表。
让我们使用以下命令运行此代码:
现在让我们访问 http://localhost:8080/fibonacci。这是输出的屏幕截图:
前面的 屏幕截图显示了 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
转换为int
)TypeError
。 - 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:js
让 Native Image builder 知道我们在 Java 代码中使用 JavaScript。所以,我们需要执行以下命令:
Native Image builder 执行静态代码分析并构建我们的多语言应用程序的最佳图像。我们应该能够在目录中找到可执行的 fibonaccicalculatorpolyglot
文件。让我们使用以下命令执行本机映像:
(在本例中,您可能会发现代码的执行速度比 JIT 模式下要慢。请参阅 第 4 章 ,Graal Just-In-Time Compiler,详细了解为什么会发生这种情况。)
Bindings
绑定对象充当 Java 和 JavaScript 之间的 中间层,以访问语言之间的方法、变量和对象。为了理解绑定是如何工作的,让我们编写一个非常简单的 JavaScript 文件,它包含三个方法 - add()
、subtract()
、和 multiply()
。这三个方法都访问两个数字并返回一个数字。我们还有一个包含简单字符串的变量。这是 JavaScript 代码,Math.js
:
这段 JavaScript 代码非常简单明了。
现在让我们编写一个 简单的 Java 类,它加载这个 JavaScript 文件并通过传递整数参数调用方法,并打印 JavaScript 方法返回的结果。此类还访问变量 helloMathMessage
并打印它。
让我们通过代码来了解它是如何工作的。这是代码,MathJSCaller.java
:
我们正在导入所有实现 Truffle 互操作性的多语言类:
在前面的代码中,我们正在创建 Context
对象并加载 JavaScript 文件并构建它。加载 JavaScript 后,要访问 JavaScript 文件中的方法成员和变量成员,我们使用 Context.getBindings()
。绑定提供了一个允许多语言访问数据和方法成员的层:
我们只是打印 绑定键以查看所有成员都暴露于什么。现在,让我们通过调用方法和访问变量来访问成员:
最后,我们打印所有结果。 技术要求部分提供的 Git 存储库链接提供了完整的源代码。
现在,让我们运行这个应用程序。以下屏幕截图显示了输出:
我们可以看到我们的 程序正在运行。它可以加载 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
对象如 addFunction
、subtractFunction
,等等只能与该线程一起使用。
让我们修改我们的 MathJSCaller
类的 runMathJS()
方法来无限期地运行一个线程,来模拟一个并发访问的情况。让我们修改前面的代码,在单独的线程中调用成员函数。这是代码片段:
我们在单独的线程中复制了对 成员方法的访问。现在让我们在一个循环中调用它,以模拟并发访问,在线程内和线程外使用相同的 Context
对象。以下代码片段显示了使用相同 Context
对象的线程外调用:
当我们运行这段代码时,在某个时刻,当两个线程同时访问对象时,我们应该得到以下异常:
为了克服这个问题,建议使用隔离的运行时。我们可以为每个线程创建单独的 Context
对象,并创建这些对象的新实例并在该线程中使用它们。这是固定代码:
现在,在线程中,我们正在 创建一个单独的 Context
对象。以下代码片段显示了更新后的代码:
如我们所见,在这段代码中,我们在线程内创建了一个单独的 context
对象,该对象是线程本地的。这不会产生异常。
this 的另一个解决方案是在正确的 同步
中访问 context
对象块或方法,以便不会同时访问运行时。这是更新后的代码,带有 synchronized
块:
我们还可以将整个 块包含为同步块,仍然使用相同的 Context
对象:
这也可以正常运行,但可能比以前的解决方案运行得慢,因为 Context
对象上可能有很多锁。
Java 对象是线程安全的,因此可以在运行不同线程的 JavaScript 运行时之间访问 Java 对象。
Asynchronous programming – Promise and await
异步编程在现代分布式应用程序中非常突出。 JavaScript 使用 Promise
。 Promise
对象 表示异步活动的完成以及最终值。 Promise
对象具有三种状态:
有时,我们可能有 让 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 之间的异步编程。
Further reading
- GraalVM 企业版 (https://docs.oracle.com/en/graalvm /enterprise/19/index.html)
- JavaScript 和 Node.js 参考(https://www.graalvm.org/reference-manual/js /)
- Truffle:自优化运行时系统(https://lafo.ssw.uni-linz.ac.at/pub/papers/2012_SPLASH_Truffle.pdf)
- Truffle 语言实现框架的对象存储模型 (https://chrisseaton.com/rubytruffle/pppj14-om/pppj14-om.pdf)