【入門編】ClangのプラグインAPIで自分だけの構文チェッカーを作る:特定のコーディング規約を強制する – 実行環境・ランタイム・コンパイラ生産性向上バイブル

こんにちは!日々のコードレビューで「またこのレガシーな関数が使われている……」「うちのプロジェクト固有の禁止ルール、何度言っても直らないな……」と頭を抱えた経験はありませんか?

世の中には素晴らしい静的解析ツール(Clang-Tidyなど)がたくさんありますが、「自社や自分のチーム特有の、ドマイナーで特殊なコーディング規約」まではカバーしてくれません。

今回は、世界中のC/C++コンパイラの裏側を支える「Clang」のプラグインAPIを使って、あなた専用のオリジナル構文チェッカーを作る方法を解説します。

これをマスターすれば、コンパイルするだけでプロジェクトのローカルルール違反を自動検知し、容赦なくビルドを落とす「最強の門番」を手に入れることができます。毎日のコードレビューの負担が劇的に減りますよ。一緒に一歩ずつ、その深淵を覗いていきましょう!

—

1. なぜClangプラグインなのか?(アーキテクトの視点)

多くの開発者は、コーディング規約のチェックを正規表現ベースのスクリプト(Linter)や、既存のClang-Tidyのカスタムチェックで何とかしようとします。しかし、それらには限界があります。

正規表現では、コメントアウトされたコードや、マクロ展開された複雑な構文を正確に判別できません。一方、Clangはソースコードを単なる文字列ではなく、「AST(抽象構文木:Abstract Syntax Tree)」という完璧なツリー構造に変換して理解しています。

ClangのプラグインAPIを直接叩くということは、コンパイラの脳みそ(フロントエンド)に直接フックをかけ、ASTを直接舐め回して意図しないパターンを検知するということです。これ以上に確実で、誤検知のない静的解析手法はありません。

—

2. 開発環境のセットアップ

Clangのプラグインを開発するには、ClangのライブラリとLLVMのビルド環境が必要です。今回は、多くのモダンな開発環境で手に入りやすいLLVM/Clangを使用します。

必要なパッケージのインストール(Ubuntuの例)

まずは、プラグイン開発に必要なヘッダファイルとコンパイラをインストールします。

LLVM、Clang、およびプラグイン開発に必要な開発用パッケージを一括インストール
sudo apt-get update
sudo apt-get install -y llvm-dev libclang-dev clang cmake build-essential

> 先輩からのアドバイス:
> Clangのバージョン(例: 14や15など)は、開発するホストの環境とプラグインを読み込ませるコンパイラのバージョンで完全に一致させる必要があります。バージョンがズレると、ABI(Application Binary Interface)の不一致でセグメンテーション違反(Segmentation Fault)が起きるので注意してください。

—

3. 実践!「危険な関数」を排除するカスタムチェッカーを作る

今回は、プロジェクト内で使用を全面禁止したい「レガシーで危険な関数(例: `strcpy`)」がコード内に現れたら、コンパイルエラーを発生させるプラグインを作ります。

プラグインの全体像としては、Clangの `ASTConsumer` と `RecursiveASTVisitor` という2つの強力な仕組みを組み合わせて実現します。

ステップ1: プラグインのソースコードを書く

以下のコードを `NoStrcpyPlugin.cpp` という名前で保存してください。

include “clang/AST/AST.h”
include “clang/AST/ASTConsumer.h”
include “clang/AST/RecursiveASTVisitor.h”
include “clang/Frontend/ASTConsumers.h”
include “clang/Frontend/CompilerInstance.h”
include “clang/Frontend/FrontendPluginRegistry.h”
include “llvm/Support/raw_ostream.h”

using namespace clang;

namespace {

// 1. AST(抽象構文木)を再帰的に訪問し、特定のノードを探すビジタークラス
class StrcpyVisitor : public RecursiveASTVisitor {
private:
ASTContext Context;

public:
explicit StrcpyVisitor(ASTContext Context) : Context(Context) {}

// 関数呼び出し(CallExpr)のノードを発見するたびに自動で呼ばれるコールバック
bool VisitCallExpr(CallExpr Call) {
// 呼び出されている関数名を取得する
if (FunctionDecl Func = Call->getDirectCallee()) {
std::string FuncName = Func->getNameInfo().getAsString();

// もし関数名が “strcpy” だったら検知する
if (FuncName == “strcpy”) {
// ソースコード上の位置情報を取得
SourceLocatiom Loc = Call->getExprLoc();

// コンパイルエラー(Diagnostic)を発生させる
DiagnosticsEngine &Diags = Context->getDiagnostics();
unsigned DiagID = Diags.getCustomDiagID(
DiagnosticsEngine::Error,
“【コーディング規約違反】セキュリティ上危険な ‘strcpy’ の使用は禁止されています。代わりに ‘strncpy’ または ‘snprintf’ を使ってください。”);

Diags.Report(Loc, DiagID);
}
}
return true;
}
};

// 2. ASTConsumer: コンパイラにASTの解析を指示するクラス
class NoStrcpyASTConsumer : public ASTConsumer {
private:
StrcpyVisitor Visitor;

public:
explicit NoStrcpyASTConsumer(ASTContext Context) : Visitor(Context) {}

// 翻訳単位(TranslationUnit)全体のAST構築が完了したときに実行される
void HandleTranslationUnit(ASTContext &Context) override {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}
};

// 3. FrontendAction: プラグインののエントリーポイントとなるクラス
class NoStrcpyAction : public PluginASTAction {
protected:
std::unique_ptr CreateASTConsumer(CompilerInstance &CI,
llvm::StringRef) override {
return std::make_unique(&CI.getASTContext());
}

bool ParseArgs(const CompilerInstance &CI,
const std::vector &args) override {
// 必要に応じてコマンドライン引数をパースできます(今回は未使用)
return true;
}
};

} // namespace

// 4. Clangのプラグインレジストリに自作プラグインを登録するマクロ
static FrontendPluginRegistry::Add
X(“no-strcpy”, “Forbid the use of strcpy function”);

—

ステップ2: プラグインをビルドする(CMakeの設定)

このプラグインをコンパイルして、共有ライブラリ(`.so` または `.dylib`)を作成します。
同じディレクトリに `CMakeLists.txt` を作成してください。

cmake_minimum_required(VERSION 3.13)
project(NoStrcpyPlugin)

LLVMとClangのCMakeパッケージを読み込む
find_package(LLVM REQUIRED CONFIG)
find_package(Clang REQUIRED CONFIG)

LLVMの定義やフラグを取り込む
include_directories(${LLVM_INCLUDE_DIRS} ${CLANG_INCLUDE_DIRS})
add_definitions(${LLVM_DEFINITIONS})

共有ライブラリとしてビルド設定
add_library(NoStrcpyPlugin MODULE
NoStrcpyPlugin.cpp
)

Clangのライブラリとリンクする
target_link_libraries(NoStrcpyPlugin PRIVATE
clangBasic
clangAST
clangFrontend
)

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

ビルド用ディレクトリを作成して移動
mkdir build && cd build

CMakeの構成(LLVMのパスを適切に指すようにします)
cmake -DCMAKE_BUILD_TYPE=Release ..

コンパイル実行
make

ビルドが成功すると、`build` ディレクトリ内に `libNoStrcpyPlugin.so`(macOSの場合は `.dylib`)というプラグイン実体が生成されます。

—

4. 動作確認:実際にルールを強制してみる

それでは、作成した自作プラグインが意図通りに動くかテストしてみましょう。
次のようなテスト用のC言語コードを用意します (`test.c`)。

include

void sample_function() {
char dest[20];
char src = “Hello, Clang Plugin!”;

// わざと禁止されている strcpy を使う
strcpy(dest, src);
}

このファイルを、先ほどビルドしたClangプラグインを読み込ませてコンパイルします。

-Xclang と -load を使ってプラグインをClangにロードさせる
-plugin でプラグインの登録名(”no-strcpy”)を指定する
clang -fsyntax-only -Xclang -load -Xclang ./build/libNoStrcpyPlugin.so -Xclang -plugin -Xclang no-strcpy test.c

実行結果(コンソール出力)

コマンドを実行すると、コンパイラが次のように華麗にエラーを吐いてビルドを止めてくれます!

test.c:8:5: error: 【コーディング規約違反】セキュリティ上危険な ‘strcpy’ の使用は禁止されています。代わりに ‘strncpy’ または ‘snprintf’ を使ってください。
strcpy(dest, src);
^
1 error generated.

大成功です!おめでとうございます。これで、あなたのプロジェクト固有の「絶対に許したくない構文」を自動で検知し、開発者の手を煩わせることなくビルドパイプラインで弾き返す仕組みが完成しました。

—

5. 現場でさらに役立たせるためのヒント

このClangプラグインの仕組みを理解すれば、応用は無限大です。

1. 社内標準ロガー以外の使用禁止: `printf` や `puts` を検知して、自社の共通ロガー関数(`AppLog_Write` など)の強制。
2. 特定の危険なマクロの排除: バグの温床になりやすい特定の独自マクロの検知。
3. コーディング標準の強制: 組織特有の命名規則や、特殊な型安全関数の強制ラップ。

既存のツールで「かゆいところに手が届かない」と感じたら、ぜひこのClangプラグインAPIを思い出してください。コンパイラの深層をハックして、最高の開発環境を自分たちの手で作り上げていきましょう!

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