libClang/LLVM を使って C++ コードを JIT コンパイルする環境を整えます.
概要
構成としては以下のようになります.
+------------------+ includes
| | JIT +------------+ +--------------+
| C++ app | <--+- | C++ code |<---| STL header |
| | | +------------+ +--------------+
+------------------+ | | C header |
| libClang | | +--------------+
+------------------+ | |
| libLLVM(MCJIT) | | |
+------------------+ | |
| STL/libc runtime |----+--------------------------+
+------------------+ link
問題点
(既存のを含め) C++ コードをそのまま JIT コンパイルしたいとなると, 通常, C++ コードは STL を使用しているため, STL のランタイム周り(libc++, libgnustl など)やヘッダッファイルの在りかの問題がからんできまして, この辺りでコンフリクトなどの不都合が起こりやすくなります.
app 側(libClang,libLLVM, STL runtime)と, JIT 実行される C++ コードは同じ ABI, STL であり, また RTTI/例外の構成も同じである必要があります.
C 言語ですと STL が無い + ABI 問題があまり無いため, JIT 環境は整えやすいですが
(Windows 環境では libc のヘッダファイルとランタイムの配布をどうするか考える必要はある),
漢たるもの, C++ のフル機能の JIT やりたいですね.
LLVM のサイトには prebuilt バイナリがありますが http://releases.llvm.org/download.html,
これは基本的には clang/llvm ツール群のスタンドアロン実行を想定していて, 付属の libLLVM*
などのライブラリは, ABI 違いや stdlib 違い(プレビルトは Ubuntu との互換性を考慮してか gnustl/libgcc でビルドされている)などで, 自作の JIT アプリでこれをそのまま使うとリンクエラーなど起きてしまいます.
Ubuntu 16.04 で試したところ, prebuilt の 6.0.0 はシンボル解決エラー. 5.0.1 はリンクできるものの, JIT をすると thread_struct
あたりが見つからないなどのエラーが出ました.
手順
libClang/LLVM をソースからコンパイルして環境を整えます.
ホストコンパイラには prebuilt の clang++ を使います(これにより, ユーザがビルドした clang や, gcc でコンパイルしたときに発生しそうな問題を減らします)
libcxx のビルド(単体向け)
後述する clang/llvm を含め一式ビルドしたほうが, のちのち面倒が無いと思いますので, ここはスキップして一式ビルドをするのをおすすめします.
参考までに単体でビルドする手順を残しておきます.
LLVM_DIR=$HOME/local/clang+llvm-6.0.0-x86_64-linux-gnu-ubuntu-14.04
LLVM_SRC_DIR=../llvm
LIBCXXABI_DIR=../libcxxabi
LIBCXX_DIR=../libcxx
INSTALL_DIR=$HOME/local/llvm-libcxx-dist
CLANG_BIN=${LLVM_DIR}/bin/clang
CLANGXX_BIN=${LLVM_DIR}/bin/clang++
curdir=`pwd`
# build libcxxabi
$ rm -rf ../libcxxabi-build
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${INSTALL_DIR} -DLIBCXXABI_LIBCXX_PATH=${LLVM_DIR} -DCMAKE_C_COMPILER=${CLANG_BIN} -DCMAKE_CXX_COMPILER=${CLANGXX_BIN} -B../libcxxabi-build -H${LIBCXXABI_DIR}
$ cd ../libcxxabi-build
$ make && make install
$ cd ${curdir}
# build libcxx with libcxxabi
$ rm -rf ../libcxx-build
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${INSTALL_DIR} -DLLVM_PATH=${LLVM_DIR} -DCMAKE_C_COMPILER=${CLANG_BIN} -DCMAKE_CXX_COMPILER=${CLANGXX_BIN} -DLIBCXX_CXX_ABI=libcxxabi -DLIBCXX_CXX_ABI_INCLUDE_PATHS=${LIBCXXABI_DIR}/include -B../libcxx-build -H${LIBCXX_DIR}
$ cd ../libcxx-build
$ make && make install
$ cd ${curdir}
よりピュアな環境を構築するのであれば,
にあるように複数回に分けて libcxx をビルドするとよいでしょう.
LLVM のビルド
clang, llvm を libcxx を使うようにしてビルドします.
以下のサイトを参考に, libcxx, libcxxabi も含め一式 clang/llvm をビルドします.
を参考に一括でビルドするスクリプトです.
ホストコンパイラには prebuilt clang/LLVM を使うとよいです.
-DLLVM_ENABLE_CXX=On
にして LLVM ビルドします.
また, TERMINFO を Off にして libtinfo 依存を無くしておくとよいです(libtinfo は, warning や error メッセージを色付きで出す用?).
build type は Release
or MinSizeRel
にしておきましょう.
Debug だと clang の動作が遅くなり使い物になりません! RelWithDebInfo であれば clang の動作は速くはなりますが, ファイルサイズは 30 GB くらい消費してしまいます!
デフォルトでは RTTI/例外なしでビルドされます. 必要であれば RTTI/例外を有効にしてビルドしてください.
version=6.0.0
bases="llvm-${version}.src cfe-${version}.src compiler-rt-${version}.src"
bases="${bases} libcxx-${version}.src libcxxabi-${version}.src"
bases="${bases} libunwind-${version}.src lld-${version}.src lldb-${version}.src" \
#bases="${bases} openmp-${version}.src polly-${version}.src"
#bases="${bases} clang-tools-extra-${version}.src"
llvm_root=llvm-${version}.src
rm -rf ${llvm_root}
# "test-suite-${version}.src" is not included because reasons. It's easy enough
# to add if you want it.
for base in ${bases}
do
if [ ! -f "${base}.tar.xz"]; then wget -t inf -c http://llvm.org/releases/${version}/${base}.tar.xz; fi
tar xvf ${base}.tar.xz
done
mv -v cfe-${version}.src ${llvm_root}/tools/clang
#mv -v clang-tools-extra-${version}.src ${llvm_root}/tools/clang/tools/extra
mv -v compiler-rt-${version}.src ${llvm_root}/projects/compiler-rt
mv -v libcxx-${version}.src ${llvm_root}/projects/libcxx
mv -v libcxxabi-${version}.src ${llvm_root}/projects/libcxxabi
mv -v libunwind-${version}.src ${llvm_root}/projects/libunwind
mv -v lld-${version}.src ${llvm_root}/tools/lld
mv -v lldb-${version}.src ${llvm_root}/tools/lldb
#mv -v openmp-${version}.src ${llvm_root}/projects/openmp
#mv -v polly-${version}.src ${llvm_root}/tools/polly
mkdir ${llvm_root}/build
cd ${llvm_root}/build
cmake -DCMAKE_CXX_COMPILER=/path/to/clang++ -DCMAKE_C_COMPILER=/path/to/clang -DLLVM_ENABLE_TERMINFO=Off -DLLVM_ENABLE_LIBCXX=On -DCMAKE_INSTALL_PREFIX=$HOME/local/clang+llvm-${version} -DCMAKE_BUILD_TYPE=MinSizeRel ..
make -j8 && make install
よりファイルサイズを減らす.
アーキテクチャが固定(たとえば x86-64 だけでよい, aarch64 だけでよい)であれば, 以下のようにしてファイルサイズを減らせます.
Minimal (34MB) LLVM build with C API
こちらでビルドだけですが, 試した限りでは clang もいれて 8.0.0 で 70 MB くらいでした(libcxx などはのぞく).
アプリ側での JIT コンパイル設定
上記でビルドした clang++/libclang/libLLVM で JIT アプリをコンパイルします.
注意点
アプリ側の関数を JIT される C++ コードから呼ぶ場合は, アプリを shared library(+ PIC, PIE)でコンパイルします.
そうしないと, JIT C++ コード側からアプリにリンクされているライブラリのシンボルが見えなくなります(正確には, 同じヘッダ定義を読んでいるのであればシンボルは解決できるが, コードの配置位置がずれるなどで関数(メソッド)を呼んだときに segmentation faulut する).
設定例
LLVM の JIT サンプルと, softcompute の JIT 周りを参考ください.
JIT アプリ側では, libClang でコンパイルするときのパラメータを調整します.
-nostdinc++
で標準のヘッダファイルを探索せず, ユーザ定義のヘッダを見るようにします.
-I
で libcxx のヘッダのパスを, -isystem
でビルドした clang/LLVM のシステムインクルードパスを設定します.
(上の jit-engine.cc ではまだ未反映)
-nostdinc++ -I/path/to/llvm-libcxx-dist/include/c++/v1 -isystem=/path/to/llvm-libcxx-dist/lib/clang/7.0.0/include
(7.0.0 は適当に置き換えてください)
-isystem
を使わないと, include_next
で stddef.h が見つからないなどのエラーがでます.
_GNU_SOURCE
も定義しないほうがいいかもしれません.
上記のパス設定は, アプリをビルドするときと, JIT コンパイルするときの設定の両方で行います.
name mangling
アプリから C++ JIT コードの C++ 関数を直接呼びたい場合は, Module::getFunction
には mangle 後のシンボル名を指定します.
これにより, STL を含んだ C++ コードを JIT コンパイルできます!
まとめ
libClang/LLVM を使って C++ コードの JIT をできるようにしました.
問題点
そこそこ簡単なコードでも, テンプレート多用している C++ コードを JIT コンパイルはやっぱり遅くてつらい(3~4 秒くらいかかる. テンプレートなしだと 1~2 秒くらい).
Windows の場合は, Visual Studio(MSC) でコンパイルする場合は msvcrt ランタイムと, Visual Studio の C++ header が必要になりますが, これが LLVM JIT 側が対応しているかは不明です.
最近は clang for Visual Studio も出てきて, windows 関連のヘッダは clang でも問題なくパースできるようになってきているようですが, MSVC との ABI 絡みはどうでしょうか?
また, 動作したとしても, MSVCRT と MSC STL ヘッダーファイル再配布の問題があります.
Windows の場合は, ヘッダファイル再配布 OK の MinGW ベースとするのがよさそうです.
追記(Microsoft STL がオープンソースになりました. 2019 年 9 月 17 日)
Microsfot STL のソースコードがオープンソースになりました.
これにより, Windows での C++ JIT 用に STL ヘッダファイルを配布することが可能になります.
(Visual Studio で提供されている STL とは微妙に違うようですが, binary compatible ではあるらしい)
ただし libc(MSVCRT)のヘッダはありませんので, printf
などは使えません
自前で無理やり定義するのも手か, 誰かが non-GPL な MSVCRT 用 C ヘッダを公開しているかも? musl は Windows は考慮されていませんでした. あとは頑張って nanolibc の実装を増やしていくかですかね
TODO
- NanoSTL https://github.com/lighttransport/nanostl を利用した C++ JIT を試す.
- Windows(MSVC) で nanostl or Microsoft STL, nanolibc で JIT 側へ C/C++ ヘッダを見せる
- clang module あたりでプリコンパイルヘッダを試す
- テンプレートメタプログラムのコンパイルを早くする方法を探す
- 例外を有効にして, JIT した C++ プログラムが実行時エラーになったときでも app 本体が落ちないようにする.
- OrcJIT などの新しい JIT 形式を試す(筆者は MCJIT が出た頃の知識で止まっているので OrcJIT をよく知らない)