0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PythonのlibclangでC++のヘッダーファイルから情報を抽出する

Last updated at Posted at 2023-09-09

概要

Pythonのlibclangを使用してC++のヘッダーファイルから関数宣言の情報を抽出する方法の例です。

具体例のリポジトリは以下で公開しています。
https://github.com/ishioka0222/libclang_example

この記事には、以下のようなヘッダーファイル(説明のため、わざと意味のないincludeをしています)をパースし、

header.h
#include <iostream>

int my_add(int a, int b); // addの宣言と同じ行にあるコメント

int my_sub(int a, int b); // subの宣言と同じ行にあるコメント

以下のようなJSONファイルを出力するサンプルコードを載せています。

[
    {
        "name": "my_add",
        "return_type": "int",
        "params": [
            {
                "name": "a",
                "type": "int"
            },
            {
                "name": "b",
                "type": "int"
            }
        ],
        "filename": "header.h",
        "line_number": 3,
        "comment": "// addの宣言と同じ行にあるコメント"
    },
    {
        "name": "my_sub",
        "return_type": "int",
        "params": [
            {
                "name": "a",
                "type": "int"
            },
            {
                "name": "b",
                "type": "int"
            }
        ],
        "filename": "header.h",
        "line_number": 5,
        "comment": "// subの宣言と同じ行にあるコメント"
    }
]

関数宣言の情報を抽出する

まずはlibclangをインストールします。

pip install libclang

main.pyという名前で以下のようなファイルを作成します。

main.py
import argparse
import json
import os

from clang.cindex import CursorKind, Index, TokenKind


def main():
    # コマンドライン引数をパースする。
    parser = argparse.ArgumentParser()
    parser.add_argument("filename", help="Input file")
    args = parser.parse_args()

    # 入力ファイル名を取得する。
    input_filename = os.path.basename(args.filename)

    # libclang の Index オブジェクトを作成する。
    index = Index.create()
    # 入力ファイルをパースする。
    # -x c++ オプションを指定することで、C++ のソースコードとしてパースする。
    translation_unit = index.parse(args.filename, args=["-x", "c++"])

    # コメントの行番号からコメント文字列へのディクショナリを作成する。
    comments = {}
    for token in translation_unit.cursor.get_tokens():
        if token.kind == TokenKind.COMMENT:
            line_number = token.location.line
            comment = token.spelling

            comments[line_number] = comment

    # 関数宣言の情報からなるリストを作成する。
    functions = []
    for child in translation_unit.cursor.get_children():
        # 関数宣言以外は無視する。
        if child.kind != CursorKind.FUNCTION_DECL:
            continue

        # ファイル名がない場合は無視する。
        if child.location.file is None:
            continue

        # ファイル名を取得する。
        filepath = child.location.file.name
        filename = os.path.basename(filepath)

        # 関数宣言があるファイルが入力ファイルに一致しない場合は無視する。
        # これがないと #include で読み込んだヘッダファイルの関数宣言も出力されてしまう。
        if filename != input_filename:
            continue

        # 各種情報を取得する。
        function_name = child.spelling
        return_type = child.result_type.spelling

        line_number = child.location.line
        comment = comments.get(line_number, "")

        params = []
        for param in child.get_arguments():
            param_name = param.spelling
            param_type = param.type.spelling

            params.append({
                "name": param_name,
                "type": param_type
            })

        functions.append({
            "name": function_name,
            "return_type": return_type,
            "params": params,
            "filename": filename,
            "line_number": line_number,
            "comment": comment
        })

    # JSON 形式で出力する。
    print(json.dumps(functions, indent=4, ensure_ascii=False))


if __name__ == "__main__":
    main()

main.pyの引数としてC++のヘッダーファイルを渡すことで、最初の例のような出力が得られます。

$ python main.py header.h
[
    {
        "name": "my_add",
        "return_type": "int",
        "params": [
            {
                "name": "a",
                "type": "int"
            },
            {
                "name": "b",
                "type": "int"
            }
        ],
        "filename": "header.h",
        "line_number": 3,
        "comment": "// addの宣言と同じ行にあるコメント"
    },
    {
        "name": "my_sub",
        "return_type": "int",
        "params": [
            {
                "name": "a",
                "type": "int"
            },
            {
                "name": "b",
                "type": "int"
            }
        ],
        "filename": "header.h",
        "line_number": 5,
        "comment": "// subの宣言と同じ行にあるコメント"
    }
]

備考

関数宣言程度の情報であれば上の例のようにデフォルトで取得できますが、プリプロセッサなどの詳細な情報はデフォルトでは取得できないようです。
こういった詳細な情報も取得したい場合は、parseを呼び出すときにPARSE_DETAILED_PROCESSING_RECORDというオプションを指定する必要があります。

from clang.cindex import CursorKind, Index, TokenKind, TranslationUnit

# (略)

translation_unit = index.parse(
        args.filename, options=TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD)
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?