【入門編】GCC/Clangの診断プラグイン活用術:Clang Static Analyzerで独自チェックルールを自作する – 実行環境・ランタイム・コンパイラ生産性向上バイブル

こんにちは!日々のC言語での開発、お疲れ様です。

大規模なプロジェクトになると、「ポインタの解放忘れ」や「独自の命名規則違反」「非スレッドセーフな関数の一時的な混入」など、コンパイラ標準の `-Wall -Wextra` だけではどうしても防ぎきれない『プロジェクト固有のバグや規約違反』に頭を悩ませた経験はありませんか?コードレビューで人間が指摘するのも、お互いにとってコストがかかりますよね。

今回は、世界最高峰のC/C++フロントエンドである Clang が持つ「Static Analyzer(静的解析エンジン)」のAPIを使い、自分たち専用のカスタムチェックルール(独自警告)を自作する方法を解説します。

「コンパイラの内部をいじるなんて難しそう…」と思うかもしれませんが、安心してください。この記事を読み終える頃には、あなたのプロジェクト専用の「AIコードレビュアー」を自分の手で生み出すことができるようになっています。これをマスターすれば、毎日のコーディングやプルリクエストのチェックが劇的に楽になりますよ。

—

なぜ標準の警告では不十分なのか?

GCCやClangの標準警告は非常に優秀ですが、あくまで「一般的なバグの予兆」を検知するためのものです。例えば、次のようなチーム固有のルールは検出できません。

  • 「我が社(プロジェクト)の独自メモリ管理関数 `app_alloc()` で確保したメモリは、必ず `app_free()` で解放しなければならない(`free()` を直接使ってはいけない)」
  • 「特定のレガシーな関数を呼び出す際、直前に専用のロック取得関数を挟んでいるか確認したい」

こうした「ビジネスロジックやアーキテクチャ上の制約」を機械的に強制するためには、コンパイラの抽象構文木(AST)と実行パスを解析する「静的解析の拡張」が必要になります。それが、Clang Static Analyzerです。

—

Clang Static Analyzerの仕組みとカスタムチェッカーの正体

Clang Static Analyzerは、ソースコードをただの文字の羅列ではなく、「制御フローグラフ(CFG: Control Flow Graph)」という数学的なモデルに変換して解析します。

私たちが作るカスタムチェッカー(プラグイン)は、Clangがコード片(関数や文)をたどる途中で特定のパターン(例:関数呼び出しのノード)に遭遇したときに割り込み(コールバック)を受け取り、「これは規約違反だ!」と警告を発するプログラムです。

—

開発環境の準備(セットアップ)

カスタムチェッカーを開発するには、Clangのソースコードツリー、またはLLVM/Clangの開発用ライブラリが必要です。今回は、最も手軽にプラグインをロードできる「LLVM/Clangのロード可能モジュール(Dynamic Library)」としてのビルド環境を整えます。

1. 必要なツールのインストール(Ubuntu / Debianの例)

LLVMとClangのライブラリ、およびビルドツール(CMake, Ninja)をインストールします。

システムのパッケージリストを更新し、コンパイラおよびLLVM開発パッケージを導入する
sudo apt-get update
sudo apt-get install -y cmake ninja-build clang libclang-dev llvm-dev llvm-15-dev libclang-15-dev

—

最小限のカスタムチェッカーを実装する(Hello Worldチェッカー)

それでは、実際に「ソースコード内で `malloc` という文字列を見つけたら、プロジェクト規約違反として警告を出す」という、極めて実用的なカスタムチェッカーのソースコードを作成します。

プロジェクトディレクトリを作り、`MyCustomChecker.cpp` というファイルを作成してください。

`MyCustomChecker.cpp`

include “clang/StaticAnalyzer/Core/BugReporter/BugType.h”
include “clang/StaticAnalyzer/Core/Checker.h”
include “clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h”
include “clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h”
include “clang/StaticAnalyzer/Frontend/CheckerRegistry.h”

using namespace clang;
using namespace ento;

namespace {

// チェッカーのクラス定義:CallExpr(関数呼び出し表現)をフックする
class MyMallocChecker : public Checker {
public:
void checkASTCodeBody(const Decl D, AnalysisManager &Mgr, BugReporter &BR) const {
// AST(抽象構文木)全体を走査するためのビジターを定義
struct ASTVisitor : public RecursiveASTVisitor {
BugReporter &Reporter;
const Decl EnclosingDecl;

ASTVisitor(BugReporter &BR, const Decl D) : Reporter(BR), EnclosingDecl(D) {}

// 関数呼び出し(CallExpr)のノードに到達するたびに呼ばれる
bool VisitCallExpr(CallExpr CE) {
if (const FunctionDecl FD = CE->getDirectCallee()) {
StringRef FuncName = FD->getName();

// もし呼び出されている関数名が “malloc” だったら警告を出す
if (FuncName == “malloc”) {
PathDiagnosticLocation Loc =
PathDiagnosticLocation::createBegin(CE, Reporter.getSourceManager(), EnclosingDecl);

// 警告のメッセージを構築
Reporter.EmitBasicReport(
EnclosingDecl,
this,
“Forbidden Malloc Check”, // チェッカー名
“Local Malloc Rule”, // カテゴリ
“プロジェクト規約違反: 生の malloc() の直接呼び出しは禁止されています。app_alloc() を使用してください。”,
Loc,
CE->getSourceRange()
);
}
}
return true;
}
};

// 解析対象の関数ボディに対してビジターを実行
ASTVisitor Visitor(BR, D);
Visitor.TraverseDecl(const_cast(D));
}
};

} // namespace

// Clang Static Analyzerにこのチェッカーを登録するエントリーポイント
extern “C” const char ClangCheckerAnalyzerPluginInterfaceVersionString[] = LLVM_VERSION_STRING;

void clang_register_checkers(CheckerRegistry &registry) {
// チェッカーの名前空間と説明を登録
registry.addChecker(
“example.MyMallocChecker”,
“カスタム規約: mallocの使用を禁止し、専用アロケータを強制する”,
“”);
}

—

チェッカーのビルド

このC++ファイルを共有ライブラリ(`.so` または `.dylib`)としてコンパイルします。LLVMのビルドオプションやパスに合わせた `CMakeLists.txt` を用意します。

`CMakeLists.txt`

cmake_minimum_required(VERSION 3.13.4)
project(MyClangChecker)

LLVMのCMakeパッケージを読み込む
find_package(LLVM REQUIRED CONFIG)
include_directories(${LLVM_INCLUDE_DIRS})
add_definitions(${LLVM_DEFINITIONS})

プラグインとしての共有ライブラリ(モジュール)ターゲットを作成
add_library(MyClangChecker MODULE
MyCustomChecker.cpp
)

Clangのライブラリとリンク設定
set(CMAKE_CXX_STANDARD 17)
set_target_properties(MyClangChecker PROPERTIES
PREFIX “”
SUFFIX “.so”
)

以下のコマンドでビルドを実行します。

ビルドディレクトリを作成して移動
mkdir build && cd build
cmake -G Ninja -DCMAKE_PREFIX_PATH=/usr/lib/llvm-15 ..
ninja

ビルド成功後、build/MyClangChecker.so が生成されていることを確認
ls -l MyClangChecker.so

—

動作確認(自作チェッカーでコードを解析する)

それでは、作成したプラグインを使って、実際にC言語のソースコードを解析してみましょう。

解析対象のテストコード `test.c` を作成します。

`test.c`

include

void valid_function(void) {
// これは許容される処理のつもり(例)
int p = (int)100;
}

void invalid_function(void) {
// 規約違反:生のmallocを使っている
int p = (int)malloc(sizeof(int) 10);
free(p);
}

解析コマンドの実行

Clangの `analyze` コマンド(または `clang –analyze`)に対し、作成したプラグインとカスタムチェッカーをロードするフラグを渡します。

clang -Xclang -load -Xclang ./build/MyClangChecker.so \
-Xclang -analyzer-checker=example.MyMallocChecker \
–analyze test.c

実行結果(出力ログ)

コンソールに、私たちがプログラム内で定義したカスタム警告がズバッと出力されます。

test.c:10:19: warning: プロジェクト規約違反: 生の malloc() の直接呼び出しは禁止されています。app_alloc() を使用してください。 [-Wanalyzer-custom-error]
int p = (int)malloc(sizeof(int) 10);
^~~~~~
1 warning generated.

おめでとうございます!これで、あなた専用の静的解析ルールが動き始めました。

—

現場で役立つ実践的アドバイスとアーキテクトからのエール

今回紹介した手法は、単なる「文字列チェック」にとどまりません。ClangのASTを深く解析すれば、

  • 「特定の構造体のメンバに直接アクセスしていないか」
  • 「関数名のプレフィックスが規約に従っているか」
  • 「エラーハンドリングの戻り値チェックが漏れていないか」

といった、「そのチーム特有のレガシーな負債やアンチパターン」をビルドの段階で100%機械的に弾き返す強固な防壁を作ることができます。

これをCI/CDパイプライン(GitHub ActionsやGitLab CIなど)のビルドステップに組み込んでおけば、レビューの負担が激減し、チーム全体のコード品質が飛躍的に向上します。

「これをマスターすれば、毎日のコーディングが劇的に楽になりますよ」。ぜひ、あなたのプロジェクトの独自のルールに合わせたチェッカーの改造に挑戦してみてください。あなたの開発ライフがより洗練されたものになることを、心から応援しています!

タイトルとURLをコピーしました