POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook

本記事は、原著者の許諾のもとに翻訳・掲載しております。

※前編は こちら から

何かやってみる

前編で私が作ったモジュールにコードを入力しようと思います。できるだけ簡単なことをやってみましょう。すなわち、printfを呼び出します。

LLVMモジュールは、シンボルテーブルに既知の関数全てが含まれている状態を保持します。作成中のexeをオペレーティングシステムが実行しようとすると、LLVMモジュールはシンボルテーブルの中にある”main”と名付けられた関数を探し始め、これを呼び出します。

local mainType = LLVM.LLVMFunctionType(LLVM.LLVMVoidType(), nil, 0, false)
local mainFn = LLVM.LLVMAddFunction(builder.llvm.module, "main", mainType)

まずmain関数のための型を構築し、そのあと関数自体を構築していきます。AddFunctionが関数をシンボルテーブルに追加し、新たな関数オブジェクトが得られます。

local mainEntry = LLVM.LLVMAppendBasicBlock(mainFn, "entry")
LLVM.LLVMPositionBuilderAtEnd(builder.llvm.builder, mainEntry)

関数にコードを追加する予定なので、コードを入れられる「基本ブロック」を作っておかなければなりません。「基本ブロック」はコンパイラの基本的な用語で、内部に分岐を含まない連続したコードの断片を指します。

これ以前に「ビルダーオブジェクト」を作ってあります。これはテキストカーソルのようなものです。一連のBuild系関数があり、ビルダーが配置されているモジュール内のどこかしらのポイントに、それぞれの関数がいくつかのコードを挿入します。新しい基本ブロックをこの関数オブジェクトに加えます。そしてビルダを基本ブロックの最後に配置します。基本ブロックを作ったら、名前をつけなくてはいけません。私の知る限りではその名前は任意に決めてよく、他の基本ブロックの最後からそこに直接飛べるようにしたい時のためだけにこれを作ったという想定で考えていますので、名前は”entry”とします。

次にprintfを呼び出したいと思います。しかしその前に、printfとは何かをLLVMに伝えなくてはなりません。

local printfParams = ffi.new("LLVMTypeRef [1]")
printfParams[0] = LLVM.LLVMPointerType(LLVM.LLVMInt8Type(), 0) -- address space 0 (arbitrary)
local printfType = LLVM.LLVMFunctionType(LLVM.LLVMInt32Type(), printfParams, 1, true) -- vararg
local printfFn = LLVM.LLVMAddFunction(builder.llvm.module, "printf", printfType)

ここでも、関数を構築してそれをシンボルテーブルに入れています。しかし今回はコードを追加しようとしているわけではません。単にLLVMに関数の存在とその型を教えるために入れているのです。このシンボルテーブルのエントリに適合する実装は、あとからリンカがやってくれる予定です。この型はmain関数の型よりも少し複雑で、引数を持っています。

local printString = "LET'S MAKE A JOURNEY TO THE CAVE OF MONSTERS!\n"
local printfArgs = ffi.new("LLVMValueRef [1]")
printfArgs[0] = LLVM.LLVMBuildGlobalStringPtr(builder.llvm.builder, printString, "tmpstring")
LLVM.LLVMBuildCall(builder.llvm.builder, printfFn, printfArgs, 1, "tmpcall")

これでビルダを使い始める準備が整いました。私はグローバル文字列をビルドし、次に呼び出し位置(call site)をビルドしました。文字列も呼び出し位置も「名前」を持ちます。基本ブロックの時のような使えそうな命名は思いつきませんが、基本ブロックと同様、名前は任意です。

LLVM.LLVMBuildRetVoid(builder.llvm.builder)

これで完了です。voidを返して基本ブロックを閉じたいと思います。

この13行のコードが、私が本当に必要としていたものだったのです! ジェネレータスクリプトが吐きだす新しい main.o で、私の構築したコードがLLVM IRではどう見えているのかをちらっと見ることができます。

; ModuleID = 'main'

@tmpstring = private unnamed_addr constant [47 x i8] c"LET'S MAKE A JOURNEY TO THE CAVE OF MONSTERS!\0A\00"

define void @main() {
entry:
 %tmpcall = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([47 x i8], [47 x i8]* @tmpstring, i32 0, i32 0))
 ret void
}

declare i32 @printf(i8*, ...)

とても単純明快ですよね。この13行のコードを得るために私がどれだけのことをしなければなからなったかを、少しだけお話しさせてください。

今回も、LLVMのドキュメントはあまり役にたちませんでした。

私が使っていたこれらのリファレンスドキュメントは Doxygen によって作成されたものでした。つまり、誰かがわざわざ特定の機能についてコメントしていない限り、自動生成されたドキュメントしか得られないということです。先程 LLVMBuildCall() は”Name”パラメータを持つとお話ししましたが、それが何なのか分からず調べてみました。ドキュメントを見たら何が書いてあるでしょうか? 調べると、 LLVMBuildCall() のドキュメントは自動生成されたもので、ソースの行番号しかありませんでした。まあでも、それがLLVM-Cのドキュメントですよね。では「本物の」ドキュメント、つまりC++のドキュメントを調べたら何が分かるでしょう? 結果は同じで、 IRBuilder::CreateCall() のドキュメントは自動生成されたものであり、ソースの行番号しか分かりませんでした。私はどうしても”Name”の意味するところを探し出すことができなかったのです。

やっとASTを生成できるようになった今、私はドキュメントに関する新たな問題に直面しています。現段階までにやっておくべきこととしてStackOverflowに勧められたことの多くは、LLVM IRのスニペットとして出てきます。そこからC言語に翻訳するためには(Luaに書き換えるためです)、まずLLVM IRとして指示を与えてもらい、それをC++に翻訳する方法を見つけなければなりません。しかしよい面もあり、 LLVM IRがどんどんドキュメント化されるようになった のです。

ついに、ほんの少しだけASTを生成するコードを自力で発見しました。上記のASTコードは、llvm-c-kaleidoscopeプロジェクトと、 IBMによって書かれたチュートリアル と、フランスのセキュリティ研究者の このサンプルコード を足し合わせたものから情報を得て構築したものです。このIBMの0vercl0kサンプルは、私がやろうとしていたこととほぼ同じことをしており、ものすごく役立ちました。しかしこのような素晴らしく使いやすいモデルでコーディングしていても、やはり少し四苦八苦しました。まだ話していない問題があり、それを何とかしなければならなかったからです。その問題とは、LLVMは予期しない入力が行われるとほとんどの場合クラッシュしてしまうというものでした。例えば、上記の LLVMCreateTargetMachine() は、空文字列は有効な入力として受け取りますが、NULL文字列を渡してしまうとクラッシュします。

こうしたクラッシュの多くは簡単に解決できます。上記のコードを書き始めて間もない頃、私は LLVMBuildGlobalStringPtr() を使うべきところに LLVMConstString() を使おうとしてしまいました。ということは、私はprintfに char * に相当するものを渡さないで、 char[48] に相当するものを渡そうとしていたことになります。結果、アサートが発生しました。


これで対処することができました。明確さには欠けますが、スタックはきれいになっていますし、アサートが行番号を与えてくれています。私は、MacPortsを使って私のLLVMのバージョン( sudo port patch llvm-3.9 && `port work llvm-3.9` )のソースコードを表示させることができました。このお陰で、コンテキストを検索し、 LLVMTypeOf()/LLVMDumpType() で何度かテストを行うと、問題の原因が明らかになり、どこから修復すればいいのかを知ることができたのです。

他の種類の問題も起こりましたが、それらはこういったやり方ではうまく解決できませんでした。例えば基本ブロックを作成してビルダを配置する前に LLVMBuildGlobalStringPtr() を呼び出そうとしたら、バグが発生してしまったことがありました。バグの症状は、オブジェクトファイルを発行しようとした時に、パスマネージャの最適化の深層部でセグメンテーションフォールトが起きてしまうという現象です。また、 ret void で関数を終了することができないというバグもありました。こちらの症状も同様に、パスマネージャの最適化の深層部で起き、 Assertion failed: (Val && "isa<> used on a null pointer"), function doit, file Casting.h と表示されてしまう、信じられないようなアサート障害でした。これら2つの問題に対して、私は何を間違えてしまったのか、どのように修復すればいいのか分からず、また実行コードに戻る術も思いつかずという状況で、基本的に何もすることができませんでした。この状況から抜け出すために私がしたことは、IBM/0vercl0kが作成したサンプルで行われていたことで、私がしていなかったことを見つけ出し、問題が解決するまでそれを追加するということでした。

私がなぜ、このようなことを説明しているのかというと、これらの様々な問題から学んだことがあるからです。「これは大変なことになるぞ」と。上の13行のコードはとてもシンプルではありますが、構成に丸1日費やしています。しかも他の人の例を参考に、見よう見まねで作り上げたものです。次に何か作成しようと思ったときに、参考になる例があるとは限りません。このことから、もし何かを向上させようと思ったら、やり方を変えなくてはいけないのだと学んだのです。

  • 少なくとも最初は、往来の激しい行き方にこだわる必要があります。このライブラリにはたくさんの人たちが通過するエントリポイントと、あまり使われていないエントリポイントがあります。私が気付いたのは、後者のエントリポイントの方に着手するといつでも、事は難しくなるということです。標準ライブラリではなく、LLVM-Cのユーザとして、私は少しやり過ぎてしまっています。残念ながら、恐らく私には基本から大きく外れた方法を取ることが難しいと思われます。何か賢い事をする前にシンプルで標準的な方法でコードを書きたいと思っています。私は恐らく、APIを使いながら何かやろうとする前に、LLVM IRをよく試してみる必要があるのでしょう。
  • それと私はまずLLVMコードの内部に慣れる必要があります。MacPortsからソースコードを引き出そうとはしましたが、これは十分ではありません。独自のLLVMを構築し、デバッグシンボルを得るべきなのです(例えば、ローカルを調査し、何が空値なのか分かれば、 isa<> used on a null pointer みたいなエラーが何だか分かるかもしれません)。そして、起こり得るエラーメッセージのいくつかを理解できるようになる前に、アーキテクチャの全モジュールを理解するために、ソースコードをたくさん読む必要があるのでしょう。言い換えれば、これがこれまで出てきたライブラリの1つになるのです。

Linking

しかし、最終的に私はmain.oファイルを手にしました!これを結合させれば終わりです。

このパートは好きではありません。私の本来の目的は、コンパイラがすることと同じことをしてくれるプログラムを書くことでした。しかしリンカを書くことは出来そうにもありません。リンカは、言語標準のドメインというよりも、むしろオペレーティングシステム(OS)やそのコードを読み込むシステムのドメインです。つまり、実際リンカを書くことができるのは、OSのベンダのみというわけです。なぜなら、リンカが何をするかを把握しているのは、OSのベンダだけだからです。あなたが好むCコンパイラが何であれ、結合することはありません。OSが提供しているリンカを見つけ、コマンドラインのプログラムとして起動するだけです。言い換えると、この記事の冒頭に掲載した図は、あまり正確ではないと言えます。より正確に近い図を書くとすれば以下のようになります。

私が最初に主張した「コンパイラを使わない」を正確にするには、自分でリンカを呼び出すか(これによって、この記事の説明はOS Xにのみ使えるということになります)、考えられないことではありますが、誰かがOSリンカに置き換えることを目的とした、LLVMの試験的リンカ 11d を詳しく調べるかです。

cc main.o -o main

これでプログラムが正常に起動します。

$ ./main
LET'S MAKE A JOURNEY TO THE CAVE OF MONSTERS!

プログラムの”コンパイラ”として機能するLuaスクリプトは、100行以下の長さになります。

これで非常に先が楽しみになりました!ひとたびPrintfを呼び出せたのなら、より複雑な処理を行うプログラムを作り出す基盤が持てることになるのですから。あとは、変数やフロー制御を追加するいくつかのステップを行うだけです。これが終われば、言語特有のLuaから小さなDSLを作ったり、Emily向け低水準言語システムのプロトタイプをいくつか作ったり、面白いことをすることができます。この記事での最終的なLuaスクリプトのソースコードは、 こちら から入手できます。これに関する記事を近いうちにまた投稿したいと思います。

この記事、またはリンク先のBitBudketの改訂に掲載されているコードスニペットは、 Creative Commons Zero のライセンスに基づいて入手可能です。

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。