SDL/HIDAPI 的 Windows Preparsed Data 导出工具 pp_data_dump:原理、文件格式与离线报告描述符重构测试
2026/9/14 11:19:53 网站建设 项目流程

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_datahid_pp_caphid_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 命令行小工具。使用流程非常简单:

  1. 把目标 HID 设备连接到 Windows 主机;
  2. 在命令行中直接执行pp_data_dump.exe
  3. 工具会枚举系统中所有已连接的 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顶层集合的 Usage0001
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 = 0x00000000

3.1 文件的三段式结构

从格式上可以清晰地拆成三个段落:

  1. 设备信息头(HIDAPI device info struct):以dev->前缀开头,来源于hid_enumerate()返回的hid_device_info字段,记录了 VID/PID、厂商/产品字符串、Release、接口号、Usage/Usage Page 以及完整的设备路径。测试程序用空行标记该段的结束。
  2. Preparsed Data 总览pp_data->顶层字段(MagicKey、Usage、UsagePage、Reserved)、三个caps_info(分别对应 Input、Output、Feature 三种报告类型)以及 Link Collection 数组的偏移与数量。
  3. 能力数组与 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]的能力字段BitPositionBitSize(源码内部名称为ReportSize)、ReportCountBytePositionBitCountBitFieldNextBytePosition描述该能力在报告位流中的位置与大小;LinkCollectionLinkUsagePageLinkUsage把它挂到集合树上;8 个布尔标志位(IsMultipleItemsForArrayIsButtonCapIsPaddingIsAbsoluteIsRangeIsAliasIsStringRangeIsDesignatorRange)压缩在同一个字节中(对应 hidapi_descriptor_reconstruct.h 的位域声明);
  • Range/NotRange联合体:当IsRange为真时输出Range.UsageMin/UsageMaxStringMin/MaxDesignatorMin/MaxDataIndexMin/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,其工作流程可以拆成四步:

  1. 初始化与枚举:调用hid_init()初始化 HIDAPI,随后hid_enumerate(0x0, 0x0)枚举全部 HID 设备(VID/PID 均传 0 表示不筛选),打印每个设备的信息;
  2. 打开设备并创建输出文件:对每个枚举结果调用hid_open_path(cur_dev->path)打开设备,按第二节的规则拼接出文件名并用fopen_s创建文本文件;
  3. 导出 Preparsed Data:调用 Windows APIHidD_GetPreparsedData()拿到PHIDP_PREPARSED_DATA,cast 为 HIDAPI 自定义的hidp_preparsed_data*后逐字段格式化输出(dump_pp_data());
  4. 清理HidD_FreePreparsedData()释放结构,hid_close()关闭设备,遍历链表直至hid_free_enumeration()释放枚举结果,最后hid_exit()

其中第 3 步是核心,值得展开:

  • 能力数组通过offsetof(hidp_preparsed_data, caps)得到首地址,再按caps_info[i].FirstCapcaps_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 中capsLinkCollectionArray共享同一段柔性数组成员(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

测试的工作方式是:

  1. 以两个参数运行:hid_report_reconstructor_test <xxx>.pp_data <xxx>_expected.rpt_desc
  2. 测试程序用sscanf逐行解析.pp_data文本(alloc_preparsed_data_from_file(),见 hid_report_reconstructor_test.c),依据FirstByteOfLinkCollectionArrayNumberLinkCollectionNodes动态分配内存,重建出与真实设备内存布局一致的hidp_preparsed_data
  3. 调用hid_winapi_descriptor_reconstruct_pp_data()重构出 HID Report Descriptor 字节流;
  4. _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=0x03Usage=0x0020(不是范围起点)、LogicalMax=100等字段之间存在明确的对应关系,这正是"Preparsed Data → 描述符重构"要还原的信息。

5.1 测试用例的注册机制

在 src/hidapi/windows/test/CMakeLists.txt 中,通过HID_DESCRIPTOR_RECONSTRUCT_TEST_CASES列表注册了 23 组测试用例(046D_C52F_0001_000C17CC_1130_0000_FF01046A_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_includehidapi_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 WinHidReportReconstructTest

pp_data_dump.c顶部还针对 MinGW 做了预处理:定义__USE_MINGW_ANSI_STDIO以正确支持%hh等 ANSI 长度修饰符(pp_data_dump.c),测试程序同样对%zu做了兼容处理,说明这两个工具都兼顾了 MSVC 与 MinGW 工具链。

七、局限性与注意事项

  1. _HIDP_PREPARSED_DATA是未公开结构pp_data_dump与重构器所依赖的内存布局,是 HIDAPI 以 Chromium 项目中hid_preparsed_data.cc的解析实现为参考推导出来的。微软不保证该结构在未来的 Windows 版本中保持稳定,因此该工具链的二进制兼容性存在天然风险;
  2. 结构与版本绑定:源码中对hid_pp_link_collection_nodesizeof == 16的编译期断言(见 hidapi_descriptor_reconstruct.h),一旦编译器产生的内存布局与预期不符会直接编译失败,这正体现了对该未公开结构二进制布局的高度敏感;
  3. 输出文件数量:工具会为每个已连接设备的每个 Top-Level Collection生成一个文件,设备较多或集合较多时会产生大量文件;文件生成在程序当前工作目录下;
  4. 运行环境:该工具仅面向 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询