SDL/HIDAPI 的 Windows Preparsed Data 导出工具 pp_data_dump:原理、文件格式与离线报告描述符重构测试
【免费下载链接】SDLSimple DirectMedia Layer项目地址: https://gitcode.com/GitHub_Trending/sd/SDL
导读
本文围绕 SDL 仓库内 HIDAPI 子系统(src/hidapi)自带的 Windows 命令行工具pp_data_dump.exe展开,讲解它如何把 Windows HID 子系统内部不透明的_HIDP_PREPARSED_DATA结构以人类可读的文本形式导出为.pp_data文件,以及这份文件如何被hid_report_reconstructor_test.exe单元测试离线消费,用于在没有真实硬件的情况下验证 HID 报告描述符重构逻辑。读完本文,你将掌握该工具的完整使用方式、.pp_data文本格式的每一个字段含义、其背后对应的源码实现,以及如何用仓库自带的测试用例和示例数据复现整套"导出—重构—比对"工作流。
一、工具定位:把 Windows 不透明的 Preparsed Data 变成可读文本
Windows HID 子系统会把设备上报的 HID Report Descriptor 解析成一个被称为Preparsed Data(预解析数据)的结构体。应用程序通过HidD_GetPreparsedData()获取它的句柄,再配合HidP_GetCaps()、HidP_GetValueCaps()等 API 查询其中的能力信息。但这个结构体对开发者来说是不透明的:_HIDP_PREPARSED_DATA的内部布局被微软保留为"仅供系统内部使用",官方文档不公开其内存布局。
HIDAPI 在 src/hidapi/windows/hidapi_descriptor_reconstruct.h 中自行定义了一套与_HIDP_PREPARSED_DATA二进制布局兼容的结构体(hidp_preparsed_data、hid_pp_cap、hid_pp_link_collection_node等),以便从 Preparsed Data 反推出原始 HID Report Descriptor(即"描述符重构"功能,实现在 src/hidapi/windows/hidapi_descriptor_reconstruct.c 中)。
pp_data_dump.exe 正是这座桥梁:它直接读取真实设备的 Preparsed Data,把 HIDAPI 内部结构体的全部字段逐一格式化输出到文本文件。这份文本既方便人阅读,又可以被重构逻辑的单元测试程序重新解析回内存结构,从而在没有硬件设备连接的情况下完成离线测试。
二、使用方法:零参数,即插即用
pp_data_dump.exe是一个没有命令行参数的 Windows 命令行小工具。使用流程非常简单:
- 把目标 HID 设备连接到 Windows 主机;
- 在命令行中直接执行
pp_data_dump.exe; - 工具会枚举系统中所有已连接的 HID 设备,并针对每个设备的每一个 Top-Level Collection生成一个文件。
启动时它会打印编译期与运行期 HIDAPI 版本信息,并逐一输出枚举到的设备概要(厂商 ID、产品 ID、路径、序列号、厂商字符串、产品字符串、Release 号、接口号、Usage/Usage Page),例如源码 src/hidapi/windows/pp_data_dump/pp_data_dump.c 中所示:
pp_data_dump tool. Compiled with hidapi version X, runtime version X. Device Found type: 046d b010 path: \\?\hid#... serial_number: ... Manufacturer: Logitech Product: Logitech Bluetooth Wireless Mouse Release: 0 Interface: -1 Usage (page): 01 (0C)输出文件命名规则
每个文件按以下格式命名(源码 pp_data_dump.c 中以%04X_%04X_%04X_%04X.pp_data生成,即全部使用大写的 4 位十六进制):
<vendor_id>_<product_id>_<usage>_<usage_table>.pp_data| 占位符 | 含义 | 示例 |
|---|---|---|
vendor_id | 厂商 ID(VID) | 046D |
product_id | 产品 ID(PID) | B010 |
usage | 顶层集合的 Usage | 0001 |
usage_table | 顶层集合的 Usage Page(Usage 表) | 000C |
例如046D_B010_0001_000C.pp_data表示罗技(Logitech,VID0x046D)产品0xB010上 Usage Page0x000C(Consumer,消费类页面)、Usage0x0001(Consumer Control)的顶层集合。仓库测试数据目录 src/hidapi/windows/test/data 中存放的正是历史上用该工具采集的真实设备数据。
三、文件内容详解:一份 .pp_data 长什么样
.pp_data文件是纯文本格式,逐字段复刻了 HIDAPI 内部用来表示 Preparsed Data 的结构体。以下完整示例取自仓库测试数据 src/hidapi/windows/test/data/046D_B010_0001_000C.pp_data,与 README 中给出的样例一致,对应一台罗技蓝牙无线鼠标的 Consumer Control 集合:
# HIDAPI device info struct: dev->vendor_id = 0x046D dev->product_id = 0xB010 dev->manufacturer_string = "Logitech" dev->product_string = "Logitech Bluetooth Wireless Mouse" dev->release_number = 0x0000 dev->interface_number = -1 dev->usage = 0x0001 dev->usage_page = 0x000C dev->path = "\\?\hid#{00001124-0000-1000-8000-00805f9b34fb}_vid&0002046d_pid&b010&col02#8&1cf1c1b9&3&0001#{4d1e55b2-f16f-11cf-88cb-001111000030}" # Preparsed Data struct: pp_data->MagicKey = 0x48696450204B4452 pp_data->Usage = 0x0001 pp_data->UsagePage = 0x000C pp_data->Reserved = 0x00000000 # Input caps_info struct: pp_data->caps_info[0]->FirstCap = 0 pp_data->caps_info[0]->LastCap = 1 pp_data->caps_info[0]->NumberOfCaps = 1 pp_data->caps_info[0]->ReportByteLength = 2 # Output caps_info struct: pp_data->caps_info[1]->FirstCap = 1 pp_data->caps_info[1]->LastCap = 1 pp_data->caps_info[1]->NumberOfCaps = 0 pp_data->caps_info[1]->ReportByteLength = 0 # Feature caps_info struct: pp_data->caps_info[2]->FirstCap = 1 pp_data->caps_info[2]->LastCap = 1 pp_data->caps_info[2]->NumberOfCaps = 0 pp_data->caps_info[2]->ReportByteLength = 0 # LinkCollectionArray Offset & Size: pp_data->FirstByteOfLinkCollectionArray = 0x0068 pp_data->NumberLinkCollectionNodes = 1 # Input hid_pp_cap struct: pp_data->cap[0]->UsagePage = 0x0006 pp_data->cap[0]->ReportID = 0x03 pp_data->cap[0]->BitPosition = 0 pp_data->cap[0]->BitSize = 8 pp_data->cap[0]->ReportCount = 1 pp_data->cap[0]->BytePosition = 0x0001 pp_data->cap[0]->BitCount = 8 pp_data->cap[0]->BitField = 0x02 pp_data->cap[0]->NextBytePosition = 0x0002 pp_data->cap[0]->LinkCollection = 0x0000 pp_data->cap[0]->LinkUsagePage = 0x000C pp_data->cap[0]->LinkUsage = 0x0001 pp_data->cap[0]->IsMultipleItemsForArray = 0 pp_data->cap[0]->IsButtonCap = 0 pp_data->cap[0]->IsPadding = 0 pp_data->cap[0]->IsAbsolute = 1 pp_data->cap[0]->IsRange = 0 pp_data->cap[0]->IsAlias = 0 pp_data->cap[0]->IsStringRange = 0 pp_data->cap[0]->IsDesignatorRange = 0 pp_data->cap[0]->Reserved1 = 0x000000 pp_data->cap[0]->pp_cap->UnknownTokens[0].Token = 0x00 pp_data->cap[0]->pp_cap->UnknownTokens[0].Reserved = 0x000000 pp_data->cap[0]->pp_cap->UnknownTokens[0].BitField = 0x00000000 pp_data->cap[0]->pp_cap->UnknownTokens[1].Token = 0x00 pp_data->cap[0]->pp_cap->UnknownTokens[1].Reserved = 0x000000 pp_data->cap[0]->pp_cap->UnknownTokens[1].BitField = 0x00000000 pp_data->cap[0]->pp_cap->UnknownTokens[2].Token = 0x00 pp_data->cap[0]->pp_cap->UnknownTokens[2].Reserved = 0x000000 pp_data->cap[0]->pp_cap->UnknownTokens[2].BitField = 0x00000000 pp_data->cap[0]->pp_cap->UnknownTokens[3].Token = 0x00 pp_data->cap[0]->pp_cap->UnknownTokens[3].Reserved = 0x000000 pp_data->cap[0]->pp_cap->UnknownTokens[3].BitField = 0x00000000 pp_data->cap[0]->NotRange.Usage = 0x0020 pp_data->cap[0]->NotRange.Reserved1 = 0x0020 pp_data->cap[0]->NotRange.StringIndex = 0 pp_data->cap[0]->NotRange.Reserved2 = 0 pp_data->cap[0]->NotRange.DesignatorIndex = 0 pp_data->cap[0]->NotRange.Reserved3 = 0 pp_data->cap[0]->NotRange.DataIndex = 0 pp_data->cap[0]->NotRange.Reserved4 = 0 pp_data->cap[0]->NotButton.HasNull = 0 pp_data->cap[0]->NotButton.Reserved4 = 0x000000 pp_data->cap[0]->NotButton.LogicalMin = 0 pp_data->cap[0]->NotButton.LogicalMax = 100 pp_data->cap[0]->NotButton.PhysicalMin = 0 pp_data->cap[0]->NotButton.PhysicalMax = 0 pp_data->cap[0]->Units = 0 pp_data->cap[0]->UnitsExp = 0 # Output hid_pp_cap struct: # Feature hid_pp_cap struct: # Link Collections: pp_data->LinkCollectionArray[0]->LinkUsage = 0x0001 pp_data->LinkCollectionArray[0]->LinkUsagePage = 0x000C pp_data->LinkCollectionArray[0]->Parent = 0 pp_data->LinkCollectionArray[0]->NumberOfChildren = 0 pp_data->LinkCollectionArray[0]->NextSibling = 0 pp_data->LinkCollectionArray[0]->FirstChild = 0 pp_data->LinkCollectionArray[0]->CollectionType = 1 pp_data->LinkCollectionArray[0]->IsAlias = 0 pp_data->LinkCollectionArray[0]->Reserved = 0x000000003.1 文件的三段式结构
从格式上可以清晰地拆成三个段落:
- 设备信息头(HIDAPI device info struct):以
dev->前缀开头,来源于hid_enumerate()返回的hid_device_info字段,记录了 VID/PID、厂商/产品字符串、Release、接口号、Usage/Usage Page 以及完整的设备路径。测试程序用空行标记该段的结束。 - Preparsed Data 总览:
pp_data->顶层字段(MagicKey、Usage、UsagePage、Reserved)、三个caps_info(分别对应 Input、Output、Feature 三种报告类型)以及 Link Collection 数组的偏移与数量。 - 能力数组与 Link Collection 明细:
pp_data->cap[n](每个能力的完整描述)与pp_data->LinkCollectionArray[n](集合树的节点信息)。
3.2 关键字段语义
pp_data->MagicKey:Preparsed Data 的魔数标识(0x48696450204B4452是 ASCII 的 "HidP KDR"),用于校验结构合法性;caps_info[0..2]:分别描述 Input / Output / Feature 报告的区间与布局,其中FirstCap/LastCap构成半开区间[FirstCap, LastCap),指示能力数组的有效遍历范围;NumberOfCaps包括LastCap之后的空槽位;ReportByteLength是该类报告的字节长度。注意本示例中 Output/Feature 的FirstCap = LastCap = 1,表示区间为空、没有任何能力——所以文件后面出现"# Output hid_pp_cap struct:" 后没有任何内容;cap[n]的能力字段:BitPosition、BitSize(源码内部名称为ReportSize)、ReportCount、BytePosition、BitCount、BitField、NextBytePosition描述该能力在报告位流中的位置与大小;LinkCollection、LinkUsagePage、LinkUsage把它挂到集合树上;8 个布尔标志位(IsMultipleItemsForArray、IsButtonCap、IsPadding、IsAbsolute、IsRange、IsAlias、IsStringRange、IsDesignatorRange)压缩在同一个字节中(对应 hidapi_descriptor_reconstruct.h 的位域声明);Range/NotRange联合体:当IsRange为真时输出Range.UsageMin/UsageMax、StringMin/Max、DesignatorMin/Max、DataIndexMin/Max;否则输出NotRange单值形式(本示例即单值形式,Usage 为0x0020,即 Consumer Control 页面下的 AC Pan);Button/NotButton联合体:IsButtonCap为真时只有 LogicalMin/Max 有意义;否则(本示例)输出HasNull、Logical/Physical Min/Max 等值字段信息;pp_cap->UnknownTokens[4]:HIDAPI 为尚未被完全解析的"未知全局项"预留的槽位,每个 Token 记录该项的单字节前缀Token、保留字节和BitField(数据部分),用于重构时无损保留无法识别的描述符项;LinkCollectionArray[n]:集合树节点,Parent/NumberOfChildren/NextSibling/FirstChild构成兄弟/孩子链表结构(对应 hidapi_descriptor_reconstruct.h),CollectionType用 8 位位域表示集合类型,值为 1 对应 Application Collection。
四、源码实现:从枚举设备到逐字段落盘
pp_data_dump的完整实现只有一份 C 源文件 src/hidapi/windows/pp_data_dump/pp_data_dump.c,其工作流程可以拆成四步:
- 初始化与枚举:调用
hid_init()初始化 HIDAPI,随后hid_enumerate(0x0, 0x0)枚举全部 HID 设备(VID/PID 均传 0 表示不筛选),打印每个设备的信息; - 打开设备并创建输出文件:对每个枚举结果调用
hid_open_path(cur_dev->path)打开设备,按第二节的规则拼接出文件名并用fopen_s创建文本文件; - 导出 Preparsed Data:调用 Windows API
HidD_GetPreparsedData()拿到PHIDP_PREPARSED_DATA,cast 为 HIDAPI 自定义的hidp_preparsed_data*后逐字段格式化输出(dump_pp_data()); - 清理:
HidD_FreePreparsedData()释放结构,hid_close()关闭设备,遍历链表直至hid_free_enumeration()释放枚举结果,最后hid_exit()。
其中第 3 步是核心,值得展开:
- 能力数组通过
offsetof(hidp_preparsed_data, caps)得到首地址,再按caps_info[i].FirstCap到caps_info[i].LastCap的区间分别遍历 Input、Output、Feature 三类能力并调用dump_hid_pp_cap()(pp_data_dump.c); - Link Collection 数组则通过
caps首地址加上pp_data->FirstByteOfLinkCollectionArray偏移量定位(pp_data_dump.c),印证了 hidapi_descriptor_reconstruct.h 中caps与LinkCollectionArray共享同一段柔性数组成员(union)的内存布局设计; - 对布尔标志位、位域字段的打印做了编译器兼容处理:例如 Link Collection 的位域统一强转为
unsigned int再输出,注释说明这是因为不同编译器对ULONG位域的符号性处理不一致(pp_data_dump.c)。
五、下游消费方:hid_report_reconstructor_test 离线单元测试
.pp_data文件最重要的用途,是作为 HIDAPI 的 Windows 报告描述符重构器(report descriptor reconstructor)的离线测试输入。对应测试程序是 src/hidapi/windows/test/hid_report_reconstructor_test.c,它实现了 README 中所说的hid_report_reconstructor_test.exe。
测试的工作方式是:
- 以两个参数运行:
hid_report_reconstructor_test <xxx>.pp_data <xxx>_expected.rpt_desc; - 测试程序用
sscanf逐行解析.pp_data文本(alloc_preparsed_data_from_file(),见 hid_report_reconstructor_test.c),依据FirstByteOfLinkCollectionArray和NumberLinkCollectionNodes动态分配内存,重建出与真实设备内存布局一致的hidp_preparsed_data; - 调用
hid_winapi_descriptor_reconstruct_pp_data()重构出 HID Report Descriptor 字节流; - 把
_expected.rpt_desc(以0x.., 0x..十六进制文本形式存储的期望描述符,见 046D_C52F_0001_000C_expected.rpt_desc)解析为字节数组,与重构结果逐字节比对,完全一致则测试通过。
例如上节示例中罗技鼠标那一个 Input 能力,重构出的期望描述符片段为:
0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x03, 0x19, 0x01, 0x2A, 0x8C, 0x02, 0x15, 0x01, 0x26, 0x8C, 0x02, 0x75, 0x10, 0x95, 0x02, 0x81, 0x00, 0xC0,对照标准 HID 项编码可以逐项验证:0x05 0x0C(Usage Page = Consumer)、0x09 0x01(Usage = Consumer Control)、0xA1 0x01(Application Collection 开始)、0x85 0x03(Report ID = 3)、0x19/0x2A(Usage 范围 1..396)、0x15/0x26(Logical 范围 1..396)、0x75/0x95(Report Size 16、Report Count 2)、0x81 0x00(Input Data 项)、0xC0(集合结束)——与.pp_data中记录的ReportID=0x03、Usage=0x0020(不是范围起点)、LogicalMax=100等字段之间存在明确的对应关系,这正是"Preparsed Data → 描述符重构"要还原的信息。
5.1 测试用例的注册机制
在 src/hidapi/windows/test/CMakeLists.txt 中,通过HID_DESCRIPTOR_RECONSTRUCT_TEST_CASES列表注册了 23 组测试用例(046D_C52F_0001_000C、17CC_1130_0000_FF01、046A_0011_0006_0001等),每组用例要求存在两个文件:
<name>.pp_data—— Preparsed Data 的文本表示(必需);<name>_expected.rpt_desc—— 重构出的期望 HID Report Descriptor(必需);<name>_real.rpt_desc—— 可选的"真实原始描述符",用于人工对照(非测试必需)。
每个用例通过add_test()注册为WinHidReportReconstructTest_<TEST_CASE>形式的 CTest 用例,WORKING_DIRECTORY指向 hidapi 动态库输出目录;若开启了 ASan(HIDAPI_ENABLE_ASAN),还会为 MSVC 追加工具链目录到 PATH 并设置ASAN_SAVE_DUMPS环境变量以便收集崩溃转储。测试目录下的.pp_data数据覆盖了多种真实设备(Logitech 鼠标/键盘、Plantronics 耳机、Dell 键盘、Steam 控制器等),涉及000C(Consumer)、FF00/FF01(厂商自定义)、0001(Generic Desktop)、0006(Generic Device)、000B(Telephony)等多个 Usage Page,为重构器提供了多样的输入覆盖。
六、构建与运行
pp_data_dump与测试程序都通过 CMake 构建,二者均要求 C11 标准:
- src/hidapi/windows/pp_data_dump/CMakeLists.txt:
add_executable(pp_data_dump pp_data_dump.c),链接hidapi_winapi库,并可通过install()安装到CMAKE_INSTALL_BINDIR; - src/hidapi/windows/test/CMakeLists.txt:构建
hid_report_reconstructor_test,链接hidapi_include与hidapi_winapi。
典型流程(在 Windows 上使用 CMake + Visual Studio 或 MinGW):
cmake -B build -S . -DHIDAPI_BUILD_TESTS=ON cmake --build build --config Release ctest --test-dir build -C Release -R WinHidReportReconstructTestpp_data_dump.c顶部还针对 MinGW 做了预处理:定义__USE_MINGW_ANSI_STDIO以正确支持%hh等 ANSI 长度修饰符(pp_data_dump.c),测试程序同样对%zu做了兼容处理,说明这两个工具都兼顾了 MSVC 与 MinGW 工具链。
七、局限性与注意事项
_HIDP_PREPARSED_DATA是未公开结构:pp_data_dump与重构器所依赖的内存布局,是 HIDAPI 以 Chromium 项目中hid_preparsed_data.cc的解析实现为参考推导出来的。微软不保证该结构在未来的 Windows 版本中保持稳定,因此该工具链的二进制兼容性存在天然风险;- 结构与版本绑定:源码中对
hid_pp_link_collection_node有sizeof == 16的编译期断言(见 hidapi_descriptor_reconstruct.h),一旦编译器产生的内存布局与预期不符会直接编译失败,这正体现了对该未公开结构二进制布局的高度敏感; - 输出文件数量:工具会为每个已连接设备的每个 Top-Level Collection生成一个文件,设备较多或集合较多时会产生大量文件;文件生成在程序当前工作目录下;
- 运行环境:该工具仅面向 Windows(依赖
hid.c与 Windows HID API),且需要真实硬件连接才能采集数据;而hid_report_reconstructor_test则完全离线运行,仅依赖.pp_data文本。
八、总结
pp_data_dump.exe是 HIDAPI Windows 后端一套完整的"采集—重构—验证"方法论中的采集环节:它将微软未公开的 Preparsed Data 结构固化为 HIDAPI 自定义结构的可读文本,使开发者得以理解 Windows 如何解释 HID Report Descriptor;而配套的hid_report_reconstructor_test.exe与 src/hidapi/windows/test/data 中 23 组真实设备样本,则让报告描述符重构逻辑可以在无硬件环境下反复回归验证。这套工具链既是排查 Windows HID 设备枚举问题的实用抓手,也是理解 HID Report Descriptor 与 Preparsed Data 之间双向映射关系的最佳学习材料。
【免费下载链接】SDLSimple DirectMedia Layer项目地址: https://gitcode.com/GitHub_Trending/sd/SDL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考