LibTooling - Clang AST & Refactoring API

2012-08-17 14:22

Durch Vorträge auf der GoingNative und C++Now! wurde ich dieses Jahr aufmerksam auf die API von Clang. Chandler Carruth stellte dort die API für das Refactoring von C++ Code vor. Man kann komplett auf den AST (AST = Abstract Syntax Tree) zugreifen, ihn mittels Visitor Pattern durchsuchen, und auch verändern. Die Refactoring API erleichtert einem dies noch etwas, sie stellt unter anderem Suchklassen (ASTMatcher/Finder) zur Verfügung, welche auf dem AST arbeiten.

Wenn man das alles live sieht, ist man schon beeindruckt, und fragt sich, wie leicht es wohl ist, selber damit herum zu spielen, und evtl. über eigene Tools nachdenken zu können. Noch ein kleiner Hinweis: in diesem Artikel geht es nicht um die Benutzung von Clang als Compiler, sondern wie man auf sein Toolset via C++ zugreifen kann. Dazu kann auch der Compiler gehören.

Getting Started

Wo fängt man also an? Als erstes muss man llvm und clang (+ einiges anderes) aus dem SVN oder git auschecken, genaue Details dazu stehen hier. Dann sollte man sich für eine Entwicklungs-plattform entscheiden, Clang unterstützt GNU/Make via configure, und kann auch für diverse Compiler via CMake gebaut werden. Da ich evtl. Qt als Frontend für spätere Tools nutzen möchte, und den QtCreator sehr gerne nutze, entschied ich mich dafür Clang mit CMake für MinGW 4.4 zu bauen. Auf meinem Notebook baut das dann ca. eine Stunde, aber das geht auf besseren Rechnern sicher deutlich schneller.

 

Clang & QtCreator

Clang und LLVM alleine sind schon eine riesige Code Sammlung, auch hat LLVM scheinbar keine Abhängigkeiten nach aussen, ich musste z.b. nicht angeben wo boost liegt etc. Da Clang komplett auf CMake setzt, kann man auch eigene Tools sehr gut mit Clang anlegen. Mit der Entscheidung für QtCreator und Qt ist dies sogar kompatibel, aber ich wollte auch aus QMake heraus die Clang APIs verwenden können. Dies war nicht ganz einfach, aber mit der richtigen Reihenfolge der Includeverzeichnisse klappt es:

 

DEFINES += __STDC_LIMIT_MACROS __STDC_CONSTANT_MACROS __STDC_LIMIT_MACROS _GNU_SOURCE


INCLUDEPATH += C:/cpp/boost_1_49_0
INCLUDEPATH += C:/cpp/llvm/mybuild/include
INCLUDEPATH += C:/cpp/llvm/tools/clang/include
INCLUDEPATH += C:/cpp/llvm/include
INCLUDEPATH += C:/cpp/llvm/mybuild/tools/clang/include

 

Es müssen relativ viele Libraries gelinkt werden, daher verzichte ich wegen der Leserlichkeit auf diese hier. Wir verwenden nun die APIs von Clang, als Beispiel wollte ich gerne den AST durchsuchen, und Klassennamen, Methoden, Membervariablen und Funktionen ausgeben. Zum ersten Einstieg in die LibTooling eignet sich auch die Startseite von clang.LibTooling. Dort sieht man den generellen Aufbau eines Tools, welches die Clang Infrastruktur nutzt. Und es werden auch die zu linkenden Bibliotheken aufgelistet. Da mein erstes Ziel nur ist, den AST zu besuchen, habe ich mein Beispiel Programm auf dem Beispiel zu ASTFrontendAction aufgebaut. Eine weitere, sehr detaillierte Dokumentation der Clang AST APIs findet sich auch hier.

 

Zugriff auf den AST einer C++ Datei

Wie oben bereits beschrieben, möchte ich gerne die "Kernelemente" einer C++ Datei ausgeben, sprich die Namen von Klassen, Methoden, Membervariablen und Funktionen. Hierfür sind 3 Klassen notwendig, damit wir Zugriff auf den AST von Clang bekommen. Die ersten beiden Klassen sind eigentlich nur Boilerplate Code, um unsere Visitorklasse mit der Clang API bekannt zu machen:

 

class FindNamedClassConsumer : public clang::ASTConsumer {

public:
  explicit FindNamedClassConsumer(ASTContext *Context)
    : Visitor(Context) {}

  virtual void HandleTranslationUnit(clang::ASTContext &Context) {
    Visitor.TraverseDecl(Context.getTranslationUnitDecl());
  }
private:
  FindNamedClassVisitor Visitor;
};

class FindNamedClassAction : public clang::ASTFrontendAction {
public:
  virtual clang::ASTConsumer *CreateASTConsumer(
    clang::CompilerInstance &Compiler, llvm::StringRef InFile) {
    return new FindNamedClassConsumer(&Compiler.getASTContext());
  }
};

 

Natürlich können diese Klassen noch um Funktionen erweitert werden, für das eigentliche Besuchen ist aber die von RecursiveASTVisitor abgeleitete Klasse zuständig:

 

class FindNamedClassVisitor

  : public RecursiveASTVisitor<FindNamedClassVisitor> {
public:
  explicit FindNamedClassVisitor(ASTContext *Context)
    : Context(Context) {}

  bool VisitCXXRecordDecl(CXXRecordDecl *declaration)
{
    //Besuch einer Klassendeklaration:
     return true;
  }

  bool VisitDecl(Decl* decl)
  {
      if(const CXXMethodDecl* method = dyn_cast<CXXMethodDecl>(decl))
      {

          FullSourceLoc FullLocation = Context->getFullLoc(method->getLocStart());

          if (FullLocation.isValid()&& !FullLocation.isInSystemHeader())
          {
              llvm::outs() << "\tFound Method declaration "
                           << method->getParent()->getNameAsString() << "::"
                           << method->getNameAsString() << "\n";
          }
      }
      else if(const FunctionDecl* method = dyn_cast<FunctionDecl>(decl))
      {

          FullSourceLoc FullLocation = Context->getFullLoc(method->getLocStart());

          if (FullLocation.isValid()&& !FullLocation.isInSystemHeader())
          {
              llvm::outs() << "\tFound Function declaration "

                           << method->getNameAsString() << "\n";
          }
      }
      return true;
  }
private:
  ASTContext *Context;
};

 

Ich habe den Code etwas gekürzt. RecursiveASTVisitor stellt verschiedene Methoden zum Besuchen bestimmter Nodes im AST zur Verfügung. Dies gilt aber nicht für alle NodeTypen, daher müssen wir in VisitDecl wieder mit dynamic_cast den eigentlichen Typen der Deklaration prüfen. Als Beispiel dient hier die Erkennung von Methoden und Funktionen. Membervariablen findet sind in der Klasse FieldDecl enthalten. Alle Decl Typen haben eine bestimmte Vererbungshierarchie, die API von LibTooling wird dadurch relativ groß, und ist am Anfang etwas unübersichtlich. Die Lernkurve ist manchmal auch etwas steil, aber die Doxygen Dokumentation und Mailingliste bieten meistens Hilfe.

Die Klasse ASTContext enthält den gesamten AST Baum, welchen man besucht.

 

Refactoring

Etwas was die APIs von Clang auch sehr interessant macht, ist die Fähigkeit C++ Code zu manipulieren. Mit der LibTooling lassen sich Tools entwickeln, welche aktiv den Quellcode verändern. Unter llvm/tools/clang/tools/extra/ findet man hierfür ein Beispiel, und einen Template, um ein eigenes Tool zu entwickeln. Das aktuelle Beispiel fix-c_str-calls ist ein Tool, welches überflüssige Aufrufe von std::string::c_str aus dem Code löscht. Dies ist relativ komplex, daher beziehe ich mich in diesem Blogbeitrag nur auf das Tool Template:

 

class ToolTemplateCallback : public MatchFinder::MatchCallback {

 public:
  ToolTemplateCallback(Replacements *Replace) : Replace(Replace) {}

  virtual void run(const MatchFinder::MatchResult &Result) {
//  TODO: This routine will get called for each thing that the matchers find.
//  At this point, you can examine the match, and do whatever you want,
//  including replacing the matched text with other text
  (void) Replace; // This to prevent an "unused member variable" warning;
  }

 private:
  Replacements *Replace;
};

 

Für jedesmal wenn im AST das gesuchte gefunden wird, wird run mit Result aufgerufen. In dieser Methode würde man dann auch das Refactoring anstoßen. Diese Methode im fix-c_str-calls Beispiel sieht dann so aus:

 

virtual void run(const ast_matchers::MatchFinder::MatchResult &Result) {

    const CallExpr *Call =
        Result.Nodes.getStmtAs<CallExpr>("call");
    const Expr *Arg =
        Result.Nodes.getStmtAs<Expr>("arg");
    const bool Arrow =
        Result.Nodes.getStmtAs<MemberExpr>("member")->isArrow();
    // Replace the "call" node with the "arg" node, prefixed with '*'
    // if the call was using '->' rather than '.'.
    const std::string ArgText = Arrow ?
        formatDereference(*Result.SourceManager, *Arg) :
        getText(*Result.SourceManager, *Arg);
    if (ArgText.empty()) return;

    Replace->insert(Replacement(*Result.SourceManager, Call, ArgText));
  }

 

Hier sieht man auch eine Unsitte in Clang: Variablen im Code und in Beispielen werden grundsätzlich groß geschrieben. Finde das etwas verwirrend, kenne auch keine andere Library in C++ die dies so hält. Doch zurück zum ToolTemplate. Denn mit der MatchCallback ist es noch nicht ganz getan.

 

RefactoringTool Tool(*Compilations, SourcePaths);

ast_matchers::MatchFinder Finder;
ToolTemplateCallback Callback(&Tool.getReplacements());

// TODO: Put your matchers here.
// Use Finder.addMatcher(...) to define the patterns in the AST that you
// want to match against. You are not limited to just one matcher!

return Tool.run(newFrontendActionFactory(&Finder));

 

Dies ist das Ende der Main Funktion aus ToolTemplate.cpp, wo die entsprechende Refactoring API von Clang instanziiert wird, um schließlich dann den Callback zu bekommen. Das eigentlich entscheidende, nämlich die Matcher fehlt hier allerdings. Die Datei soll ja nur als Template für eigene Tools dienen. Schauen wir kurz bei RemoveCStrCalls als Beispiel, was hier möglich ist:

 

 

Finder.addMatcher(

      constructorCall(
          hasDeclaration(method(hasName(StringConstructor))),
          argumentCountIs(2),
          // The first argument must have the form x.c_str() or p->c_str()
          // where the method is string::c_str().  We can use the copy
          // constructor of string instead (or the compiler might share
          // the string object).
          hasArgument(
              0,
              id("call", memberCall(
                  callee(id("member", memberExpression())),
                  callee(method(hasName(StringCStrMethod))),
                  on(id("arg", expression()))))),
          // The second argument is the alloc object which must not be
          // present explicitly.
          hasArgument(
              1,
              defaultArgument())),
      &Callback);

 

Dies ist auch ein schönes Beispiel für die komplexität der Clang API. Mittels zahlreicher Funktionen kann man verschiedene Matcher für C++ Code bauen. Man kann auf Klassennamen, Ableitung, Methodenaufrufe und vieles mehr Matchen. Als zweites Argument gibt man dann noch den Callback für diesen einen spezifischen Matcher an.

StringConstructor und StringCStrMethod werden im Code wie folgt definiert:

 

const char *StringConstructor =

    "::std::basic_string<char, std::char_traits<char>, std::allocator<char> >"
    "::basic_string";

const char *StringCStrMethod =
    "::std::basic_string<char, std::char_traits<char>, std::allocator<char> >"
    "::c_str";

 

Als einen ersten Einstieg in das Thema empfehle ich den Vortrag von Chandler Carruth "Clang Refactoring C++", welchen er auf der C++ Now! hielt.

 

Zurück