Clang is not just a great compiler...

-- Doug Gregor, Apple Inc.

clangは、モダンなコンパイラとして有名であるが、 中間形式である AST を使ったりして静的解析の基板として使えるようにもなっている。 実際に、Apple の Xcode では、Clang を使ってコード補完を実現しているそうだ。

そんな、Clang 使って AST を覗く方法の話。

昔は XML で構文木を吐き出す機能があったらしい

$ clang -cc1 -ast-print-xml source.c

今のバージョンは -ast-print-xml オプションはない。 -emit-ast で我慢しろ、らしい。

$ clang -emit-ast source.c

これで生成される source.ast ファイルがバイナリ形式だから困ったものである。

AST を解析する方法

オープンソースなので、clang のソースに手を入れるとか 手段はいくらでもあるのだろうけれど、 正攻法は libclang を使ってアクセスするらしい。

最も安直な構文のなぞり方は、下記のように clang_visitChildren() で各要素ごとにウォークスルーする。

#include <stdio.h>
#include <clang-c/Index.h>

CXChildVisitResult visitChildrenCallback(CXCursor cursor, CXCursor parent, CXClientData client_data)
{
  CXString usrStr  = clang_getCursorKindSpelling(clang_getCursorKind(cursor));

  printf("%s\n", usrStr);

  clang_disposeString(usrStr);

  return CXChildVisit_Recursive;
}

int main(int argc, char *argv[])
{
  // create index w/ excludeDeclsFromPCH = 1, displayDiagnostics=1.
  CXIndex index = clang_createIndex(1, 1);

  // load a *.ast file.
  CXTranslationUnit tu = clang_createTranslationUnit(index, argv[1]);
  if (tu != NULL) {
    clang_visitChildren(clang_getTranslationUnitCursor(tu),
                        visitChildrenCallback,
                        NULL);

    clang_disposeTranslationUnit(tu);
  }

  clang_disposeIndex(index);
}

なお、コンパイルは以下のように。 (Clang でコンパイルしたかったが、なんかうまくリンクが通らなかったので GCC でやった。)

$ g++ -o astana astana.cpp -L/usr/lib64/llvm -lclang

ちなみに使い方は、引数に ast ファイルを渡してやる。 つまり、全体的な使い方としては以下のようになる。

$ clang -emit-ast source.c
$ astana source.ast > source.txt

ちょっとした XML を吐こうとすると

XML 出力には libxml2 使って

#include <clang-c/Index.h>
#include <libxml/encoding.h>
#include <libxml/xmlwriter.h>
#include <stdio.h>

xmlChar *getLocationAsString(CXCursor cursor)
{
  xmlChar locStr[BUFSIZ];
  xmlChar *ret = NULL;

  // get filename, line # and column #.
  CXSourceLocation loc = clang_getCursorLocation(cursor);
  CXString filename;
  unsigned lineno;
  unsigned column;
  clang_getPresumedLocation(loc, &filename, &lineno, &column);

  xmlStrPrintf(locStr, BUFSIZ-1,
               BAD_CAST "%s:%d:%d",
               clang_getCString(filename),
               lineno,
               column);
  ret = xmlStrdup(locStr);

  clang_disposeString(filename);

  return ret;
}

CXChildVisitResult visitChildrenCallback(CXCursor cursor, CXCursor parent, CXClientData client_data)
{
  xmlTextWriterPtr xmlWriter = static_cast<xmlTextWriterPtr>(client_data);

  CXString usrStr = clang_getCursorUSR(cursor);
  CXString kindStr = clang_getCursorKindSpelling(clang_getCursorKind(cursor));
  xmlChar *location = getLocationAsString(cursor);

  xmlTextWriterStartElement(xmlWriter, BAD_CAST "entity");
  xmlTextWriterWriteAttribute(xmlWriter,
                              BAD_CAST "usr",
                              BAD_CAST clang_getCString(usrStr));
  xmlTextWriterWriteAttribute(xmlWriter,
                              BAD_CAST "kind",
                              BAD_CAST clang_getCString(kindStr));
  xmlTextWriterWriteAttribute(xmlWriter,
                              BAD_CAST "src",
                              location);

  clang_disposeString(usrStr);
  clang_disposeString(kindStr);
  free(location);

  // visits children recursive.
  clang_visitChildren(cursor,
                      visitChildrenCallback,
                      xmlWriter);

  xmlTextWriterEndElement(xmlWriter); // entity

  return CXChildVisit_Continue;
}

int main(int argc, char *argv[])
{
  // create index w/ excludeDeclsFromPCH = 1, displayDiagnostics=1.
  CXIndex index = clang_createIndex(1, 1);

  // load a *.ast file.
  CXTranslationUnit tu = clang_createTranslationUnit(index, argv[1]);
  if (tu != NULL) {
    xmlBufferPtr xmlBuffer = xmlBufferCreate();
    xmlTextWriterPtr xmlWriter = xmlNewTextWriterMemory(xmlBuffer, 1);

    xmlTextWriterStartDocument(xmlWriter, NULL, "UTF-8", NULL);
    xmlTextWriterStartElement(xmlWriter, BAD_CAST "ast");

    clang_visitChildren(clang_getTranslationUnitCursor(tu),
                        visitChildrenCallback,
                        xmlWriter);

    xmlTextWriterEndElement(xmlWriter); // ast
    xmlTextWriterEndDocument(xmlWriter);
    xmlFreeTextWriter(xmlWriter);

    // print the XML text to the standard output.
    xmlBufferDump(stdout, xmlBuffer);
    xmlBufferFree(xmlBuffer);

    clang_disposeTranslationUnit(tu);
  } else {
    fprintf(stderr, "Could not load \"%s\" as an AST file.\n", argv[1]);
  }

  clang_disposeIndex(index);

  return 0;
}

Makefile は、例えば、

CXX             = g++
CXXFLAGS        = -O2 -g
CLANGFLAGS      = -L/usr/lib64/llvm -lclang
LIBXML2FLAGS    = `pkg-config --libs --cflags libxml-2.0`

astana: astana.cpp
    $(CXX) $(CXXFLAGS) -o $@ $^ $(CLANGFLAGS) $(LIBXML2FLAGS)

使い方は、前のと同じく

$ clang -emit-ast source.c
$ astana source.ast > source.xml

しかし、さすがに自力で XML を吐こうとするとメンドイ。 これでも表示項目足りないくらいだし。

References