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()で解放してあげる必要があると思う。