Windows SetupAPI:获取设备接口实例详解
2025-03-03 16:14:26
Windows Setup API:如何从设备实例获取接口实例?
有时候,我们需要遍历系统上的所有设备,并针对每个设备,遍历它的接口。 问题来了,如果已经有了 SP_DEVINFO_DATA
对象,怎么才能遍历该设备的接口呢? 是不是需要接口类的 GUID? 如果需要,又该怎么获取呢?
先看看我尝试过的代码 (为清晰起见,省略了错误检查):
#include <iostream>
#include <Windows.h>
#include <SetupAPI.h>
using namespace std;
int
main()
{
HDEVINFO devInfo;
HDEVINFO devInfo2;
SP_DEVINFO_DATA devInfoData;
SP_DEVICE_INTERFACE_DATA interfaceData;
// 遍历所有设备
devInfo = SetupDiGetClassDevs(NULL, NULL, NULL, DIGCF_ALLCLASSES | DIGCF_PRESENT);
devInfoData.cbSize = sizeof(devInfoData);
for (DWORD i = 0; SetupDiEnumDeviceInfo(devInfo, i, &devInfoData); ++i)
{
DWORD idSize;
SetupDiGetDeviceInstanceIdA(devInfo, &devInfoData, NULL, 0, &idSize);
char * id = new char[idSize + 1];
SetupDiGetDeviceInstanceIdA(devInfo, &devInfoData, id, idSize, NULL);
cout << i << ": devinst = " << devInfoData.DevInst << " " << id << endl;
// 遍历此设备的所有接口
// 这部分代码有问题; 我估计是因为 SP_DEVINFO_DATA.ClassGuid
// 和接口类不一样.
devInfo2 = SetupDiGetClassDevs(&devInfoData.ClassGuid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
interfaceData.cbSize = sizeof(interfaceData);
for (DWORD j = 0; SetupDiEnumDeviceInterfaces(devInfo2, NULL, &devInfoData.ClassGuid, j, &interfaceData); ++j) {
PSP_DEVICE_INTERFACE_DETAIL_DATA_A detailData = NULL;
DWORD requiredLength;
SetupDiGetDeviceInterfaceDetailA(devInfo2, &interfaceData, NULL, 0, &requiredLength, NULL);
detailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA_A)malloc(requiredLength);
detailData->cbSize = sizeof(*detailData);
SetupDiGetDeviceInterfaceDetailA(devInfo2, &interfaceData, detailData, requiredLength, NULL, NULL);
cout << " " << j << ": " << detailData->DevicePath << endl;
free(detailData);
}
SetupDiDestroyDeviceInfoList(devInfo2);
}
SetupDiDestroyDeviceInfoList(devInfo);
return 0;
}
问题原因分析
如上所示,问题的核心在于, 用于枚举设备的 SP_DEVINFO_DATA
中的 ClassGuid
并不是用于枚举设备接口的 Interface Class GUID
。ClassGuid
代表的是设备的安装类 (Setup Class),而我们要找的是设备接口类 (Interface Class)。 打个比方, "显示适配器"是一个安装类,而 "显示器" 或 "图形处理器" 可以作为其接口类。 一个设备可以属于某个安装类,同时又暴露多个不同类型的接口。
解决方案
解决思路就是,先获取设备实例关联的所有接口类 GUID,然后利用这些 GUID 来枚举每个接口类的实例。
方法一: 使用 SetupDiGetDeviceInterfaceClassGuids
(推荐)
SetupDiGetDeviceInterfaceClassGuids
这个函数可以直接获取到和指定设备实例相关联的所有接口类 GUID。
-
原理:
SetupDiGetDeviceInterfaceClassGuids
函数可以直接获取与给定设备实例关联的所有接口类的 GUID 列表。这省去了我们自己去查询注册表或执行其他复杂操作的步骤。 -
代码示例:
#include <iostream> #include <Windows.h> #include <SetupAPI.h> #include <vector> using namespace std; int main() { HDEVINFO devInfo; SP_DEVINFO_DATA devInfoData; // 遍历所有设备 devInfo = SetupDiGetClassDevs(NULL, NULL, NULL, DIGCF_ALLCLASSES | DIGCF_PRESENT); devInfoData.cbSize = sizeof(devInfoData); for (DWORD i = 0; SetupDiEnumDeviceInfo(devInfo, i, &devInfoData); ++i) { DWORD idSize; SetupDiGetDeviceInstanceIdA(devInfo, &devInfoData, NULL, 0, &idSize); char* id = new char[idSize + 1]; SetupDiGetDeviceInstanceIdA(devInfo, &devInfoData, id, idSize, NULL); cout << i << ": devinst = " << devInfoData.DevInst << " " << id << endl; delete[] id; // 释放内存 // 获取接口类 GUID DWORD requiredSize = 0; SetupDiGetDeviceInterfaceClassGuids(&devInfoData, nullptr, 0, &requiredSize); // 先获取需要的缓冲区大小 if (requiredSize == 0) { continue; } vector<GUID> classGuids(requiredSize); if (SetupDiGetDeviceInterfaceClassGuids(&devInfoData, classGuids.data(), requiredSize, &requiredSize)) { // 遍历每一个接口类 GUID for (const GUID& interfaceClassGuid : classGuids) { // 用 SetupDiGetClassDevs 和 SetupDiEnumDeviceInterfaces 枚举接口 HDEVINFO devInfo2 = SetupDiGetClassDevs(&interfaceClassGuid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT); SP_DEVICE_INTERFACE_DATA interfaceData; interfaceData.cbSize = sizeof(interfaceData); for (DWORD j = 0; SetupDiEnumDeviceInterfaces(devInfo2, &devInfoData, &interfaceClassGuid, j, &interfaceData); ++j) { PSP_DEVICE_INTERFACE_DETAIL_DATA_A detailData = NULL; DWORD requiredLength; SetupDiGetDeviceInterfaceDetailA(devInfo2, &interfaceData, NULL, 0, &requiredLength, NULL); detailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA_A)malloc(requiredLength); detailData->cbSize = sizeof(*detailData); SetupDiGetDeviceInterfaceDetailA(devInfo2, &interfaceData, detailData, requiredLength, NULL, NULL); cout << " " << j << ": " << detailData->DevicePath << endl; free(detailData); } if (devInfo2) SetupDiDestroyDeviceInfoList(devInfo2); } } } SetupDiDestroyDeviceInfoList(devInfo); return 0; }
-
安全建议: 在使用 SetupAPI 时,要注意资源释放。例如
HDEVINFO
句柄需要使用SetupDiDestroyDeviceInfoList
释放, 动态分配的内存(例如detailData
)需要用free
释放,防止内存泄漏。
方法二:使用 CM_Get_Device_Interface_List
这个方法使用 Configuration Manager 函数来获取设备接口列表, 它通过设备实例 ID (Device Instance ID) 直接获取相关的设备接口路径.
-
原理:
CM_Get_Device_Interface_List
函数需要提供设备的 Instance ID。 通过设备的 Instance ID,可以直接获取到该设备所有已注册接口的路径列表。 这个方法避免了遍历 GUID 的过程,比较直接。 -
代码示例:
#include <iostream>
#include <Windows.h>
#include <SetupAPI.h>
#include <Cfgmgr32.h> // 需要包含此头文件
using namespace std;
int main() {
HDEVINFO devInfo;
SP_DEVINFO_DATA devInfoData;
// Iterate over all devices
devInfo = SetupDiGetClassDevs(NULL, NULL, NULL, DIGCF_ALLCLASSES | DIGCF_PRESENT);
devInfoData.cbSize = sizeof(devInfoData);
for (DWORD i = 0; SetupDiEnumDeviceInfo(devInfo, i, &devInfoData); ++i)
{
DWORD idSize;
SetupDiGetDeviceInstanceIdA(devInfo, &devInfoData, NULL, 0, &idSize);
char* id = new char[idSize + 1];
SetupDiGetDeviceInstanceIdA(devInfo, &devInfoData, id, idSize, NULL);
cout << i << ": devinst = " << devInfoData.DevInst << " " << id << endl;
// Get the device interface list.
ULONG bufferLength = 0;
CONFIGRET cr = CM_Get_Device_Interface_List_SizeA(&bufferLength, (LPGUID)&GUID_NULL, id, 0); // 注意这里使用 GUID_NULL
if (cr == CR_SUCCESS) {
char* buffer = new char[bufferLength];
cr = CM_Get_Device_Interface_ListA((LPGUID)&GUID_NULL, id, buffer, bufferLength, 0); // 同样使用 GUID_NULL
if (cr == CR_SUCCESS)
{
char* currentInterface = buffer;
int j = 0;
while (*currentInterface)
{
cout << " " << j++ << ": " << currentInterface << endl;
currentInterface += strlen(currentInterface) + 1; // 移动到下一个接口字符串
}
}
delete[] buffer; // 释放内存
}
delete[] id;
}
SetupDiDestroyDeviceInfoList(devInfo);
return 0;
}
- 注意: 虽然
CM_Get_Device_Interface_List
可以和GUID_NULL
一起使用,但官方文档明确指出,结果列表 不会 按任何特定顺序排列,而且仅限于 激活的 接口(即与当前存在的设备实例关联的接口)。因此结果不保证完整. 如果确实需要获取所有的接口(不论激活与否), 还需结合方法一.
进阶技巧: 设备接口属性查询
拿到设备接口路径 (DevicePath) 后, 除了打开设备进行通信, 还可以进一步查询接口的属性。 可以使用 SetupDiGetDeviceInterfaceProperty
函数。
// 假设已通过上述方法获取到 detailData->DevicePath
// 查询 DEVPKEY_DeviceInterface_FriendlyName 属性 (接口的友好名称)
DEVPROPTYPE propertyType;
DWORD requiredSize;
WCHAR buffer[256];
if (SetupDiGetDeviceInterfacePropertyW(devInfo2, &interfaceData, &DEVPKEY_DeviceInterface_FriendlyName,
&propertyType, (PBYTE)buffer, sizeof(buffer), &requiredSize, 0)) {
if (propertyType == DEVPROP_TYPE_STRING) {
wcout << L" Friendly Name: " << buffer << endl;
}
}
通过修改 DEVPKEY_XXX
可以查询各种不同的设备接口属性。可以查看 MSDN 文档中的 "Device Interface Properties" 部分,找到所有可用的属性键。