かおるノート

cordx56のブログです

inkwellを使ってRustでLLVMをやっていく

こんにちは。 そろそろ夏ですね。

今回の記事はRustでLLVMをやっていくことができるクレート、inkwellについてです。 inkwellを紹介している記事自体はあるのですが、実際にinkwellを利用して何かを作ってる記事が少なかったのでこの記事を書きました。

LLVM自体はそれなりに情報はあり、またきつねさん本などもあることから、トラブルシューティングはしやすいほうだと思います。 とは言いつつLLVMに対しては触りながら知見をためていったこともあり、様々の理解に少し時間がかかってしまいました。 この記事や私の書いたソースコードが少しでもRustでLLVMをやろうとする人の役に立てば幸いです。

inkwellを利用して実際に作ったものはこちらです。

github.com

小さな独自の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がなんとなくわかってきたら実際の言語作成に取り掛かってみるのが良いのではないでしょうか。

是非皆さんもRustでオレオレコンパイラフロントエンドを作っていきましょう3


  1. こちらです。メンテナ不足で悩んでいるようですが……

  2. aptが使える環境をご利用の方はこっちのほうが楽でいいかもしれません

  3. 次回はプログラミング言語自作に入門する記事が書きたいですね……