Clang LibToolingを用いたクラス内依存関係抽出ツールの開発

久々に数学とは関係ない記事を書いてみる。

最近、「レガシーコード改善ガイド」という本を読んでいる。概要としては、「レガシーコードとはユニットテストが存在しないコードのことである」という独自の哲学の元、レガシーコードをいかにして改善するかということについて記載されている。

この本の中で「機能スケッチ」というものが登場する。これは、クラス内のメソッドが他のメンバー変数やメソッドをどのように利用しているかという依存関係を絵に描き起こすものである。これによって、巨大なクラスからいくつかのグループを見つけ出し、分割するためのヒントが得られる。

さて、本ではこれを手描きするように説明されているわけだが、絶望的に巨大なクラスに対してそんなことしてられないということもあるだろう。そこでツールによる自動化ができないかと考えた。こういうとき、まず思いつくのはPython等でテキスト処理をゴリゴリする方法だ。しかし、プログラミング言語の解析をそのように行うことは、もはや自前でパーサーを書くに等しい労力がかかるだろう。

そこで、clangのFrontendを利用できないかと考えた。ClangとはLLVMベースのC/C++/Objective-Cコンパイラである。通常、コンパイラは大きく2つのレイヤーに分かれる。すなわち、ソースファイルから抽象的な構文木中間言語を生成するためのFrontend、及びFrontendの出力を受けて機械語のコードを生成するBackendである。

実は、clangにはFrontend部分の出力を利用したコード解析系ツールの開発を支援してくれるLibToolingというライブラリが存在する。具体的には、clangが生成するASTと呼ばれる抽象構文木を辿ってノードの情報を引っ張り出すことができるようなライブラリが提供されている。

LibTooling — Clang 13 documentation

Introduction to the Clang AST — Clang 13 documentation

今回はこのclang LibToolingを用いて機能スケッチを自動生成するツールを作ってみたので、それについて説明する。

LibToolingのインストール

まずはインストール方法についてだが、公式ドキュメントを見ると「ソースからビルドしてインストールしてね」と書かれていて、いきなり心を折ってくる。頼むからパッケージからインストールさせてくれと思うわけだが、実は可能である。例えば私が使ったUbuntu20.04の場合であれば以下のパッケージをインストールすればよい。

  • clang
  • libclang-dev

LibToolingを用いた自作ツールのビルド

次にビルドである。手始めに以下のページに記載されているサンプルコードをビルドしてみようと思ったのだが、これがundefined referenceエラーで全然通らない。ここが一番苦しいポイントだった。

Tutorial for building tools using LibTooling and LibASTMatchers — Clang 13 documentation

結論だけ言うと、以下のサイトに従ったらうまくビルドできた。感謝しかない。

qiita.com

AST Matcherを用いたツール開発の流れ

LibToolingの重要な機能の1つにAST Matcherというものがある。これは、ASTから条件にマッチしたノードを検索するための仕組みである。AST Matcherを使った処理の流れは以下のようになる。

  1. AST Matcherを用いてASTノードの検索条件を記述する。
  2. 検索にマッチした際に実行するコールバック関数を記述する。
  3. MatchFinderと呼ばれるオブジェクトに検索条件とコールバック関数の組を登録する(複数登録可能)。

AST Matcherによる検索条件の記述

AST Matcherの条件の記述方針については以下のドキュメントに説明がある。

Matching the Clang AST — Clang 13 documentation

上記ページからリンクされているAST Matcher Referenceというページは頻繁に参照することになる。

AST Matcher Reference

ざっくりいうと、上記のreferenceを見て、自分のやりたいことに合致する条件式をガチャガチャと組み合わせていく流れである。慣れていないと、これはかなり厳しい作業になる。特に分かりづらい点として、ASTのノードはStmtやDeclなどのいくつかの型に分かれており、それらは共通の基底クラスやインターフェイスを持たない。そのため、今自分が操作したいノードが何の型なのかというのを意識しながら処理を記述する必要がある。

こういうものこそまさにCompositeパターンでシンプルに作れるのでは?と思ってしまうが、clangのASTはそうなっていないので仕方ない。

Matcherについては以下のサイトが非常に参考になるので、こちらを先に一読するとよい。

Exploring Clang Tooling Part 2: Examining the Clang AST with clang-query | C++ Team Blog

Clang queryについて

AST Matcherの条件式を作るのは苦痛を伴う作業であるが、幸いにしてこの苦痛を大幅に緩和するためのツールがある。それがclang-queryである。上でリンクを張ったMicrosoftのサイトでもclang-queryを使っている。

Clang-queryとは、LibToolingで実際に使える条件式を単独で実行し、結果を出力するためのツールである。Ubuntu20.04の場合はclang-toolsパッケージをインストールすることで使用できる。

条件式を考えたら、まずclang-queryにて試しに検索を行ってみるとよい。これにより、どういうコードが検索に引っかかるのかを実際に確かめることができ、条件式を期待通り記述するための助けになる。

ちょっとハマった点としては、C++11以降の機能を使いたい場合、clang-queryにその旨を示すオプションを付けてやる必要がある。例えばC++17以降の機能を使用しているlayer.cppを解析したい場合、以下のようにすればよい。

clang-query --extra-arg='-std=c++17' ~/programs/cnn/src/layer.cpp --

最後の"--"はなぜ必要かいまいち分からないが、これがないとエラーになる。

条件式の型について

条件式はプログラム上ではオブジェクトとして表現される。これの型をどうすべきなのか、本当に全然説明が見つからないので未だに分からない。私の場合はコンパイルエラーから類推してclang::ast_matchers::DeclarationMatcherとかclang::ast_matchers::StatementMatcherなどを使ってみて、これでコンパイルは通ったが、未だに正解がよく分からない。

以下にサンプルコードを示す。

MemberAccessMatcher.h

#ifndef MEMBER_ACCESS_MATCHER
#define MEMBER_ACCESS_MATCHER
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include <string>


class MemberAccessMatcher
{
public :
    MemberAccessMatcher(std::string targetName);
    clang::ast_matchers::StatementMatcher getMatcher();

private:
    clang::ast_matchers::StatementMatcher matcher;
};

#endif

MemberAccessMatcher.cpp

#include "../include/MemberAccessMatcher.h"
#include "clang/AST/ASTContext.h"

using namespace clang::ast_matchers;

MemberAccessMatcher::MemberAccessMatcher(std::string targetName) :
    matcher(memberExpr(hasAncestor(cxxMethodDecl(isExpansionInMainFile(), ofClass(hasName(targetName))).bind("fdecl"))).bind("mexpr"))
{
}

StatementMatcher MemberAccessMatcher::getMatcher()
{
    return matcher;
}

MemberAccessMatcherクラスのmatcherという変数が条件式を格納する変数である。この例ではイニシャライザにて条件式を設定している。

bindとノードの操作

AST Matcherでは検索に引っかかったノードを識別するためにbindという仕組みを使う。条件式にbind("文字列key")という形式で文字列keyを登録しておくことで、コールバック関数にて該当するノードの情報を引っ張り出すことができる。

そうやって引っ張り出してきたノードに対しては、そのノードの型に応じて様々な処理が可能である。それぞれの型についてどういった処理が行えるかは公式ドキュメントを見る必要がある。このドキュメントは殺意が湧くほど説明が足りないので、関数名から処理を推測し、trial and errorでコーディングしていく必要がある。

clang: clang Namespace Reference

ノードに対して行える操作の典型例を以下に示す。

  • 変数名・関数名の取得
  • 変数の型の取得
  • 関数の戻り値の取得
  • 関数の引数名や型の取得
  • メンバー変数・メンバー関数が所属するクラスの取得
  • クラスが継承している基底クラスの情報の取得

最後に書いた基底クラスの情報の取得については私が作ったツールでも使用しているのだが、これも殺意が湧くほど分かりづらいので、使用される方は覚悟が必要である。

コールバック関数

コールバック関数についても簡単に説明しておく。やれることは、ASTノードへのアクセスとContextへのアクセスである。Context (正式名称はASTContext) にはコードの行数など、ノードには含まれない副次的な情報が格納されているらしいが、詳しいことは分からない。気になる方は以下を参照のこと。

clang: clang::ASTContext Class Reference

以下にコード例を示す。

MemberAccessFinder.h

#ifndef MEMBER_ACCESS_FINDER
#define MEMBER_ACCESS_FINDER
#include "../include/Target.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include <memory>


class MemberAccessFinder : public clang::ast_matchers::MatchFinder::MatchCallback {
public :
    MemberAccessFinder(std::shared_ptr<Target> tgt);
    // MatchCallbackクラスのrunメソッドをoverrideする
    virtual void run(const clang::ast_matchers::MatchFinder::MatchResult &result) override;

    /* 省略 */
};

#endif

MemberAccessFinder.cpp

void MemberAccessFinder::run(const MatchFinder::MatchResult &result) {
    ASTContext *context = result.Context;  // Contextの取得
    MethodInfoExtractor fie;
    std::string dep;
    // "fdecl"というkeyにbindされたノードをCXXMethodDecl型として取得
    if(auto *node = result.Nodes.getNodeAs<clang::CXXMethodDecl>("fdecl")) {
        // 関数名を取得 (詳細は後述のgithub参照)
        dep += "\"" + fie.extractFuncName(*node) + "\"";
    }

    dep += " -> ";

    // "mexpr"というkeyにbindされたノードをMemberExpr型として取得
    if(auto *node = result.Nodes.getNodeAs<clang::MemberExpr>("mexpr")) {
        // node->dump();
        auto decl = node->getMemberDecl();
        auto cls = node->Classify(*context);  // Contextからもいろいろ情報が取れる
        if(cls.getKind() == clang::Expr::Classification::Kinds::CL_MemberFunction) {
    /* 省略 */
}

おまけ:オプションパーサーについて

LibToolingではデフォルトでオプションをパースするための機能が備わっているので、基本的にはそれを利用する。自前でオプションを追加することも可能である。以下のサイトが参考になった。

clang-developers.42468.n3.nabble.com

こちらの質問者さんはうまく動かないと言っているが、私の環境ではこれに従ったら問題なくオプションが追加できた。

クラス内依存関係抽出ツールについて

いよいよ私が作ったツールについて説明する。ソースは以下のgithubリポジトリに置いた。

GitHub - peng225/class_dep: Class field and method dependency analysis tool.

ツールの概要としては、クラス内のメンバー変数・メンバー関数の依存関係を解析し、graphvizにてpngファイルとして結果を出力するものである。

例を見てみよう。まず、入力するソースコードを以下に示す。

sample.h

#ifndef SAMPLE_H
#define SAMPLE_H

class Sample
{
public:
    Sample();
    void setVal(int i);
    int getVal();
    virtual int addValAndGet(int addVal);
    int trivial();
protected:
    int hoge;
};

#endif

sample.cpp

#include "sample.h"

Sample::Sample() : hoge(0)
{
}

void Sample::setVal(int i)
{
    hoge = i;
}

int Sample::getVal()
{
    return hoge;
}


int Sample::addValAndGet(int addVal)
{
    return getVal() + hoge + addVal;
}


int Sample::trivial()
{
    return 5;
}

依存性解析だけが目的で、中身は全く無意味かつ命名もめちゃくちゃだがご容赦頂きたい。

このソースコードから得られるツールの出力は以下のようになる。

f:id:peng225:20210510150539p:plain
Sampleクラスの依存性解析結果

見た目にあまり気を使っていないので適当だが、四角がメソッドで丸がフィールドである。このように、依存関係が一目で理解できるようになった。

まとめ

以上、clangのLibToolingを用いて作成したクラス内依存関係解析ツールについて説明した。感想としては、clangのドキュメントが「あるだけマシ」というレベルで非常に辛かった。

コンパイラのFrontend機能を利用できるというのは普通に便利だしinnovativeだと思うので、もう少しドキュメントが整備されることを願って止まない。