Top 10 Things To Do With GraalVM

このエントリは以下のエントリをベースにしています。
This entry is based on the following one written by Chris Seaton (PhD in Ruby, TruffleRuby lead. Oracle Labs VM research group).
https://medium.com/graalvm/graalvm-ten-things-12d9111f307d

Chris SeatonがGraalVMの主な機能10個を取り上げて説明しています。以下の機能はすべてGraalVM 19.0.0で確認できます(原文執筆当時、現時点の最新版は19.1.0)。この記事では、ChrisはEnterprise EditionをmacOSで利用していますが、Community Editionでも動作します。

  1. High-performance modern Java
  2. Low-footprint, fast-startup Java
  3. Combine JavaScript, Java, Ruby, and R
  4. Run native languages on the JVM
  5. Tools that work across all languages
  6. Extend a JVM-based application
  7. Extend a native application
  8. Java code as a native library
  9. Polyglot in the database
  10. Create your own language

サンプルコードはすべてGitHubに上がっています。

Top 10 Things To Do With GraalVM
http://github.com/chrisseaton/graalvm-ten-things/

Setup

GraalVM 19.0.0 Enterprise Editionをダウンロード、展開の上、$PATH を通しておきます。

 GraalVM Downloads
https://www.graalvm.org/downloads/

$ git clone https://github.com/chrisseaton/graalvm-ten-things.git
$ cd foo
$ tar -zxf graalvm-ee-darwin-amd64-19.0.0.tar.gz.tar.gz
    # or graalvm-ee-darwin-linux-19.0.0.tar.gz on Linux
$ export PATH=graalvm-ee-19.0.0/Contents/Home/bin:$PATH
    # or PATH=graalvm-ee-19.0.0/bin:$PATH on Linux

GraalVMにはJavaScriptエンジンがすでに含まれていますが、同梱されているguというパッケージマネージャを使って言語を追加インストールできます。今回はRuby、Python、R言語をインストールしました。その他、native-imageツールもインストールしています。これらはすべてGitHubからダウンロードしています。

$ gu install native-image
$ gu install ruby
$ gu install python
$ gu install R

javaもしくはjsを実行すると、呼び出したランタイムとGraalVMのバージョンを確認できます。

$ java -version
java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b31)
Java HotSpot(TM) GraalVM EE 19.0.0 (build 25.212-b31-jvmci-19-b01, mixed mode)

$ js --version
GraalVM JavaScript (GraalVM EE Native 19.0.0)

1. High-performance modern Java

GraalVMのGraalという名前はGraalコンパイラに由来しています。これは1個のコンパイラですべてを統制します。つまり、様々な用途に利用できるライブラリとして作成された、1個のコンパイラ実装です。

One VM to Rule Them All
http://lafo.ssw.uni-linz.ac.at/papers/2013_Onward_OneVMToRuleThemAll.pdf

例えば、Graalコンパイラを使ってAOTコンパイルやJITコンパイルをしたり、複数アーキテクチャ向けに複数のプログラミング言語のコンパイルをしたりできますが、Java JITコンパイラとしてGraalVMを利用するのが簡単です。

ドキュメントの頻出単語トップ10を出力するサンプルプログラムを試してみます。

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);
}
}
}
view raw TopTen.java hosted with ❤ by GitHub

GraalVMにはjavacコンパイラが同梱されていますが、このデモ目的では標準のjavacコンパイラとの違いがありません。試したい場合には、GraalVM同梱ではないjavacを使って比較されてもよいでしょう。

$ javac TopTen.java

GraalVMのjavacを実行すると、自動的にGraal JITコンパイラを使います。150MBのlarge.txtを読ませた場合のプログラム実行時間をtimeコマンドで確認しています。

$ 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  0m12.950s
user  0m17.827s
sys 0m0.622s

Graalコンパイラは他のJava用JITコンパイラがC++で記述されているのとは異なり、Javaで記述されているため、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  0m19.602s
user  0m20.357s
sys 0m0.498s

この結果を見ると、Graalコンパイラは標準のHotSpotコンパイラの2/3の時間でJavaプログラムを実行することがわかります。パフォーマンスの1桁%の向上が大きな向上として扱っているものとしては、この結果は大きなものです。

Community Editionを使っている場合、Enterprise Editionほどではないにせよ、HotSpotよりもよい結果を得られることでしょう。

TwitterはGraalVMを運用環境で利用している企業の一社であり、GraalVMでコスト節減ができたと言っています。TwitterはScalaアプリケーションを実行するためにGraalVMを使用しています。GraalVMはJVMバイトコードのレベルで動作するので、あらゆるJVM言語で利用できます。

Graal Accelerates Processing at Twitter

これがGraalVMでできることその1です。既存のJavaアプリケーションを高速にする、置き換え可能なJITコンパイラです。

2. Low-footprint, fast-startup Java

Javaプラットフォームは特に長時間実行するプロセスやピークパフォーマンスに優れていますが、短時間実行プロセスは起動時間の長さや比較的メモリ利用量の大きさの影響を受けます。例えば同じアプリケーションを150MBではなく1KBという小さな入力で起動した場合、小さなファイルにもかかわらず、実行完了までに要する時間が長く、メモリも70MB使います。time -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.17 real         0.28 user         0.04 sys
  70737920  maximum resident set size
...

こうした問題に対応するため、GraalVMではgccのようにAhead-Of-Timeコンパイルによりネイティブ実行イメージを生成できます。

$ native-image --no-server TopTen
[topten:37970]    classlist:   1,801.57 ms
[topten:37970]        (cap):   1,289.45 ms
[topten:37970]        setup:   3,087.67 ms
[topten:37970]   (typeflow):   6,704.85 ms
[topten:37970]    (objects):   6,448.88 ms
[topten:37970]   (features):     820.90 ms
[topten:37970]     analysis:  14,271.88 ms
[topten:37970]     (clinit):     257.25 ms
[topten:37970]     universe:     766.11 ms
[topten:37970]      (parse):   1,365.29 ms
[topten:37970]     (inline):   3,829.55 ms
[topten:37970]    (compile):  34,674.51 ms
[topten:37970]      compile:  41,412.71 ms
[topten:37970]        image:   2,741.41 ms
[topten:37970]        write:     619.13 ms
[topten:37970]      [total]:  64,891.52 ms

上記のコマンドではtoptenというネイティブ実行ファイルを生成しています。この実行ファイルはJVMのランチャーではなく、JVMへもリンクしていませんし、JVMをバンドルしてもいません。native-imageコマンドがJavaコードと使用するJavaライブラリをコンパイル、リンクして、シンプルなマシンコードを生成します。GCなどの実行時コンポーネントのために、SubstrateVMと呼ばれる、GraalVMと同様Javaで記述された新たなVMを実行しています。

toptenが使うライブラリを見ると、標準のシステムライブラリだけであることがわかると思います。JVMをインストールしたことのないシステムにこのtoptenというファイルだけを移動し、実行すれば、JVMやその他のファイルを使っていないことが確認できるでしょう。このファイルは非常に小さく、今回の場合は8MBを下回っています。

$ otool -L topten    # ldd topten on Linux
topten:
  libSystem.B.dylib (current version 1252.250.1)
  CoreFoundation (current version 1575.12.0)
  /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
$ du -h topten 
7.5M  topten

実行ファイルを実行すると、JVM上で同じプログラムを実行するよりも、およそ1桁速く起動し、およそ1桁少ないメモリを使用することがわかります。コマンドラインで使用していると、経過時間に気づかないほどに高速で、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
   3158016  maximum resident set size
...

native-imageツールにはリフレクション関連の制約や、コンパイル時にすべてのクラスが利用可能である必要があるといった制限があります。詳細は以下のURLからどうぞ。static initializerがコンパイル中に実行されるという点で、基本的なコンパイルに比べてメリットがあるため、アプリケーションロード時の作業を減らすことができます。

Native Image Java Limitations
https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md

これがGraalVMでできることその2です。既存のJavaプログラムを高速に起動し低フットプリントで実行でき、さらに実行時のJARファイルの探索といった構成上の問題からも解放されます。これを使えばより小さなDockerイメージを作成できます。

3. Combine JavaScript, Java, Ruby, and R

Javaだけでなく、GraalVMにはJavaScript、Ruby、R、Pythonの新たな実装が含まれています。これらはTruffleと呼ばれる新たな言語実装フレームワークを使って作成されています。Truffleを使うとシンプルかつ高性能な言語インタプリタを作成できるので、Truffleを使って言語インタプリタを作成すると、JITコンパイラとしてGraalを利用できます。つまり、Graal/GraalVMはJIT、AOTコンパイラだけでなく、JavaScript、Ruby、R、PythonのJITコンパイラでもあるのです。

GraalVMの言語は既存の言語の置き換えを目指しています。例えばNode.jsモジュールをインストールできます。

$ npm install color
...
+ color@3.1.1
added 6 packages from 6 contributors and audited 7 packages in 6.931s

このモジュールを使ってRGB HTMLカラーをHSLカラーに変換するちょっとしたプログラムを作成できます。

var Color = require('color');
process.argv.slice(2).forEach(function (val) {
print(Color(val).hsl().string());
});
view raw color.js hosted with ❤ by GitHub

通常の方法で実行できます。

$ node color.js '#42aaf4'
hsl(204.89999999999998, 89%, 60.8%)

GraalVMのある言語から別言語のコードを実行するためのAPIがあるので、Polyglotプログラムを記述できます。これは利用したい別言語のライブラリがある場合に利用できる、ということです。例えば、CSSの色名称をNode.jsで16進表現に変換する上で、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&#39;)
});
view raw color-server.js hosted with ❤ by GitHub

コードでは、Rubyコードを文字列として実行するように指定していますが、ここではライブラリを含むRubyオブジェクトを返すことを期待しています。Rubyからこのオブジェクトを使う方法は、通常Color::RGB.by_name(name).htmlを使います。color_rgbはRubyのオブジェクトやメソッドであるにもかかわらず、実際には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.17.0
added 50 packages from 37 contributors and audited 143 packages in 22.431s

そして、他言語アクセスのための –polyglot と、 –jvm というオプションをつけて、nodeを実行します。

$ node --polyglot --jvm color-server.js
serving at http://localhost:8080

ふつうのブラウザで、 aquamarine やその他の色名称を指定して http://localhost:8080/css/{colorname} にアクセスします。

多くの言語やモジュールを使う大規模な例を試してみましょう。

JavaScriptには、任意の大きさの整数に対する優れたソリューションはありません。big-integerのようなモジュールはあるにせよ、これらは数値のコンポーネントをJavaScriptの浮動小数点数として保存するため、効率的ではありません。JavaのBigIntegerクラスの方が効率的なので、こちらを使って任意の大きさの整数演算を行いましょう。

JavaScriptにはグラフ描画のビルトインサポートもありませんが、Rにはあるので、Rのsvgモジュールを使って三角関数の3D散布図を作成します。いずれの場合も、GraalVMで動作する多くの言語の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!')
})
view raw polyglot.js hosted with ❤ by GitHub

http://localhost:3000/ をブラウザで開いて結果を確認してください。

これがGraalVMでできることその3です。複数の言語で書かれたプログラムを実行し、それらの言語のモジュールを一緒に利用できます。目下の問題に最適と思われる言語や、任意の言語ライブラリも利用できる、という点で、言語とモジュールの一種のコモディティ化と考えています。

4. Run native languages on the JVM

GraalVMでサポートするもう一つの言語はCです。GraalVMはCのコードをJavaScriptやRubyのように実行できます。

GraalVMは実際にはCを直接サポートするのではなく、LLVMツールチェーン (LLVMビットコード) の出力の実行をサポートしています。これは、既存のツールをCだけでなく、将来的にはC++、Fortranなど、LLVMを出力できる他の言語でも使用できることを意味します。デモ目的のため、Stephen McCamant氏がメンテナンスするgzipの特別な単一ファイル版を実行してみます。簡単のために、gzipソース・コードとautoconf構成を1つのファイルに連結したものです。また、macOS上でclangを使うためにパッチを当てる必要がありましたが、GraalVMでは動作しませんでした。

Large single compilation-unit C programs
http://people.csail.mit.edu/smcc/projects/single-file-programs/
Stephen McCamant
http://people.csail.mit.edu/smcc/

GraalVMは、実のところ直接C言語をサポートしているのではなく、LLBMツールチェーンの出力、つまりLLVM bitcodeの実行をサポートしています。これはつまり、既存のCのツールをC言語とともに利用でき、そしてLLVMを出力可能なその他の言語、例えばC++やFortranといった言語も利用できるということです。デモ目的で簡単にするために、特別な1ファイルのgzip(Stephen McCamantがメンテナンスしています) を実行します。これはgzipソースコードとautoconfの設定を一つのファイルにまとめたものです。macOSとclangで動作するよういくつかパッチを適用しました。

その後、標準のclang (LLVM Cコンパイラ)を使ってコンパイルできるようになりました。このgzipをコンパイルして、ネイティブアセンブリではなくGraalVMが実行可能なLLVM bitcodeにします。今回はclang 4.0.1を使っています。

$ clang -c -emit-llvm gzip.c

LLVM bitcodeインタプリタのlliコマンドを使ってGraalVMでこのgzipを直接実行します。作成した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エクステンションを実行しています。つまり、VM内でCエクステンションを実行でき、これらの従来のネイティブ拡張インターフェースをサポートしながらも、高いパフォーマンスを維持することができます。

これがGraalVMでできることその4です。CやC ++などのネイティブ言語で書かれたプログラムを実行し、さらにJRubyなどの既存のJVM実装では不可能なPythonやRubyなどの言語へのC拡張を実行できます。

5. Tools that work across all languages

Javaでプログラムを書く場合、おそらくIDE、デバッガ、プロファイラなどの非常に高品質のツールに慣れているでしょう。すべての言語にこの種のツールがあるわけではありませんが、GraalVMの言語を使用する場合は入手してください。

現時点ではJavaを除くすべてのGraalVMの言語は、共通の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));
}
view raw fizzbuzz.js hosted with ❤ by GitHub

JavaScriptのプログラムはGraalVMを使って通常通り実行できます。

$ 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の値を確認できます。そして続行したりデバッグインターフェースの残りを調べたりできます。

Chromeデバッガは通常JavaScriptで使用しますが、GraalVMのJavaScriptだからといって特別なことは何もありません。このフラグは、私たちのPython、Ruby、Rの実装でも利用でき、動作します。それぞれのプログラムのソースを示すことはしませんが、まったく同じ方法で実行すると、それぞれのコードのプログラムで同様のChromeデバッガーインターフェースを取得できます。

$ graalpython --jvm --inspect fizzbuzz.py
$ ruby --inspect fizzbuzz.rb
$ Rscript --inspect fizzbuzz.r

すでにJavaで利用されている方が慣れてらっしゃるかもしれないもう一つのツールとしてVisualVMがあります。これはローカルもしくはリモートのJVMのメモリやスレッドの使用状況を調査できる、GUIベースのツールです。

GraalVMにはVisualVMが含まれています。標準のjvisualvmコマンドが利用できます。

$ jvisualvm &> /dev/null &

TopTenアプリケーションを実行しているときに実行した場合は、時間の経過とともにメモリ使用量を監視したり、ヒープダンプを取ってヒープ内でメモリを使用しているオブジェクトの種類を調べたりできます。

$ java TopTen large.txt

このRubyプログラムは、時間の経過とともにゴミ(garbage)を生成するように作成したものです。

require 'erb'
x = 42
template = ERB.new <<-EOF
The value of x is: <%= x %>
EOF
loop do
puts template.result(binding)
end
view raw render.rb hosted with ❤ by GitHub

JRubyのような標準のJVM言語をVisualVMで実行する場合は、言語のオブジェクトに関する情報ではなく、基盤となるJavaオブジェクトが見えるので残念に思われることでしょう。

GraalVMバージョンのRubyを使うと、VisualVMはRubyオブジェクト自体を認識します。VisualVMではネイティブバージョンのRubyをサポートしていないため、–jvmコマンドをつけて実行する必要があります。

$ ruby --jvm render.rb

必要に応じて、基になるJavaオブジェクトと同様のヒープの状態を見ることができますし、[Summary]の下のRuby Heapを選択して代わりに適切なRubyオブジェクトを表示することもできます。

Truffleフレームワークは、言語とツールにとって一種の結節点です。Truffleを使って言語を実装し、TruffleのツールAPIに対してデバッガのようなツールを実装する場合、それぞれのツールはそれぞれの言語で動作します。各言語に対して作成する必要はなく、1回ツール書くだけでOKです。

Fast, Flexible, Polyglot Instrumentation Support for Debuggers and other Tools
http://programming-journal.org/2018/2/14/

これがGraalVMでできることその5です。Chrome DebuggerやVisualVMのような独自のツールを構築するためのサポートが備わっていない言語向けの、高品質ツールを入手するためのプラットフォームとして利用できます。

6. Extend a JVM-based application

これらの言語とツールは、スタンドアロンの言語実装として使用できるだけでなく、多言語のユースケースで一緒に使用できるため、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);
}
}
}
}
}
view raw ExtendJava.java hosted with ❤ by GitHub

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などのコマンドを使用して入手した、ハイパフォーマンスなpolyglotバージョンであり、GraalVM実行可能ファイルと同じです。

これがGraalVMでできることその6です。Javaアプリケーションに多数の様々な言語を埋め込むための単一インターフェースたり得ます。polyglot APIを使ってゲスト言語オブジェクトを、Javaインターフェースとその他の洗練された相互運用目的で利用できます。

7. Extend a native application

GraalVMには、ネイティブアプリケーションからGraalVMの任意の言語で書かれたコードを実行できる、1つのネイティブライブラリがすでに含まれています。V8のようなJavaScriptランタイムやCPythonのようなPythonインタプリタは、ライブラリとして別のアプリケーションにリンクできるという意味で、埋め込み可能なことが多いのですが、GraalVMを使うと、この1つのpolyglot埋め込みライブラリにリンクすることで、埋め込みコンテキストで任意の言語を使用できます。

このライブラリはGraalVMを入手した時点で既にビルド済みですが、デフォルトでは組み込み言語のJavaScriptしか含まれていません。polyglotライブラリを再構築して他の言語を含めるには、次のコマンドを使用しますが、Oracle GraalVM Enterprise Edition Native Image preview for macOS(19.1.0.0)をOTNからダウンロードする必要があります。

Oracle GraalVM Enterprise Edition
https://www.oracle.com/technetwork/graalvm/downloads/index.html

再構築には数分かかるので、まずはJavaScriptを試してみてください。JavaScriptだけが必要な場合は、再構築する必要はありません。

$ gu install --force --file native-image-installable-svm-svmee-darwin-amd64-19.0.0.jar
$ gu rebuild-images libpolyglot

コマンドラインに渡されたGraalVMの任意の言語でコマンドを実行するという、間単なC言語のプログラムを作成します。今回はExtendJavaの例と同等のことを行いますが、Cをホスト言語として使用します。

#include <stdlib.h>
#include <stdio.h>
#include <polyglot_api.h>
int main(int argc, char **argv) {
poly_isolate isolate = NULL;
poly_thread thread = NULL;
if (poly_create_isolate(NULL, &isolate, &thread) != poly_ok) {
fprintf(stderr, "poly_create_isolate error\n");
return 1;
}
poly_context context = NULL;
if (poly_create_context(thread, NULL, 0, &context) != poly_ok) {
fprintf(stderr, "poly_create_context error\n");
goto exit_isolate;
}
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_open_handle_scope(thread) != poly_ok) {
fprintf(stderr, "poly_open_handle_scope error\n");
goto exit_context;
}
if (poly_context_eval(thread, context, language, "eval", argv[n], &result) != poly_ok) {
fprintf(stderr, "poly_context_eval error\n");
const poly_extended_error_info *error;
if (poly_get_last_error_info(thread, &error) != poly_ok) {
fprintf(stderr, "poly_get_last_error_info error\n");
goto exit_scope;
}
fprintf(stderr, "%s\n", error->error_message);
goto exit_scope;
}
char buffer[1024];
size_t length;
if (poly_value_to_string_utf8(thread, result, buffer, sizeof(buffer), &length) != poly_ok) {
fprintf(stderr, "poly_value_to_string_utf8 error\n");
goto exit_scope;
}
if (poly_close_handle_scope(thread) != poly_ok) {
fprintf(stderr, "poly_close_handle_scope error\n");
goto exit_context;
}
buffer[length] = '\0';
printf("%s\n", buffer);
}
}
if (poly_context_close(thread, context, true) != poly_ok) {
fprintf(stderr, "poly_context_close error\n");
goto exit_isolate;
}
if (poly_tear_down_isolate(thread) != poly_ok) {
fprintf(stderr, "poly_tear_down_isolate error\n");
return 1;
}
return 0;
exit_scope:
poly_close_handle_scope(thread);
exit_context:
poly_context_close(thread, context, true);
exit_isolate:
poly_tear_down_isolate(thread);
return 1;
}
view raw extendc.c hosted with ❤ by GitHub

システムCコンパイラを使ってコンパイル、実行し、GraalVMのネイティブPolyglotライブラリにリンクできます。再度言いますが、JVMは必要ありません。

$ clang -Igraalvm-ee-19.0.0/Contents/Home/jre/lib/polyglot -rpath graalvm-ee-19.0.0/Contents/Home -Lgraalvm-ee-19.0.0/Contents/Home/jre/lib/polyglot -lpolyglot extendc.c -o extendc
$ otool -L extendc
extendc:
  libpolyglot.dylib (current version 0.0.0)
  /usr/lib/libSystem.B.dylib (current version 1252.250.1)
$ ./extendc '14 + 2'

$ ./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]
$ ./extendc -ruby '(0...8).map { |n| 2 ** n }'
[1, 2, 4, 8, 16, 32, 64, 128]

これがGraalVMでできることその7です。ネイティブアプリケーションの1個のライブラリを使って任意のGraalVM言語に組み込むことができます。

8. Java code as a native library

Javaには、ネイティブアプリケーションや他のマネージド言語といった、他のエコシステムでは利用できないことが多い、非常に高品質のライブラリが多数あります。ネイティブアプリケーションからJavaライブラリを使用したい場合は、JVMを埋め込むことができますが、これは非常に大きなアプリケーションになったり、複雑になったりします。

GraalVMを使用すると、市販のJavaライブラリや作成したJavaライブラリを使い、他のネイティブ言語から使用するためのスタンドアロンのネイティブライブラリにコンパイルできます。以前のネイティブコンパイルと同様に、JVMを実行する必要はありません。

Apache SIS geospatialライブラリを使って、地球上の2地点間の大圏距離を計算するアプリケーションを作成しました。今回はSIS 0.8を使っています。以下のURLからダウンロードし、JARファイルを展開します。

The Apache SIS™ library
http://sis.apache.org/

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));
}
}
view raw Distance.java hosted with ❤ by GitHub

通常通りコンパイルできるので、その後これを使ってLondon(北緯51.507222、西経0.1275)とNew York(北緯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

これを実行ファイルではなくネイティブ共有ライブラリとしてビルドすることもできます。この場合、1個以上のメソッドを@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);
}
...
}
view raw Distance.java hosted with ❤ by GitHub

javacコマンドラインを変更する必要はありません。それはGraalVMが自動的にこれらの新しいAPIをクラスパスに設定してくれるからです。この後、コンパイルして共有ライブラリと、自動生成されたヘッダーファイルにすることができます。

$ native-image --no-server -cp sis.jar:. --shared -H:Name=libdistance
$ otool -L libdistance.dylib   # .so on Linux
libdistance.dylib:
  .../graalvm-ten-things/libdistance.dylib (current version 0.0.0)
  /usr/lib/libSystem.B.dylib (current version 1252.250.1)
  CoreFoundation (current version 1575.12.0)
  /usr/lib/libz.1.dylib (current version 1.2.11)
$ du -h libdistance.dylib
1.8M  libdistance.dylib

それからライブラリを使うための小さなCプログラムを記述できます。ネイティブライブラリへのインタフェースにはちょっとしたお作法があります。VMではヒープ、スレッド、ガベージコレクタ、その他のサービスを管理する必要があるので、システムのインスタンスを作成してメインスレッドを伝える必要があります。

#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, &thread) != 0) {
fprintf(stderr, "graal_create_isolate 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("%.2f km\n", distance(thread, a_lat, a_long, b_lat, b_long));
if (graal_detach_thread(thread) != 0) {
fprintf(stderr, "graal_detach_thread error\n");
return 1;
}
return 0;
}
view raw distance.c hosted with ❤ by GitHub

これを標準のシステムツールでコンパイルし実行できます(Linuxでは、環境変数 LD_LIBRARY_PATHに指定してください)。

$ clang -I. -L. -ldistance distance.c -o distance
$ otool -L distance
distance:
 .../graalvm-blog-post/libdistance.dylib (current version 0.0.0)
 libSystem.B.dylib (current version 1252.0.0)
$ ./distance 51.507222 -0.1275 40.7127 -74.0059
5570.25 km

これがGraalVMでできること、その8です。Javaコードをコンパイルして、フルJVMがなくてもネイティブアプリケーションで利用可能なネイティブライブラリにすることができます。

9. Polyglot in the database

組み込み言語のためのPolyglotライブラリの用途はOracle Databaseにあります。このライブラリを使って、GraalVM言語とSQLからのモジュールの使用をサポートするOracle Database Multilingual Engine(MLE)を作成しました。

例えば、フロントエンドがすでにJavaScriptで記述されており、JavaScriptのvalidatorモジュールを使用して電子メールアドレスの検証を行っているとします。SQLまたはPLSQLで書かれたデータベース内に同じアプリケーションに対するロジックがある場合、結果が同じになるようにまったく同じバリデータを使用できるようにしたいと考えることでしょう。

以下のURLからDockerイメージとしてMLEをダウンロードできます。ダウンロードできたらDockerにロードします。

Using MLE with Docker
https://oracle.github.io/oracle-db-mle/releases/0.2.7/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

対話型SQLツールであるsqlplusを実行できるのであれば、このBashターミナルからデータベースに接続して起動しましょう。

$ sqlplus scott/tiger@localhost:1521/ORCLCDB

では、Dockerで動作している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

これで、validator モジュールを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でできることその9です。Oracle Database内でGraalVMの言語を実行し、フロントエンドやバックエンドのロジックと同じロジックをやデータベース内で利用できます。データベースからアプリケーションサーバーにロジックを取り出す必要はありません。

10. Create your own language

Oracle Labsと私たちの学術的共同研究者は言語実装の開発を簡単にするためのTruffleフレームワークを開発したことにより、比較的小さなチームでJavaScript、R、Ruby、PythonとCの新しい高性能実装を作ることができました。

Truffleは言語の抽象構文木(AST)インタプリタを書くためのJavaライブラリです。 ASTインタプリタは、パーサの出力に直接作用し、バイトコードや従来のコンパイラ技術を必要としないため、おそらく言語を実装する最も簡単な方法ですが、実行が遅いことが多々あります。そのため、部分評価と呼ばれるテクニックをASTインタプリタと組み合わせました。これは、ASTインタプリタベースにするだけで、TruffleがGraalを使用して自動的に言語にJITコンパイラを提供するものです。

Truffleを使用して、独自の新しいプログラミング言語を実装したり、既存のプログラミング言語の高性能実装を作成したり、ドメイン固有の言語を実装したりできます。私たちのプロジェクトでは、TruffleとGraalの詳細について多くのことを話していますが、Truffleが言語を実装するための簡単な方法であることを伝え忘れることが多々あります。そして、あなたは自動的にデバッガのような機能を入手しています。プログラミング言語実装について学部課程を修了した人であれば、基本的なスキルを身につけているはずです。Oracle Labsは、数ヶ月で1名のインターンが、以前のどの作業よりも早くRubyの基本バージョンを実装しました。

ここで完全な言語の完全な詳細はお伝えできませんが、SimpleLanguageは、簡素化されたJavaScriptふうの、Truffleを使って独自の言語を作成する方法の実行可能なチュートリアルです。 例えば、if文の実装を見てください。

A simple example language built using the Truffle API
https://github.com/graalvm/simplelanguage
SLIfNode.java
https://github.com/graalvm/simplelanguage/blob/master/language/src/main/java/com/oracle/truffle/sl/nodes/controlflow/SLIfNode.java

Oracle Labs以外の人がTruffleを使って作成したその他の言語には、以下のようなものがあります。

Lispの例にはチュートリアルもあります。

Tutorial
http://cesquivias.github.io/

Conclusion

GraalVMはより強力な言語とツールを構築し、それらをより多くの環境に導入するためのプラットフォームで、非常に多様な新機能を実現します。プログラムがどこで実行されていても、利用中の言語に関係なく、必要な言語とモジュールを選択できます。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中