こんにちは。 そろそろ夏ですね。
今回の記事はRustでLLVMをやっていくことができるクレート、inkwellについてです。 inkwellを紹介している記事自体はあるのですが、実際にinkwellを利用して何かを作ってる記事が少なかったのでこの記事を書きました。
LLVM自体はそれなりに情報はあり、またきつねさん本などもあることから、トラブルシューティングはしやすいほうだと思います。 とは言いつつLLVMに対しては触りながら知見をためていったこともあり、様々の理解に少し時間がかかってしまいました。 この記事や私の書いたソースコードが少しでもRustでLLVMをやろうとする人の役に立てば幸いです。
inkwellを利用して実際に作ったものはこちらです。
小さな独自のLISP方言をLLVM IRにコンパイルするプログラムです。 まだ全然関数などがそろっていないため実用には程遠いです。
inkwell
inkwellの公式ドキュメントはこちらです。
inkwell自体はllvm-sysを安全に(unsafeで囲う必要がなく)、Rust風のAPIで使えるようにしたものです。
導入
LLVMの導入
まずはLLVM自体を導入してあげる必要があります。 LLVMのバージョンを指定しての導入にはllvmenv1がおすすめです2。
依存ビルドツールにcmake、make、ninja、g++/clang++があるので、これらをインストールします。 あとは次のコマンドで導入できます。
$ cargo install llvmenv $ llvmenv init $ llvmenv build-entry 12.0.0
inkwellの導入
inkwellを導入は、Cargo.tomlのdependenciesに以下を記述してあげるだけです。
inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", features = ["llvm12-0"] }
featuresは利用したいLLVMのバージョンに合わせて変更します。詳しくは inkwellのREADME を見てください。
inkwellを使うためには、環境変数でllvm-sysにLLVMの場所を教えなくてはいけません。 fishでは次のようにして場所を指定します(各々のシェルに応じてコマンドは変えてください)。
$ set -x LLVM_SYS_120_PREFIX (llvmenv prefix)
これでinkwellの導入まで完了です。
実際に動かしてみる
では実際にinkwellを用いてLLVMのプログラムをビルドし動かしてみましょう。
use inkwell::context::Context; use inkwell::OptimizationLevel; fn main() { let context = Context::create(); // moduleを作成 let module = context.create_module("main"); // builderを作成 let builder = context.create_builder(); // 型関係の変数 let i32_type = context.i32_type(); let i8_type = context.i8_type(); let i8_ptr_type = i8_type.ptr_type(inkwell::AddressSpace::Generic); // printf関数を宣言 let printf_fn_type = i32_type.fn_type(&[i8_ptr_type.into()], true); let printf_function = module.add_function("printf", printf_fn_type, None); // main関数を宣言 let main_fn_type = i32_type.fn_type(&[], false); let main_function = module.add_function("main", main_fn_type, None); // main関数にBasic Blockを追加 let entry_basic_block = context.append_basic_block(main_function, "entry"); // builderのpositionをentry Basic Blockに設定 builder.position_at_end(entry_basic_block); // ここからmain関数に命令をビルドしていく // globalに文字列を宣言 let hw_string_ptr = builder.build_global_string_ptr("Hello, world!", "hw"); // printfをcall builder.build_call(printf_function, &[hw_string_ptr.as_pointer_value().into()], "call"); // main関数は0を返す builder.build_return(Some(&i32_type.const_int(0, false))); // JIT実行エンジンを作成し、main関数を実行 let execution_engine = module.create_jit_execution_engine(OptimizationLevel::Aggressive).unwrap(); unsafe { execution_engine.get_function::<unsafe extern "C" fn()>("main").unwrap().call(); } }
このプログラムは、LLVMでmain関数を宣言し、main関数内でprintf関数を呼び出すコードを実行しています。 実行すると、Hello, world!と表示されるはずです。
inkwellを使えば、Rustからこれだけ簡単にLLVMをいじることができます。 LLVMの他の命令を使うには、Builderのメソッドを参照するとよいでしょう。 ドキュメント自体はそんなに充実しているとは言えませんが、命令をビルドするメソッドの引数と返り値の型がわかれば後は組み立てていけばいいので、慣れてくればそんなに迷うことはなくなるはずです。
おわりに
今回はRustで簡単にLLVMを扱うクレート、inkwellを紹介し、その導入と命令の記述についてサンプルコードとともに簡単に説明しました。 初めてinkwellやLLVMに触る人は、とりあえずは上のサンプルコードに様々な命令を追加してみて、inkwellとLLVMがなんとなくわかってきたら実際の言語作成に取り掛かってみるのが良いのではないでしょうか。