Rust
LLVM
RustDay 15

RustでLLVM IRを出力するためのcrate「inkwell」について


はじめに

 RustでLLVM IRを出力するcrateとしてLLVM C APIのbindingであるllvm-sys.rsがあります。しかしllvm-sys.rsは非常に薄く、不足する機能は自力で提供してやらなければなりません。またドキュメントやテストもほとんどなく、初見の場合は既存の実装を参照しながら手探りでやっていく必要が出てきます。

 同様にLLVM IRを出力するcrateとしてinkwellがあります。inkwellは便利なllvm-sys.rsのラッパーです。llvm-sys.rsで不足していた機能やドキュメントなどが大幅に追加されています。本記事ではそんな素敵なinkwellを紹介したいと思います。


inkwellの良いところ

他にもたくさんあるとは思いますが、個人的に良いなと思ったのは以下です。主に上2つについて簡単に説明できればと思います。私はllvm-sys.rsを用いておもちゃレベルの言語を作ったことしかありません。もっと使い込めば異なる感想が出るかもしれませんが、その点はご了承ください。


  • LLVMValueRefやLLVMTypeRef以外の型が提供されている

  • ドキュメントやテスト、コメントが豊富

  • unsafeな処理を可能な限り減らしている

  • stringが扱いやすくなった(Null終端などをオプショナルで追加してくれる etc.)


LLVMValueRefやLLVMTypeRef以外の型が提供されている

とりあえずサンプルソースを見ていただきたいです。

/* 説明のために型を明示的に宣言 */

// llvm-sys.rs
unsafe {
// builder: LLVMBuilderRefなどの生成過程は略
let int_32_type: LLVMTypeRef = LLVMInt32Type();
let const_int: LLVMValueRef = LLVMConstInt(int_32_type, 10, 0);
let const_int_ref: LLVMValueRef = LLVMBuildAlloca(builder, int_32_type, CString::new("int_value").unwrap().as_ptr());
let _: LLVMValueRef = LLVMBuildStore(builder, const_int, const_int_ref);
}

// inkwell
// builder: Builderなどの生成過程は略
let int_32_type: IntType = IntType::i32_type();
let const_int: IntValue = int_32_type.const_int(10, false);
let const_int_ref: PointerValue = builder.build_alloca(int_32_type, "int_value");
let _: InstructionValue = builder.build_store(const_int_ref, const_int);

 ちなみに出力結果としては同じで、以下のようになります。

// 出力結果: LLVM IR

%int_value = alloca i32
store i32 10, i32* %int_value

 各行数はほとんど変わらないですが、llvm-sys.rsはほとんどLLVMValuRefになります。(typeを使いたい場合はLLVMTypeRefになります)これはi8だろうがi8*だろうが変わりません。全部LLVMValueRefです。それに対してinkwellはIntValue, PointerValueなどの型が存在します。

 llvm-sys.rsでもLLVMVerifyModuleを使えば一応エラーは吐いてくれますが、エラーメッセージがstringとして出力されるだけで元のプログラムの行数はおろかLLVM IRの何行目かすら分かりません。それに対してinkwellは完全ではありませんが型を提供してくれているのでRustのコンパイラがエラーの箇所を教えてくれます。

 それと着目するべき場所としてはllvm-sys.rsに対してinkwellはunsafeを使っていないことでしょうか。inkwellは正式名称「It's a New Kind of Wrapper for Exposing LLVM (Safely)」(長い)にもある通り、一部の関数を除いてunsafeではありません。淡々とunsafeな関数をラップしてsafeな関数にしていくという面倒な作業がなくなるというのは大変ありがたいです。


テストも便利

それともう一つ見ていただきたいのが以下のコードです。

// テストしたい関数の型

type SumFunc = unsafe extern "C" fn(u64, u64) -> u64;

#[test]
fn example_run_function() {
// 環境ごとに設定してやる必要がある模様(macならこれで動く)
Target::initialize_native(&InitializationConfig::default()).unwrap();

let context = Context::create();
let module = context.create_module("test_module");
let builder = context.create_builder();

create_function(&context, &module, &builder);
let execution_engine = module.create_jit_execution_engine(OptimizationLevel::None).unwrap();

unsafe {
let funcion_in_rust: JitFunction<SumFunc> = execution_engine.get_function("sum").unwrap();
assert!(
// LLVM IR上で宣言した関数を実行する
3 == funcion_in_rust.call(1, 2),
"test failed!"
);
};
}

// 実際にテストしたい関数を生成する
fn create_function(context: &Context, module: &Module, builder: &Builder) {
let i64_type = context.i64_type();
let fn_type = i64_type.fn_type(&[i64_type.into(), i64_type.into()], false);

let sum = module.add_function("sum", fn_type, None);
let basic_block = context.append_basic_block(&sum, "entry");
builder.position_at_end(&basic_block);

let x = sum.get_nth_param(0).unwrap().into_int_value();
let y = sum.get_nth_param(1).unwrap().into_int_value();

builder.build_return(Some(&builder.build_int_add(x, y, "")));
}

 長い割にさほど内容はないのですが上記はLLVM IR上で宣言した関数をRustで実際に実行していると言うコードです。テストに便利ですね。llvm-sys.rsでも同様の機能があるのですが例によって型はLLVMValueRefのみです。関数ごとに「unsafe extern "C"」を使って宣言してやらないといけないのは少し面倒ですが、型のサポートをinkwellであれば使用することができます。


ドキュメントやテスト、コメントが豊富(サンプルにkaleidoscopeがあるよ!)

 llvm-sys.rsはテストやドキュメントなどがほとんどありません。慣れてしまえばさほど苦労はないのかもしれませんが、初心者にはとても厳しいです。しかしinkwellは違います。ちゃんとドキュメントが存在します。サンプルも数は現状2つと少ないですが、そのうちの1つはkaleidoscopeです。

 kaleidoscopeはLLVMの公式ページのチュートリアルで作成する言語です。LexerやParser、LLVM IR、簡易的な最適化、コード生成などについてkaleidoscopeという言語を作りながら学習することができます。つまりRustを触りながらLLVMの勉強ができる訳ですね。作ってくださった作者の方に感謝です。

 またテストコードやソースコード内のコメントもしっかりと存在していて、個人的には読んでいてとても楽しいレポジトリです。「この関数は呼ぶ必要がないのではないだろうか?」的なコメントとか、多分製作者の解釈であろうLinkageとかの説明のコメントとかがガッツリ書き込まれていてとても勉強になります。


inkwellの駄目なところ


  • LLVM7.0以外のドキュメントがない

 今の所このあたりしか問題点を見つけられていないです。しかしそもそもllvm-sys.rsはドキュメントらしいドキュメントがないので、それを考えると大した問題ないのではないかと思います。既にllvm-sys.rsを使った言語を書いており、型の実装が済んでいる場合、ひょっとしたらinkwellの型が邪魔になるかもしれません。そこまで強力な型ではないので多分大丈夫だとは思いますが。


まとめ

 inkwellはllvm-sys.rsと比較して全体的に非常に使いやすく、気軽にLLVM IRを出力できると思います。コメントやexampleも豊富なのでLLVM C APIの資料としても役立つのではないかとも思います。正直llvm-sys.rsを使い始める前に知っていたらという気分が抜けません。今まで興味があるけど手を出せなかった方もこれを機会にLLVMに触れてみたら如何でしょうか。