ZACKYこと山崎進です。
いよいよコード生成に取組みます。今回は Rustler から LLVM 7.0 を動かしてみました。
準備〜Mac編
まず「macOS / Homebrew での exenv / erlenv を用いた Elixir / Erlang のソースコードインストール」にしたがって Erlang と Elixir をインストールします。Erlang は OTP21以降,Elixirは1.7以降をインストールします。
次に rustup をインストールします。こちらのrustupホームページに書かれているスクリプトを実行します。
それからLLVM 7.0をインストールする必要があります。Homebrew は最近 LLVM 7.0 が含まれるようになりました。
$ brew install llvm
$ brew link llvm --force
次のようにして libedit
をインストールする必要があります。
$ brew install libedit
準備〜Ubuntu 16.04編
まず「Linux (Ubuntu 16.04) での exenv / erlenv を用いた Elixir / Erlang のソースコードインストール」にしたがって Erlang と Elixir をインストールします。Erlang は OTP21以降,Elixirは1.7以降をインストールします。
次に rustup をインストールします。こちらのrustupホームページに書かれているスクリプトを実行します。
それからLLVM 7.0をインストールする必要があります。Ubuntuの標準パッケージはバージョンが古いので,llvm.org の指示にしたがってインストールします。
まず,sudo vi /etc/apt/sources.list
として,末尾に次の記述を足します。
# LLVM 7
deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main
deb-src http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main
次のコマンドを実行して認証します。
$ wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add -
途中で止まったら,sudo
のパスワードを入力します。
次のコマンドを実行してインストールします。
$ sudo apt-get update
$ sudo apt-get install clang-7 lldb-7 lld-7
途中,「検証なしにこれらのパッケージをインストールしますか? [y/N]」と表示されるので,y
を押します。
そして,libedit
をインストールします。
$ sudo apt-get install libedit-dev
サンプルコードの実行
$ git clone https://github.com/zeam-vm/nif_llvm_2.git
$ cd nif_llvm_2
$ mix deps.get
$ mix run -e "NifLlvm2.generate_code"
次のように表示されれば無事実行できました。
; ModuleID = 'my_module'
source_filename = "my_module"
define i32 @main() {
entry:
%a = alloca i32
store i32 32, i32* %a
%b = alloca i32
store i32 16, i32* %b
%b_val = load i32, i32* %b
%a_val = load i32, i32* %a
%ab_val = add i32 %a_val, %b_val
ret i32 %ab_val
}
32 + 16 = 48
サンプルコードについて
次の記事を参考にしました。
まず「Elixirから簡単にRustを呼び出せるRustler #1 準備編」に沿って Rustler をセットアップします。
変更点は次の通りです。
-
mix rustler.new
のところで Elixir モジュール名をNifLlvm2
,Rustユニット名をllvm
としました。 - lib/example.ex は lib/nif_llvm_2.ex に,native/example/src/lib.rs は native/llvm/src/lib.rs にそれぞれ置き換わっています。
- mix.exs を下記のようにしています。
mix.exs
defmodule NifLlvm2.MixProject do
use Mix.Project
def project do
[
app: :nif_llvm_2,
version: "0.1.0",
elixir: "~> 1.7",
compilers: [:rustler] ++ Mix.compilers,
rustler_crates: rustler_crates(),
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
defp rustler_crates() do
[llvm: [
path: "native/llvm",
mode: :release,
]]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:rustler, "~> 0.18.0"}
]
end
end
ここまでで一旦コンパイルできることを確かめます。
次に native/llvm/Cargo.toml を次のようにします。
native/llvm/Cargo.toml
[package]
name = "llvm"
version = "0.1.0"
authors = []
[lib]
name = "llvm"
path = "src/lib.rs"
crate-type = ["dylib"]
[dependencies]
rustler = "0.18.0"
rustler_codegen = "0.18.0"
lazy_static = "1.0"
llvm-sys = "70"
末尾の llvm-sys = "70"
が追加した項目です。番号は LLVM のバージョン番号と対応しています。詳しくはllvm-sys のドキュメントを参照してください。
そして native/llvm/.cargo/config を作成して次のように入れます。(これがわからなくてハマった。。。)
native/llvm/.cargo/config
[build]
rustflags = ["-lffi", "-ledit"]
さらに native/llvm/src/lib.src を次のようにします。
native/llvm/src/lib.src
#[macro_use] extern crate rustler;
// #[macro_use] extern crate rustler_codegen;
#[macro_use] extern crate lazy_static;
extern crate llvm_sys;
use rustler::{Env, Term, NifResult, Encoder};
use llvm_sys::core::*;
use llvm_sys::target;
use llvm_sys::analysis::{LLVMVerifyModule, LLVMVerifierFailureAction};
use llvm_sys::execution_engine::*;
use std::ffi::CString;
use std::os::raw::{c_char};
mod atoms {
rustler_atoms! {
atom ok;
//atom error;
//atom __true__ = "true";
//atom __false__ = "false";
}
}
rustler_export_nifs! {
"Elixir.NifLlvm2",
[("generate_code", 0, generate_code)],
None
}
/*
int main() {
int a = 32;
int b = 16;
return a + b;
}
define i32 @main() #0 {
%1 = alloca i32, align 4
%a = alloca i32, align 4
%b = alloca i32, align 4
store i32 0, i32* %1
store i32 32, i32* %a, align 4
store i32 16, i32* %b, align 4
%2 = load i32, i32* %a, align 4
%3 = load i32, i32* %b, align 4
%4 = add nsw i32 %2, %3
ret i32 %4
}
*/
fn initialize_llvm() {
unsafe {
if target::LLVM_InitializeNativeTarget() != 0 {
panic!("Could not initialize target")
}
if target::LLVM_InitializeNativeAsmPrinter() != 0 {
panic!("Could not initialize ASM Printer")
}
}
}
fn generate_code<'a>(env: Env<'a>, _args: &[Term<'a>]) -> NifResult<Term<'a>> {
let llvm_error = 1;
let val1 = 32;
let val2 = 16;
initialize_llvm();
// setup our builder and module
let builder = unsafe { LLVMCreateBuilder() };
let mod_name = CString::new("my_module").unwrap();
let module = unsafe { LLVMModuleCreateWithName(mod_name.as_ptr()) };
// create our function prologue
let function_type = unsafe {
let mut param_types = [];
LLVMFunctionType(LLVMInt32Type(), param_types.as_mut_ptr(), param_types.len() as u32, 0)
};
let function_name = CString::new("main").unwrap();
let function = unsafe { LLVMAddFunction(module, function_name.as_ptr(), function_type)};
let entry_name = CString::new("entry").unwrap();
let entry_block = unsafe { LLVMAppendBasicBlock(function, entry_name.as_ptr())};
unsafe { LLVMPositionBuilderAtEnd(builder, entry_block); }
// int a = 32
let a_name = CString::new("a").unwrap();
let a = unsafe { LLVMBuildAlloca(builder, LLVMInt32Type(), a_name.as_ptr())};
unsafe { LLVMBuildStore(builder, LLVMConstInt(LLVMInt32Type(), val1, 0), a); }
// int b = 16
let b_name = CString::new("b").unwrap();
let b = unsafe { LLVMBuildAlloca(builder, LLVMInt32Type(), b_name.as_ptr())};
unsafe { LLVMBuildStore(builder, LLVMConstInt(LLVMInt32Type(), val2, 0), b); }
// return a + b
let b_val_name = CString::new("b_val").unwrap();
let b_val = unsafe { LLVMBuildLoad(builder, b, b_val_name.as_ptr()) };
let a_val_name = CString::new("a_val").unwrap();
let a_val = unsafe { LLVMBuildLoad(builder, a, a_val_name.as_ptr()) };
let ab_val_name = CString::new("ab_val").unwrap();
unsafe {
let res = LLVMBuildAdd(builder, a_val, b_val, ab_val_name.as_ptr());
LLVMBuildRet(builder, res);
}
// verify it's all good
let mut error: *mut c_char = 0 as *mut c_char;
let ok = unsafe {
let buf: *mut *mut c_char = &mut error;
LLVMVerifyModule(module, LLVMVerifierFailureAction::LLVMReturnStatusAction, buf)
};
if ok == llvm_error {
let err_msg = unsafe { CString::from_raw(error).into_string().unwrap() };
panic!("cannot verify module '{:?}.\nError: {}", mod_name, err_msg);
}
// Clean up the builder now that we are finished using it.
unsafe { LLVMDisposeBuilder(builder) }
// Dump the LLVM IR to stdout so we can see what we've created
unsafe { LLVMDumpModule(module) }
// create our exe engine
let mut engine: LLVMExecutionEngineRef = 0 as LLVMExecutionEngineRef;
let ok = unsafe {
error = 0 as *mut c_char;
let buf: *mut *mut c_char = &mut error;
let engine_ref: *mut LLVMExecutionEngineRef = &mut engine;
LLVMLinkInInterpreter();
LLVMCreateInterpreterForModule(engine_ref, module, buf)
};
if ok == llvm_error {
let err_msg = unsafe { CString::from_raw(error).into_string().unwrap() };
println!("Execution error: {}", err_msg);
} else {
// run the function!
let func_name = CString::new("main").unwrap();
let named_function = unsafe { LLVMGetNamedFunction(module, func_name.as_ptr()) };
let mut params = [];
let func_result = unsafe { LLVMRunFunction(engine, named_function, params.len() as u32, params.as_mut_ptr()) };
let result = unsafe { LLVMGenericValueToInt(func_result, 0) };
println!("{} + {} = {}", val1, val2, result);
}
// Clean up the module after we're done with it.
unsafe { LLVMDisposeModule(module) }
Ok(atoms::ok().encode(env))
}
最後に lib/nif_llvm_2.ex を次のようにします。
lib/nif_llvm_2.ex
defmodule NifLlvm2 do
use Rustler, otp_app: :nif_llvm_2, crate: :llvm
@moduledoc """
Documentation for NifLlvm2.
"""
@doc """
## Examples
iex> NifLlvm2.generate_code()
:ok
"""
def generate_code(), do: exit(:nif_not_loaded)
end
解説
このコードは,次のコード相当を LLVM で生成して JIT 実行するというものです。
int main() {
int a = 32;
int b = 16;
return a + b;
}
define i32 @main() #0 {
%1 = alloca i32, align 4
%a = alloca i32, align 4
%b = alloca i32, align 4
store i32 0, i32* %1
store i32 32, i32* %a, align 4
store i32 16, i32* %b, align 4
%2 = load i32, i32* %a, align 4
%3 = load i32, i32* %b, align 4
%4 = add nsw i32 %2, %3
ret i32 %4
}
詳しい解説は,後で追記します。「Go言語で利用するLLVM入門」に解説がされていますので,急ぐ人はそちらを参照してください。