LoginSignup
3
0

More than 3 years have passed since last update.

Clang の libTooling を利用して C/C++ のソースコードを字句解析するための調査

Last updated at Posted at 2021-03-08

はじめに

いろいろと調べて理解した(つもりの)ことをメモしておきます。

内容は、非効率だったり間違いだったりする可能性があります。

環境

  • Ubuntu Server 20.04.2 LTS
  • Clang 10.0.0
  • CMake 3.16.3
  • Ninja 1.10.0

参照情報

※2021/03/08 現在は "13.0.0git" の表記があり、実行環境とバージョンが異なる点に留意。

準備

以下のようにパッケージをインストールしました。

sudo apt-get install clang
sudo apt-get install libclang-dev
sudo apt-get install cmake
sudo apt-get install ninja-build

以下のようにディレクトリを作成しました。また AddClang.cmake をダウンロードしました。

mkdir -p ~/workspace/sample
cd ~/workspace/sample
mkdir build src
mkdir -p cmake/modules
cd cmake/modules
curl -L -O https://github.com/llvm/llvm-project/raw/llvmorg-10.0.0/clang/cmake/modules/AddClang.cmake

AddClang.cmake は後述の CMakeLists.txt の中で使用したいのですが、Clang のパッケージに含まれていないようだったのでダウンロードしました。

以下のリンク先のやりとりから、AddClang.cmake は今後の Clang のバージョンのパッケージには含まれるような気がします。

ソースファイルの作成

Sample.cpp というファイル名で作成しました。

cd ~/workspace/sample/src
code Sample.cpp

内容はチュートリアルにあるサンプルコード (以下のリンク先) をそのまま使用しました。

CMakeLists.txt の作成

Sample.cpp と同じディレクトリに作成しました。

cd ~/workspace/sample/src
code CMakeLists.txt

内容は以下のようにしました。

cmake_minimum_required(VERSION 3.16.3)

project(sample)

# LLVMConfig.cmake を読み込む。
find_package(LLVM REQUIRED CONFIG)
# ClangConfig.cmake を読み込む。
find_package(Clang REQUIRED CONFIG)

# LLVM_LINK_LLVM_DYLIB は LLVMConfig.cmake の中で ON に設定されている。
# CLANG_LINK_CLANG_DYLIB も ON に設定する。
set(CLANG_LINK_CLANG_DYLIB ${LLVM_LINK_LLVM_DYLIB})

# add_clang_executable の中で使用される add_llvm_executable のために AddLLVM.cmake を読み込む。
list(APPEND CMAKE_MODULE_PATH ${LLVM_DIR})
include(AddLLVM)

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/modules")
include(AddClang)

include_directories(${LLVM_INCLUDE_DIRS})
include_directories(${CLANG_INCLUDE_DIRS})

# https://github.com/llvm/llvm-project/blob/llvmorg-10.0.0/clang/cmake/modules/AddClang.cmake#L139
add_clang_executable(sample Sample.cpp)

# https://github.com/llvm/llvm-project/blob/llvmorg-10.0.0/clang/cmake/modules/AddClang.cmake#L181
# CLANG_LINK_CLANG_DYLIB があると動的リンク (clang-cpp とリンク) になる。
clang_target_link_libraries(sample PRIVATE clangFrontend clangTooling)

ビルド

以下のようにビルドしました。compile_commands.json も生成するようにしました。

cd ~/workspace/sample/build
cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ../src
ninja

サンプルをそのまま実行

以下のように SyntaxOnlyAction は何も表示しない Action でした。

$ cd ~/workspace/sample/build
$ ./sample -p compile_commands.json ../src/Sample.cpp
(表示なし)

Action を書き換えて実行

以下のように SyntaxOnlyAction を DumpRawTokensAction に書き換えて実行してみました。

$ cd ~/workspace/sample/src
$ cp Sample.cpp Sample.cpp.old
$ code Sample.cpp
$ diff Sample.cpp.old Sample.cpp
27c27
<   return Tool.run(newFrontendActionFactory<clang::SyntaxOnlyAction>().get());
---
>   return Tool.run(newFrontendActionFactory<clang::DumpRawTokensAction>().get());
$ cd ~/workspace/sample/build
$ ninja
$ ./sample -p compile_commands.json ../src/Sample.cpp
comment '// Declares clang::SyntaxOnlyAction.'   [StartOfLine]  Loc=</home/ayweak/workspace/sample/src/Sample.cpp:1:1>
unknown '
'               Loc=</home/ayweak/workspace/sample/src/Sample.cpp:1:37>
hash '#'         [StartOfLine]  Loc=</home/ayweak/workspace/sample/src/Sample.cpp:2:1>
...省略...

字句解析して得られたトークンの情報を表示してくれました。

その他にもいくつかの Action が用意されているようです。以下のリンク先で確認できました。

同じ表示は clang -cc1 -dump-raw-tokens で可能

今回の調査の後に気づいたのですが、clang の -dump-raw-tokens オプションで同じ表示が可能でした。

$ clang -cc1 --help
...省略...
  -dump-raw-tokens        Lex file in raw mode and dump raw tokens
...省略...
$ cd ~/workspace/sample
$ clang -cc1 -dump-raw-tokens src/Sample.cpp
comment '// Declares clang::SyntaxOnlyAction.'   [StartOfLine]  Loc=<src/Sample.cpp:1:1>
unknown '
'               Loc=<src/Sample.cpp:1:37>
hash '#'         [StartOfLine]  Loc=<src/Sample.cpp:2:1>
...省略...

独自の Action を作成して実行

トークンの情報を JSON 形式で表示したいと考えて、以下の処理を参考に JSONDumpRawTokensAction を作成しました。

JSONDumpRawTokensAction.h は以下のように作成しました。src 内に配置しました。

#ifndef JSON_DUMP_RAW_TOKENS_ACTION_H
#define JSON_DUMP_RAW_TOKENS_ACTION_H

#include "clang/Frontend/FrontendAction.h"

class JSONDumpRawTokensAction : public clang::PreprocessorFrontendAction {
protected:
  void ExecuteAction() override;
};

#endif

JSONDumpRawTokensAction.cpp は以下のように作成しました。src 内に配置しました。

#include "JSONDumpRawTokensAction.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Lex/Token.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/raw_ostream.h"

using namespace clang;

void JSONDumpRawTokensAction::ExecuteAction() {
  Preprocessor &PP = getCompilerInstance().getPreprocessor();
  SourceManager &SM = PP.getSourceManager();
  llvm::json::OStream J(llvm::errs());

  const llvm::MemoryBuffer *FromFile = SM.getBuffer(SM.getMainFileID());
  Lexer RawLex(SM.getMainFileID(), FromFile, SM, PP.getLangOpts());
  RawLex.SetKeepWhitespaceMode(true);

  Token RawTok;
  RawLex.LexFromRawLexer(RawTok);

  J.object([&]{
    if (RawTok.is(tok::eof)) {
        return;
    }
    J.attribute("file", SM.getFilename(RawTok.getLocation()));
    J.attributeArray("tokens", [&]{
      while (RawTok.isNot(tok::eof)) {
        J.object([&]{
          J.attribute("kind", tok::getTokenName(RawTok.getKind()));
          J.attribute("spelling", PP.getSpelling(RawTok));
          if (RawTok.isAtStartOfLine()) {
            J.attribute("start_of_line", true);
          }
          if (RawTok.hasLeadingSpace()) {
            J.attribute("leading_space", true);
          }
          if (RawTok.isExpandDisabled()) {
            J.attribute("expand_disabled", true);
          }
          if (RawTok.needsCleaning()) {
            const char *Start = SM.getCharacterData(RawTok.getLocation());
            J.attribute("unclean", StringRef(Start, RawTok.getLength()));
          }
          J.attribute("line", SM.getSpellingLineNumber(RawTok.getLocation()));
          J.attribute("column", SM.getSpellingColumnNumber(RawTok.getLocation()));
        });
        RawLex.LexFromRawLexer(RawTok);
      }
    });
  });
}

Sample.cpp は以下のように変更しました。

$ diff Sample.cpp.old Sample.cpp
0a1
> #include "JSONDumpRawTokensAction.h"
27c28
<   return Tool.run(newFrontendActionFactory<clang::SyntaxOnlyAction>().get());
---
>   return Tool.run(newFrontendActionFactory<JSONDumpRawTokensAction>().get());

CMakeLists.txt は以下のように変更しました。

$ diff CMakeLists.txt.old CMakeLists.txt
19c19
< add_clang_executable(sample Sample.cpp)
---
> add_clang_executable(sample Sample.cpp JSONDumpRawTokensAction.cpp)

ビルドして実行してみると JSON 形式で表示することを確認できました。

$ cd ~/workspace/sample/build
$ cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ../src
$ ninja
$ ./sample -p compile_commands.json ../src/Sample.cpp 2>&1 | python3 -mjson.tool
{
    "file": "/home/ayweak/workspace/sample/src/Sample.cpp",
    "tokens": [
        {
            "kind": "hash",
            "spelling": "#",
            "start_of_line": true,
            "line": 1,
            "column": 1
        },
...省略...

まとめ

ある程度の方法は理解できたような気がします。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0