Compiling Native Projects via the GraalVM LLVM Toolchain

原文はこちら。
The original was written by Josef Eisl (Oracle Labs).
https://medium.com/graalvm/graalvm-llvm-toolchain-f606f995bf

GraalVMは、JavaScript、Ruby、R、Python、およびJava、Scala、KotlinなどのJVM言語を含むさまざまな言語をサポートする高性能な多言語ランタイムです。

GraalVM
https://www.graalvm.org/
GraalVM JavaScript Reference
https://www.graalvm.org/docs/reference-manual/languages/js/
Reference manual for TruffleRuby
https://www.graalvm.org/docs/reference-manual/languages/ruby/
Reference manual for R
https://www.graalvm.org/docs/reference-manual/languages/r/
Reference manual for Python
https://www.graalvm.org/docs/reference-manual/languages/python/
JVM Language Reference
https://www.graalvm.org/docs/reference-manual/languages/jvm/

GraalVMの言語ファミリーには、ちょっと毛色の違う言語が入っています。前述の言語はすべてマネージド言語であり、言語ランタイムはすべてのメモリ要件を管理しますが、LLVMランタイムはLLVMビットコードを実行するものの、マネージド言語ではありません。つまり、メモリを自動的に解放するガベージコレクタはありません。代わりに、ユーザーは素のポインターを手に入れ、手作業でメンテナンスする必要がありますし、配列は境界チェックされないため、プログラマーはサイズをチェックする責任があります。

Reference manual for LLVM
https://www.graalvm.org/docs/reference-manual/languages/llvm/
LLVM Language Reference Manual
https://llvm.org/docs/LangRef.html

LLVMビットコードのサポートにより、CやC++などの言語がGraalVMのPolyglotな世界に入ってきます。これにより、例えばJavaScriptオブジェクトをCコードに渡し、あたかもC構造体であるかのようにアクセスできますし、またはその逆も可能です。このとき、基礎となるデータを別の表現に変換する必要はありません(詳細は以下のLLVMランタイムリファレンスを参照してください)。

Interoperability
https://www.graalvm.org/docs/reference-manual/languages/llvm/#interoperability

実行前にJavaバイトコードにコンパイルされるJVM言語と同様に、LLVMビットコードは手書きできないバイナリ形式です。代わりに、フロントエンドがソース言語をビットコードにコンパイルします。たとえば、clangはC/C++をビットコードに変換するフロントエンドであり、LLVMコードジェネレーターを使用してネイティブコードにコンパイルします。

LLVM Bitcode File Format
https://llvm.org/docs/BitCodeFormat.html
Clang: a C language family frontend for LLVM
https://clang.llvm.org/
The LLVM Compiler Infrastructure
https://llvm.org/

Javaプロジェクトの場合、Javaバイトコードがデフォルトの出力形式ですが、ネイティブプロジェクトをLLVMビットコードにコンパイルする場合、より困難な場合があります。LLVMビットコードは主にコンパイル時の中間表現として使用されるため、ほとんどのツールはそのユースケースに焦点を当てています。このプロセスを簡単にするために、GraalVM 19.2.0でLLVMツールチェーンを実験的な機能として出荷し始めました。それ以来、アーリーアダプターからのフィードバックを取り入れて、ツールチェーンのユーザーエクスペリエンスをさらに向上させました。その結果、GraalVM 19.3.0ではツールチェーンは実験的な機能ではなくなりました。このエントリでは、ツールチェーンでできること、そしてその動作の詳細をご紹介します。

Release Notes (19.2.0)
https://www.graalvm.org/docs/release-notes/19_2/#1920
Release Notes (19.3.0)
https://www.graalvm.org/docs/release-notes/19_3/#1930

Prerequisites

以下の手順はLinuxとmacOSでテスト済みです。

  • GraalVM (version 19.3.0 以後)
    Community EditionでOKですが、LLVMランタイムのマネージドモードを試したい場合にはEnterprise Editionが必要です。
  • C 標準ライブラリヘッダー
    Linuxでは利用中のディストリビューションに依存します。Debianベースのディストリビューションでは、libc6-devというパッケージです。rpmベースのディストリビューションでは、glibc-headersパッケージです。macOSの場合、Xcodeをインストールする必要があります。
  • Automake や make (ソフトウェアプロジェクトの構成ならびにビルド用途)
    パッケージマネージャが支援してくれるはずです(macOSの場合はHomebrewからダウンロードできます)。
  • Cコンパイラ (オプション)
    ネイティブ参照実行を最初にビルドする場合、gccもしくはclangをインストールしてください。macOSの場合はXcodeに含まれています。.
  • ユーティリティツール (オプション)
    結果の調査目的で、file、objdump、dwarfdump、nm、ldd (Linux) もしくはotool (macOS) を使います。ほとんどのツールはデフォルトでインストール済みのはずです。

以後では、GraalVMのbinフォルダーにPATHが通っている前提とします。Linuxの場合、graalvm-*/binですが、macOSでは、graalvm-*/Contents/Home/binです。構成の確認は、、lli –helpを実行し、下部にあるwww.graalvm.orgを探してください。

The Running Example

「The Computer Language Benchmarks Game」のpidigitsベンチマークのCバージョンをこのエントリで使います。説明によれば、プログラムは、Unbounded Spigot Algorithmを使用して、任意の桁数のπを計算します。一見簡単なプロジェクトですが、これを使ってツールチェーンの多くの興味深い機能をご紹介します。

The Computer Language Benchmarks Game
https://benchmarksgame-team.pages.debian.net/benchmarksgame/
Description
https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/pidigits.html
pidigits description
https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/pidigits.html#pidigits
pidigits C gcc program
https://benchmarksgame-team.pages.debian.net/benchmarksgame/program/pidigits-gcc-1.html

Building a Native pidigits

まず、GraalVMを使用しない場合の手順を確認するために、ネイティブ実行可能ファイルをビルドします。pidigits.cのソースコードを新しいディレクトリにコピーして、${TMP_DIR}で参照します。「pidigits C gcc program」のページに、プログラムのコンパイル方法が記載されています。必要に応じて、gccをclangに置き換えてください。

cd ${TMP_DIR}
gcc -pipe -Wall -O3 -fomit-frame-pointer -march=native pidigits.c -o pidigits.gcc_run -lgmp

-pipe、-fomit-frame-pointer、-march = nativeなどのコンパイラフラグのほとんどは実際には気にする必要はないので、簡潔にするために省略しますが、使いたい場合にはご自由にどうぞ。

ほとんどの場合、上記のコマンドは以下のメッセージを出して失敗します。

pidigits.c:9:10: fatal error: gmp.h: No such file or directory

このpidigitsでは、GNU Multiple Precision Arithmetic Library(libgmp)を使用して任意の精度の数値を処理します。上記のエラーは、libgmp.hというヘッダーファルが利用できないことを示しています(上記のコンパイルコマンドが成功した場合は、次章をスキップしてください)。

The GNU Multiple Precision Arithmetic Library
https://gmplib.org/

Building a Native libgmp

パッケージマネージャを使ってライブラリをインストールできますが、コンパイルが楽しいこと、そしてこのエントリのために自身でビルドすることにしましょう。ソースコードは以下からダウンロードしてください。

The GNU Multiple Precision Arithmetic Library
https://gmplib.org/

cd ${TMP_DIR}
curl -L -O https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2
tar -xf gmp-6.1.2.tar.bz2

libgmpプロジェクトはAutoconfという、特にGNUソフトウェア向けの人気のあるソフトウェア構成システムが生成するconfigureスクリプトを使います。Autoconfベースのパッケージのビルドは同様の流れであり、configureスクリプトで依存関係を調査し、ビルドシステムを設定した上で、 makeでプロジェクトをビルド、make installでファイルを最終デプロイ先に配置します。

Autoconf
https://www.gnu.org/software/autoconf/

ではディレクトリを作成し、その中でlibgmpをビルドしていきましょう。

mkdir build-gmp-native
cd build-gmp-native
../gmp-6.1.2/configure --prefix=${TMP_DIR}/native

–prefixオプションでconfigureに対しインストール先をオーバーライドします。今回は${TMP_DIR}/native というディレクトリを指定しています。configureでのエラーは、多くの場合は依存関係がないことが原因です。configureが成功すると、libgmpのコンパイルならびにインストールの準備が出来た状態です。

make
make install

全てうまくいけば、ライブラリが ${TMP_DIR}/native/lib にできあがっているはずです。Linuxの場合、libgmp.so であり、macOSの場合は libgmp.dylib です。

これで、pidigitsのコンパイルに戻ることができます。-Iおよび-Lコンパイラフラグをそれぞれ使用して、gmp.hヘッダーファイルとlibgmpの場所をコンパイラに指示する必要があります。Linuxでは、libgmpのmake installで-Wl、-rpath -Wl、LIBDIRというリンカ用フラグの使用を推奨してきますが、これらは、実行時に動的リンカがライブラリを見つける支援をしてくれます。macOSでは、ライブラリはデフォルトで絶対ファイル名を使用して配置されるため、これらのフラグは必要ありません(ただし、これらの設定をしても問題は起きません)。

cd ${TMP_DIR}
clang -Wall -O3 pidigits.c -o pidigits.gcc_run -lgmp \
-I${TMP_DIR}/native/include -L${TMP_DIR}/native/lib -Wl,-rpath -Wl,${TMP_DIR}/native/lib

これでPiの任意の桁数を計算できるようになりました。

./pidigits.gcc_run 1000

結果をもう少し詳しく見てみましょう。最初に、結果ファイルのファイルタイプを確認します。

file pidigits.gcc_run

このコマンドを実行すると、ELF 64ビットLSB共有オブジェクト、Linuxの場合はx86-64、macOSの場合はMach-O 64ビット実行可能ファイルx86_64などが出力されます。これらはこれらのプラットフォームのデフォルトの実行可能形式です。ldd(Linux)またはotool -L(macOS)ツールを使用すると、実行可能ファイルの依存関係を表示できます。Linuxであれば次のようなものが表示されます(linux.soの依存関係は今回は気にしません)。

ldd ./pidigits.gcc_run
...
libgmp.so.10 => ${TMP_DIR}/native/lib/libgmp.so.10
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
...

macOSの場合は少々異なります。

otool -L pidigits.gcc_run
pidigits.gcc_run:
${TMP_DIR}/native/lib/libgmp.10.dylib
/usr/lib/libSystem.B.dylib

結果から、pidigitsプログラムはlibgmpとlibcに依存していることがわかります(macOSではlibcはlibSystemの一部です)。libgmpの依存関係を確認すると、libcにも依存していることがわかります。pidigits、libgmp、およびlibcの3つの部分はすべて、現在のアーキテクチャ(ほとんどの場合x86_64)のマシンコードを含むネイティブバイナリです。

Hello Bitcode, Hello GraalVM LLVM Toolchain!

マシンコードをネイティブ実行する代わりに、GraalVM LLVMランタイムlliを使用して、ビットコードプログラムとしてpidigitsを実行してみましょう。それを行う前に、プログラムをビットコードにコンパイルする必要があります。ここでLLVMツールチェーンが関わってきます。LLVMツールチェーンは、ネイティブプロジェクトをビットコードにコンパイルするためのCコンパイラやリンカなどのビルドツールのセットです。LLVMツールチェーンはデフォルトではGraalVMに同梱されていませんが、GraalVMアップデータ (gu) を使用して簡単にインストールできます。

gu install llvm-toolchain

The GraalVM LLVMランタイムランチャー (lli) を使えば、ツールチェーンの場所を確認できます。

lli --print-toolchain-path
/path/to/graalvm/.../languages/llvm/native/bin

このディレクトリには、clang、gcc、ldなどの実行ファイルとシンボリックリンクがあり、多くは同じ実行ファイルを指していますが、一部のビルドシステムではコンパイラなどのツールに特定の名前が付いていることを期待しているため、シンボリックリンクを張ります。ツールチェーンの一般的な考え方は、ビルドシステムでツールチェーンディレクトリのツールを使用できるようにすることであり、コンパイルの結果はGraalVM LLVMランタイムで実行できます。

pidigits はビルドシステムを使わないため、コンパイラを直接呼び出します。ツールチェーンのコンパイラを使うためには、ツールチェーンのディレクトリを環境変数PATHに設定します。PATHが通っていることを確認するには、which clangでツールチェーンを指しているかどうかを確認します。

cd ${TMP_DIR}
export TOOLCHAIN_PATH=`lli --print-toolchain-path`
export PATH=${TOOLCHAIN_PATH}:$PATH
clang -Wall -O3 pidigits.c -o pidigits.bitcode_run -lgmp \
-I${TMP_DIR}/native/include -L${TMP_DIR}/native/lib -Wl,-rpath -Wl,${TMP_DIR}/native/lib

ネイティブコンパイルとの違いに気づきましたか?分かりませんでしたか?それはよかったです。なぜなら、ツールチェーンは、可能な限りシームレスに溶け込むことを目的としているためです。ほとんどの場合、ツールチェーンディレクトリからツールに切り替えるだけで十分です。これで、GraalVM LLVMランタイムを使用して結果を実行できます。

lli pidigits.bitcode_run 1000

What is going on?

この新たな実行ファイルを見ていきましょう。pidigits.bitcode_runというファイルは先ほどと同じ結果を返し、ldd / otool は何も新たな情報を表示しません。ではどこにビットコードがいるのでしょうか?その答えは、実行ファイルの特別なセクションに埋め込まれている、というものです。実行ファイルのセクションのリストは以下のコマンドで取得できます。

objdump -h pidigits.bitcode_run

ファイル形式がLinuxとmacOSで異なるため、結果は少々違いますが、Linuxの場合ビットコードは.llvmbcと呼ばれるセクションに格納されています。

...
9 .llvmbc 00002144 0000000000200860 0000000000200860 00000860 2**4
CONTENTS, ALLOC, LOAD, READONLY, DATA
...

In Mach-O files on macOSの場合、Mach-Oファイルではビットコードは__bundleセクションにあります。

...
8 __bundle 00002d70 0000000100002000 DATA
...

otool -l pidigits.bitcode_runを呼び出すと、Mach-Oファイルについてもう少し情報を取得できます。

...
Section
sectname __bundle
segname __LLVM
addr 0x0000000100002000
size 0x0000000000002d70
...

この結果、__bundleセクションは__LLVMセグメントにあることがわかります。

セクション名はGraalVMによるものではなく、-fembed-bitcodeオプションを付けてコンパイルすることでclangが命名します。「でもそんなオプション付けてない」と仰有ることでしょう。そうですね、このツールチェーンがやってくれています。( 今回の場合はclangですが)${TOOLCHAIN_PATH} にある実行ファイルは単なるラッパーにすぎません。これらのラッパーはオプションを処理し、追加のコンパイラフラグを付けて埋め込みビットコード付きの実行ファイルを生成します。

Why ELF and Mach-O files with Embedded Bitcode?

ツールチェーンがプレーンなビットコードファイルではなく埋め込みビットコードセクションを持つ実行可能ファイルを生成する理由は2つあります。前述のように、可能な限り多くのビルドシステムでツールチェーンをそのまま使用できるようにしたいのですが、実際にはコンパイラが実行可能ファイルを生成しない場合、ほとんどのビルドシステムは満足できません。ビットコードを実行可能ファイルに載せることで、多くのトラブルを回避できます。ただし、実行可能ファイルを使用すると、プレーンなビットコードファイル、つまり依存関係レジストリよりも多くの機能が提供されます。LLVMビットコードは、ライブラリの依存関係の登録をサポートしていません(lddとotool -Lでわかることです)。GraalVM LLVMランタイムは、ELFやMach-Oファイルからの情報を利用して、依存関係を見つけてロードします。長所は、ビルドシステムがそれらを無料で挿入する点です。

Under the Hood

裏で何が起きているのかを知りたいのであれば、-vフラグを付けて冗長出力するようにコンパイラを呼び出してください。

clang -v -Wall -O3 pidigits.c -c -o pidigits.o -I${TMP_DIR}/native/include

他の多くの引数とともに、フラグ-flto = fullを確認できます。このフラグは、リンク時間最適化(LTO)を有効化します。

LLVM Link Time Optimization: Design and Implementation
https://llvm.org/docs/LinkTimeOptimization.html

通常、clangおよび他のほとんどのコンパイラは、各ソースファイルをネイティブマシンコードを含むオブジェクトファイルに変換し、これらのオブジェクトファイルをリンカに渡し、リンカがそれらを結合して実行可能ファイルにします。そのため、clangは単一のソースファイルのコンパイル中にビットコードを使用していましたが、リンク時には、すべてのビットコードはすでになくなっています。これは、ビットコードを含む実行可能ファイルを構築するという目標と矛盾しています。それを修正するためにLTOを利用します。LTOを使用すると、clangはCのソースをコンパイルする際にネイティブオブジェクトファイルを生成せず、ビットコードファイルを生成します。ビットコード生成はfile pidigits.oを実行して確認できます。この呼び出しでLLVM bitcodeのような表現が出るはずです。リンカがビットコードファイルを処理するようになったため、LLVM IRレベルでモジュール間最適化を実行できます。リンク済みビットコードはネイティブコードに変換されます。この段階で、リンカにマシンコードだけでなく、生成される実行可能ファイルにLLVMビットコードも含めるように指示します。

この最後の重要なステップは、LLVMは現在デフォルトではサポートしていません。そのため、ツールチェーンにLLVMのカスタムバージョンを同梱していますが、現在、変更をLLVMに寄贈するプロセスを進めています。

[LTO] Support for embedding bitcode section during LTO
https://reviews.llvm.org/D68213

pidigits in Bitcode

ここまでのことをまとめましょう。デフォルトのコンパイラからツールチェーンのコンパイラに切り替えるだけで、GraalVM LLVMランタイムで実行できるビットコードが埋め込まれた実行ファイルが得られました。ビットコードはELFまたはMach-Oファイルに埋め込まれているため、LLVMランタイムはロードすべきライブラリも認識しています。依存関係は以前と同様に見えます。

pidigitsプログラムはビットコードとして実行されるようになっていますが、他の部分、libgmpおよびlibcはすべてネイティブコードのままです。つまり、pidigitsがgmpライブラリ関数を呼び出すたびに、LLVMランタイムがネイティブ呼び出しを実行します。ネイティブコードはLLVMランタイムにとってブラックボックスであるため、このような呼び出しは最適化の障壁です。

libgmp in Bitcode

では、LLVMランタイムにlibgmpを最適化させるためには何ができるでしょうか。ビットコードにコンパイルすることもできます!ツールチェーンが手元にあれば、これも簡単です。clangがGraalVMツールチェーンディレクトリを指しているPathを確認して、ツールチェーンがPath上にあることを確認します。それでは、libgmpをコンパイルして、新しいディレクトリにインストールしましょう。

cd ${TMP_DIR}
mkdir build-gmp-bitcode
cd build-gmp-bitcode
../gmp-6.1.2/configure --prefix=${TMP_DIR}/bitcode
make
make install

ビットコードのlibgmpに対してリンクするpidigitsの新バージョンをビルドする必要もあります。

cd ${TMP_DIR}
clang -Wall -O3 pidigits.c -o pidigits.bitcode_gmp_run -lgmp \
-I${TMP_DIR}/bitcode/include -L${TMP_DIR}/bitcode/lib -Wl,-rpath -Wl,${TMP_DIR}/bitcode/lib

動作するか試してみましょう

lli pidigits.bitcode_gmp_run 1000

… 動きませんね。

External function __gmpn_mul_1 cannot be found.

残念! 何が悪かったのか調べてみましょう。このエラーは、GraalVM LLVMランタイムがlibgmpでシンボル__gmpn_mul_1を見つけることができない、という意味です。nmを使用して、ライブラリ内のすべてのシンボルを列挙できます(macOSでは、ライブラリはlibgmp.dylibと呼ばれます)。

nm ${TMP_DIR}/bitcode/lib/libgmp.so | grep __gmpn_mul_1

コマンドを実行すると以下のような表示が現れるはずです。

000000000003eb50 T ___gmpn_mul_1

出力内のTは、シンボルがライブラリのテキストセクションにあり、そこにネイティブマシンコードが格納されていることを示しています。

nm
https://docs.oracle.com/cd/E88353_01/html/E37839/nm-1.html

nmはネイティブコードのみを出力し、ビットコードシンボルは出力しないことに注意してください。そこで、__ gmpn_mul_1がネイティブコードに存在することを確認しましたが、LLVMランタイムは、埋め込まれたビットコードから欠落していると言っているので、もっと深く掘り下げる必要があります。関数がどこから来たのかを知るために、デバッグ情報を見てみましょう。GraalVM LLVMランタイムがサポートするLinuxおよびmacOSの両プラットフォームは、dwarfdumpツールで検査できるDWARF形式を使っています。Linuxの場合、デバッグ情報はライブラリに含まれています。

The DWARF Debugging Standard
http://dwarfstd.org/

dwarfdump ${TMP_DIR}/bitcode/lib/libgmp.so

macOSの場合、通常は別ディレクトリに格納されています。

dwarfdump ${TMP_DIR}/build-gmp-bitcode/.libs/libgmp.10.dylib.dSYM/Contents/Resources/DWARF/libgmp.10.dylib

欠落している関数_gmpn_mul_1を探しましょう。

...
DW_AT_name _gmpn_mul_1
DW_AT_decl_file 0x00000001 .../build-gmp-bitcode/mpn/tmp-mul_1.s
...

欠落している関数は、tmp-mul_1.sというファイルで定義されています。.s拡張子は、このファイルがアセンブリファイルであることを示しています。コンパイル中、そのファイルはclangによってCコードからビットコードにコンパイルされませんが、アセンブラはアセンブリコードをネイティブオブジェクトファイルに直接変換します。ただし、LLVMランタイムで関数を実行できるようにするには、その関数のビットコードバージョンが必要です。

残念ながら、この問題に対する一般的な解決策はありませんが、多くの場合なんとかなります。プロジェクトの一部をアセンブラで作成する理由は通常、パフォーマンスの最適化のためです。プロセッサは特別なマシン命令をサポートする場合がありますが、これは単純なC言語のコードでは簡単に対象にできません。アセンブラーでアプリケーションのパフォーマンス上重要なちょっとした箇所をアセンブラで記述すると、許容可能なメンテナンスのオーバーヘッドでパフォーマンスが向上します。

アセンブラの部分は常にアーキテクチャに固有です。クロスプラットフォーム互換にするために、多くのプロジェクトには、特殊なアセンブラーバージョンを使わずに、プラットフォームで使用される汎用C実装が含まれています。ビルドシステムによっては、これを利用してビットコードの実装を取得できる場合があります。

libgmpの場合、configureで何か出力するかどうかを確認できます。

cd ${TMP_DIR}
gmp-6.1.2/configure --help

幸いに、オプションの長いリストの中にアセンブリに関連するエントリがありました。

...
--enable-assembly enable the use of assembly loops [default=yes]`
...

それでは、–disable-assemblyフラグを付けて再構成し、libgmpを再ビルドしましょう。

cd ${TMP_DIR}
mkdir build-gmp-bitcode-no-asm
cd build-gmp-bitcode-no-asm
../gmp-6.1.2/configure --prefix=${TMP_DIR}/bitcode --disable-assembly
make
make install

再ビルドしたlibgmpを同じディレクトリに配置したので、pidigitsは再コンパイルする必要はありません。単純にプログラムを再実行すればOKです。

cd ${TMP_DIR}
lli pidigits.bitcode_gmp_run 1000

今度は成功です。

pidigitsとlibgmpの両方がGraalVM LLVMランタイムによって実行されました。両者間の呼び出しが最適化されている可能性がありますが、libcは依然としてネイティブマシンコードです。

前の例では、アプリケーションランタイムでビットコードとネイティブコードを組み合わせて実行しました。つまり、LLVMランタイムで実行される部分とネイティブに実行される部分の間でデータの受け渡しがあります。LLVMランタイムで実行される部分でデータ利用を任意に編成できますが、ネイティブlibcはrawメモリのみを処理できます。したがって、GraalVMのLLVMランタイムは、ネイティブライブラリと対話するモードでネイティブメモリも使用するため、ビットコードでのメモリ割り当ては、LLVMランタイムによるネイティブヒープ割り当てをトリガーします。ロードやストアなどのメモリ操作は、単純にヒープへのrawポインタで動作します(GraalVM LLVMランタイムのネイティブモード)。

Memory Access and Addressing Operations
https://llvm.org/docs/LangRef.html#memory-access-and-addressing-operations

このモードではネイティブライブラリとの完全な互換性が得られますが、ネイティブコードと同じ問題、例えばメモリ解放後の使用の問題、バッファオーバーフロー、セグメンテーションフォルトが発生する可能性があります。実際にそれを見たいですか?ではやってみましょう。

lli pidigits.bitcode_gmp_run

pidigitsのパラメータを追加することを忘れた場合は、セグメンテーション違反に直面します(macOSでは、プログラムがハングすることがありますが、これは同じ問題で発生する別の症状です。セグメンテーションフォールトも確認したい場合は、-jvmフラグを指定してlliを実行してみてください)。メッセージによると、自分が確保していないメモリにアクセスしている、とのことです。pidigits.cのmain関数を見ると、その問題が明らかになります。

int main(int argc, char **argv) {
ui d, k, i;
int n = atoi(argv[1]);
...

プログラムに渡された引数の個数(argc) をチェックせずに引数argv[1]にアクセスしています。参考までに、ネイティブバージョンの./pidigits.gcc_runを引数なしで呼び出してもまったく同じことが起こります。実行時に範囲外のargv配列にアクセスしていると通知されればいいのですが、アンマネージ言語では、配列の長さに関する情報は実行時に利用できません。この例のセグメンテーションフォールトは、実際にはそれほど悪くはありません。失敗がわかるという状況にいるからです。エラーが報告されず、プログラムがプライベートなデータにアクセスしている場合、事態はさらに悪化します。

Managed Execution of Native Code

GraalVM Enterprise Edition (EE) のLLVMランタイムは、上記のような状況で役立つマネージドモードをサポートします。

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

アイデアはシンプルなもので、ネイティブヒープを割り当てるのではなく、全てのメモリをランタイムで管理し、全てのアクセスをチェックする、というものです。これにより、不正ポインタアクセスや境界外の配列アクセスといった上記の問題を回避できます。さらに、ガベージコレクションも付いてきます。ここでは非常に基本的なマネージドモードのプロパティを説明するだけになるので、もし詳細を知りたい方は、この機能を説明しているエントリをご覧ください。

Safe and sandboxed execution of native code
https://medium.com/graalvm/safe-and-sandboxed-execution-of-native-code-f6096b35c360

マネージドモードでは全てのメモリが抽象化されているため、ネイティブメモリに単純に渡すことはできません。これは安全性の保証が弱くなり、メモリレイアウトの最適化が妨げられるためです。したがって、アプリケーションのすべての部分は、libcを含めてビットコードで利用できる必要があります。GraalVM EEのLLVMランタイムには、ビットコードにコンパイルされたmusl libcのバージョンがすでに付属しています。musl libcは他のlibc実装とバイナリ互換ではないため、マネージドモードで使用する場合はプログラムを再コンパイルする必要があります。

musl libc
https://www.musl-libc.org/

また、Cライブラリはシステムコールを使用してOSと通信します。システムコールによりセキュリティレイヤーが弱体化する可能性があるため、マネージモードではシステムコールを仮想化します。システムコールはプラットフォームごとに異なるため、この方法をスケーラブルにするべく、マネージドモードはLinux x86_64のシステムコールのみをサポートします。macOSなどの異なるプラットフォームではクロスコンパイルが必要です。繰り返しになりますが、ツールチェーンではこの複雑さを可能な限り隠蔽しようとしています。

Managed pidigits

ではマネージドモードでの実行サンプルを再ビルドしましょう。まず、マネージドモードのツールチェーンを入手する必要があります。コマンドは以前と同じですが、マネージドモードを有効化する–llvm.managedというフラグを付けてlliを実行します。

export MANAGED_TOOLCHAIN_PATH=`lli --llvm.managed --print-toolchain-path`
export PATH=${MANAGED_TOOLCHAIN_PATH}:$PATH

which clangで表示されたPathのmanagedを探すことで、動作したかどうかを確認できます。これでマネージドのlibgmpのビルド準備ができました。

cd ${TMP_DIR}
mkdir build-gmp-bitcode-managed
cd build-gmp-bitcode-managed
../gmp-6.1.2/configure --prefix=${TMP_DIR}/bitcode-managed --disable-assembly --host=x86_64-unknown-linux
make
make install

–host=x86_64-unknown-linuxフラグを使って、configureにLinux x86_64向けのビルドをしたい、と指示します。厳密に言えば、これはLinux以外のx86_64プラットフォームでのみ必要なのですが、常に付けていても問題ありません。macOSでlibgmp.dylibの代わりにlibgmp.soをビルドする必要があるなど、現状をconfigureに伝えるためにのみこの設定が必要なことに注意してください。マネージドツールチェーンのコンパイラは、常にLinux x86_64用にコンパイルするので、pidigitsのビルドは以前と同様です。

cd ${TMP_DIR}
clang -Wall -O3 pidigits.c -o pidigits.bitcode-managed_gmp_run -lgmp \
-I${TMP_DIR}/bitcode-managed/include -L${TMP_DIR}/bitcode-managed/lib -Wl,-rpath -Wl,${TMP_DIR}/bitcode-managed/lib

マネージドモードのpidigitsが実行できるようになりました。

lli --llvm.managed pidigits.bitcode-managed_gmp_run 1000

同じ結果が確認できるはずです。

アプリケーションの全てがマネージドビットコードになっており、GraalVM LLVMランタイムで実行されています。

ではセグメンテーションフォルトの例でどうなるか確認してみましょう。

lli --llvm.managed pidigits.bitcode-managed_gmp_run

And indeed, instead of a segmentation fault, we get a Illegal null pointer access error message. An embedder could catch this error and continue gracefully. (If you are wondering why it is a null pointer access and not an index out of bounds error, the argv array is always terminated by a null pointer according to the C Standard section 5.1.2.2.1/2.)

セグメンテーション違反ではなく、無効なNULLポインタアクセスエラーメッセージが表示されます。埋め込みプログラムはこのエラーをキャッチし、正常に続行できるでしょう(なぜこれがNULLポインターアクセスで、インデックスの範囲外エラーではないのか疑問に思っているのであれば、C標準セクション5.1.2.2.1/2に従って、argv配列は常にNULLポインタで終了するからです)。

Embed Languages with the GraalVM Polyglot API
https://www.graalvm.org/docs/reference-manual/embed/
C Standard section 5.1.2.2.1/2
http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf

Conclusion

このエントリでは、LLVMツールチェーンを使用してネイティブプロジェクトをビットコードにコンパイルし、GraalVM LLVMランタイムで実行する方法を紹介しました。適切な結果を得るまでに少々の調整が必要でしたが、ビットコードへのコンパイルは、ネイティブ実行可能ファイルへのコンパイルと根本的な違いはありませんでした。ただし、ツールチェーンはベストエフォート型のアプローチであり、できるだけ多くのすぐに使えるユースケースをサポートするために一生懸命努力しましたが、引き続き改善していきます。

このエントリでは、ツールチェーンのコマンドラインインターフェイスについてのみ説明しましたが、GraalVM内の他の言語からアクセスするためのJava APIもあります。

Toolchain.java
https://github.com/oracle/graal/blob/master/sulong/projects/com.oracle.truffle.llvm.api/src/com/oracle/truffle/llvm/api/Toolchain.java

Python、Ruby、およびRは、このAPIを使用して、ネイティブextensionをビットコードにコンパイルします。

Reference Manual for Python
https://www.graalvm.org/docs/reference-manual/languages/python/
Reference Manual for Ruby
https://www.graalvm.org/docs/reference-manual/languages/ruby/
Reference Manual for R
https://www.graalvm.org/docs/reference-manual/languages/r/

言語のユーザーは、たとえばRubyでgemを使用するなどしてパッケージをインストールするだけで、ツールチェーンはネイティブコードをビットコードにコンパイルします。

現在、ツールチェーンは、ネイティブモードでCおよびC++、マネージドモードでCをサポートしています。将来、ツールチェーンを拡張して他の言語もカバーする可能性がありますが、よくある一般的なケースに注力していきます。

ツールチェーンを試してみて、是非フィードバックしてください。うまくいったこと、いかなかったことは何ですか?GraalVM LLVMランタイム用に特定のプロジェクトをコンパイルするには、どの構成フラグが必要でしたか?フィードバックや機能のリクエストがある場合は、GithubリポジトリでIssueを立てるか、Twitterで@graalvmまでご連絡ください。

GraalVM: Run Programs Faster Anywhere
https://github.com/oracle/graal
GraalVM Twitter
https://twitter.com/graalvm

コメントを残す

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

WordPress.com ロゴ

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

Google フォト

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

Twitter 画像

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

Facebook の写真

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

%s と連携中