OCamlでLLVM JITを試した
自作言語熱が高まっていて、バックエンドをLLVMでやりたいと思っている。
OCamlでLLVMというとLLVMプロジェクト公式のKaleidoscope言語をOCamlで実装するチュートリアルが有名、なのだが、10年近く前に壊れたAPIを使っていたりと実際に使えるものではなく、最近のチュートリアルサイトからは消されている。
最大の問題はJITコンパイルするためのAPIが変更され、コンパイルされた結果を実行するためにはC関数ポインタを返すLlvm_executionengine.get_function_address
を使って、FFIでその関数を呼び出す必要があるようになった点。
FFIするためにLLVMとは別にCtypesというライブラリを入れる必要が出てくる。
Ctypesを使ったFFIの詳細に関してはReal World OCamlに詳しく載っている:
Ctypesに関して特筆すべきは(Real World OCamlより引用):
open Foreign let initscr = foreign "initscr" (void @-> returning window)
上記のvoid @-> returning window
のような「Cの関数型を表現するデータ」の存在である。
Llvm_executionengine.get_function_address
のシグニチャがstring -> 'a Ctypes.typ -> llexecutionengine -> 'a
なので、このC言語の型の表現を作成して渡してやる必要がある。
ありがたいことにKaleidoscopeチュートリアルのコードをこの新しいAPIに合わせて書いている人がいた:
文章付きのチュートリアル形式にはなっていないが、非常に参考になる。
勉強がてら最小構成、単一ファイル(ビルド用のduneコンフィグ以外)で完結する例を書いてgistに上げてみた:
実行すると以下のような出力になる:
define double @__toplevel1() #0 { entry: ret double 3.000000e+00 } 3.
とりあえずOCamlプログラム内でLLVM IRを作成しJITコンパイルして実行できるようになっている。
よく見ると
let n = L.const_float double_type 1.0 let m = L.const_float double_type 2.0 let ret_val = L.build_add n m "addtmp" builder
の部分が定数畳み込みで簡略化されているのがわかる。最適化パスは指定していないので何故こうなるのかはよくわからないが・・・
あと多分Llvm_analysis.verify_function the_function
を省略しているのは危険なんじゃないかという気がしている。具体的にどれだけの問題が起き得るのかはわからないが、メモリ安全性などは完全に度外視された問題のあるコードが実行され得るのではないか。悪魔が鼻から出てくるかもしれない。
とにかくとりあえずOCamlのLLVMバインディングを使ってLLVM IRを作成し、Llvm_executionengineとCtypesを使ってJITコンパイル&実行できたので、これから自作言語のバックエンドとして使っていきたい。