不久前 Oracle 发布了 GraalVM,一套通用型虚拟机,能执行各类高性能与互操作性任务,并在无需额外成本的前提下允许用户构建多语言应用程序。
GraalVM 包含了很多不同的部分,所以即使你之前听过这个名字,或者听过我们的一些演讲,仍然有一些东西是你不知道的。在本文中,我们将列出 GraalVM 的一些不同的特性,并展示它的用途。
- 高性能 Java
- 占用内存小、启动速度快的 Java
- 组合 JavaScript、Java、Ruby 和 R 语言
- 在 JVM 上运行本地语言
- 适用于所有编程语言的工具
- 扩展基于 JVM 的应用程序
- 扩展本地应用程序
- 将 Java 代码作为本地库
- 数据库中的 polyglot
- 创建自己的语言
我们可以使用 GraalVM 1.0.0 RC 1( https://www.graalvm.org/downloads )来重现本文所述的内容。我是在 macOS 上运行 GraalVM 企业版,不过在 Linux 上运行 GraalVM 社区版也是一样的。文中运行的代码可以从 http://github.com/chrisseaton/graalvm-ten-things 下载。
安装
我从 https://www.graalvm.org/downloads 下载了 GraalVM 1.0.0 RC 1 企业版,并将它放到 $PATH 路径中。
$ git clone https://github.com/chrisseaton/graalvm-ten-things.git $ cd foo $ tar -zxf graalvm-ee-1.0.0-rc1-macos-amd64.tar.gz # or graalvm-ee-1.0.0-rc1-linux-amd64.tar.gz on Linux $ export PATH=graalvm-1.0.0-rc1/Contents/Home/bin:$PATH # or PATH=graalvm-1.0.0-rc1/bin:$PATH on Linux
GraalVM 内置了 JavaScript,并带有一个叫作 gu 的软件包管理器,可用它来安装其他语言。我已经安装了从 GitHub 下载的 Ruby、Python 和 R 语言。
$ gu install -c org.graalvm.ruby $ gu install -c org.graalvm.python $ gu install -c org.graalvm.R
我们可以通过运行 java 或 js 来获得这些运行时的版本信息。
$ java -version java version "1.8.0_161" Java(TM) SE Runtime Environment (build 1.8.0_161-b12) GraalVM 1.0.0-rc1 (build 25.71-b01-internal-jvmci-0.42, mixed mode) $ js --version Graal JavaScript 1.0 (GraalVM 1.0.0-rc1)
1. 高性能 Java
GraalVM 中的 Graal 得名于 Graal 编译器。Graal 是一种“万能”编译器,也就是说,虽然它是单一的实现,却可以用于很多用途。例如,我们可以使用 Graal 进行预编译(ahead-of-time)和即时编译(just-in-time),也可用于编译多种编程语言。
我们可以将 Graal 简单地用作 Java JIT 编译器。
以下的示例程序将会输出一篇文档的前十个单词,它使用了 Stream 和 Collector 等 Java 语言特性。
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; public class TopTen { public static void main(String[] args) { Arrays.stream(args) .flatMap(TopTen::fileLines) .flatMap(line -> Arrays.stream(line.split("\\b"))) .map(word -> word.replaceAll("[^a-zA-Z]", "")) .filter(word -> word.length() > 0) .map(word -> word.toLowerCase()) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet().stream() .sorted((a, b) -> -a.getValue().compareTo(b.getValue())) .limit(10) .forEach(e -> System.out.format("%s = %d%n", e.getKey(), e.getValue())); } {1} private static Stream<String> fileLines(String path) { try { return Files.lines(Paths.get(path)); } catch (IOException e) { throw new RuntimeException(e); } } }
GraalVM 包含了一个 javac 编译器,但在本例中,它与标准的编译器并没有什么区别。因此,如果你愿意,也可以使用系统的 javac。
$ javac TopTen.java
如果我们运行 GraalVM 提供的 java 命令,将会自动调用 Graal JIT 编译器,不需要做额外的配置。我使用 time 命令来获得整个程序从开始到运行结束所花费的时间,而不是进行复杂的微基准测试。我使用了大量的输入,这样就不用去纠结几秒钟的差别。我使用的 large.txt 文件大小为 150MB。
$ make large.txt $ time java TopTen large.txt sed = 502701 ut = 392657 in = 377651 et = 352641 id = 317627 eu = 317627 eget = 302621 vel = 300120 a = 287615 sit = 282613 real 0m17.367s user 0m32.355s sys 0m1.456s
Graal 是使用 Java 开发的,而其他大多数 Java JIT 编译器是使用 C++ 开发的。我们因此能够比其他编译器更快地改进它,而且它具备了强大的优化功能,比如 HotSpot 标准 JIT 编译器中所没有的部分转义分析功能。这项优化功能可以让 Java 程序的运行速度明显加快。
为了与不使用 Graal JIT 编译器时的速度进行比较,我使用 -XX:-UseJVMCICompiler 标记来运行程序。JVMCI 是 Graal 和 JVM 之间的接口。当然,我们也可以拿它与标准的 JVM 进行比较。
$ time java -XX:-UseJVMCICompiler TopTen large.txt sed = 502701 ut = 392657 in = 377651 et = 352641 id = 317627 eu = 317627 eget = 302621 vel = 300120 a = 287615 sit = 282613 real 0m23.511s user 0m24.293s sys 0m0.579s
结果显示,使用 Graal 运行程序的时间大约是标准 HotSpot 的四分之三。在习惯于将单个位数的百分比性能增长视为显著改进的今天,这个数字算得上是一个巨大的提升。
Twitter 是在生产环境中使用 Graal 的公司之一,他们表示,Graal 确确实实为他们省下了不少钱。Twitter 使用 Graal 来运行 Scala 应用程序。因为 Graal 执行的是 JVM 字节码,因此适用于任何基于 JVM 的语言。
这是 GraalVM 的第一个用途——将它作为 Java 应用程序的一个更好的 JIT 编译器。
2. 占用内存小、启动速度快的 Java
Java 对于长时间运行的进程来说是相当强大的,但短时间运行的进程可能会因较长的启动时间和较高的内存占用而饱受其苦。
例如,如果我们使用更小的输入来运行相同的应用程序(文件大小约为 1KB,而不是 150MB),似乎需要花费更长的时间,并且需要 60MB 的内存。我们使用 -l 选项打印出它所消耗的内存和运行时间。
$ make small.txt $ /usr/bin/time -l java TopTen small.txt # -v on Linux instead of -l sed = 6 sit = 6 amet = 6 mauris = 3 volutpat = 3 vitae = 3 dolor = 3 libero = 3 tempor = 2 suscipit = 2 0.32 real 0.49 user 0.05 sys 59846656 maximum resident set size ...
GraalVM 为我们提供了解决这个问题的工具。可以说 Graal 就是一个编译器软件包,有许多不同的使用方式,其中之一就是将源码预编译为本地可执行镜像,而不是在运行时进行即时编译。这与传统的编译器 gcc 类似。
$ native-image --no-server TopTen classlist: 1,513.82 ms (cap): 2,333.95 ms setup: 3,584.09 ms (typeflow): 4,642.13 ms (objects): 3,073.58 ms (features): 156.34 ms analysis: 8,059.94 ms universe: 353.02 ms (parse): 1,277.02 ms (inline): 1,412.08 ms (compile): 10,337.76 ms compile: 13,776.23 ms image: 2,526.63 ms write: 1,525.03 ms [total]: 31,439.47 ms
这个命令会生成一个名为 topten 的本地可执行文件。这个可执行文件并不是 JVM 的启动程序,它不需要链接到 JVM,也不以任何方式捆绑 JVM。native-image 会将 Java 代码和所使用的 Java 库直接编译成机器码。我们还使用了 SubstrateVM 虚拟机,它和 Graal 一样,也是用 Java 编写的。
如果我们看一下 topten 所使用的库,就会发现它们都只是标准的系统库。我们也可以将这个文件移动到一个从未安装过 JVM 的系统上运行,以此来验证它确实没有使用 JVM 或任何其他文件。它的体积很小——这个可执行文件不到 6MB。
$ otool -L topten # ldd topten on Linux topten: .../CoreFoundation.framework ... .../libz.1.dylib ... .../libSystem.B.dylib ... $ du -h topten 5.7M topten
与在 JVM 上运行相同的程序相比,可执行文件的启动速度快了一个数量级,使用的内存少了一个数量级。它的速度非常快,快到让你注意不到在命令行上所花费的时间,而且感觉不到停顿。
$ /usr/bin/time -l ./topten small.txt sed = 6 sit = 6 amet = 6 mauris = 3 volutpat = 3 vitae = 3 dolor = 3 libero = 3 tempor = 2 suscipit = 2 0.02 real 0.00 user 0.00 sys 4702208 maximum resident set size ...
不过,native-image 这个工具也存在一些约束,比如所有的类在编译期间都必须可用。除此之外,在反射方面也存在一些限制。除了基本的编译功能之外,它还提供了额外的高级特性,即在编译期间运行静态初始化器,以便在加载应用程序时可以少做一些事情。
这是 GraalVM 的第二个用途——以低内存占用和快速启动来运行现有的 Java 程序。它为我们省去了一些配置问题,比如如何在运行时查找正确的 jar 文件。它还能让 Docker 镜像的体积变得更小。
3. 组合 JavaScript、Java、Ruby 和 R 语言
除了 Java,GraalVM 还包含了 JavaScript、Ruby、R 语言和 Python 的实现。它们都是使用一个叫作 Truffle 的语言实现框架开发的,Truffle 让实现简单且高性能的语言解释器成为可能。在使用 Truffle 开发语言解释器时,会自动使用 Graal 作为 JIT 编译器。因此,Graal 不仅是 Java 的 JIT 编译器和预编译器,也可以是 JavaScript、Ruby、R 语言和 Python 的 JIT 编译器。
GraalVM 中的语言旨在成为现有语言的直接替代品。例如,我们可以安装一个 Node.js 模块:
$ npm install --global color ... + color@3.0.0 added 6 packages in 14.156s
我们可以使用此模块编写一个小程序,将 RGB HTML 颜色转换为 HSL:
var Color = require('color'); process.argv.slice(2).forEach(function (val) { print(Color(val).hsl().string()); });
然后用常规的方式运行它:
$ node color.js '#42aaf4' hsl(204.89999999999998, 89%, 60.8%)
GraalVM 提供了一个 API,用以在一门语言中运行另一门语言的代码。因此,我们可以使用多种语言来开发一个应用程序。
我们可能希望用一种语言开发应用程序的主要部分,同时又使用另一种语言的软件包。例如,假设我们用 Node.js 开发一个将 CSS 颜色名称转换为十六进制数的应用程序,但又希望使用 Ruby 颜色库来完成转换。
var express = require('express'); var app = express(); color_rgb = Polyglot.eval('ruby', ` require 'color' Color::RGB `); app.get('/css/:name', function (req, res) { color = color_rgb.by_name(req.params.name).html() res.send('<h1 style="color: ' + color + '" >' + color + '</h1>'); }); app.listen(8080, function () { console.log('serving at http://localhost:8080') });
我们以字符串形式提供了一小串 Ruby 代码——导入必要的库,然后返回一个 Ruby 对象。要在 Ruby 中使用这个对象,通常需要这样:Color::RGB.by_name(name).html。而在我们的例子中,我们是在 JavaScript 里调用这些方法,即使它们是 Ruby 的对象和方法。并且,我们传给它一个 JavaScript 字符串,然后把结果连接起来,结果里包含了 Ruby 字符串和其他 JavaScript 字符串。
下面安装 Ruby 和 JavaScript 的依赖项。
$ gem install color Fetching: color-1.8.gem (100%) Successfully installed color-1.8 1 gem installed $ npm install express + express@4.16.2 updated 1 package in 10.393s
然后,我们需要使用以下几个选项来运行 node:--polyglot 表示我们想要访问其他语言,--jvm 表示要使用 JVM,因为默认情况下 node 本地镜像只包含了 JavaScript。
$ node --polyglot --jvm color-server.js serving at http://localhost:8080
然后在浏览器中打开 http://localhost:8080/css/aquamarine 。除了 aquamarine,也可以使用其他颜色名称。
接下来让我们尝试使用更多的语言和模块。
对于任意大的整数,JavaScript 并没有很好的解决方案。我发现了几个像 big-integer 这样的模块,但这些模块的性能并不好,因为它们将数字的组成部分存储为 JavaScript 浮点数。Java 的 BigInteger 性能更好,所以我们用它来做一些任意大的整数运算。
JavaScript 也不提供对图形绘制的内置支持,而 R 语言却对此提供了很好的支持。我们使用 R 语言的 svg 模块绘制三角函数的三维散点图。
我们可以使用 GraalVM 的 polyglot API,并将其他语言代码的运行结果添加到 JavaScript 中。
const express = require('express') const app = express() const BigInteger = Java.type('java.math.BigInteger') app.get('/', function (req, res) { var text = 'Hello World from Graal.js!<br> ' // Using Java standard library classes text += BigInteger.valueOf(10).pow(100) .add(BigInteger.valueOf(43)).toString() + '<br>' // Using R interoperability to create graphs text += Polyglot.eval('R', `svg(); require(lattice); x <- 1:100 y <- sin(x/10) z <- cos(x^1.3/(runif(1)*5+10)) print(cloud(x~y*z, main="cloud plot")) grDevices:::svg.off() `); res.send(text) }) app.listen(3000, function () { console.log('Example app listening on port 3000!') })
在浏览器中打开 http://localhost:3000/ 查看结果。
这是 GraalVM 的第三个用途——运行使用多种语言编写的程序,并组合使用这些语言的模块。我认为这是语言和模块的大众化——你可以为你的问题选择任何一门合适的语言以及任何你想要的库。
4. 在 JVM 上运行本地语言
GraalVM 也支持 C 语言,GraalVM 可以像运行 JavaScript 和 Ruby 之类的语言一样运行 C 代码。
实际上,GraalVM 通过运行 LLVM 位码的方式来支持 C 语言,而不是直接运行 C 代码。也就是说,我们可以将现有工具与 C 语言一起使用,还可以使用其他可输出 LLVM 的语言,例如 C++、Fortran 和未来可能出现的其他语言。为了简化演示,我使用了由 Stephen McCamant 维护的 gzip 的单文件版本。为简单起见,它只是将 gzip 源代码和 autoconf 配置连成一个单独的文件。我还需要修改一些东西才能让它在 macOS 上运行起来,但不能在 GraalVM 上运行。
然后我们使用标准 clang(LLVM C 语言编译器)来编译它,并把它编译成 LLVM 位码,而不是本地汇编代码,这样就可以在 GraalVM 上运行。我使用的是 clang 4.0.1。
$ clang -c -emit-llvm gzip.c
然后我们使用 lli 命令(LLVM 位码解释器)直接在 GraalVM 上运行编译后的位码。我们先使用系统 gzip 来压缩文件,然后使用运行在 GraalVM 上的 gzip 进行解压缩。
$ cat small.txt Lorem ipsum dolor sit amet... $ gzip small.txt $ lli gzip.bc -d small.txt.gz $ cat small.txt Lorem ipsum dolor sit amet...
GraalVM 中的 Ruby 和 Python 实现也使用了这种技术来运行 C 扩展。也就是说,我们可以在虚拟机内部运行 C 扩展,同时保持高性能。
这是 GraalVM 的第四个用途——运行使用 C 和 C++ 等本地语言编写的程序,并且还可以运行 C 语言扩展,而像 JRuby 这样的 JVM 是无法做到这点的。
5. 适用于所有编程语言的工具
如果你使用 Java 编程,可能已经习惯了使用那些高质量的工具,比如 IDE、调试器和分析器,但并非所有的编程语言都有这么好用的工具。不过如果你是在 GraalVM 中使用某种语言,就可以获得这样的工具。
所有 GraalVM 语言(目前除了 Java)都是使用 Truffle 框架实现的,所以一个功能只要开发一次(比如调试器),就可以用在所有语言上。
为了试验这个功能,我们开发了一个简单的 FizzBuzz 程序。它将内容输出到屏幕上,逻辑分支很清晰,只需要进行少量迭代,我们因此可以很容易地设置断点。我们先使用 JavaScript 来实现。
function fizzbuzz(n) { if ((n % 3 == 0) && (n % 5 == 0)) { return 'FizzBuzz'; } else if (n % 3 == 0) { return 'Fizz'; } else if (n % 5 == 0) { return 'Buzz'; } else { return n; } } for (var n = 1; n <= 20; n++) { print(fizzbuzz(n)); }
我们可以像平常一样使用 GraalVM 运行这个 JavaScript 程序。
$ js fizzbuzz.js 1 2 Fizz 4 Buzz Fizz ...
我们也可以用 --inspect 标记来运行它,它会输出一个可以在 Chrome 中打开的链接,并在调试器中暂停程序的运行。
$ js --inspect fizzbuzz.js Debugger listening on port 9229. To start debugging, open the following URL in Chrome: chrome-devtools://devtools/bundled/inspector.html?ws=127.0.0.1:9229/6c478d4e-1350b196b409 ...
然后我们在 FizzBuzz 代码中设置一个断点,并继续执行。在跳过断点后,我们可以看到 n 的值,然后继续,或者查看调试接口的其余部分。
这个标志也可用在 Python、Ruby 和 R 语言上。我就不展示每个程序的源代码了,它们的运行方式都是一样的。
$ graalpython --jvm --inspect fizzbuzz.py
$ ruby --inspect fizzbuzz.rb
$ Rscript --inspect fizzbuzz.r
你可能对 Java 的另一个工具 VisualVM 很熟悉,它为我们提供了一个用户界面,可以将它连接到本机或网络上的某个 JVM,以便检查各种问题,比如内存和线程的使用情况。
GraalVM 也包含了带有标准 jvisualvm 命令的 VisualVM。
$ jvisualvm &> /dev/null &
如果我们在运行 TopTen Java 应用程序的同时运行 jvisualvm,就可以看到内存随时间变化的情况,或者我们可以做一些其他的事情,比如进行堆转储,然后检查堆中的对象类型。
$ java TopTen large.txt
我写了一个用来生成垃圾的 Ruby 程序。
require 'erb' x = 42 template = ERB.new <<-EOF The value of x is: <%= x %> EOF loop do puts template.result(binding) end
如果使用 VisualVM 来运行标准的 JVM 语言(如 JRuby),则会感到失望,因为你看到的是底层的 Java 对象,而不是所使用语言的对象的信息。
如果我们使用 Ruby 的 GraalVM 版本,VisualVM 将会识别出 Ruby 对象。我们需要使 --jvm 选项来启动 VisualVM,因为它不支持本地版本的 Ruby。
$ ruby --jvm render.rb
如果有需要的话,我们还可以看到底层 Java 对象的堆转储,或者在 Summary 下选择 Ruby Heap 来查看 Ruby 对象。
Truffle 框架就像是编程语言和工具之间的一种联系。如果我们使用 Truffle 来开发自己的编程语言,并基于 Truffle 的工具 API 开发各种工具(比如调试器),那么开发出来的工具可以适用于每一种语言,而且只需要开发一次即可。
这是 GraalVM 的第五个用途——为编程语言提供高质量的工具。
6. 扩展基于 JVM 的应用程序
除了可用作独立语言实现和用于多语言编程,这些语言和工具也可以嵌入到 Java 应用程序中。新的 org.graalvm.polyglot API 可用于加载和运行其他语言的代码。
import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; public class ExtendJava { public static void main(String[] args) { String language = "js"; try (Context context = Context.newBuilder().allowNativeAccess(true).build()) { for (String arg : args) { if (arg.startsWith("-")) { language = arg.substring(1); } else { Value v = context.eval(language, arg); System.out.println(v); } } } } }
如果我们使用了 GraalVM 的 javac 和 java 命令,那么 org.graalvm…就已经存在于类路径中,可以直接编译并运行代码,不需要使用任何额外的标记。
$ javac ExtendJava.java $ java ExtendJava '14 + 2' 16 $ java ExtendJava -js 'Math.sqrt(14)' 3.7416573867739413 $ java ExtendJava -python '[2**n for n in range(0, 8)]' [1, 2, 4, 8, 16, 32, 64, 128] $ java ExtendJava -ruby '[4, 2, 3].sort' [2, 3, 4]
这些版本的语言与通过使用 node 和 ruby 这些命令运行的代码一样,都具备了很高的性能。
这是 GraalVM 的第六个用途——作为在 Java 应用程序中嵌入不同语言的接口。我们可以借助 polyglot API 来获取其他语言的对象,并将它们用作 Java 接口,实现其他复杂的操作。
7. 扩展本地应用程序
GraalVM 已经包含了一个本地库——可用它在本地应用程序中运行使用 GraalVM 语言编写的代码。像 V8 这样的 JavaScript 运行时和像 CPython 这样的 Python 解释器通常是可嵌入的,也就是说,它们可以作为一个库链接到另一个应用程序中。GraalVM 允许我们在嵌入上下文中使用任何语言,只要将它们链接到 polyglot 库即可。
在安装 GraalVM 时,该库已经生成,但默认情况下,它只包含 JavaScript。我们可以使用下面的命令来重建 polyglot 库,把其他语言也包含进来。
$ graalvm-1.0.0-rc1/Contents/Home/jre/lib/svm/bin/rebuild-images libpolyglot
我们可以开发一个简单的 C 语言程序,用来运行从命令行传递进来的命令。我们将演示与 ExtendJava 相同的示例,不过这次使用 C 语言作为主语言。
#include <stdlib.h> #include <stdio.h> #include <polyglot_api.h> int main(int argc, char **argv) { graal_isolate_t *isolate = NULL; graal_isolatethread_t *thread = NULL; if (graal_create_isolate(NULL, &isolate) != 0 || (thread = graal_current_thread(isolate)) == NULL) { fprintf(stderr, "initialization error\n"); return 1; } poly_context context = NULL; if (poly_create_context(thread, NULL, 0, &context) != poly_ok) { fprintf(stderr, "initialization error\n"); return 1; } char* language = "js"; for (int n = 1; n < argc; n++) { if (argv[n][0] == '-') { language = &argv[n][1]; } else { poly_value result = NULL; if (poly_context_eval(thread, context, language, "unicalc", argv[n], &result) != poly_ok) { fprintf(stderr, "eval error\n"); return 1; } char buffer[1024]; size_t length; if (poly_value_to_string_utf8(thread, result, buffer, sizeof(buffer), &length) != poly_ok) { fprintf(stderr, "to string error\n"); return 1; } buffer[length] = '\0'; printf("%s\n", buffer); poly_destroy_handle(thread, result); } } return 0; }
然后,我们使用系统 C 编译器来编译和运行它,并将它链接到 GraalVM 的 polyglot 库。同样,它也不需要 JVM。
$ clang -Igraalvm-1.0.0-rc1/Contents/Home/jre/lib/polyglot / -rpath graalvm-1.0.0-rc1/Contents/Home / -Lgraalvm-1.0.0-rc1/Contents/Home/jre/lib/polyglot / -lpolyglot extendc.c -o extendc $ otool -L extendc extendc: .../libpolyglot.dylib ... .../libSystem.B.dylib ... $ ./extendc '14 + 2' 16 $ ./extendc -js 'Math.sqrt(14)' 3.7416573867739413 $ ./extendc -python '[2**n for n in range(0, 8)]' [1, 2, 4, 8, 16, 32, 64, 128]
这是 GraalVM 的第七个用途——在本地应用程序中使用单个库来嵌入任意 GraalVM 语言。
8. 将 Java 代码作为本地库
Java 生态系统拥有许多非常高质量的库,它们在其他生态系统中通常不可用,比如本地应用程序及其他托管语言。要想在本地应用程序中使用 Java 库,需要嵌入 JVM,但这样会让事情变得非常复杂。
GraalVM 允许我们使用已有或自己开发的 Java 库,并将它们编译为单独的本地库以供其他本地语言使用。与之前的本地编译一样,它们不需要 JVM 就能运行。
我开发了一个应用程序,它使用 Apache SIS 地理空间库来计算地球上两点之间的大圆距离。我使用了 SIS 0.8,从 http://sis.apache.org 下载,并从中提取了相关的 jar 包。
import org.apache.sis.distance.DistanceUtils; public class Distance { public static void main(String[] args) { final double aLat = Double.parseDouble(args[0]); final double aLong = Double.parseDouble(args[1]); final double bLat = Double.parseDouble(args[2]); final double bLong = Double.parseDouble(args[3]); System.out.printf("%f km%n", DistanceUtils.getHaversineDistance(aLat, aLong, bLat, bLong)); } }
我们可以正常编译,然后用它来计算伦敦(北纬 51.507222,东经 -0.1275)和纽约(40.7127,-74.0059)之间的距离。
$ javac -cp sis.jar -parameters Distance.java $ java -cp sis.jar:. Distance 51.507222 -0.1275 40.7127 -74.0059 5570.25 km
我们可以将它编译为本机可执行文件,就像之前的 topten 程序一样。
$ native-image --no-server -cp sis.jar:. Distance ... $ ./distance 51.507222 -0.1275 40.7127 -74.0059 5570.25 km
我们也可以将它作为本地共享库而不是可执行文件。为此,我们需要将一个或多个方法声明为 @CEntryPoint。
... import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.c.function.CEntryPoint; public class Distance { ... @CEntryPoint(name = "distance") public static double distance(IsolateThread thread, double a_lat, double a_long, double b_lat, double b_long) { return DistanceUtils.getHaversineDistance(a_lat, a_long, b_lat, b_long); } ... }
我们不需要修改 javac 命令行参数,因为 GraalVM 会自动将这些新的 API 放到类路径中,然后就可以编译成一个共享库,同时自动生成头文件。
$ native-image --no-server -cp sis.jar:. -H:Kind=SHARED_LIBRARY \ -H:Name=libdistance $ otool -L libdistance.dylib # .so on Linux libdistance.dylib: .../libdistance.dylib ... .../CoreFoundation.framework ... .../libz.1.dylib ... .../libSystem.B.dylib ... $ du -h libdistance.dylib 4.8M libdistance.dylib
然后我们开发一个 C 语言程序来使用这个库。本地库的接口确实有一些繁琐,因为虚拟机需要管理堆、线程、垃圾回收器和其他服务,所以我们需要创建一个实例,并告诉它主线程是哪个。
#include <stdlib.h> #include <stdio.h> #include <libdistance.h> int main(int argc, char **argv) { graal_isolate_t *isolate = NULL; graal_isolatethread_t *thread = NULL; if (graal_create_isolate(NULL, &isolate) != 0 || (thread = graal_current_thread(isolate)) == NULL) { fprintf(stderr, "initialization error\n"); return 1; } double a_lat = strtod(argv[1], NULL); double a_long = strtod(argv[2], NULL); double b_lat = strtod(argv[3], NULL); double b_long = strtod(argv[4], NULL); printf("%f km\n", distance(thread, a_lat, a_long, b_lat, b_long)); return 0; }
我们使用标准的系统工具进行编译,然后运行可执行文件(在 Linux 上设置 LD_LIBRARY_PATH =.)。
$ clang -I. -L. -ldistance distance.c -o distance $ otool -L distance distance: .../libdistance.dylib ... .../libSystem.B.dylib ... $ ./distance 51.507222 -0.1275 40.7127 -74.0059 5570.25 km
这是 GraalVM 的第八个用途——将 Java 代码编译为本地库,然后在本地应用程序中使用它,而无需使用完整的 JVM。
9. 数据库中的 polyglot
说到 polyglot 库在嵌入语言方面的应用,Oracle 数据库就是应用场景之一。我们用它创建了 Oracle 数据库多语言引擎(MLE),用于支持在 SQL 中使用 GraalVM 语言和模块。
例如,假设我们有一个使用 JavaScript 编写的前端,它使用 JavaScript 模块验证器对电子邮件地址进行验证。如果使用 SQL 或 PLSQL 开发的数据库应用程序中有相同的逻辑,我们就可以使用相同的验证器,让结果保持一致。
可以从 https://oracle.github.io/oracle-db-mle/releases/0.2.7/docker/ 下载 MLE 作为 Docker 镜像,然后将它加载到 Docker 中。
$ docker load --input mle-docker-0.2.7.tar.gz
我们要运行这个镜像,然后在完成加载时(可能需要几分钟),打开一个 Bash 终端。
$ docker run mle-docker-0.2.7 $ docker ps $ docker exec -ti <container_id> bash -li
如果我们可以在这个 Bash 终端中运行交互式 SQL 工具 sqlplus,并连接到数据库,就表示镜像已经在运行中。
$ sqlplus scott/tiger@localhost:1521/ORCLCDB
然后,我们通过 Bash 终端安装 validator 模块,运行 dbjs 命令将其部署到数据库中。然后再次运行 sqlplus。
$ npm install validator $ npm install @types/validator $ dbjs deploy -u scott -p tiger -c localhost:1521/ORCLCDB validator $ sqlplus scott/tiger@localhost:1521/ORCLCDB
现在可以将验证器模块作为 SQL 表达式的一部分。
SQL> select validator.isEmail('hello.world@oracle.com') from dual; VALIDATOR.ISEMAIL('HELLO.WORLD@ORACLE.COM') ------------------------------------------- 1 SQL> select validator.isEmail('hello.world') from dual; VALIDATOR.ISEMAIL('HELLO.WORLD') -------------------------------- 0
这是 GraalVM 的第九个用途——在 Oracle 数据库中运行 GraalVM 语言,以便在数据库逻辑中重用前端或后端的逻辑。
10. 创建自己的语言
Oracle 实验室和我们的学术合作伙伴已经能够用相对较小的团队开发出 JavaScript、R 语言 Ruby、Python 和 C 语言的高性能实现,因为我们开发的 Truffle 框架简化了这一过程。
Truffle 是一个 Java 库,用于为语言编写抽象语法树(AST)解释器。 AST 解释器可能是实现新语言最简单的方法,因为它直接使用解析器的输出,并且不涉及任何字节码或常规编译器技术,但它的速度通常都很慢。因此,我们将它与部分求值(partial evaluation)技术结合在一起,这项技术允许 Truffle 使用 Graal 自动为新语言提供即时编译器。
我们可以使用 Truffle 来实现自己的编程语言、实现已有编程语言的高性能版本,或者实现特定的领域语言。我们还可以自动获得调试器等功能。任何一个完成编程语言课程的人应该都具备了所需的基本技能,Oracle 实验室的一个实习生在几个月内就实现了一个 Ruby 的基本版。
我们没有足够的篇幅来显示一门完整的语言,甚至是一个很小的语言,不过大家可以参考 SimpleLanguage,它是一个可运行的教程,介绍了如何使用 Truffle 创建自己的语言。
Oracle 实验室以外的人使用 Truffle 编写的其他语言包括 Smalltalk 变体、Newspeak 变体和 Lisp 变体。
结论
GraalVM 支持多种新功能,它是一个平台,我们可以在这个平台上构建更强大的语言和工具,并将它们放入更多的环境中。无论程序在哪里运行,或者使用了哪种语言,它都可以让我们选择所需的语言和模块。
要体验 GraalVM,请访问 https://www.graalvm.org ,这里有下载和文档链接以及更多的例子。
其他相关链接:
SimpleLanguage: https://github.com/graalvm/simplelanguage
Smalltalk 变体: https://github.com/SOM-st/TruffleSOM
Newspeak 变体: https://github.com/smarr/SOMns
Lisp 变体: https://github.com/cesquivias/mumbler
Lisp 变体教程: http://cesquivias.github.io/
原文链接: https://medium.com/graalvm/graalvm-ten-things-12d9111f307d
感谢张婵对本文的审校。
评论 1 条评论