Enumerating Serial Ports and Handling Hardware Changes via Win32 APIs
Enumerating Ports via the SetupAPI
The SetupAPI provides a structured way to query hardware devices currently present on the system. By targeting the GUID_DEVCLASS_PORTS class GUID, developers can retrieve both the logical port identifier and the associated hardware description.
std::vector<std::wstring> EnumeratePortsViaSetupApi() {
std::vector<std::wstring> discovered_ports;
HDEVINFO device_set = SetupDiGetClassDevsW(
&GUID_DEVCLASS_PORTS, nullptr, nullptr, DIGCF_PRESENT
);
if (device_set == INVALID_HANDLE_VALUE) {
return discovered_ports;
}
SP_DEVINFO_DATA device_info{};
device_info.cbSize = sizeof(SP_DEVINFO_DATA);
for (DWORD index = 0; ; ++index) {
if (!SetupDiEnumDeviceInfo(device_set, index, &device_info)) {
if (GetLastError() == ERROR_NO_MORE_ITEMS) break;
continue;
}
wchar_t hardware_desc[256] = {0};
DWORD prop_size = sizeof(hardware_desc);
if (!SetupDiGetDeviceRegistryPropertyW(
device_set, &device_info, SPDRP_DEVICEDESC, nullptr,
reinterpret_cast<PBYTE>(hardware_desc), prop_size, &prop_size)) {
continue;
}
HKEY device_params_key = SetupDiOpenDevRegKey(
device_set, &device_info, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ
);
if (device_params_key == INVALID_HANDLE_VALUE) continue;
wchar_t port_label[64] = {0};
DWORD label_len = sizeof(port_label);
if (RegGetValueW(device_params_key, nullptr, L"PortName",
RRF_RT_REG_SZ, nullptr, port_label, &label_len) == ERROR_SUCCESS) {
std::wstring full_entry = port_label;
full_entry += L" - ";
full_entry += hardware_desc;
discovered_ports.push_back(full_entry);
}
RegCloseKey(device_params_key);
}
SetupDiDestroyDeviceInfoList(device_set);
return discovered_ports;
}
This approach offers hardware context but has limitations. Certain virtual or bridged serial adapters register under custom device classes, meaning they will not appear when filtering strictly by the stendard ports GUID. Modifying the property query allows identification of specific interfaces like Bluetooth or USB-to-serial converters.
Querying the System Registry
An alternative method involves reading the hardware device map maintained by the kernel. The SERIALCOMM key maps logical COM identifiers to physical or virtual endpoints.
std::vector<std::wstring> EnumeratePortsViaRegistry() {
std::vector<std::wstring> com_list;
HKEY serial_map_key = nullptr;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"HARDWARE\\DEVICEMAP\\SERIALCOMM",
0, KEY_READ, &serial_map_key) != ERROR_SUCCESS) {
return com_list;
}
for (DWORD index = 0; ; ++index) {
wchar_t val_name[128] = {0};
wchar_t port_name[64] = {0};
DWORD name_len = ARRAYSIZE(val_name);
DWORD data_len = sizeof(port_name);
LSTATUS status = RegEnumValueW(serial_map_key, index, val_name, &name_len,
nullptr, nullptr, reinterpret_cast<LPBYTE>(port_name), &data_len);
if (status != ERROR_SUCCESS) {
if (status == ERROR_NO_MORE_ITEMS) break;
continue;
}
com_list.emplace_back(port_name);
}
RegCloseKey(serial_map_key);
return com_list;
}
The strings returned from this routine are clean COM designations that can be passed directly to file handle creation routines. However, this method strips away hadrware metadata, making it impossible to distinguish between a physical UART and a network-based virtual adapter.
Unifying Enumeration Results
Combining both techniques ensures comprehensive coverage. The registry list acts as a fallback for devices missing from the SetupAPI enumeration.
void ConsolidatePortLists(std::vector<std::wstring>& primary, const std::vector<std::wstring>& secondary) {
for (const auto& reg_port : secondary) {
bool already_exists = false;
for (const auto& pnp_port : primary) {
if (pnp_port.find(reg_port) == 0) {
already_exists = true;
break;
}
}
if (!already_exists) {
primary.push_back(reg_port);
}
}
}
Tracking Connection State Changes
Monitoring dynamic hardware attachment or removal requires periodic state comparison or direct OS notification.
A polling strategy involves capturing the active port list at fixed intervals. By maintaining two collections representing the current and previous states, the application can compute the difference. Newly added entries indicate an insertion, while missing entries from the prior collection signal a removal. Swapping pointers or references between the collections after each cycle optimizes memory allocation, functioning similarly to a double-buffered rendering pipeline.
For deterministic and low-latency responses, intercepting system broadcast messages is preferred. The window procedure should handle WM_DEVICECHANGE and inspect the wParam value.
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
if (msg == WM_DEVICECHANGE) {
if (wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE) {
DEV_BROADCAST_HDR* hdr = reinterpret_cast<DEV_BROADCAST_HDR*>(lParam);
if (hdr && hdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) {
DEV_BROADCAST_DEVICEINTERFACEW* dev_iface =
reinterpret_cast<DEV_BROADCAST_DEVICEINTERFACEW*>(hdr);
// Process dev_iface->dbcc_name to identify the specific COM port
}
} else if (wParam == DBT_DEVICEREMOVEPENDING) {
// Initiate graceful state preservation before the device is detached
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
This event-driven model eliminates polling overhead and provides immediate notification. Applications must ensure critical data is flushed to persistent storage during the pending removal phase, as the OS does not guarantee a delay for cleanup routines.