47

历数GraalVM的十大用途

 5 years ago
source link: http://www.infoq.com/cn/articles/graalvm-ten-things?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

不久前Oracle发布了GraalVM,一套通用型虚拟机,能执行各类高性能与互操作性任务,并在无需额外成本的前提下允许用户构建多语言应用程序。

GraalVM包含了很多不同的部分,所以即使你之前听过这个名字,或者听过我们的一些演讲,仍然有一些东西是你不知道的。在本文中,我们将列出GraalVM的一些不同的特性,并展示它的用途。

  1. 高性能Java
  2. 占用内存小、启动速度快的Java
  3. 组合JavaScript、Java、Ruby和R语言
  4. 在JVM上运行本地语言
  5. 适用于所有编程语言的工具
  6. 扩展基于JVM的应用程序
  7. 扩展本地应用程序
  8. 将Java代码作为本地库
  9. 数据库中的polyglot
  10. 创建自己的语言

我们可以使用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()));
    }

    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
...
+ [email protected]
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
+ [email protected]
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,也可以使用其他颜色名称。

 6961-1532260242901.png 

接下来让我们尝试使用更多的语言和模块。

对于任意大的整数,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/查看结果

 5452-1532260241845.png 

这是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框架实现的,所以一个功能只要开发一次(比如调试器),就可以用在所有语言上。

为了试验这个功能,我们开发了一个简单的FizzBu​​zz程序。它将内容输出到屏幕上,逻辑分支很清晰,只需要进行少量迭代,我们因此可以很容易地设置断点。我们先使用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
...

然后我们在FizzBu​​zz代码中设置一个断点,并继续执行。在跳过断点后,我们可以看到n的值,然后继续,或者查看调试接口的其余部分。

4353-1532260241164.png 

这个标志也可用在Python、Ruby和R语言上。我就不展示每个程序的源代码了,它们的运行方式都是一样的。

$ graalpython --jvm --inspect fizzbuzz.py

 3674-1532260242380.png 

$ ruby --inspect fizzbuzz.rb

 3055-1532260241495.png 

$ Rscript --inspect fizzbuzz.r

 2696-1532260242100.png 

你可能对Java的另一个工具VisualVM很熟悉,它为我们提供了一个用户界面,可以将它连接到本机或网络上的某个JVM,以便检查各种问题,比如内存和线程的使用情况。

GraalVM也包含了带有标准jvisualvm命令的VisualVM。

$ jvisualvm &> /dev/null &

如果我们在运行TopTen Java应用程序的同时运行jvisualvm,就可以看到内存随时间变化的情况,或者我们可以做一些其他的事情,比如进行堆转储,然后检查堆中的对象类型。

$ java TopTen large.txt

 2257-1532260242640.png 

我写了一个用来生成垃圾的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对象。

 1858-1532260240637.png

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('[email protected]') from dual;
VALIDATOR.ISEMAIL('[email protected]')
-------------------------------------------
                                          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 ,这里有下载和文档链接以及更多的例子。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK