返回

Windows SetupAPI:获取设备接口实例详解

windows

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 GUIDClassGuid 代表的是设备的安装类 (Setup Class),而我们要找的是设备接口类 (Interface Class)。 打个比方, "显示适配器"是一个安装类,而 "显示器" 或 "图形处理器" 可以作为其接口类。 一个设备可以属于某个安装类,同时又暴露多个不同类型的接口。

解决方案

解决思路就是,先获取设备实例关联的所有接口类 GUID,然后利用这些 GUID 来枚举每个接口类的实例。

方法一: 使用 SetupDiGetDeviceInterfaceClassGuids (推荐)

SetupDiGetDeviceInterfaceClassGuids 这个函数可以直接获取到和指定设备实例相关联的所有接口类 GUID。

  1. 原理: SetupDiGetDeviceInterfaceClassGuids 函数可以直接获取与给定设备实例关联的所有接口类的 GUID 列表。这省去了我们自己去查询注册表或执行其他复杂操作的步骤。

  2. 代码示例:

    #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;
    }
    
    
  3. 安全建议: 在使用 SetupAPI 时,要注意资源释放。例如 HDEVINFO 句柄需要使用 SetupDiDestroyDeviceInfoList 释放, 动态分配的内存(例如 detailData)需要用 free 释放,防止内存泄漏。

方法二:使用 CM_Get_Device_Interface_List

这个方法使用 Configuration Manager 函数来获取设备接口列表, 它通过设备实例 ID (Device Instance ID) 直接获取相关的设备接口路径.

  1. 原理: CM_Get_Device_Interface_List 函数需要提供设备的 Instance ID。 通过设备的 Instance ID,可以直接获取到该设备所有已注册接口的路径列表。 这个方法避免了遍历 GUID 的过程,比较直接。

  2. 代码示例:

#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;
}

  1. 注意: 虽然 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" 部分,找到所有可用的属性键。