構(gòu)體解析器(2))
#include cstdint #pragma pack(push, 1) struct MsgHeader { uint16_t msgId; uint8_t msgType; uint32_t dataLength; }; #pragma pack(pop)#include QCoreApplication #include QDebug #include QFile #include clang-c/Index.h // 解析流程創(chuàng)建 CXIndex → 解析得到 CXTranslationUnit → 遍歷 AST → 查詢布局。 // CXCursor 代表 AST 節(jié)點(diǎn)如結(jié)構(gòu)體、字段CXType 代表節(jié)點(diǎn)對(duì)應(yīng)的類型。 // Cursor 和 Type 都依賴所屬的 TranslationUnit必須在 unit 釋放前使用。 // CXString 由 libclang 管理需要調(diào)用者顯式釋放。 static QString toQString(CXString value) { // getCString() 只借用指針先復(fù)制到 QString再 disposeString()避免懸空指針。 const QString result QString::fromUtf8(clang_getCString(value)); clang_disposeString(value); return result; } // 通過回調(diào)的 CXClientDatavoid *傳遞結(jié)果避免使用全局變量。 struct LayoutResult { bool found false; bool valid true; }; // 回調(diào)參數(shù)依次為當(dāng)前節(jié)點(diǎn)、父節(jié)點(diǎn)、用戶數(shù)據(jù)此處不使用父節(jié)點(diǎn)。 // Continue下一個(gè)同級(jí)節(jié)點(diǎn)Recurse先進(jìn)入子節(jié)點(diǎn)Break結(jié)束本次遍歷。 static CXChildVisitResult visitField(CXCursor cursor, CXCursor, CXClientData data) { // 結(jié)構(gòu)體的子節(jié)點(diǎn)可能包含方法等聲明FieldDecl 才是非靜態(tài)成員字段。 if (clang_getCursorKind(cursor) ! CXCursor_FieldDecl) return CXChildVisit_Continue; LayoutResult result *static_castLayoutResult *(data); // 從字段節(jié)點(diǎn)取得類型大小和類型的自然對(duì)齊值均以字節(jié)為單位。 const CXType type clang_getCursorType(cursor); const long long size clang_Type_getSizeOf(type); const long long alignment clang_Type_getAlignOf(type); // 字段偏移以 bit 為單位相對(duì)于結(jié)構(gòu)體起點(diǎn)本例除以 8 得到字節(jié)偏移。 // 如果以后解析位域要保留 bit 偏移不能只看整數(shù)除法后的字節(jié)值。 const long long offsetBits clang_Cursor_getOffsetOfField(cursor); // 布局查詢返回負(fù)數(shù)表示 CXTypeLayoutError例如類型不完整不能當(dāng)作有效布局。 if (size 0 || alignment 0 || offsetBits 0) { qCritical() Cannot read layout for toQString(clang_getCursorSpelling(cursor)); result.valid false; return CXChildVisit_Continue; } // getCursorSpelling() 取得字段名getTypeSpelling() 取得類型的文本表示。 // getCanonicalType() 展開 typedef 等別名例如 uint16_t 對(duì)應(yīng) unsigned short。 // pack(1) 影響字段位置但 uint32_t 類型本身的自然對(duì)齊值仍然為 4。 qInfo().noquote() QString( %1: %2 (%3), size%4 bytes, type alignment%5 bytes, offset%6 bits (%7 bytes)) .arg(toQString(clang_getCursorSpelling(cursor))) .arg(toQString(clang_getTypeSpelling(type))) .arg(toQString(clang_getTypeSpelling(clang_getCanonicalType(type)))) .arg(size).arg(alignment).arg(offsetBits).arg(offsetBits / 8); return CXChildVisit_Continue; } // 第一層遍歷在主文件的 AST 中查找 MsgHeader 的完整定義。 static CXChildVisitResult visitRecord(CXCursor cursor, CXCursor, CXClientData data) { // AST 也包含 #include 引入的聲明這里跳過 cstdint 等包含文件中的節(jié)點(diǎn)。 if (!clang_Location_isFromMainFile(clang_getCursorLocation(cursor))) return CXChildVisit_Continue; // StructDecl 表示 struct 聲明isCursorDefinition 排除 struct MsgHeader; 這樣的前置聲明。 // 其他主文件節(jié)點(diǎn)返回 Recurse以便繼續(xù)查找命名空間等節(jié)點(diǎn)內(nèi)部的結(jié)構(gòu)體。 if (clang_getCursorKind(cursor) ! CXCursor_StructDecl || !clang_isCursorDefinition(cursor) || toQString(clang_getCursorSpelling(cursor)) ! QStringLiteral(MsgHeader)) return CXChildVisit_Recurse; LayoutResult result *static_castLayoutResult *(data); result.found true; // 對(duì)結(jié)構(gòu)體類型查詢布局libclang 會(huì)按目標(biāo)平臺(tái)和 #pragma pack 計(jì)算對(duì)齊與填充。 // 當(dāng)前大小為 7、對(duì)齊為 1這里沒有手動(dòng)累加字段大小。 const CXType type clang_getCursorType(cursor); const long long size clang_Type_getSizeOf(type); const long long alignment clang_Type_getAlignOf(type); if (size 0 || alignment 0) { qCritical() Cannot read MsgHeader layout: size alignment; result.valid false; return CXChildVisit_Break; } qInfo().noquote() QString(MsgHeader: size%1 bytes, alignment%2 bytes) .arg(size).arg(alignment); // 第二層遍歷以結(jié)構(gòu)體為父節(jié)點(diǎn)查詢其各個(gè)字段data 繼續(xù)傳遞同一個(gè)結(jié)果對(duì)象。 clang_visitChildren(cursor, visitField, data); // 已處理目標(biāo)結(jié)構(gòu)體結(jié)束外層遍歷。 return CXChildVisit_Break; } int main(int argc, char *argv[]) { // 強(qiáng)制 Qt 日志輸出到標(biāo)準(zhǔn)錯(cuò)誤避免 Windows 下只發(fā)送給調(diào)試器。 qputenv(QT_FORCE_STDERR_LOGGING, 1); QCoreApplication app(argc, argv); // 命令行可指定待解析文件默認(rèn)路徑 MSG_HEADER_PATH 由 .pro 定義。 const QByteArray sourceFile QFile::encodeName( app.arguments().size() 1 ? app.arguments().at(1) : QStringLiteral(MSG_HEADER_PATH)); // 這些是解析頭文件的參數(shù)與編譯當(dāng)前 Qt 程序的選項(xiàng)是兩回事。 // FullArgv 要求首項(xiàng)為編譯器路徑以幫助定位 MSYS2 的標(biāo)準(zhǔn)庫(kù)頭文件。 // libclang 在進(jìn)程內(nèi)解析不會(huì)啟動(dòng) clang.exe 子進(jìn)程。 // -x c按 C 解析-stdc11語言標(biāo)準(zhǔn)--target決定 ABI 和布局的目標(biāo)平臺(tái)。 const char *arguments[] { LIBCLANG_ROOT /bin/clang.exe, -x, c, -stdc11, --targetx86_64-w64-windows-gnu }; // CXIndex 是創(chuàng)建翻譯單元的上下文可用于解析多個(gè)文件。 // 第一個(gè) 0不排除預(yù)編譯頭聲明第二個(gè) 0不自動(dòng)打印診斷稍后自行處理。 CXIndex index clang_createIndex(0, 0); if (!index) { qCritical() Cannot create libclang index; return 1; } CXTranslationUnit unit nullptr; // 翻譯單元包含主文件及包含文件的解析結(jié)果和 AST。 // 參數(shù)順序索引、文件路徑、編譯參數(shù)數(shù)組、參數(shù)數(shù)量、內(nèi)存文件數(shù)組、內(nèi)存文件數(shù)量、選項(xiàng)、輸出指針。 // nullptr, 0 表示沒有 CXUnsavedFile內(nèi)存中的未保存文件直接讀取磁盤文件。 // CXTranslationUnit_None 表示不啟用額外選項(xiàng)unit 接收創(chuàng)建出來的翻譯單元。 const CXErrorCode error clang_parseTranslationUnit2FullArgv( index, sourceFile.constData(), arguments, static_castint(sizeof(arguments) / sizeof(arguments[0])), nullptr, 0, CXTranslationUnit_None, unit); if (error ! CXError_Success || !unit) { qCritical() libclang parse failed: error sourceFile; if (unit) clang_disposeTranslationUnit(unit); clang_disposeIndex(index); return 1; } // 接口返回 CXError_Success 仍可能有語法錯(cuò)誤或缺失頭文件必須繼續(xù)檢查診斷。 // Diagnostic 包含警告、錯(cuò)誤及源代碼位置出現(xiàn) Error/Fatal 就停止后續(xù)布局查詢。 bool hasErrors false; const unsigned diagnosticCount clang_getNumDiagnostics(unit); for (unsigned i 0; i diagnosticCount; i) { // 每次取得的 Diagnostic 需 dispose格式化診斷得到的 CXString 由 toQString() 釋放。 CXDiagnostic diagnostic clang_getDiagnostic(unit, i); hasErrors | clang_getDiagnosticSeverity(diagnostic) CXDiagnostic_Error; qWarning().noquote() toQString(clang_formatDiagnostic( diagnostic, clang_defaultDiagnosticDisplayOptions())); clang_disposeDiagnostic(diagnostic); } LayoutResult result; if (!hasErrors) { qInfo().noquote() libclang: toQString(clang_getClangVersion()); qInfo().noquote() Source: sourceFile; // 從 AST 根節(jié)點(diǎn)的直接子節(jié)點(diǎn)開始回調(diào)是否遞歸取決于 visitRecord 的返回值。 // result 會(huì)原樣傳入回調(diào)的 data 參數(shù)在回調(diào)中轉(zhuǎn)回 LayoutResult * 即可使用。 clang_visitChildren(clang_getTranslationUnitCursor(unit), visitRecord, result); if (!result.found) qCritical() MsgHeader definition not found; } // 先釋放翻譯單元再釋放索引。Cursor/Type 無需單獨(dú)釋放但 unit 釋放后不能再用。 clang_disposeTranslationUnit(unit); clang_disposeIndex(index); return !hasErrors result.found result.valid ? 0 : 1; }libclang: clang version 23.1.4 (https://github.com/msys2/MINGW-packages 39c2da431174df09576b662535bdaa688a9bf8e0) Source: C:/Users/Administrator/Documents/ChatGPT/clangTest/MsgHeader.h MsgHeader: size7 bytes, alignment1 bytes msgId: uint16_t (unsigned short), size2 bytes, type alignment2 bytes, offset0 bits (0 bytes) msgType: uint8_t (unsigned char), size1 bytes, type alignment1 bytes, offset16 bits (2 bytes) dataLength: uint32_t (unsigned int), size4 bytes, type alignment4 bytes, offset24 bits (3 bytes)Release v1.0.2(basic) · lianpeiliang/clangTest進(jìn)階把這個(gè)結(jié)構(gòu)體的內(nèi)容擴(kuò)展一下吧再?gòu)?fù)雜一點(diǎn)可以嗎先做一版short ,unsigned short ,unsigned char就是各種基礎(chǔ)數(shù)據(jù)類型都有的結(jié)構(gòu)體[MsgHeader.h]現(xiàn)在涵蓋char、signed char、unsigned char、boolshort、int、long、long long及對(duì)應(yīng)無符號(hào)類型float、double、long doublewchar_t、char16_t、char32_t保留了原有字段、pack(1)和中文注釋解析流程保持原樣。#include cstdint // 保存原有打包設(shè)置將成員對(duì)齊上限設(shè)為 1 字節(jié)避免成員之間的填充。 #pragma pack(push, 1) struct MsgHeader { // 原有的固定寬度整數(shù)用來和下面的基礎(chǔ)類型對(duì)比。 uint16_t msgId; uint8_t msgType; uint32_t dataLength; // 字符與布爾類型。char 的有無符號(hào)由目標(biāo)平臺(tái)決定因此單獨(dú)列出 signed/unsigned char。 char charValue; signed char signedCharValue; unsigned char unsignedCharValue; bool boolValue; // 各種有符號(hào)和無符號(hào)整數(shù)short、int、long 默認(rèn)都是有符號(hào)的。 short shortValue; unsigned short unsignedShortValue; int intValue; unsigned int unsignedIntValue; long longValue; unsigned long unsignedLongValue; long long longLongValue; unsigned long long unsignedLongLongValue; // 浮點(diǎn)類型long double 的大小可能隨目標(biāo)平臺(tái)和工具鏈變化。 float floatValue; double doubleValue; long double longDoubleValue; // 寬字符與 C11 字符類型這里只存放單個(gè)字符不是字符串。 wchar_t wcharValue; char16_t char16Value; char32_t char32Value; }; // 恢復(fù)之前的打包設(shè)置避免影響后續(xù)聲明的結(jié)構(gòu)體。 #pragma pack(pop)libclang: clang version 23.1.4 (https://github.com/msys2/MINGW-packages 39c2da431174df09576b662535bdaa688a9bf8e0) Source: C:/Users/Administrator/Documents/ChatGPT/clangTest/MsgHeader.h MsgHeader: size83 bytes, alignment1 bytes msgId: uint16_t (unsigned short), size2 bytes, type alignment2 bytes, offset0 bits (0 bytes) msgType: uint8_t (unsigned char), size1 bytes, type alignment1 bytes, offset16 bits (2 bytes) dataLength: uint32_t (unsigned int), size4 bytes, type alignment4 bytes, offset24 bits (3 bytes) charValue: char (char), size1 bytes, type alignment1 bytes, offset56 bits (7 bytes) signedCharValue: signed char (signed char), size1 bytes, type alignment1 bytes, offset64 bits (8 bytes) unsignedCharValue: unsigned char (unsigned char), size1 bytes, type alignment1 bytes, offset72 bits (9 bytes) boolValue: bool (bool), size1 bytes, type alignment1 bytes, offset80 bits (10 bytes) shortValue: short (short), size2 bytes, type alignment2 bytes, offset88 bits (11 bytes) unsignedShortValue: unsigned short (unsigned short), size2 bytes, type alignment2 bytes, offset104 bits (13 bytes) intValue: int (int), size4 bytes, type alignment4 bytes, offset120 bits (15 bytes) unsignedIntValue: unsigned int (unsigned int), size4 bytes, type alignment4 bytes, offset152 bits (19 bytes) longValue: long (long), size4 bytes, type alignment4 bytes, offset184 bits (23 bytes) unsignedLongValue: unsigned long (unsigned long), size4 bytes, type alignment4 bytes, offset216 bits (27 bytes) longLongValue: long long (long long), size8 bytes, type alignment8 bytes, offset248 bits (31 bytes) unsignedLongLongValue: unsigned long long (unsigned long long), size8 bytes, type alignment8 bytes, offset312 bits (39 bytes) floatValue: float (float), size4 bytes, type alignment4 bytes, offset376 bits (47 bytes) doubleValue: double (double), size8 bytes, type alignment8 bytes, offset408 bits (51 bytes) longDoubleValue: long double (long double), size16 bytes, type alignment16 bytes, offset472 bits (59 bytes) wcharValue: wchar_t (wchar_t), size2 bytes, type alignment2 bytes, offset600 bits (75 bytes) char16Value: char16_t (char16_t), size2 bytes, type alignment2 bytes, offset616 bits (77 bytes) char32Value: char32_t (char32_t), size4 bytes, type alignment4 bytes, offset632 bits (79 bytes)我修改了頭文件相關(guān)的解析代碼不需要改變嗎是通用的嗎你目前這種修改不需要改解析代碼。增刪字段、調(diào)整順序、修改基礎(chǔ)類型或pack設(shè)置都會(huì)在下次運(yùn)行時(shí)自動(dòng)解析。因?yàn)榇a沒有逐個(gè)寫死字段名而是讓libclang 遍歷所有成員對(duì)每個(gè)成員執(zhí)行相同的查詢。clang_getCursorType(cursor); // 這個(gè)字段是什么類型 clang_Type_getSizeOf(type); // 類型占多少字節(jié) clang_Type_getAlignOf(type); // 類型的自然對(duì)齊是多少 clang_Cursor_getOffsetOfField(cursor);// 字段從結(jié)構(gòu)體的第幾個(gè) bit 開始所以short、unsigned char、double等都能走同一套流程。目前有幾個(gè)范圍限制修改當(dāng)前代碼能否直接處理增刪基礎(chǔ)類型字段、修改名稱和順序可以修改#pragma pack可以自動(dòng)重新計(jì)算布局把結(jié)構(gòu)體改名需要修改代碼中寫死的MsgHeader添加嵌套結(jié)構(gòu)體能讀取整個(gè)成員的大小和偏移展開內(nèi)部字段需要補(bǔ)充遍歷添加位域能讀取 bit 偏移顯示位寬需要補(bǔ)充查詢頭文件保存后直接重新運(yùn)行即可因?yàn)槌绦蚴窃谶\(yùn)行時(shí)讀取它的。什么是自然對(duì)齊自然對(duì)齊就是一個(gè)類型在默認(rèn)情況下要求從什么樣的地址開始存放。例如在你當(dāng)前的環(huán)境中類型自然對(duì)齊默認(rèn)存放要求char1 字節(jié)任意地址short2 字節(jié)地址是 2 的倍數(shù)int4 字節(jié)地址是 4 的倍數(shù)double8 字節(jié)地址是 8 的倍數(shù)Release v1.0.3(頭文件復(fù)雜版) · lianpeiliang/clangTest添加界面可以在界面中查看相關(guān)的信息。Release v1.0.4(界面解析版) · lianpeiliang/clangTest我想問下我現(xiàn)在的程序只能解析名字是MsgHeader的結(jié)構(gòu)體嗎目前通過界面使用時(shí)是的只會(huì)查找名為MsgHeader的結(jié)構(gòu)體。但底層解析函數(shù)已經(jīng)支持指定其他名稱。例如要解析MyHeader可以改成const ParseResult result parseHeader(path, QStringLiteral(MyHeader));另外頭文件名可以任意取限制的是文件內(nèi)的結(jié)構(gòu)體名稱。當(dāng)前只查找所選文件中定義的結(jié)構(gòu)體不查找它通過#include引入的結(jié)構(gòu)體。那再優(yōu)化一版吧默認(rèn)名字為MsgHeader可以修改QLineEdit可以嗎Release v1.0.5 · lianpeiliang/clangTestv1.0.5可修改結(jié)構(gòu)體名稱有可執(zhí)行程序下一步我希望可以引入一個(gè)嵌套型的結(jié)構(gòu)體你理解嗎理解就是一個(gè)結(jié)構(gòu)體的成員也是結(jié)構(gòu)體類型例如struct Address { unsigned short source; unsigned short destination; }; struct PacketHeader { unsigned int sequence; Address address; // 嵌套結(jié)構(gòu)體成員 unsigned short payloadLength; };目前程序會(huì)把a(bǔ)ddress顯示為一個(gè)字段能讀取它的類型、大小和偏移但不會(huì)展開里面的source、destination。添加一個(gè)嵌套示例輸入NestedPacketHeader查看外層結(jié)構(gòu)體包含嵌套成員address總大小10 字節(jié)。輸入PacketAddress單獨(dú)查看內(nèi)部的source、destination字段。我覺得如果我想要展示這種嵌套結(jié)構(gòu)的話使用QTreeWidget會(huì)比較好吧是不是是的QTreeWidget很適合當(dāng)前這種嵌套結(jié)構(gòu)展示可以展開、折疊成員同時(shí)保留現(xiàn)有的類型、大小、對(duì)齊和偏移等列。實(shí)現(xiàn)時(shí)需要兩部分配合解析層遞歸讀取嵌套成員讓FieldInfo保存子字段。界面層將QTableWidget換成QTreeWidget遞歸創(chuàng)建樹節(jié)點(diǎn)。偏移建議明確區(qū)分“相對(duì)所屬結(jié)構(gòu)體”和“相對(duì)最外層結(jié)構(gòu)體”。例如destination在address內(nèi)偏移為2 字節(jié)在整個(gè)NestedPacketHeader內(nèi)偏移為6 字節(jié)。Release v1.0.6 · lianpeiliang/clangTestv1.0.6支持嵌套結(jié)構(gòu)體在現(xiàn)有的邏輯下我在想可以考慮給參數(shù)賦值的事情了你覺得呢可以現(xiàn)有的樹形展示已經(jīng)適合繼續(xù)做結(jié)構(gòu)體字段賦值。建議先實(shí)現(xiàn)這一版增加“值”列允許編輯基礎(chǔ)類型字段。嵌套結(jié)構(gòu)體作為分組展開后編輯內(nèi)部字段例如address.source。根據(jù)類型校驗(yàn)輸入例如整數(shù)檢查范圍bool使用下拉選項(xiàng)浮點(diǎn)數(shù)檢查格式。指針、引用和數(shù)組暫時(shí)保持只讀。Release v1.0.7 · lianpeiliang/clangTestv1.0.7可以輸入值我們思考一個(gè)問題我感覺根據(jù)頭文件可以生成一份json文件然后根據(jù)這個(gè)json文件再推演出包含值的實(shí)例json文件這樣我們實(shí)際上是讀取和寫入這份實(shí)例json文件。我的思路有沒有問題你的思路是成立的。這樣可以把結(jié)構(gòu)定義和實(shí)際數(shù)據(jù)分開界面就負(fù)責(zé)根據(jù)定義展示、校驗(yàn)并讀寫實(shí)例文件。建議分成兩類 JSON結(jié)構(gòu)描述 JSON由頭文件通過 libclang 生成保存結(jié)構(gòu)體名稱、字段類型、嵌套關(guān)系、取值范圍以及大小、對(duì)齊、偏移等信息。實(shí)例 JSON保存某個(gè)結(jié)構(gòu)體的一組實(shí)際值同一份結(jié)構(gòu)描述可以對(duì)應(yīng)多個(gè)實(shí)例。程序的流程就變?yōu)轭^文件 → 結(jié)構(gòu)描述 JSON → 創(chuàng)建或打開實(shí)例 JSON → 編輯、校驗(yàn) → 保存實(shí)例 JSON。流程圖可以分成“生成結(jié)構(gòu)定義”和“實(shí)例讀寫”兩個(gè)階段結(jié)構(gòu)描述 JSON 決定“有哪些字段、允許填什么”實(shí)例 JSON 保存“每個(gè)字段實(shí)際填了什么”。同一份結(jié)構(gòu)描述可以創(chuàng)建多份實(shí)例文件。能不能增加一個(gè)按鈕我想看看我編輯好后這個(gè)結(jié)構(gòu)體對(duì)應(yīng)的16進(jìn)制編碼可以嗎已增加“查看二進(jìn)制”按鈕。按當(dāng)前字段值、嵌套關(guān)系和內(nèi)存布局生成十六進(jìn)制快照。使用小端序填充字節(jié)補(bǔ)零。支持復(fù)制十六進(jìn)制修改值后再次點(diǎn)擊即可刷新。無效輸入或不支持編碼的字段會(huì)阻止生成并提示原因。Release v1.0.8 · lianpeiliang/clangTestv1.0.8可以查看二進(jìn)制