结构体解析器(2)

发布时间:2026/10/29 2:18:28
结构体解析器(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 // 解析流程创建 CXIndex → 解析得到 CXTranslationUnit → 遍历 AST → 查询布局。 // CXCursor 代表 AST 节点如结构体、字段CXType 代表节点对应的类型。 // Cursor 和 Type 都依赖所属的 TranslationUnit必须在 unit 释放前使用。 // CXString 由 libclang 管理需要调用者显式释放。 static QString toQString(CXString value) { // getCString() 只借用指针先复制到 QString再 disposeString()避免悬空指针。 const QString result QString::fromUtf8(clang_getCString(value)); clang_disposeString(value); return result; } // 通过回调的 CXClientDatavoid *传递结果避免使用全局变量。 struct LayoutResult { bool found false; bool valid true; }; // 回调参数依次为当前节点、父节点、用户数据此处不使用父节点。 // Continue下一个同级节点Recurse先进入子节点Break结束本次遍历。 static CXChildVisitResult visitField(CXCursor cursor, CXCursor, CXClientData data) { // 结构体的子节点可能包含方法等声明FieldDecl 才是非静态成员字段。 if (clang_getCursorKind(cursor) ! CXCursor_FieldDecl) return CXChildVisit_Continue; LayoutResult result *static_castLayoutResult *(data); // 从字段节点取得类型大小和类型的自然对齐值均以字节为单位。 const CXType type clang_getCursorType(cursor); const long long size clang_Type_getSizeOf(type); const long long alignment clang_Type_getAlignOf(type); // 字段偏移以 bit 为单位相对于结构体起点本例除以 8 得到字节偏移。 // 如果以后解析位域要保留 bit 偏移不能只看整数除法后的字节值。 const long long offsetBits clang_Cursor_getOffsetOfField(cursor); // 布局查询返回负数表示 CXTypeLayoutError例如类型不完整不能当作有效布局。 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 对应 unsigned short。 // pack(1) 影响字段位置但 uint32_t 类型本身的自然对齐值仍然为 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 等包含文件中的节点。 if (!clang_Location_isFromMainFile(clang_getCursorLocation(cursor))) return CXChildVisit_Continue; // StructDecl 表示 struct 声明isCursorDefinition 排除 struct MsgHeader; 这样的前置声明。 // 其他主文件节点返回 Recurse以便继续查找命名空间等节点内部的结构体。 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; // 对结构体类型查询布局libclang 会按目标平台和 #pragma pack 计算对齐与填充。 // 当前大小为 7、对齐为 1这里没有手动累加字段大小。 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); // 第二层遍历以结构体为父节点查询其各个字段data 继续传递同一个结果对象。 clang_visitChildren(cursor, visitField, data); // 已处理目标结构体结束外层遍历。 return CXChildVisit_Break; } int main(int argc, char *argv[]) { // 强制 Qt 日志输出到标准错误避免 Windows 下只发送给调试器。 qputenv(QT_FORCE_STDERR_LOGGING, 1); QCoreApplication app(argc, argv); // 命令行可指定待解析文件默认路径 MSG_HEADER_PATH 由 .pro 定义。 const QByteArray sourceFile QFile::encodeName( app.arguments().size() 1 ? app.arguments().at(1) : QStringLiteral(MSG_HEADER_PATH)); // 这些是解析头文件的参数与编译当前 Qt 程序的选项是两回事。 // FullArgv 要求首项为编译器路径以帮助定位 MSYS2 的标准库头文件。 // libclang 在进程内解析不会启动 clang.exe 子进程。 // -x c按 C 解析-stdc11语言标准--target决定 ABI 和布局的目标平台。 const char *arguments[] { LIBCLANG_ROOT /bin/clang.exe, -x, c, -stdc11, --targetx86_64-w64-windows-gnu }; // CXIndex 是创建翻译单元的上下文可用于解析多个文件。 // 第一个 0不排除预编译头声明第二个 0不自动打印诊断稍后自行处理。 CXIndex index clang_createIndex(0, 0); if (!index) { qCritical() Cannot create libclang index; return 1; } CXTranslationUnit unit nullptr; // 翻译单元包含主文件及包含文件的解析结果和 AST。 // 参数顺序索引、文件路径、编译参数数组、参数数量、内存文件数组、内存文件数量、选项、输出指针。 // nullptr, 0 表示没有 CXUnsavedFile内存中的未保存文件直接读取磁盘文件。 // CXTranslationUnit_None 表示不启用额外选项unit 接收创建出来的翻译单元。 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 仍可能有语法错误或缺失头文件必须继续检查诊断。 // Diagnostic 包含警告、错误及源代码位置出现 Error/Fatal 就停止后续布局查询。 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 根节点的直接子节点开始回调是否递归取决于 visitRecord 的返回值。 // result 会原样传入回调的 data 参数在回调中转回 LayoutResult * 即可使用。 clang_visitChildren(clang_getTranslationUnitCursor(unit), visitRecord, result); if (!result.found) qCritical() MsgHeader definition not found; } // 先释放翻译单元再释放索引。Cursor/Type 无需单独释放但 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进阶把这个结构体的内容扩展一下吧再复杂一点可以吗先做一版short ,unsigned short ,unsigned char就是各种基础数据类型都有的结构体[MsgHeader.h]现在涵盖char、signed char、unsigned char、boolshort、int、long、long long及对应无符号类型float、double、long doublewchar_t、char16_t、char32_t保留了原有字段、pack(1)和中文注释解析流程保持原样。#include cstdint // 保存原有打包设置将成员对齐上限设为 1 字节避免成员之间的填充。 #pragma pack(push, 1) struct MsgHeader { // 原有的固定宽度整数用来和下面的基础类型对比。 uint16_t msgId; uint8_t msgType; uint32_t dataLength; // 字符与布尔类型。char 的有无符号由目标平台决定因此单独列出 signed/unsigned char。 char charValue; signed char signedCharValue; unsigned char unsignedCharValue; bool boolValue; // 各种有符号和无符号整数short、int、long 默认都是有符号的。 short shortValue; unsigned short unsignedShortValue; int intValue; unsigned int unsignedIntValue; long longValue; unsigned long unsignedLongValue; long long longLongValue; unsigned long long unsignedLongLongValue; // 浮点类型long double 的大小可能随目标平台和工具链变化。 float floatValue; double doubleValue; long double longDoubleValue; // 宽字符与 C11 字符类型这里只存放单个字符不是字符串。 wchar_t wcharValue; char16_t char16Value; char32_t char32Value; }; // 恢复之前的打包设置避免影响后续声明的结构体。 #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)我修改了头文件相关的解析代码不需要改变吗是通用的吗你目前这种修改不需要改解析代码。增删字段、调整顺序、修改基础类型或pack设置都会在下次运行时自动解析。因为代码没有逐个写死字段名而是让libclang 遍历所有成员对每个成员执行相同的查询。clang_getCursorType(cursor); // 这个字段是什么类型 clang_Type_getSizeOf(type); // 类型占多少字节 clang_Type_getAlignOf(type); // 类型的自然对齐是多少 clang_Cursor_getOffsetOfField(cursor);// 字段从结构体的第几个 bit 开始所以short、unsigned char、double等都能走同一套流程。目前有几个范围限制修改当前代码能否直接处理增删基础类型字段、修改名称和顺序可以修改#pragma pack可以自动重新计算布局把结构体改名需要修改代码中写死的MsgHeader添加嵌套结构体能读取整个成员的大小和偏移展开内部字段需要补充遍历添加位域能读取 bit 偏移显示位宽需要补充查询头文件保存后直接重新运行即可因为程序是在运行时读取它的。什么是自然对齐自然对齐就是一个类型在默认情况下要求从什么样的地址开始存放。例如在你当前的环境中类型自然对齐默认存放要求char1 字节任意地址short2 字节地址是 2 的倍数int4 字节地址是 4 的倍数double8 字节地址是 8 的倍数Release v1.0.3(头文件复杂版) · lianpeiliang/clangTest添加界面可以在界面中查看相关的信息。Release v1.0.4(界面解析版) · lianpeiliang/clangTest我想问下我现在的程序只能解析名字是MsgHeader的结构体吗目前通过界面使用时是的只会查找名为MsgHeader的结构体。但底层解析函数已经支持指定其他名称。例如要解析MyHeader可以改成const ParseResult result parseHeader(path, QStringLiteral(MyHeader));另外头文件名可以任意取限制的是文件内的结构体名称。当前只查找所选文件中定义的结构体不查找它通过#include引入的结构体。那再优化一版吧默认名字为MsgHeader可以修改QLineEdit可以吗Release v1.0.5 · lianpeiliang/clangTestv1.0.5可修改结构体名称有可执行程序下一步我希望可以引入一个嵌套型的结构体你理解吗理解就是一个结构体的成员也是结构体类型例如struct Address { unsigned short source; unsigned short destination; }; struct PacketHeader { unsigned int sequence; Address address; // 嵌套结构体成员 unsigned short payloadLength; };目前程序会把address显示为一个字段能读取它的类型、大小和偏移但不会展开里面的source、destination。添加一个嵌套示例输入NestedPacketHeader查看外层结构体包含嵌套成员address总大小10 字节。输入PacketAddress单独查看内部的source、destination字段。我觉得如果我想要展示这种嵌套结构的话使用QTreeWidget会比较好吧是不是是的QTreeWidget很适合当前这种嵌套结构展示可以展开、折叠成员同时保留现有的类型、大小、对齐和偏移等列。实现时需要两部分配合解析层递归读取嵌套成员让FieldInfo保存子字段。界面层将QTableWidget换成QTreeWidget递归创建树节点。偏移建议明确区分“相对所属结构体”和“相对最外层结构体”。例如destination在address内偏移为2 字节在整个NestedPacketHeader内偏移为6 字节。Release v1.0.6 · lianpeiliang/clangTestv1.0.6支持嵌套结构体在现有的逻辑下我在想可以考虑给参数赋值的事情了你觉得呢可以现有的树形展示已经适合继续做结构体字段赋值。建议先实现这一版增加“值”列允许编辑基础类型字段。嵌套结构体作为分组展开后编辑内部字段例如address.source。根据类型校验输入例如整数检查范围bool使用下拉选项浮点数检查格式。指针、引用和数组暂时保持只读。Release v1.0.7 · lianpeiliang/clangTestv1.0.7可以输入值我们思考一个问题我感觉根据头文件可以生成一份json文件然后根据这个json文件再推演出包含值的实例json文件这样我们实际上是读取和写入这份实例json文件。我的思路有没有问题你的思路是成立的。这样可以把结构定义和实际数据分开界面就负责根据定义展示、校验并读写实例文件。建议分成两类 JSON结构描述 JSON由头文件通过 libclang 生成保存结构体名称、字段类型、嵌套关系、取值范围以及大小、对齐、偏移等信息。实例 JSON保存某个结构体的一组实际值同一份结构描述可以对应多个实例。程序的流程就变为头文件 → 结构描述 JSON → 创建或打开实例 JSON → 编辑、校验 → 保存实例 JSON。流程图可以分成“生成结构定义”和“实例读写”两个阶段结构描述 JSON 决定“有哪些字段、允许填什么”实例 JSON 保存“每个字段实际填了什么”。同一份结构描述可以创建多份实例文件。能不能增加一个按钮我想看看我编辑好后这个结构体对应的16进制编码可以吗已增加“查看二进制”按钮。按当前字段值、嵌套关系和内存布局生成十六进制快照。使用小端序填充字节补零。支持复制十六进制修改值后再次点击即可刷新。无效输入或不支持编码的字段会阻止生成并提示原因。Release v1.0.8 · lianpeiliang/clangTestv1.0.8可以查看二进制