SATOXのシテオク日記

~ふもっふ、ふもふも~

C#(libclang)でCソース解析

人類は誰しも2つの種類に分類される。

C言語ソースファイルを解析したいか解析したくないかである。

自分は前者。

……というわけで、C#C言語のソースを解析しようと、数ヶ月じゃ済まないくらい試行錯誤した末、自力ではかなり難しいという結論に達しまして、素直にlibclangを利用することにしました。 正直、C言語解析なんて余裕だぜと舐めていたのですが、一朝一夕でできるもんじゃないんですよね。libclang.dllのバイナリサイズ144MBが物語っています。

というお話。

ごちゃごちゃ説明はせず、シンプルに情報だけまとめておきます。

nuget Package

ClangSharpというlibclangラッパーモジュールを使用しました。

  • ClangSharp Ver.16.0.0
  • libClangSharp.runtime.win-x64 Ver.16.0.6

ClangSharpはマネージドラッピングされているわけではなく、少しユーティリティがあるくらいの薄いモジュールなのでunsafe属性が必要です。

サンプルコード

以下、解析関数のサンプル。 いろいろな解析方法があると思いますが、VisitChildren関数を使用しています。

using System.Diagnostics;
using ClangSharp.Interop;

public unsafe void AnalyzeCSourceFile(String filePath)
{
    var index = CXIndex.Create();
    var errCode = CXTranslationUnit.TryParse(
        index, 
        filePath,
        new String[0],
        Array.Empty<CXUnsavedFile>(),
        CXTranslationUnit_Flags.CXTranslationUnit_None,
        out var translationUnit);
    if (errCode != CXErrorCode.CXError_Success) {
        throw new Exception($"CXTranslationUnit.TryParse failed, errCode = {errCode.ToString()}");
    }

    // Specify a callback function to analyse
    translationUnit.Cursor.VisitChildren(this.VisitChildrenCb, new CXClientData());
    
    clang.disposeTranslationUnit(translationUnit);
    clang.disposeIndex(index);
}

private unsafe CXChildVisitResult VisitChildrenCb(CXCursor cursor, CXCursor parent, void* client_data)
{
    cursor.Location.GetFileLocation(out var cxVisitFilePath, out var line, out var column, out var offset);

    Debug.WriteLine($"{cxVisitFilePath}({line},{column}): {cursor.KindSpelling}, {cursor.DisplayName}");

    return CXChildVisitResult.CXChildVisit_Continue;
}

VisitChildren関数に渡されたVisitChildrenCbコールバック関数に、ASTと呼ばれる解析要素単位で解析結果がコールされます。

プリプロセス前の情報が必要な場合は、下記フラグに変更すると、より詳しい情報が取得できます。

  • CXTranslationUnit_Flags.CXTranslationUnit_DetailedPreprocessingRecord

なにしろいろいろな解析フラグがあって難しい。

clang: Translation unit manipulation

出力例

test.h(5,16): StructDecl, HeaderStructTag
test.h(8,3): TypedefDecl, HeaderStruct
test.h(10,9): TypedefDecl, bool
test.h(12,14): EnumDecl, SceneStateTag
test.h(19,3): TypedefDecl, SceneState
test.c(8,16): StructDecl, StructTestTag
test.c(15,3): TypedefDecl, StructTest
test.c(17,24): TypedefDecl, StructTestHn
test.c(19,5): VarDecl, g_global
test.c(21,12): VarDecl, s_static
test.c(25,5): FunctionDecl, GlobalFunc(int, long)
test.c(35,12): FunctionDecl, staticFunc()

Cソースからヘッダがインクルードされている場合、ヘッダ側の定義も列挙される。(この例ではtest.cからtest.hがインクルードされてる)

VisualStudioのデバッグ出力にこのようなファイル名形式で出力すると、ダブルクリックでソースの該当箇所にジャンプして表示できるので便利。

Cソースの文字コードシフトJISの場合、文字化けする

CソースがシフトJISの場合、取得した文字が化けます。

その場合は、文字列をバイト配列で取得し任意の文字テキストエンコーディング変換してあげる。

private unsafe static String SJISCXStringToString(CXString csString)
{
    var pCString = clang.getCString(csString);

    if (pCString is null) {
        return String.Empty;
    }
    var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte *)pCString);
    var ret = Encoding.GetEncoding("Shift-JIS").GetString(span.ToArray());

    return ret;
}

どうもCXCursorなどから取得するCXString型は解放されないようなので、変換後にはclang.disposeString()で解放してあげる必要があると思う。