Clang の LibTooling をプログラムに組み込もうとしてリンクエラー

(この漫画は左から右に読みます。) (この漫画は左から右に読みます。)

C++ のソースコードを解析しようと思い、Clang の LibTooling を使ってみることにしました。 ところがぱっと見でわからないリンクエラーにはまり丸一日ほど費やしてしまいました。 今日は Clang の LibTooling を最小限の労力でリンクし、C++ プログラムに組みこむまでの手順をおぼえ書きとして記します。

NOTE:
LLVM/Clang のウェブページには、LibTooling の使い方を説明したチュートリアルページが用意してあります。

体系的に学びたい方は、こちらのチュートリアルを参照することをおすすめします。 上記のチュートリアルでは CMake や ninja を使って、最新の Clang のソースコードを git clone するところから書かれています。 僕は、CMake や ninja は使わず、すでにビルドされた LLVM/Clang のバイナリをダウンロードし Xcode でさくっと動かすことにしました。

今回は、Xcode 上で1 C++ プログラムに LibTooling をリンクして動かしてみます。手順は次の通りです。

  1. Clang の Pre-Built Binaries をダウンロード
  2. ライブラリサーチパスとインクルードサーチパスを通す
  3. Clang のライブラリをリンクする
  4. Clang を動かすために必要なシステムライブラリをリンクする
  5. LLVM/Clang の警告を無視する
  6. プリプロセッサマクロの定義
  7. -fno-exceptions-fno-rtti を C++ フラグに追加する 2
  8. 実行時に libclang の動的ライブラリを実行ファイルから見つけられるようにする

それぞれ順にかいつまんでみていきます。

Clang の Pre-Built Binaries をダウンロード

ビルド済み3の Clang を公式ページからダウンロードします。 ここでは、 LLVM 3.7.0 - Clang for Mac OS X をダウンロードし、適当なディレクトリに解凍しました4。 説明のために、ディレクトリパスは /path/to/clang+llvm-3.7.0/ とします。

ライブラリサーチパスとインクルードサーチパスを通す

ライブラリサーチパスとインクルードサーチパスを通すためにそれぞれ Xcode プロジェクトに設定します:

  • Xcode > Build Setting > Header Search Paths/path/to/clang+llvm-3.7.0/include を追加
  • Xcode > Build Setting > Library Search Paths/path/to/clang+llvm-3.7.0/lib を追加

Xcode を使わず g++ や clang++ を直接使う場合は、-I オプションや -L オプションに読み替えてください:

-I/path/to/clang+llvm-3.7.0/include
-L/path/to/clang+llvm-3.7.0/lib

後述する llvm-config ツールを使うと、これらのコンパイルオプションを自動生成することができます。

Clang のライブラリをリンクする

Xcode プロジェクトに、libclang.a などのライブラリファイルを追加します。 Xcode > Build Phases > Link Binary With Libraries に必要なライブラリファイルをすべて追加します。

プラットフォームや Clang のバージョンごとに必要なライブラリは異なってきます。 Clang に同梱されている llvm-config を使うと列挙することができます。

$ cd /path/to/clang+llvm-3.7.0/bin
$ ./llvm-config --libs

こんなふうにもれなく出力してくれます:

libLLVMLTO.a libLLVMObjCARCOpts.a libLLVMLinker.a libLLVMBitWriter.a libLLVMIRReader.a libLLVMBPFCodeGen.a libLLVMBPFDesc.a libLLVMBPFInfo.a
...

g++ や clang++ を直接使う場合は、次のコマンドで必要なコンパイルオプションを自動的に出力してくれるので便利です。

$ ./llvm-config --cxxflags --ldflags --libs
-I/path/to/clang+llvm-3.7.0/include  -DNDEBUG -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -O3  -std=c++11 -fvisibility-inlines-hidden -fno-exceptions -fno-rtti -fno-common -Wcast-qual
-L/path/to/clang+llvm-3.7.0/lib
-lLLVMLTO -lLLVMObjCARCOpts -lLLVMLinker -lLLVMBitWriter
...

ちなみに LLVM/Clang がリンクできていないと次のリンクエラーが出ます。

Undefined symbols for architecture x86_64:
  "llvm::cl::OptionCategory::registerCategory()", referenced from:
      llvm::cl::OptionCategory::OptionCategory(char const*, char const*) in hello.o
  "llvm::cl::extrahelp::extrahelp(char const*)", referenced from:
      ___cxx_global_var_init1 in hello.o
      ___cxx_global_var_init2 in hello.o
  "clang::FrontendAction::FrontendAction()", referenced from:
...
"vtable for clang::tooling::ToolAction", referenced from:
    clang::tooling::ToolAction::ToolAction() in hello.o
NOTE: a missing vtable usually means the first non-inline virtual member function has no definition.
"vtable for clang::tooling::FrontendActionFactory", referenced from:
    clang::tooling::FrontendActionFactory::FrontendActionFactory() in hello.o
NOTE: a missing vtable usually means the first non-inline virtual member function has no definition.

Clang を動かすために必要なシステムライブラリをリンクする

OS X で Clang を使う場合は次のシステムライブラリが必要になります。

libz
libpthread
libedit
libcurses
libm

他のライブラリと同様に Xcode > Build Phases > Link Binary With Libraries で追加します。 Clang のバージョンやプラットフォームごとによって、これまた必要なシステムライブラリは異なってくるので次のコマンドで確認できます。

$ ./llvm-config --system-libs

こんなふうに出力してくれます:

-lz -lpthread -ledit -lcurses -lm

LLVM/Clang の警告を無視する

さっそく LLVM/Clang をソースコードに include してみたところ、次のような警告がたくさん出てきてコンパイルできないことがあります。

clang/Basic/VersionTuple.h:41:34: Declaration shadows a field of 'clang::VersionTuple'
clang/Basic/VersionTuple.h:45:34: Declaration shadows a field of 'clang::VersionTuple'
clang/Basic/VersionTuple.h:45:50: Declaration shadows a field of 'clang::VersionTuple'
llvm/ADT/iterator_range.h:35:28: Declaration shadows a field of 'iterator_range<IteratorT>'
llvm/Support/MathExtras.h:469:13: Implicit conversion loses integer precision: 'unsigned long' to 'unsigned int'
...
fatal error: too many errors emitted, stopping now [-ferror-limit=]
20 errors generated.

コンパイラの警告レベルを下げるか、または次のように #pragma GCC diagnostic で警告を部分的に無視できます。

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wsign-compare"
#pragma GCC diagnostic ignored "-Wconversion"

#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/CommandLine.h"

#pragma GCC diagnostic pop

#pragma GCC diagnostic は GCC または Clang で有効です。 Visual Studio の場合は、代わりに #pragma warning を使います。

完全を期すため、今回 Xcode で動かした LibTooling アプリケーションのソースコードを載せます。 このコードは、LibTooling — Clang 3.8 documentation で紹介されているサンプルコードを参考にしました。

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wsign-compare"
#pragma GCC diagnostic ignored "-Wconversion"

#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/CommandLine.h"

#pragma GCC diagnostic pop

using namespace clang::tooling;
using namespace llvm;

static cl::OptionCategory MyToolCategory("my-tool options");

int main(int argc, const char** argv)
{
    CommonOptionsParser options(argc, argv, MyToolCategory);
    ClangTool tool(options.getCompilations(), options.getSourcePathList());
    return tool.run(newFrontendActionFactory<clang::SyntaxOnlyAction>().get());
}

プリプロセッサマクロの定義

コンパイルするときに次の 2 つのプリプロセッサマクロを定義しておく必要があります。

__STDC_CONSTANT_MACROS
__STDC_LIMIT_MACROS

Xcode > Build Settings > Preprocessor Macros にこれらのマクロを追加してください。 定義していない場合は次のようなコンパイルエラーが出ます:

llvm/Support/DataTypes.h:58:3: "Must #define __STDC_LIMIT_MACROS before #including Support/DataTypes.h"
llvm/Support/DataTypes.h:62:3: "Must #define __STDC_CONSTANT_MACROS before "         "#including Support/DataTypes.h"

必要であれば __STDC_FORMAT_MACROS も定義します。

-fno-exceptions と -fno-rtti を C++ フラグに追加する

Xcode > Build Settings > Other C++ Flags (OTHER_CPLUSPLUSFLAGS) に次のフラグを追加します。

-fno-exceptions -fno-rtti

これらのフラグの指定がないと、次のような typeinfo に関するリンクエラーが発生します:

Undefined symbols for architecture x86_64:
  "typeinfo for clang::tooling::FrontendActionFactory", referenced from:
      typeinfo for std::__1::unique_ptr<clang::tooling::FrontendActionFactory, std::__1::default_delete<clang::tooling::FrontendActionFactory> > clang::tooling::newFrontendActionFactory<clang::SyntaxOnlyAction>()::SimpleFrontendActionFactory in MyApp.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

必要な場合は -fno-common も指定します。次のコマンドでコンパイラフラグを確認することができます。

$ ./llvm-config --cxxflags
-I/path/to/clang+llvm-3.7.0/include  -DNDEBUG -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -O3  -std=c++11 -fvisibility-inlines-hidden -fno-exceptions -fno-rtti -fno-common -Wcast-qual

NOTE
この typeinfo のリンクエラーについては当初原因の見当がつかず、解決したときはうれしかったです。 解決方法は先に示した通り、例外テーブルの生成と RTTI を無効にすることです。 nm -o libclangTooling.a | grep FrontendActionFactory するとシンボルは見つかるし、ライブラリもリンクしている、それなのにリンクエラーが起きるのはなんでだろう、と途方にくれていたところでした。 RTTI を無効にするなんてめったに思いつきません…。 そう言われてみれば、少し前 Visual Studio で Visual C++ Compiler の代わりに Clang を使ったときに、「例外と RTTI は使えないよ」とエラーが出力されたことを思い出しました。

実行時に libclang の動的ライブラリを実行ファイルから見つけられるようにする

ここまでくれば、リンクも問題なくできて LibTooling を使ったアプリケーションをビルドできるはずです。 実行時にアプリケーションは libclang の動的ライブラリをロードするので、適当なところに動的ライブラリをコピーまたはアプリケーションにバンドルしてください。 OS X の場合は libclang.dylib を、Windows の場合は libclang.dll を、Linux の場合は libclang.so が該当します。 もっとも手軽なのは実行ファイルと同じディレクトリに、ライブラリファイルをコピーすることです。

実行時に libclang.dylib がうまくロードできていない場合、次のような実行時エラーが出ます。

dyld: Library not loaded: @rpath/libclang.dylib
  Referenced from: ~/Library/Developer/Xcode/DerivedData/MyApp/Build/Products/Debug/MyApp
  Reason: image not found
Program ended with exit code: 9

その他・おぼえ書き

シェルのバッククォート表現と llvm-config を使って、clang++ や g++ に簡単にコンパイルオプションを渡すことができます。

$ clang++ main.cpp -std=c++14 -stdlib=libc++ \
  `/path/to/clang+llvm-3.7.0/bin/llvm-config --cxxflags --ldflags --libs --system-libs`

静的ライブラリに、任意のクラスのシンボル情報が含まれているかどうかを調べるには nm コマンドを使います。

$ nm -o lib/libclangTooling.a | grep FrontendActionFactory

参考文献


  1. 試していないのではっきりとは書けませんが、Visual Studio や、直接 g++ コマンドや clang++ コマンドからビルドした場合も同じ手順で組み込むことができると思います。 

  2. 僕は、このコンパイラフラグを設定し忘れていて今回リンクエラーにはまってしまいました。 

  3. 時間と説明を省略するためにビルド済みのバイナリを使いました。少し時間はかかりますが、リポジトリから最新のソースコードを手に入れてビルドするのは想像よりも難しくありません。ninja コマンドを入力するだけです。 

  4. このときお好みで /usr/local/lib 以下にインストールしたり、シンボリックリンクを追加してもかまいません。 

Leave a Reply