LLVM フレームワークで実用的コンパイラーを作成する: 第 2 回
clang を使用して C/C++ コードのプリプロセスを行う
この連載の第
1 回では、LLVM の中間表現 (IR) について詳しく探りました。具体的には、自作の「Hello World」テスト・プログラムを作成し、LLVM
が持つ特異な性質 (型キャストなど) をいくつか学んだ上で、同じテスト・プログラムを LLVM のアプリケーション・プログラミング・インターフェース (API)
を使用して作成しました。このプロセスを通して、llc
や lli
などの LLVM ツールについても説明し、llvm-gcc を使用して LLVM IR を自動的に出力する方法も明らかにしました。これ以外にも LLVM
で行える便利な処理はあり、そのいくつかをこの連載最終回となる第 2
回の記事では探っていきます。具体的には、まずコードのインストルメント化、つまり最終的に生成された実行可能プログラムに情報を追加することを検討します。さらに、C
、C++
、Objective-C をサポートする LLVM のフロントエンドである
clang についても簡単に取り上げます。clang API を使用して C/C++
コードの抽象構文木 (AST) のプリプロセスおよび生成を行います。
LLVM のパス
LLVM は、最適化機能を提供することで知られています。最適化は、パスとして実装されます (「参考文献」に LLVM のパスに関する非常に詳しい説明へのリンクがあるので参照してください)。ここで注目すべき点は、LLVM では必要最小限のコードで実用的なパスを作成できることです。例えば、関数の名前を「hello」で始めたくないとしたら、実用的なパスを使って完全に対処することができます。
LLVM の opt ツールについて
opt
の man ページには、「opt
コマンドはモジュール式の
LLVM
オプティマイザー兼アナライザーです」という説明があります。カスタム・パスのコードが用意できたら、後はそのコードを共有ライブラリーにコンパイルするにも、その共有ライブラリーをロードするにも、opt
を使用します。LLVM が正常にインストールされていれば、システムで opt
を使用できる状態になっているはずです。opt
コマンドは LLVM IR 形式 (.ll 拡張子) と LLVM
ビット・コード形式 (.bc 拡張子) の両方を受け入れ、出力を LLVM IR またはビット・コードとして生成することができます。opt
を使用してカスタム共有ライブラリーをロードする方法は以下のとおりです。
tintin# opt –load=mycustom_pass.so –help –S
コマンドラインから opt -help
を実行すると、LLVM
が実行するパスを長々と連ねたリストが生成されることも覚えておいてください。help
に load
オプションを指定すれば、カスタム・パスに関する情報を記載したヘルプ・メッセージを生成することもできます。
LLVM のカスタム・パスを作成する
LLVM パスを宣言する場所は、Pass.h ファイルです。私のシステムの場合、このファイルは /usr/include/llvm にインストールされています。Pass.h
ファイルは、個々のパスのインターフェースを Pass
クラスの一部として定義します。Pass
クラスから派生するそれぞれのパスの型も、このファイルに宣言されます。パスの型には以下に挙げるものがあります。
BasicBlockPass
クラス: ローカル最適化を実装するために使用します。一般に、ローカル最適化は一度に 1 つの基本ブロックまたは命令に作用します。FunctionPass
クラス: グローバル最適化に使用されます。一度に 1 つの関数に作用します。ModulePass
クラス: プロシージャー間の構造化されていない最適化を実行するために使用されます。
ここで作成しようとしているパスは、名前が「hello」で始まる関数があると警告するというものなので、FunctionPass
から派生させて独自のパスを作成する必要があります。リスト 1 のコードを Pass.h からコピーしてください。
リスト 1. FunctionPass の runOnFunction クラスをオーバーライドする
Class FunctionPass : public Pass { /// explicit FunctionPass(char &pid) : Pass(PT_Function, pid) {} /// runOnFunction - Virtual method overridden by subclasses to do the /// per-function processing of the pass. /// virtual bool runOnFunction(Function &F) = 0; /// … };
これと同様に、BasicBlockPass
クラスは runOnBasicBlock
を宣言し、ModulePass
クラスは runOnModule
という純粋な仮想メソッドを宣言します。仮想メソッドの定義は、子クラスが提供する必要があります。
リスト 1 の runOnFunction
メソッドに話を戻すと、メソッドへの入力が Function
型のオブジェクトとなっています。/usr/include/llvm/Function.h ファイルの中身を調べると、Function
クラスは、LLVM が C/C++
関数の機能をカプセル化するために使用するクラスであることが容易に見て取れます。Function
は、Value.h
に定義された Value
クラスから派生したクラスで、getName
メソッドをサポートします。リスト 2 のコードを見てください。
リスト 2. LLVM のカスタム・パスを作成する
#include "llvm/Pass.h" #include "llvm/Function.h" class TestClass : public llvm::FunctionPass { public: virtual bool runOnFunction(llvm::Function &F) { if (F.getName().startswith("hello")) { std::cout << "Function name starts with hello\n"; } return false; } };
リスト 2 のコードには、以下に挙げる 2 つの重要な詳細が抜け落ちています。
FunctionPass
コンストラクターには、LLVM が内部で使用するchar
が必要です。LLVM はchar
のアドレスを使用するため、何を使用して初期化するかは問題ではありません。- 作成したクラスが新しいパスであることを LLVM システムに理解させるために、何らかの方法が必要です。そこで活躍するのが、
RegisterPass
LLVM テンプレートです。RegisterPass
テンプレートは、 PassSupport.h ヘッダー・ファイルで宣言します。このファイルは Pass.h でインクルードされているので、追加のヘッダー・ファイルは必要ありません。
リスト 3 に完全なコードを記載します。
リスト 3. LLVM Function パスを登録する
class TestClass : public llvm::FunctionPass { public: TestClass() : llvm::FunctionPass(TestClass::ID) { } virtual bool runOnFunction(llvm::Function &F) { if (F.getName().startswith("hello")) { std::cout << "Function name starts with hello\n"; } return false; } static char ID; // could be a global too }; char TestClass::ID = 'a'; static llvm::RegisterPass<TestClass> global_("test_llvm", "test llvm", false, false);
RegisterPass
テンプレートに含まれる template
パラメーターは、パスの名前です。コマンドラインでは、opt
にこの名前を指定します。これで、カスタム・パスは完成です。あとは、リスト 3 のコードで共有ライブラリーを作成し、opt
を実行してライブラリーをロードするだけです。ライブラリーをロードするには、ライブラリー名の後に、RegisterPass
を使用して登録したコマンドの名前 (この例の場合、test_llvm
) を続け、最後に他のパスと一緒にカスタム・パスが実行されるビット・コード・ファイルの名前を続けます。リスト 4 に、このステップの概要を示します。
リスト 4. カスタム・パスを実行する
bash$ g++ -c pass.cpp -I/usr/local/include `llvm-config --cxxflags` bash$ g++ -shared -o pass.so pass.o -L/usr/local/lib `llvm-config --ldflags -libs` bash$ opt -load=./pass.so –test_llvm < test.bc
今度はこれまで、LLVM のバックエンドを見てきたのに対して、フロントエンド clang を検討します。
clang の紹介
LLVM には固有のフロントエンドがあります。それは、(その名もふさわしく) clang というツールです。clang は強力な C/C++
/Objective-C コンパイラーで、そのコンパイル速度は GCC (GNU Compiler Collection)
ツールに匹敵するか、あるいはそれを上回るほどです (詳細は「参考文献」のリンクを参照)。さらに重要なことに、clang
のコード・ベースはハッキングできるので、機能を簡単に拡張することができます。第 1 回で LLVM のバックエンド API を使用してカスタム・プラグインを作成したように、この記事では LLVM のフロントエンド API を使用して、簡単なアプリケーションを作成します。作成するのは、プリプロセスを行うアプリケーションと、構文解析を行うアプリケーションです。
一般的な clang クラス
最もよく使われる clang クラスとして、以下のクラスを十分に理解する必要があります。
CompilerInstance
Preprocessor
FileManager
SourceManager
DiagnosticsEngine
LangOptions
TargetInfo
ASTConsumer
Sema
ParseAST
(おそらく最も重要な clang メソッドです)
ParseAST
メソッドについては、追ってその詳細を説明します。
事実上、CompilerInstance
はコンパイラーであると考えるのが正解です。このクラスはインターフェースを提供し、AST
へのアクセスを管理し、入力ソースのプリプロセスを行うとともに、ターゲット情報を保持します。典型的なアプリケーションでは、あらゆる実用的な処理を行うために、CompilerInstance
オブジェクトを作成する必要があります。リスト 5 に、CompilerInstance.h ヘッダー・ファイルの内容を記載します。
リスト 5. CompilerInstance クラス
class CompilerInstance : public ModuleLoader { /// The options used in this compiler instance. llvm::IntrusiveRefCntPtr<CompilerInvocation> Invocation; /// The diagnostics engine instance. llvm::IntrusiveRefCntPtr<DiagnosticsEngine> Diagnostics; /// The target being compiled for. llvm::IntrusiveRefCntPtr<TargetInfo> Target; /// The file manager. llvm::IntrusiveRefCntPtr<FileManager> FileMgr; /// The source manager. llvm::IntrusiveRefCntPtr<SourceManager> SourceMgr; /// The preprocessor. llvm::IntrusiveRefCntPtr<Preprocessor> PP; /// The AST context. llvm::IntrusiveRefCntPtr<ASTContext> Context; /// The AST consumer. OwningPtr<ASTConsumer> Consumer; /// \brief The semantic analysis object. OwningPtr<Sema> TheSema; //… the list continues };
C ファイルのプリプロセス
clang でプリプロセッサー・オブジェクトを作成するには、少なくとも以下の 2 つの方法があります。
Preprocessor
オブジェクトを直接インスタンス化するCompilerInstance
クラスにPreprocessor
オブジェクトを作成させる
まずは、後者の方法から説明します。
プリプロセスに必要なヘルパーおよびユーティリティー・クラス
Preprocessor
は、単独ではそれほど役に立ちません。このクラスには、ファイルを読み込むための FileManager
クラスと、診断用にソース・ロケーションを追跡するための SourceManager
クラスが必要です。FileManager
クラスは、ファイルシステムの検索、ファイルシステムのキャッシング、およびディレクトリーの検索のサポートを実装します。FileEntry
クラスの中身を覗いてみると、そこにはソース・ファイルの clang 抽象化が定義されています。リスト 6 に、FileManager.h ヘッダー・ファイルの一部を抜粋します。
リスト 6. clang の FileManager クラス
class FileManager : public llvm::RefCountedBase<FileManager> { FileSystemOptions FileSystemOpts; /// \brief The virtual directories that we have allocated. For each /// virtual file (e.g. foo/bar/baz.cpp), we add all of its parent /// directories (foo/ and foo/bar/) here. SmallVector<DirectoryEntry*, 4> VirtualDirectoryEntries; /// \brief The virtual files that we have allocated. SmallVector<FileEntry*, 4> VirtualFileEntries; /// NextFileUID - Each FileEntry we create is assigned a unique ID #. unsigned NextFileUID; // Statistics. unsigned NumDirLookups, NumFileLookups; unsigned NumDirCacheMisses, NumFileCacheMisses; // … // Caching. OwningPtr<FileSystemStatCache> StatCache;
一方の SourceManager
クラスは、概して SourceLocation
オブジェクトに関する問い合わせの対象となりがちです。リスト 7 に、SourceManager.h
ヘッダー・ファイルに含まれる SourceLocation
オブジェクトの情報を記載します。
リスト 7. SourceLocation の情報
/// There are three different types of locations in a file: a spelling /// location, an expansion location, and a presumed location. /// /// Given an example of: /// #define min(x, y) x < y ? x : y /// /// and then later on a use of min: /// #line 17 /// return min(a, b); /// /// The expansion location is the line in the source code where the macro /// was expanded (the return statement), the spelling location is the /// location in the source where the macro was originally defined, /// and the presumed location is where the line directive states that /// the line is 17, or any other line.
SourceManager
が裏で FileManager
に依存しているのは明らかです。実際、SourceManager
クラスのコンストラクターは、入力引数として FileManager
クラスを受け入れます。さらに、ソースの処理中に発生したエラーを追跡してレポートするためには、DiagnosticsEngine
クラスを使用する必要があります。それには、Preprocessor
と同じく以下の 2 つの方法があります。
- すべての必要なオブジェクトを独自に作成する
CompilerInstance
にすべてをまかせる
ここでも後者の方法を採用することにします。リスト 8 に、Preprocessor
のコードを記載します。このクラス以外のコードの部分については、すべて説明済みです。
リスト 8. clang API を使用してプリプロセッサーを作成する
using namespace clang; int main() { CompilerInstance ci; ci.createDiagnostics(0,NULL); // create DiagnosticsEngine ci.createFileManager(); // create FileManager ci.createSourceManager(ci.getFileManager()); // create SourceManager ci.createPreprocessor(); // create Preprocessor const FileEntry *pFile = ci.getFileManager().getFile("hello.c"); ci.getSourceManager().createMainFileID(pFile); ci.getPreprocessor().EnterMainSourceFile(); ci.getDiagnosticClient().BeginSourceFile(ci.getLangOpts(), &ci.getPreprocessor()); Token tok; do { ci.getPreprocessor().Lex(tok); if( ci.getDiagnostics().hasErrorOccurred()) break; ci.getPreprocessor().DumpToken(tok); std::cerr << std::endl; } while ( tok.isNot(clang::tok::eof)); ci.getDiagnosticClient().EndSourceFile(); }
リスト 8 では CompilerInstance
クラスを使用して、順次、DiagnosticsEngine
(ci.createDiagnostics
メソッド呼び出し)、FileManager
(ci.createFileManager
および ci.CreateSourceManager
)
を自動的に作成します。FileEntry
を使用してファイルの関連付けが終わると、続いてソース・ファイルに含まれる各トークンの処理をソース・ファイルの終わり (EOF)
に達するまで行います。最後にプリプロセッサーの DumpToken
メソッドがトークンを画面にダンプ出力します。
リスト 8 のコードをコンパイルして実行するには、お使いの clang および LLVM インストール・フォルダーに合わせてリスト 9 の makefile を調整して使用してください。ここで意図しているのは、llvm-config ツールを使用して、必要な LLVM インクルード・パスおよびライブラリーを提供することです。したがって、g++ コマンドラインを使って、これらのパスとライブラリーをリンクしようとはしないでください。
リスト 9. プリプロセッサー・コードをビルドするための makefile
CXX := g++ RTTIFLAG := -fno-rtti CXXFLAGS := $(shell llvm-config --cxxflags) $(RTTIFLAG) LLVMLDFLAGS := $(shell llvm-config --ldflags --libs) DDD := $(shell echo $(LLVMLDFLAGS)) SOURCES = main.cpp OBJECTS = $(SOURCES:.cpp=.o) EXES = $(OBJECTS:.o=) CLANGLIBS = \ -L /usr/local/lib \ -lclangFrontend \ -lclangParse \ -lclangSema \ -lclangAnalysis \ -lclangAST \ -lclangLex \ -lclangBasic \ -lclangDriver \ -lclangSerialization \ -lLLVMMC \ -lLLVMSupport \ all: $(OBJECTS) $(EXES) %: %.o $(CXX) -o $@ $< $(CLANGLIBS) $(LLVMLDFLAGS)
上記のコードをコンパイルして実行すると、リスト 10 の内容が出力されるはずです。
リスト 10. リスト 9 のコードを実行中に発生したエラー
Assertion failed: (Target && "Compiler instance has no target!"), function getTarget, file /Users/Arpan/llvm/tools/clang/lib/Frontend/../.. /include/clang/Frontend/CompilerInstance.h, line 294. Abort trap: 6
エラーが発生した理由は、CompilerInstance
の設定を 1
つだけ見逃していたからです。その設定とは、このコードのコンパイル対象のターゲット・プラットフォームです。そこで登場するのが、TargetInfo
クラスと TargetOptions
クラスです。clang
の TargetInfo.h ヘッダー・ファイルによると、コードを生成するために必要なターゲット・システムの情報は、TargetInfo
クラスに格納されます。このクラスが作成されてからでないと、コンパイルもプリプロセスも確実に行われません。ご想像のとおり、TargetInfo
には整数と浮動小数点数のビット幅、アライメントなどの情報が格納されるようです。リスト 11 に、TargetInfo.h ヘッダー・ファイルの一部を抜粋します。
リスト 11. clang の TargetInfo クラス
class TargetInfo : public llvm::RefCountedBase<TargetInfo> { llvm::Triple Triple; protected: bool BigEndian; unsigned char PointerWidth, PointerAlign; unsigned char IntWidth, IntAlign; unsigned char HalfWidth, HalfAlign; unsigned char FloatWidth, FloatAlign; unsigned char DoubleWidth, DoubleAlign; unsigned char LongDoubleWidth, LongDoubleAlign; // …
TargetInfo
クラスは、初期化のための引数として DiagnosticsEngine
と TargetOptions
の 2
つを取ります。このうち、後者には、Triple
ストリングに現在使用しているプラットフォームに適した値が設定されていなければなりません。LLVM は、この点で重宝します。リスト 9
にプリプロセッサーが機能するように内容を付け足すと、リスト 12 のコードになります。
リスト 12. コンパイラーのターゲットに関するオプションを設定する
int main() { CompilerInstance ci; ci.createDiagnostics(0,NULL); // create TargetOptions TargetOptions to; to.Triple = llvm::sys::getDefaultTargetTriple(); // create TargetInfo TargetInfo *pti = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), to); ci.setTarget(pti); // rest of the code same as in Listing 9… ci.createFileManager(); // …
これで完成です。単純な hello.c テストでこのコードを実行して、結果の出力を確認してください。
#include <stdio.h> int main() { printf("hello world!\n"); }
リスト 13 に、プリプロセッサーの出力の一部を記載します。
リスト 13. プリプロセッサーの出力 (抜粋)
typedef 'typedef' struct 'struct' identifier '__va_list_tag' l_brace '{' unsigned 'unsigned' identifier 'gp_offset' semi ';' unsigned 'unsigned' identifier 'fp_offset' semi ';' void 'void' star '*' identifier 'overflow_arg_area' semi ';' void 'void' star '*' identifier 'reg_save_area' semi ';' r_brace '}' identifier '__va_list_tag' semi ';' identifier '__va_list_tag' identifier '__builtin_va_list' l_square '[' numeric_constant '1' r_square ']' semi ';'
自らの手で Preprocessor オブジェクトを作成する
clang ライブラリーの利点の 1 つは、同じ結果を複数の方法で実現できることです。このセクションでは、Preprocessor
オブジェクトの作成を CompilerInstance
に丸ごと任せるのではなく、自らの手で Preprocessor オブジェクトを作成します。リスト 14 に、Preprocessor.h ヘッダー・ファイルに含まれる
Preprocessor
のコンストラクターを記載します。
リスト 14. Preprocessor オブジェクトのコンストラクター
Preprocessor(DiagnosticsEngine &diags, LangOptions &opts, const TargetInfo *target, SourceManager &SM, HeaderSearch &Headers, ModuleLoader &TheModuleLoader, IdentifierInfoLookup *IILookup = 0, bool OwnsHeaderSearch = false, bool DelayInitialization = false);
このコンストラクターを調べると、この大層なオブジェクトを起動するには 6 種類のオブジェクトを作成しなければならないことがわかります。DiagnosticsEngine
、TargetInfo
、SourceManager
については、すでに説明しました。CompilerInstance
は ModuleLoader
から派生されます。したがって、残る 2 つのオブジェクトとして、LangOptions
のオブジェクトと HeaderSearch
のオブジェクトを新しく作成する必要があります。LangOptions
クラスでは、C99
、C11
、C++0x
を含む一連の C/C++
の方言をコンパイルすることができます。詳細については、ヘッダー・ファイルの LangOptions.h および
LangOptions.def を参照してください。もう一方の HeaderSearch
は、何よりも検索対象とする
std::vector
を保管するクラスです。リスト 15 に、Preprocessor
のコードを記載します。
リスト 15. 自らの手で作成したプリプロセッサー
using namespace clang; int main() { DiagnosticOptions diagnosticOptions; TextDiagnosticPrinter *printer = new TextDiagnosticPrinter(llvm::outs(), diagnosticOptions); llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> diagIDs; DiagnosticsEngine diagnostics(diagIDs, printer); LangOptions langOpts; clang::TargetOptions to; to.Triple = llvm::sys::getDefaultTargetTriple(); TargetInfo *pti = TargetInfo::CreateTargetInfo(diagnostics, to); FileSystemOptions fsopts; FileManager fileManager(fsopts); SourceManager sourceManager(diagnostics, fileManager); HeaderSearch headerSearch(fileManager, diagnostics, langOpts, pti); CompilerInstance ci; Preprocessor preprocessor(diagnostics, langOpts, pti, sourceManager, headerSearch, ci); const FileEntry *pFile = fileManager.getFile("test.c"); sourceManager.createMainFileID(pFile); preprocessor.EnterMainSourceFile(); printer->BeginSourceFile(langOpts, &preprocessor); // … similar to Listing 8 here on }
リスト 15 のコードでは、以下の点に注意してください。
- 上記の
HeaderSearch
は、特定のディレクトリーを指すように初期化されていませんが、実際には特定のディレクトリーを指すように初期化する必要があります。 - clang API は、
TextDiagnosticPrinter
をヒープに割り当てることを条件とします。スタックに割り当てると、エラーが発生します。 CompilerInstance
をコードから取り除けていません。結局CompilerInstance
を使用するのであれば、より快適な clang API を使わずにわざわざ自らの手で作成する必要はありません。
言語オプション: C++
これまでは C
テスト・コードを扱ってきましたが、C++
の場合はどうでしょうか?リスト 15 のコードに langOpts.CPlusPlus
= 1;
を追加して、リスト 16 のテスト・コードを試してください。
リスト 16. プリプロセッサーの C++ テスト・コード
template <typename T, int n> struct s { T array[n]; }; int main() { s<int, 20> var; }
リスト 17 に、プログラムからの出力を抜粋します。
リスト 17. リスト 16 のコードによるプロプロセッサーの出力 (抜粋)
identifier 'template' less '<' identifier 'typename' identifier 'T' comma ',' int 'int' identifier 'n' greater '>' struct 'struct' identifier 's' l_brace '{' identifier 'T' identifier 'array' l_square '[' identifier 'n' r_square ']' semi ';' r_brace '}' semi ';' int 'int' identifier 'main' l_paren '(' r_paren ')'
構文解析木の作成
clang/Parse/ParseAST.h に定義されている ParseAST
メソッドは、clang が提供する特に重要なメソッドのうちの 1 つです。以下に、ParseAST.h からコピーした、ルーチンの宣言の 1 つを記載します。
void ParseAST(Preprocessor &pp, ASTConsumer *C, ASTContext &Ctx, bool PrintStats = false, TranslationUnitKind TUKind = TU_Complete, CodeCompleteConsumer *CompletionConsumer = 0);
ASTConsumer
は、派生元となる抽象インターフェースを提供します。さまざまなクライアントが AST
をさまざまな方法でダンプあるいは処理することが考えられるので、派生元を提供するのは適切なことです。クライアント・コードは、ASTConsumer
から派生させます。ASTContext
クラスは、型宣言に関する情報を格納します (他にもいろいろな情報を格納します)。これを試す最も簡単な方法として、clang の ASTConsumer API
を使用して、コード内のグローバル変数のリストを出力してください。多くの技術系企業では、C++
コードでのグローバル変数の使用について厳格なルールを設けているので、これがカスタム lint ツールを作成する出発点となるはずです。リスト 18 に、カスタム・コンシューマーのコードを記載します。
リスト 18. カスタム AST コンシューマー・クラス
class CustomASTConsumer : public ASTConsumer { public: CustomASTConsumer () : ASTConsumer() { } virtual ~ CustomASTConsumer () { } virtual bool HandleTopLevelDecl(DeclGroupRef decls) { clang::DeclGroupRef::iterator it; for( it = decls.begin(); it != decls.end(); it++) { clang::VarDecl *vd = llvm::dyn_cast<clang::VarDecl>(*it); if(vd) std::cout << vd->getDeclName().getAsString() << std::endl;; } return true; } };
上記では、(ASTConsumer
に元々提供されている) HandleTopLevelDecl
メソッドを独自のバージョンでオーバーライドしています。clang
からグローバル変数のリストが渡されるので、そのリストを繰り返し処理して、変数名を出力します。リスト 19 に記載する ASTConsumer.h からの抜粋に、クライアント・コンシューマーのコードでオーバーライドできるその他のメソッドが示されています。
リスト 19. クライアント・コードでオーバーライドできるその他のメソッド
/// HandleInterestingDecl - Handle the specified interesting declaration. This /// is called by the AST reader when deserializing things that might interest /// the consumer. The default implementation forwards to HandleTopLevelDecl. virtual void HandleInterestingDecl(DeclGroupRef D); /// HandleTranslationUnit - This method is called when the ASTs for entire /// translation unit have been parsed. virtual void HandleTranslationUnit(ASTContext &Ctx) {} /// HandleTagDeclDefinition - This callback is invoked each time a TagDecl /// (e.g. struct, union, enum, class) is completed. This allows the client to /// hack on the type, which can occur at any point in the file (because these /// can be defined in declspecs). virtual void HandleTagDeclDefinition(TagDecl *D) {} /// Note that at this point it does not have a body, its body is /// instantiated at the end of the translation unit and passed to /// HandleTopLevelDecl. virtual void HandleCXXImplicitFunctionInstantiation(FunctionDecl *D) {}
最後に、これまでの作業で開発したカスタム AST コンシューマー・クラスを使用する、実際のクライアント・コードをリスト 20 に記載します。
リスト 20. カスタム AST コンシューマーを使用するクライアント・コード
int main() { CompilerInstance ci; ci.createDiagnostics(0,NULL); TargetOptions to; to.Triple = llvm::sys::getDefaultTargetTriple(); TargetInfo *tin = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), to); ci.setTarget(tin); ci.createFileManager(); ci.createSourceManager(ci.getFileManager()); ci.createPreprocessor(); ci.createASTContext(); CustomASTConsumer *astConsumer = new CustomASTConsumer (); ci.setASTConsumer(astConsumer); const FileEntry *file = ci.getFileManager().getFile("hello.c"); ci.getSourceManager().createMainFileID(file); ci.getDiagnosticClient().BeginSourceFile( ci.getLangOpts(), &ci.getPreprocessor()); clang::ParseAST(ci.getPreprocessor(), astConsumer, ci.getASTContext()); ci.getDiagnosticClient().EndSourceFile(); return 0; }
まとめ
この全 2 回からなる連載の記事で取り上げた話題は、広範に及びます。その内容をおさらいすると、LLVM IR の詳細、LLVM IR を手作業で生成する方法と LLVM
API を使って生成する方法、LLVM バックエンドのカスタム・プラグインを作成する方法、LLVM
フロントエンドとその充実したヘッダー・ファイルのセットについて説明しました。また、このフロントエンドを使用してプリプロセスを行い、AST を使用する方法についても学びました。C++
のような複雑な言語の場合にはなおさらのこと、コンパイラーを作成して拡張することは、コンピューティングの歴史が始まった頃のロケット科学に似ていますが、LLVM によってその作業は単純化されています。文書化は、LLVM と clang でまだ作業が必要な部分ですが、それが一段落するまでは、温かいものでも飲みながら VIM/Doxygen でヘッダー・ファイルを参照してください。LLVM を使ってお楽しみください!
ダウンロード可能なリソース
関連トピック
- 「LLVM フレームワークで実用的コンパイラーを作成する: 第 1 回 LLVM とその中間表現を使用してカスタム・コンパイラーを構築する」(Arpan Sen 著、developerWorks、2012年6月) で、LLVM の基礎を学んでください。この強力な LLVM コンパイラー・インフラストラクチャーを使えば、どのプログラミング言語を使うかに関わらず、アプリケーションを最適化することができます。また、カスタム・コンパイラーを作成するのも簡単です!
- LLVM パスの詳細を学んでください。
- clang の開発者向けメーリング・リストに登録してください。
- clangをビルドしてインストールする方法についての詳細は、「Getting Started: Building and Running Clang」を読んでください。
- LLVM を知る絶好の手引書として、公式の LLVM Tutorial を利用してください。
- LLVM API に欠かせない資料、「LLVM Programmer's Manual」を徹底的に調べてください。
- developerWorks Open source ゾーンには、オープソースのツールおよびオープンソース技術の使用に関する情報が豊富に揃っています。
- developerWorks Linux ゾーンで、Linux 開発者および管理者向けのハウツー記事とチュートリアル、そしてダウンロード、ディスカッション、フォーラムなど、豊富に揃った資料を探してください。
- developerWorks Web development に、多種多様な Web ベースのソリューションを話題にした記事が揃っています。
- Twitter で developerWorks をフォローしてください。
- LLVM プロジェクトのサイトにアクセスして、最新バージョンをダウンロードしてください。
- LLVM サイトで、clang についての詳細を調べてください。
- ご自分に最適な方法で IBM 製品を評価してください。評価の方法としては、製品の試用版をダウンロードすることも、オンラインで製品を試してみることも、クラウド環境で製品を使用することもできます。また、SOA Sandbox では、数時間でサービス指向アーキテクチャーの実装方法を効率的に学ぶことができます。