はじめに
こんにちは。ファームウェアエンジニアチーム、インターンの後藤です。 修士論文の発表と提出が無事に終わり、毎日が幸せいっぱいです。
レガシーコードリファクタの一環として、ヨーダ記法を置換しようという話になりました。 今回はlibclangを用いてヨーダ記法を置換するお話です。
ヨーダ記法とは
ヨーダ記法とは比較演算子==
, !=
のうち、左辺に固定値、右辺に変数がある記法のことです。
例えば以下のような条件式が挙げられます。
if (42 == everything) { ... }
C, C++では代入演算子=
は値として評価できるため、if文の比較内部でも利用可能です。*1
このことで比較演算子==
と代入演算子=
を間違えてしまうことがあり、目に優しくないデバッグを強いられることがあります。
このような間違いを防ぐための記法としてヨーダ記法が生まれました。
誤った代入演算子の場合、コンパイラは代入演算子の左辺に変数があることを期待するため、コンパイル時にエラーとすることができます。
しかし、現在ではこのような記法を用いずとも、コンパイラの発展により容易に検出が可能です。 更に、左辺に変数がある方が可読性の観点からよいとされています。 このため今回はヨーダ記法の置換を試みました。
ヨーダ記法の置換で用いたのがlibclangです。 libclangはC, C++のソースコードを分析することができるライブラリの一つです
libclangとは
プログラミング言語処理系では実行形式を作成するために、字句解析や構文解析を行いAST(abstract syntax tree)を生成することが多いです。 ASTは実行形式を作る際に必要な情報を木構造で保管しており、作成したASTをもとにコンパイラは実行形式を作成します。 このためASTを解析することができれば、どのようにプログラムが解釈されたかがわかります。
代表的なC, C++のコンパイラもコンパイルの中間段階でASTをつくり、実行ファイルを生成しています。 C系コンパイラの一つであるclangによって生成されたASTを、簡単に解析することができるライブラリがlibclangです。 つまりlibclangを使うことでCのASTを解析することができ、ヨーダ記法の検出が可能になります。
libclangにはCインタフェースとPythonバインディングが存在します。*2 今回は
からCインタフェースを用いました。
対象、非対象のヨーダ記法例
以下が今回のコードで置換できる例です
if (42 == answer) { ... } if (3.14 == satori && yutori == 3) { ... }
上記のヨーダ記法は以下のように変換されます
if (answer == 42) { ... } if (satori == 3.14 && yutori == 3) { ... }
今回のコードでは変換されない例として以下のようなものがあります。 論理的に考えると左辺は定数ですが、今回は簡単な判定条件を用いているので定数とみなされません。*5
if ((int)57 == prime_number) { ... } if (4 + 4 == happy) { ... } if (NULL == gaxtu) { ... }
置換までの流れ
デバッグを容易にするために、変更箇所を含んだJSONを作成してJSONからCファイルの生成を行います。 すなわち以下の手順でヨーダ記法の検知を行います。
- ヨーダ記法をlibclangを用いて検出し、修正に必要な情報をJSON形式で保存する
- JSON形式のファイルを読み込み、ヨーダ記法である箇所の左右辺を入れ替え修正したC, C++ファイルを生成する
- 生成したC, C++ファイルをもとのC, C++ファイルと置き換える
速習libclang
libclangの機能の中で今回必要になる機能をざっと紹介します。
libclangの構造
ASTの操作に用いる代表的な構造体がIndex
, TranslationUnit
, Cursor
です。
木構造のノードを指し示す構造体がCursor
、一つのソースコードを木構造として保持する構造体がTranslationUnit
、複数のTranslationUnit
を保存する構造体がIndex
です。
*6
*7
以下の流れで、あるC系ファイルのrootノードを取得できます。
- 関数
clang_createIndex
を用いてIndexの作成 - 関数
clang_parseTranslationUnit
を用いてTranslationUnit
を作りIndex
に紐付け - 関数
clang_getTranslationUnitCursor
を用いてTranslationUnit
のrootノードを取得
以下はドキュメントにあるサンプルコードです。上記の一連の流れを表しています。
libclangを用いるためにclang-c/Index.h
のインクルードが必要です。
#include <clang-c/Index.h> #include <iostream> int main() { CXIndex index = clang_createIndex(0, 0); // Create index CXTranslationUnit unit = clang_parseTranslationUnit( index, "file.cpp", nullptr, 0, nullptr, 0, CXTranslationUnit_None); // Parse "file.cpp" if (unit == nullptr) { std::cerr << "Unable to parse translation unit. Quitting.\n"; return 0; } CXCursor cursor = clang_getTranslationUnitCursor(unit); // Obtain a cursor at the root of the translation unit }
ASTの探索方法
ASTの探索にはCursor
を用います。
Cursor
は子ノードの情報を保持するため、rootノードに対応するCursor
を用いるとAST全体を探索することができます。
探索に用いるのが関数clang_visitChildren
です。
関数clang_visitChildren
に関数ポインタを渡すことでノードに対する操作を行うことができます。
関数ポインタとして渡す関数の戻り値にはCXChildVisit_Recurse
, CXChildVisit_Continue
, CXChildVisit_Break
の三種類の候補があり、探索方法に応じて以下のように指定します。
CXChildVisit_Recurse
は子ノードを含めた再帰的な探索(行きがけの順(深さ優先探索))CXChildVisit_Continue
は自身の子ノードは探索しないCXChildVisit_Break
は探索をやめる
以下のコードはCXChildVisit_Recurse
を返して再帰的な探索を行うことにより、深さ優先でノードの名前を出力する例です
clang_visitChildren( cursor, // Root cursor [](CXCursor current_cursor, CXCursor parent, CXClientData client_data) { CXString current_display_name = clang_getCursorDisplayName(current_cursor); // Allocate a CXString representing the name of the current cursor std::cout << "Visiting element " << clang_getCString(current_display_name) << "\n"; // Print the char* value of current_display_name clang_disposeString(current_display_name); // Since clang_getCursorDisplayName allocates a new CXString, it must be freed. This applies // to all functions returning a CXString return CXChildVisit_Recurse; }, // CXCursorVisitor: a function pointer nullptr // client_data );
ノードの判別
ノードの判別はCursor
を介して行うことができます。
Cursor
で判別に使うことができるのはCXCursorKind
とCXType
です。*8
CXCursorKind
は関数clang_getCursorKind
で、CXType
は関数clang_getCursorType
で取得できます。
以下がCXCursorKind
を用いて整数リテラルを判別する例です。
CXCursorKind kind = clang_getCursorKind(lhs_cursor); switch (kind) { case CXCursor_IntegerLiteral: return true; default: return false; }
ASTとソースコードとの対応
Cursor
にはソースコードとの対応が記録されています。
ソースコードの該当箇所取得には関数clang_getCursorExtent
を用います。
関数clang_getCursorExtent
で取得できるのは、Cursor
の最初の位置情報と最後の位置情報を持つCXSourceRange
です。
ここで得られる最初と最後の位置とは、子ノード全体を含んだ範囲の最初と最後の位置になります。
CXSourceRange
の取得後、最初と最後の位置情報であるCXSourceLocation
取得することができます。
以下がCursor
の最初と最後の行と列を取得する例です。
CXSourceRange range = clang_getCursorExtent(cursor); CXSourceLocation start = clang_getRangeStart(range); CXSourceLocation end = clang_getRangeEnd(range); unsigned start_line, start_column; unsigned end_line, end_column; clang_getExpansionLocation(start, nullptr, &start_line, &start_column, nullptr); clang_getExpansionLocation(end, nullptr, &end_line, &end_column, nullptr);
ヨーダ記法の検知
ここからは前述した知識を使いながら、実際にヨーダ記法を検知するプログラムを書いていきます。 ヨーダ記法を検知した場合、JSONファイルにその位置情報を出力します。
検索条件
ここでは以下の条件を満たすヨーダ記法を検出します。
JSONフォーマット
出力には以下のFileInformations
をトップレベルにもつJSONフォーマットを利用します。
using FileInformations = std::vector<FileInformation>; struct FileInformation { std::string filename; std::vector<Yoda> yoda; }; struct Yoda { unsigned lhs_linestart; unsigned lhs_lineend; unsigned lhs_columnstart; unsigned lhs_columnend; std::string op; unsigned rhs_linestart; unsigned rhs_lineend; unsigned rhs_columnstart; unsigned rhs_columnend; }
ソースコード
検知処理(C++)
以下が検知処理全体のC++ソースです。検知のコア部分は関数detect_yoda_conditions
で行っています。
*9
*10
#include <cassert> #include <clang-c/Index.h> #include <expected> #include <iostream> #include <optional> #include <string> #include <vector> static CXFile s_sourcefile; void print_cursor(const CXCursor& cursor, const std::string prefix) { const CXSourceRange range = clang_getCursorExtent(cursor); const CXSourceLocation start = clang_getRangeStart(range); const CXSourceLocation end = clang_getRangeEnd(range); unsigned start_line, start_column; unsigned end_line, end_column; clang_getExpansionLocation(start, nullptr, &start_line, &start_column, nullptr); clang_getExpansionLocation(end, nullptr, &end_line, &end_column, nullptr); std::cout << " \"" << prefix << "linestart\": " << start_line << "," << std::endl; std::cout << " \"" << prefix << "lineend\": " << end_line << "," << std::endl; std::cout << " \"" << prefix << "columnstart\": " << start_column << "," << std::endl; std::cout << " \"" << prefix << "columnend\": " << end_column << std::endl; return; } void print_op_cursor(const CXCursor& cursor) { std::cout << " \"op\": \"" << clang_getCString(clang_getCursorSpelling(cursor)) << "\"" << std::endl; return; } void print_cursors(const CXCursor& lhs, const CXCursor& op, const CXCursor& rhs) { static bool firstflag = true; CXFile file; clang_getExpansionLocation(clang_getCursorLocation(op), &file, nullptr, nullptr, nullptr); if (file != s_sourcefile) { return; } if (!firstflag) { std::cout << " ," << std::endl; } firstflag = false; std::cout << " {" << std::endl; print_cursor(lhs, "lhs_"); std::cout << "," << std::endl; print_op_cursor(op); std::cout << "," << std::endl; print_cursor(rhs, "rhs_"); std::cout << " }" << std::endl; } void print_json_header(const std::string& sourcefilename) { std::cout << "[" << std::endl; std::cout << " {" << std::endl; std::cout << " \"filename\": \"" << sourcefilename << "\"," << std::endl; std::cout << " \"yoda\": [" << std::endl; } void print_json_footer() { std::cout << " ]" << std::endl; std::cout << " }" << std::endl; std::cout << "]" << std::endl; } bool check_lhs(const CXCursor lhs_cursor) { const CXCursorKind kind = clang_getCursorKind(lhs_cursor); switch (kind) { case CXCursor_IntegerLiteral: case CXCursor_FloatingLiteral: return true; default: return false; } } std::optional<std::vector<CXCursor>> detect_yoda_conditions(CXCursor cursor) { if (clang_getCursorKind(cursor) != CXCursor_BinaryOperator) { return std::nullopt; } const CX_BinaryOperatorKind op = clang_Cursor_getBinaryOpcode(cursor); if (op != CX_BO_EQ && op != CX_BO_NE) { return std::nullopt; } std::vector<CXCursor> cursors; clang_visitChildren( cursor, [](CXCursor c, CXCursor parent, CXClientData client_data) { std::vector<CXCursor>* cursors = (std::vector<CXCursor>*)client_data; cursors->push_back(c); return CXChildVisit_Continue; }, &cursors); // because binary operator has two children, lhs and rhs assert(cursors.size() == 2); const CXCursor& lhs_cursor = cursors[0]; if (!check_lhs(lhs_cursor)) { return std::nullopt; } return cursors; } int main(int argc, char** argv) { std::string sourcefilename; if (argc < 2) { std::cerr << "Usage: " << argv[0] << " <source_file.cpp>" << std::endl; return 1; } else { sourcefilename = argv[1]; } const std::string dump_json = "build/" + sourcefilename + ".json"; CXIndex index = clang_createIndex(0, 0); CXTranslationUnit tu = clang_parseTranslationUnit(index, sourcefilename.c_str(), nullptr, 0, nullptr, 0, CXTranslationUnit_None); if (tu == nullptr) { std::cerr << "Error parsing translation unit." << std::endl; return 1; } // stdout -> dump_json s_sourcefile = clang_getFile(tu, sourcefilename.c_str()); const CXCursor rootCursor = clang_getTranslationUnitCursor(tu); std::freopen(dump_json.c_str(), "w", stdout); print_json_header(sourcefilename); clang_visitChildren( rootCursor, [](CXCursor c, CXCursor parent, CXClientData client_data) { std::vector<CXCursor> cursors; if (auto maybe_cursors = detect_yoda_conditions(c)) { cursors = *maybe_cursors; print_cursors(cursors[0], c, cursors[1]); } return CXChildVisit_Recurse; }, nullptr); print_json_footer(); std::fclose(stdout); clang_disposeTranslationUnit(tu); clang_disposeIndex(index); return 0; }
C++ファイル生成スクリプト(Python)
PythonでJSONを読み込み置換後のファイルを生成します。
import json import sys def read_source_file(filename): """ソースファイルを読み込む""" with open(filename, 'r', encoding='utf-8') as file: return file.readlines() def write_source_file(filename, lines): """修正されたソースコードを書き戻す""" with open(filename, 'w', encoding='utf-8') as file: file.writelines(lines) def load_yoda_conditions(json_file): """JSONからヨーダ記法条件情報を取得""" with open(json_file, 'r', encoding='utf-8') as file: data = json.load(file) # 結果を展開 return data[0]["yoda"] def replace_yoda_conditions(lines, conditions): """行情報に基づいてヨーダ記法を置換""" for condition in conditions: lhs_start_line = condition["lhs_linestart"] - 1 lhs_end_line = condition["lhs_lineend"]-1 lhs_start_col = condition["lhs_columnstart"] - 1 lhs_end_col = condition["lhs_columnend"]-1 # get lhs lhs = "" for i in range(lhs_start_line, lhs_end_line+1): if (i >= len(lines)): print(len(lines), lhs_start_line, lhs_end_line) raise (IndexError("Error: lhs out of range")) if i == lhs_start_line and i == lhs_end_line: print(len(lines[i]), lhs_start_col, lhs_end_col) lhs += lines[i][lhs_start_col:lhs_end_col] elif i == lhs_start_line: lhs += lines[i][lhs_start_col:] elif i == lhs_end_line: lhs += lines[i][:lhs_end_col] else: lhs += lines[i] # get op op = condition["op"] # get rhs rhs_start_line = condition["rhs_linestart"] - 1 rhs_end_line = condition["rhs_lineend"]-1 rhs_start_col = condition["rhs_columnstart"] - 1 rhs_end_col = condition["rhs_columnend"]-1 rhs = "" for i in range(rhs_start_line, rhs_end_line+1): if i == rhs_start_line and i == rhs_end_line: rhs += lines[i][rhs_start_col:rhs_end_col] elif i == rhs_start_line: rhs += lines[i][rhs_start_col:] elif i == rhs_end_line: rhs += lines[i][:rhs_end_col] else: rhs += lines[i] new_line = rhs + "" + op + "" + lhs print(f"Replace: {lhs} {op} {rhs} -> {new_line}") for i in range(lhs_start_line, rhs_end_line+1): if i == lhs_start_line: lines[i] = lines[i][:lhs_start_col] + new_line + \ lines[rhs_end_line][rhs_end_col:] else: lines[i] = "" def main(source_file, json_file, output_file=None): # ソースファイル読み込み lines = read_source_file(source_file) # JSONからヨーダ記法情報を取得 conditions = load_yoda_conditions(json_file) # 条件式置換処理 replace_yoda_conditions(lines, conditions) # 修正後のソースを書き戻し if output_file: write_source_file(output_file, lines) else: write_source_file(source_file, lines) print(f"Yoda conditions replaced successfully in {source_file}") if __name__ == "__main__": if len(sys.argv) < 3: print(f"Usage: {sys.argv[0]} <source_file> <json_file>") sys.exit(1) elif len(sys.argv) == 3: main(sys.argv[1], sys.argv[2]) else: main(sys.argv[1], sys.argv[2], sys.argv[3])
自動化用Makefile
自動化のためのMakefileです。
上記のC++ファイルをanalyser.cpp
、Pythonファイルをgenerate_non_yoda.py
で保存し、Makefileと同一の階層に置くと実行できるはずです。
top: run CC = clang-20 CXX = clang++-20 # use libclang CXXFLAGS += $(shell llvm-config-20 --cxxflags) LDFLAGS += $(shell llvm-config-20 --ldflags --libs --system-libs) -lclang # get all c and cpp files ANALYSIS_C = $(filter-out %.gen.c, $(shell find . -name "*.c" -not -path "*/third-party/*" -not -path "*/build/*" -not -path "./third_party/*")) ANALYSIS_CPP = $(filter-out %.gen.cpp, $(shell find . -name "*.cpp" -not -path "*/third-party/*" -not -path "*/build/*" -not -path "./third_party/*")) # target files ANALYSIS_JSON = $(addprefix build/, $(ANALYSIS_CPP:.cpp=.cpp.json) $(ANALYSIS_C:.c=.c.json)) ANALYSIS_OBJS = $(addprefix build/, $(ANALYSIS_CPP:.cpp=.o) $(ANALYSIS_C:.c=.o)) ANALYSIS_GEN_C:=$(addprefix build/, $(ANALYSIS_CPP:.cpp=.gen.cpp) $(ANALYSIS_C:.c=.gen.c)) # analysis source ANALYSER_SRC = analyser.cpp GENERATER = generate_non_yoda.py re: clean run run: $(ANALYSIS_GEN_C) $(ANALYSIS_JSON) build/analysis build/%.gen.cpp: %.cpp build/%.cpp.json $(GENERATER) -mkdir -p $(dir $@) python3 $(GENERATER) $*.cpp build/$*.cpp.json build/$*.gen.cpp mv build/$*.gen.cpp $*.cpp build/%.gen.c: build/%.c.json $(GENERATER) -mkdir -p $(dir $@) python3 $(GENERATER) $*.c build/$*.c.json build/$*.gen.c mv build/$*.gen.c $*.c build/%.cpp.json: %.cpp build/analysis -mkdir -p $(dir $@) ./build/analysis $< build/%.c.json: %.c build/analysis -mkdir -p $(dir $@) ./build/analysis $< build/analysis.o: $(ANALYSER_SRC) Makefile -mkdir -p build $(CXX) $(CXXFLAGS) -c $< -o $@ build/analysis: build/analysis.o -mkdir -p build $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ clean: rm -rf build .PHONY: re run clean top
おわりに
libclangを用いてぱっとめんどくさい作業が自動化できました。 ほかにもいろいろできそうなので、興味持った方はぜひ使ってみてください。
インターンものこり一ヶ月半となりました! 少ない時間、楽しんでいきたいです!!
参考
Libclang tutorial — Clang 21.0.0git documentation
https://clang.llvm.org/doxygen/modules.html
*1:決して記法として悪いものではなく、現在でも"if ( auto* p = get_some_object() )"のように用いることがある
*2:ほかにもluaバインディングがあるみたい。書いてから知りました。全くメンテされてなさそうだけど
*3:https://ifritjp.github.io/documents/libclang/operator/
*4:https://libclang.readthedocs.io/en/latest/# 2021年からlatestの更新がなさそう
*5:以下のものを判定条件に入れるのはそこまで難しくないはずです。ぜひ。
*6:Cコンパイラは分割コンパイルによるファイル分割が可能なので、Indexが複数のTranslationUnitを持つという構造にしている。
*7:生成されるTranslationUnitの例は"clang -Xclang -ast-dump -fsyntax-only sample.c"などで見ることができる
*8:CursorをTokenに変換して文字列で判別する力技も可能
*9:もともと書き捨てのコード予定だった(人に見せるつもりがなかった)のとGPTが一瞬で出してくれたので、JSON出力はライブラリを使わずに手書きしています
*10:オブジェクトの破棄などは特に定義していません。OSの後片付けにすべてまかせています。ちゃんとしたい場合は、Cursorやファイル操作に関する終了操作が必要です。