LoginSignup
28
17

Rust+LLVMでコンパイラを作る:#1 環境構築

Last updated at Posted at 2020-05-12

本連載のバックナンバー

はじめに

普段はお仕事で Kotlin や Java と戯れていますが、実は Rust や C++ が好きな @_53a と申します。
所属会社が技術ブログ代わりの Qiita Organizationを作った、かつ投稿内容は自由ということを聞いたので、私の方からは普段趣味でやっている言語実装の、ほんの触りの部分だけでも記事にして紹介できればと思っています。

本連載では Rust 言語でプログラミング言語のコンパイラを実装することを最終目的とし、本記事ではその初回として環境構築を行います。

本記事では、

  • Rust 言語に興味がある
  • LLVM の使い方に興味がある
  • プログラミング言語を作ることに興味がある

といった読者層を想定し、持っているスキルとして

  • 趣味や仕事で少しでもプログラミングに触れたことがある
  • 最低限の技術英語が読める
  • シェル(zsh や bash, PowerShell等)を操作できる
  • 環境変数とはなにか知っており、安全に変更することができる
  • gcc か clang が手元の環境に入っているか、 (Windows以外ならば)gcc か clang を自力でインストールすることができる

といったことを期待しています。

使用するもの

  • Rust(最近のもの。stable でも nightlyでも可)
  • LLVM(8.0以上。筆者の環境ではLLVM 8.0および10.0での動作を確認しています)
  • gccまたはclang(最近のもの)

環境構築

Linux

  • gcc(またはclang)は既に導入済みのものとします
  • Rustコンパイラは rustup を使用してインストールします。インストール方法は左記のリンク先を参照してください
  • LLVMについては、お使いのパッケージマネージャでインストールしてください。例:
    • Ubuntu: sudo apt install llvm-8-dev1
    • Arch: sudo pacman -S llvm

Mac 2

  • gcc(またはclang)は既に導入済みのものとします
  • RustコンパイラはLinuxと同様 rustup を使用してインストールします
  • LLVMについては、homebrewを使用し、brew install llvm@8 3 などとしてインストールします

Windows

Windowsは公式サイトにMSVCでビルドされたものが配布されていますが、

  • static buildではない
  • llvm-configなどの便利なバイナリが含まれない
  • ビルド手順等を他のOSに極力近づけたい
  • 新バージョンが出た際に手動でアップデートするのが面倒

といった理由から、有志がgccでビルドしたものを使用します。
また、Rustコンパイラもこれに合った物を導入します4

導入手順

  1. MSYS2 を導入します(C:\msys64以下にインストールしたものと仮定します)
  2. C:\msys64\mingw64\bin, C:\msys64\usr\bin にPATHを通しておきます。また、rustup を既に導入済みの環境の場合、cargoのディレクトリ(C:\Users\user\.cargo\bin 等)より後になるようにしておきます
  3. pacman -S mingw-w64-x86_64-llvm mingw-w64-x86_64-rust mingw-w64-x86_64-gcc を叩いてLLVMとRust、gccをインストールします5
  4. rustup を既に導入済みであった場合、 rustup toolchain link mingw "C:\msys64\mingw64" を実行し、MSYS2 経由でインストールした処理系をrustupから使えるようにしておきます

動作確認

環境が正しく構築できたことを確認するために、簡単なコードを実行してみます。

まず、適当なディレクトリで cargo new llvm-test を実行します。

生成された llvm-test ディレクトリに移動し、Cargo.toml をエディタで開き、[dependencies] を以下のように書き換えます。

[dependencies]
llvm-sys = "80"
anyhow = "1.0"

次に、 src/main.rs をエディタで開き、以下のように書き換えます。

extern crate llvm_sys as llvm;
#[macro_use] extern crate anyhow;

use std::ffi::CString;
use std::mem::MaybeUninit;

use llvm::core::*;
use llvm::analysis::{LLVMVerifyModule, LLVMVerifierFailureAction};
use llvm::target::{LLVM_InitializeNativeAsmPrinter, LLVM_InitializeNativeTarget};
use llvm::target_machine::*;
use llvm::*;

use anyhow::{Context, Result};

unsafe fn gen_code(ctx: *mut LLVMContext, m: *mut LLVMModule, b: *mut LLVMBuilder) -> Result<()> {

    let emp_str = CString::new("")?;
    let mut param_ty = vec![LLVMPointerType(LLVMInt8TypeInContext(ctx), 0)];
    let printf_ty = LLVMFunctionType(LLVMVoidTypeInContext(ctx), param_ty.as_mut_ptr(), 0, 1);
    let name = CString::new("printf")?;
    let printf_body = LLVMAddFunction(m, name.as_ptr(), printf_ty);

    let mut param_ty = vec![];
    let main_ty = LLVMFunctionType(LLVMInt64TypeInContext(ctx), param_ty.as_mut_ptr(), 0, 0);
    let name = CString::new("main")?;
    let main_body=  LLVMAddFunction(m, name.as_ptr(), main_ty);

    let name = CString::new("entry")?;
    let bb_entry = LLVMAppendBasicBlockInContext(ctx, main_body, name.as_ptr());
    LLVMPositionBuilderAtEnd(b, bb_entry);

    let s = CString::new("Hello, World!\n")?;
    let mut arg = vec![LLVMBuildGlobalString(b, s.as_ptr(), emp_str.as_ptr())];
    LLVMBuildCall(b, printf_body, arg.as_mut_ptr(), 1, emp_str.as_ptr());

    LLVMBuildRet(b, LLVMConstInt(LLVMInt64TypeInContext(ctx), 0, 1));

    let mut err = MaybeUninit::uninit().assume_init();
    let is_err = LLVMVerifyModule(m, LLVMVerifierFailureAction::LLVMPrintMessageAction, &mut err);
    if is_err != 0 {
        let err = CString::from_raw(err);
        let err_ = err.clone();
        LLVMDisposeMessage(err.into_raw());
        return Err(anyhow!("Failed to verify main module: {}", err_.to_str()?));
    }
    Ok(())

}

unsafe fn get_target_machine() -> Result<*mut LLVMOpaqueTargetMachine> {

    LLVM_InitializeNativeTarget();
    LLVM_InitializeNativeAsmPrinter();

    let cpu = LLVMGetHostCPUName();
    let feat = LLVMGetHostCPUFeatures();
    let triple = LLVMGetDefaultTargetTriple();

    let mut target = MaybeUninit::uninit().assume_init();
    let mut err = MaybeUninit::uninit().assume_init();
    
    let is_err = LLVMGetTargetFromTriple(triple, &mut target, &mut err);
    if is_err != 0 {
        let err = CString::from_raw(err);
        let err_ = err.clone();
        LLVMDisposeMessage(err.into_raw());
        return Err(anyhow!("Failed to get target from triple: {}", err_.to_str()?));
    }

    let tm = LLVMCreateTargetMachine(
        target,
        triple,
        cpu,
        feat,
        LLVMCodeGenOptLevel::LLVMCodeGenLevelDefault,
        LLVMRelocMode::LLVMRelocPIC,
        LLVMCodeModel::LLVMCodeModelDefault
    );
    LLVMDisposeMessage(cpu);
    LLVMDisposeMessage(feat);
    LLVMDisposeMessage(triple);
    Ok(tm)

}

fn compile(cc: String) -> Result<()> {
    unsafe {
        let ctx = LLVMContextCreate();
        let mod_name = CString::new("test")?;
        let m = LLVMModuleCreateWithNameInContext(mod_name.as_ptr(), ctx);
        let b = LLVMCreateBuilderInContext(ctx);

        gen_code(ctx, m, b).context("Error while generating body")?;

        let tm = get_target_machine().context("Failed while getting target machine")?;

        let mut err = MaybeUninit::uninit().assume_init();
        let is_err = LLVMTargetMachineEmitToFile(tm, m, CString::new("test.o")?.into_raw(), LLVMCodeGenFileType::LLVMObjectFile, &mut err);
        if is_err != 0 {
            let err = CString::from_raw(err);
            let err_ = err.clone();
            LLVMDisposeMessage(err.into_raw());
            return Err(anyhow!("Failed to emit object file: {}", err_.to_str()?));
        }
        LLVMDisposeTargetMachine(tm);
        LLVMDisposeBuilder(b);
        LLVMDisposeModule(m);
        LLVMContextDispose(ctx);
    }

    let ext = if cfg!(windows) {".exe"} else {""};
    let compiling = std::process::Command::new(cc)
        .args(vec!["test.o".into(), "-o".into(), format!("test{}", ext)])
        .output()?;

    let stderr = String::from_utf8(compiling.stderr)?;
    let status = compiling.status.code().ok_or(anyhow!("Apparently the compiler was killed"))?;
    if status != 0 {
        return Err(anyhow!("Compile failed with code {}\nstderr: {}", status, stderr));
    }

    Ok(())
}

fn main() {
    let cc = std::env::var("CC").unwrap_or("gcc".into());
    compile(cc).context("Error while compiling").unwrap_or_else(|e|{
        eprintln!("{:?}", e);
    });
}

2020/05/12追記: Windows で rustup を既に導入済みの環境の場合、llvm-test 上で rustup override set mingw を実行し、llvm-test で使用するツールチェインをMSYS由来のものに切り替えてください。

llvm-testのトップ(Carrgo.toml があるディレクトリ)に移動してcargo run を実行し、ビルドおよび実行がエラーなく行えることを確認してください。

llvm-config に PATH が通っていない環境では、環境変数 LLVM_SYS_80_PREFIX に LLVM がインストールされたディレクトリの絶対パス(e.g. /usr/lib/llvm-8, /usr/local/opt/llvm, C:\msys64\mingw64\lib 等)をセットする必要があります。また、 gcc の代わりに clang を使用している環境では、環境変数 CCclang という文字列をセットしてください。

生成された test(Windows 環境では test.exe)を実行し、Hello, World! という文字列が出力されれば成功です。おめでとうございます。

最後に

本記事では Rust でコンパイラを作るための第一歩として、開発環境の構築を行いました。
次回からはいよいよコンパイラの制作に入っていこうと思います。

今回は構築した環境のテスト目的ということで、 Rust から LLVM を使う手段として llvm-sys を直に叩きましたが、LLVM を扱いやすくするために、次回からはラッパーとして inkwell を使用します。

それでは次回6をお楽しみに。

2020/06/24追記: 本連載の第二回はこちらです。


WHI広告7

本記事は、筆者の現所属企業(WHI)が技術ブログ代わりとして、 Qiita の Organization を立ち上げたので、その記念に書かれました。
WHIは胡散臭いベンチャーっぽい名前ですが、一部で有名なWorks Applications社から(やんごとなき事情により)去年8月に分社してできた、多少は由緒ある会社です。大企業向けにお給料を計算するソフトとか作ってて、その界隈だとそれなりにシェアが高いみたいです。
WHIではフレックスタイム制(または裁量労働制)を採用しており、昼の12時に出社することが許されています。そのため、満員電車と無縁な生活を送ることができます。後は社内の無料カフェで飲めるコーヒーが美味しいです(有名なバリスタの監修を受けたらしい)。

WHIはある程度コードが書けるプログラマーを募集しています。 8

エンジニア系 :: 株式会社Works Human Intelligence

  1. llvm-9-dev や、llvm-10-dev でもいいかと思います(llvm-10-devはUbuntu 18.04 LTSに現状入っていないようなので、使用できない環境も多いかと思いますが)

  2. 筆者は宗教上の理由でMacを持っていないので、検証はすべてGitHub Actions上でしか行えていません。実機で動かない等ありましたらお知らせください。

  3. あるいは brew install llvm で最新のstable版を入れてもいいかと思います。

  4. rustup で入る公式バイナリはビルドに使用したgccが古く、他のライブラリとのリンク時にエラーを起こすため。

  5. 時期によってmingw-w64-x86_64-z3を追加でインストールしたり、C:\msys64\mingw64\x86_64-w64-mingw32\lib\libpthread.dll.a を手動で消去しないといけない場合があるようです

  6. もしあれば

  7. この欄の元ネタの某C++エバンジェリストの方のブログ、今見たらこの広告欄がなくなってますね…

  8. 万一応募する場合には、この記事に興味を持つような方は テックリードエンジニア を志望することをオススメします。入社したら社内Slack の #topic_pl_rust チャンネルで僕と握手!

28
17
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
17