LLVM フレームワークで実用的コンパイラーを作成する: 第 2 回

clang を使用して C/C++ コードのプリプロセスを行う

LLVM コンパイラー・インフラストラクチャーは、どのプログラミング言語を使用するかに関わらず、アプリケーションを最適化する強力な手段です。この全 2 回からなる連載の第 2 回では、LLVM でコードをインストルメント化するために、clang API を使って C/C++ コードのプリプロセスを行う方法を学んでください。

Arpan Sen, Author, Independent

Arpan Sen は電子設計自動化業界でソフトウェア開発に従事する先進的なエンジニアです。彼はこれまで、Solaris や SunOS、HP-UX、IRIX など、さまざまな種類の UNIX を扱ってきており、また Linux と Microsoft Windows にも数年の経験があります。彼はソフトウェアのパフォーマンス最適化技術やグラフ理論、並列コンピューティングに大きな関心を持っています。彼はソフトウェア・システムの修士号を取得しています。



2012年 7月 19日

この連載の他の記事

連載「LLVM フレームワークで実用的コンパイラーを作成する」の他の記事を見る

この連載の第 1 回では、LLVM の中間表現 (IR) について詳しく探りました。具体的には、自作の「Hello World」テスト・プログラムを作成し、LLVM が持つ特異な性質 (型キャストなど) をいくつか学んだ上で、同じテスト・プログラムを LLVM のアプリケーション・プログラミング・インターフェース (API) を使用して作成しました。このプロセスを通して、llclli などの LLVM ツールについても説明し、llvm-gcc を使用して LLVM IR を自動的に出力する方法も明らかにしました。これ以外にも LLVM で行える便利な処理はあり、そのいくつかをこの連載最終回となる第 2 回の記事では探っていきます。具体的には、まずコードのインストルメント化、つまり最終的に生成された実行可能プログラムに情報を追加することを検討します。さらに、CC++、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 が実行するパスを長々と連ねたリストが生成されることも覚えておいてください。helpload オプションを指定すれば、カスタム・パスに関する情報を記載したヘルプ・メッセージを生成することもできます。

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 という純粋な仮想メソッドを宣言します。仮想メソッドの定義は、子クラスが提供する必要があります。

リスト 1runOnFunction メソッドに話を戻すと、メソッドへの入力が 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 の紹介

事前に知っておくべき注意事項

clang はまだ開発作業が進行中であり、この規模のプロジェクトの例に漏れず、文書化はコード・ベースよりも通常は遅れをとっています。したがって、開発者向けメーリング・リスト (「参考文献」のリンクを参照) をチェックすることが最善策です。clang ソースをビルドしてインストールしたい場合には、clang の導入ガイド (「参考文献」を参照) の説明に従ってください。注意する点として、ビルドが完了した後、デフォルトのシステム・フォルダーへインストールするために make install コマンドを実行する必要があります。この記事の残りでは、clang のヘッダー・ファイルおよびライブラリーがそれぞれ /usr/local/include、/usr/local/lib のようなシステム・フォルダーに置かれていることを前提とします。

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 クラスは、初期化のための引数として DiagnosticsEngineTargetOptions の 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 種類のオブジェクトを作成しなければならないことがわかります。DiagnosticsEngineTargetInfoSourceManager については、すでに説明しました。CompilerInstanceModuleLoader から派生されます。したがって、残る 2 つのオブジェクトとして、LangOptions のオブジェクトと HeaderSearch のオブジェクトを新しく作成する必要があります。LangOptions クラスでは、C99C11C++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;
}

まとめ

この連載の他の記事

連載「LLVM フレームワークで実用的コンパイラーを作成する」の他の記事を見る

この全 2 回からなる連載の記事で取り上げた話題は、広範に及びます。その内容をおさらいすると、LLVM IR の詳細、LLVM IR を手作業で生成する方法と LLVM API を使って生成する方法、LLVM バックエンドのカスタム・プラグインを作成する方法、LLVM フロントエンドとその充実したヘッダー・ファイルのセットについて説明しました。また、このフロントエンドを使用してプリプロセスを行い、AST を使用する方法についても学びました。C++ のような複雑な言語の場合にはなおさらのこと、コンパイラーを作成して拡張することは、コンピューティングの歴史が始まった頃のロケット科学に似ていますが、LLVM によってその作業は単純化されています。文書化は、LLVM と clang でまだ作業が必要な部分ですが、それが一段落するまでは、温かいものでも飲みながら VIM/Doxygen でヘッダー・ファイルを参照してください。LLVM を使ってお楽しみください!

参考文献

学ぶために

製品や技術を入手するために

  • LLVM プロジェクトのサイトにアクセスして、最新バージョンをダウンロードしてください。
  • LLVM サイトで、clang についての詳細を調べてください。
  • ご自分に最適な方法で IBM 製品を評価してください。評価の方法としては、製品の試用版をダウンロードすることも、オンラインで製品を試してみることも、クラウド環境で製品を使用することもできます。また、SOA Sandbox では、数時間でサービス指向アーキテクチャーの実装方法を効率的に学ぶことができます。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, Linux
ArticleID=825822
ArticleTitle=LLVM フレームワークで実用的コンパイラーを作成する: 第 2 回
publish-date=07192012