How to count physical USB ports on Windows using Go (count remains stable regardless of connected devices)?
I am developing a Windows application in Go and need to programmatically determine the number of physical USB ports (host controller or hub ports) on the system.
Key requirements:
- Count physical USB ports only (not logical devices or connected USB devices)
- The count must not change when USB devices are plugged in or removed
- Solution must work on Windows
- Preferably implemented in Go (Golang), using Windows APIs like SetupAPI, WMI, etc.
Challenges with existing approaches:
- WMI queries (e.g.,
Win32_USBHub,Win32_PnPEntity) or SetupDi enumeration return logical devices, and counts fluctuate with device connections.
Specific questions:
- Is it possible to reliably enumerate physical USB ports on Windows?
- Which Windows API or interface provides this (e.g., USB host controller topology, hub descriptors)?
- How to call these APIs from Go? Any code examples or implementation guidance?
Appreciate examples, documentation, or explanations of Windows USB topology.
To count physical USB ports on Windows using Go, you need to enumerate USB host controllers and root hubs rather than connected devices. The most reliable approach uses SetupAPI with GUID_DEVINTERFACE_USB_HOST_CONTROLLER to find root hubs, then queries hub descriptors via IOCTL_USB_GET_NODE_INFORMATION to get the physical port count. This method remains stable regardless of device connections because it targets hardware topology rather than connected devices.
Contents
- Understanding USB Topology on Windows
- Windows APIs for Physical USB Port Enumeration
- Implementation Approach in Go
- Complete Go Code Example
- Testing and Validation
- Troubleshooting Common Issues
- Alternative Approaches and Limitations
- Sources
- [Conclusion](#conclusion}
Understanding USB Topology on Windows
USB architecture on Windows follows a hierarchical structure that’s crucial for physical port enumeration. A typical USB system consists of:
- USB Host Controllers: Hardware components that manage USB traffic (e.g., Intel USB 3.1 eXtensible Host Controller)
- Root Hubs: Built into the host controller, representing physical USB ports on the motherboard
- External Hubs: Connected devices that expand the number of available ports
- USB Devices: End devices connected to ports
The key insight is that physical USB ports are represented by root hubs, which are stable elements of the system. Unlike connected devices, which come and go, root hubs remain constant, making them ideal for physical port counting.
USBView demonstrates this concept well - it displays a tree structure where controllers expand to show root hubs with fixed port counts, regardless of what’s currently connected.
Windows APIs for Physical USB Port Enumeration
There are multiple approaches to enumerate physical USB ports on Windows, but some are more reliable than others:
The Recommended Approach: SetupAPI with IOCTL
The most stable method involves:
- Using SetupAPI to enumerate USB host controllers
- Accessing root hubs via symbolic links (e.g.,
\\.\USB#ROOT_HUB#...) - Sending IOCTL_USB_GET_NODE_INFORMATION to retrieve hub descriptors
- Parsing the hub descriptor to get the port count from the
bNumberOfPortsfield
This approach is demonstrated in the Microsoft Windows Driver Kit samples, specifically in the USBView source code.
Alternative Approaches and Their Limitations
WMI Queries
While Win32_USBHub and Win32_PnPEntity might seem useful, they enumerate logical devices that change with connections, making them unsuitable for stable physical port counting.
Device Manager Interface
The Device Manager API also presents logical devices and isn’t suitable for this purpose.
USB-specific APIs
The Windows USB driver stack provides low-level access through IOCTLs, which is what we’ll use in our Go implementation.
Implementation Approach in Go
Implementing physical USB port enumeration in Go requires several components:
- CGO bindings for Windows APIs
- SetupAPI functions to enumerate host controllers
- IOCTL operations to query hub information
- Error handling for Windows-specific operations
The Go implementation will follow these high-level steps:
- Import necessary Windows API packages
- Define required constants and structures
- Enumerate USB host controllers using SetupAPI
- Open each root hub device
- Send IOCTL_USB_GET_NODE_INFORMATION
- Extract and sum port counts
- Return the total physical port count
Complete Go Code Example
Here’s a complete implementation for counting physical USB ports in Go:
package main
import (
"fmt"
"syscall"
"unsafe"
)
const (
// Windows API constants
SPDRP_DEVICEDESC = 0x00000000
// SetupAPI constants
DIGCF_PRESENT = 0x00000002
DIGCF_DEVICEINTERFACE = 0x00000010
// IOCTL codes
IOCTL_USB_GET_NODE_INFORMATION = 0x220408
)
var (
// Windows API functions
setup32 = syscall.NewLazyDLL("setupapi.dll")
setupDiGetClassDevsW = setup32.NewProc("SetupDiGetClassDevsW")
setupDiEnumDeviceInfo = setup32.NewProc("SetupDiEnumDeviceInfo")
setupDiGetDeviceRegistryPropertyW = setup32.NewProc("SetupDiGetDeviceRegistryPropertyW")
setupDiDestroyDeviceInfoList = setup32.NewProc("SetupDiDestroyDeviceInfoList")
advapi32 = syscall.NewLazyDLL("advapi32.dll")
clsidFromString = advapi32.NewProc("CLSIDFromString")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
createFileW = kernel32.NewProc("CreateFileW")
closeHandle = kernel32.NewProc("CloseHandle")
deviceIoControl = kernel32.NewProc("DeviceIoControl")
)
// USB_NODE_INFORMATION represents USB hub information
type USB_NODE_INFORMATION struct {
NumberOfPorts uint8
HubCharacteristics uint16
PowerOnToPowerGood uint8
HubDescriptorLength uint8
}
// SP_DEVINFO_DATA structure for device information
type SP_DEVINFO_DATA struct {
Size uint32
ClassGuid syscall.GUID
DevInst uint32
Reserved uint32
}
func main() {
// Get all USB host controllers
h, err := getUSBHostControllers()
if err != nil {
fmt.Printf("Error getting USB host controllers: %v\n", err)
return
}
defer setupDiDestroyDeviceInfoList.Call(uintptr(h))
// Count physical ports across all host controllers
totalPorts := 0
for i := 0; ; i++ {
var devInfo SP_DEVINFO_DATA
devInfo.Size = uint32(unsafe.Sizeof(SP_DEVINFO_DATA{}))
ret, _, err := setupDiEnumDeviceInfo.Call(h, uintptr(i), uintptr(unsafe.Pointer(&devInfo)))
if ret == 0 {
break // No more devices
}
// Get device instance ID
instanceID, err := getDeviceInstanceID(h, &devInfo)
if err != nil {
continue
}
// Try to open root hub
portCount, err := getRootHubPortCount(instanceID)
if err != nil {
continue
}
totalPorts += portCount
fmt.Printf("Host controller %s: %d physical ports\n", instanceID, portCount)
}
fmt.Printf("Total physical USB ports: %d\n", totalPorts)
}
func getUSBHostControllers() (uintptr, error) {
// Get all USB devices (includes host controllers)
var guid syscall.GUID
guid.Data1 = 0xA5DCBF10
guid.Data2 = 0x6530
guid.Data3 = 0x11D2
guid.Data4[0] = 0x90
guid.Data4[1] = 0x1F
guid.Data4[2] = 0x00
guid.Data4[3] = 0xC0
guid.Data4[4] = 0x4F
guid.Data4[5] = 0xB9
guid.Data4[6] = 0x51
guid.Data4[7] = 0xED
ret, _, err := setupDiGetClassDevsW.Call(
uintptr(unsafe.Pointer(&guid)),
0,
0,
DIGCF_PRESENT|DIGCF_DEVICEINTERFACE,
)
if ret == uintptr(^uintptr(0)) { // INVALID_HANDLE_VALUE
return 0, fmt.Errorf("SetupDiGetClassDevsW failed: %v", err)
}
return ret, nil
}
func getDeviceInstanceID(h uintptr, devInfo *SP_DEVINFO_DATA) (string, error) {
// Get device instance ID
buf := make([]uint16, 1024)
bufSize := uint32(len(buf))
ret, _, err := setupDiGetDeviceRegistryPropertyW.Call(
h,
uintptr(unsafe.Pointer(devInfo)),
SPDRP_DEVICEDESC,
nil,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(bufSize),
nil,
)
if ret == 0 {
return "", fmt.Errorf("SetupDiGetDeviceRegistryPropertyW failed: %v", err)
}
return syscall.UTF16ToString(buf), nil
}
func getRootHubPortCount(instanceID string) (int, error) {
// Construct root hub device name
deviceName := "\\\\?\\" + instanceID + "#ROOT_HUB#"
// Open the root hub device
deviceHandle, err := createFileW.Call(
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(deviceName))),
0x80000000, // GENERIC_READ
0, // No sharing
0, // Default security attributes
3, // OPEN_EXISTING
0x40000000, // FILE_FLAG_SESSION_AWARE
0, // No template file
)
if deviceHandle == uintptr(^uintptr(0)) { // INVALID_HANDLE_VALUE
return 0, fmt.Errorf("CreateFileW failed: %v", err)
}
defer closeHandle.Call(deviceHandle)
// Prepare USB node information structure
var nodeInfo USB_NODE_INFORMATION
nodeInfoSize := uint32(unsafe.Sizeof(nodeInfo))
// Send IOCTL to get hub information
ret, _, err := deviceIoControl.Call(
deviceHandle,
IOCTL_USB_GET_NODE_INFORMATION,
0,
0,
uintptr(unsafe.Pointer(&nodeInfo)),
uintptr(nodeInfoSize),
nil,
0,
)
if ret == 0 {
return 0, fmt.Errorf("DeviceIoControl failed: %v", err)
}
return int(nodeInfo.NumberOfPorts), nil
}
This code follows the same pattern as the USBView source code from Microsoft’s Windows driver samples, which demonstrates the reliable enumeration of physical USB ports.
Testing and Validation
To validate your implementation:
-
Compare results with USBView:
- Download and run USBView
- Check the port count for each host controller
- Verify that your Go implementation matches these counts
-
Test with different system configurations:
- Systems with USB 2.0, 3.0, 3.1, and 3.2 controllers
- Systems with both built-in and add-on USB controllers
- Systems with various numbers of physical ports
-
Verify stability:
- Run your program with no devices connected
- Connect USB devices and run again
- Disconnect devices and run a third time
- The count should remain consistent
Troubleshooting Common Issues
Permission Errors
If you encounter permission issues:
- Run your Go program as Administrator
- Or implement proper privilege handling in your code
Missing Controllers
If some USB controllers aren’t detected:
- Ensure you’re using the correct GUID (GUID_DEVINTERFACE_USB_DEVICE)
- Check for 64-bit vs 32-bit compatibility issues
- Verify that USB controllers aren’t disabled in Device Manager
Incorrect Port Counts
If port counts seem wrong:
- Some controllers may have disabled ports
- Virtual controllers (e.g., for USB debugging) might be included
- Different IOCTL versions may be needed for different USB specifications
API Call Failures
If API calls fail:
- Check return values and error codes
- Verify that all required Windows APIs are available
- Ensure proper memory alignment for structures
Alternative Approaches and Limitations
WMI-Based Approach
While not recommended for stable physical port counting, WMI can be used for device enumeration:
import (
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/wmi"
)
func getWMIPorts() (int, error) {
// Initialize COM
ole.CoInitialize(0)
defer ole.CoUninitialize()
// Connect to WMI
server := "localhost"
namespace := "root\\cimv2"
var connectParams wmi.ConnectionParameters
connectParams.Server = &server
connectParams.Namespace = &namespace
// Query USB hubs
query := "SELECT * FROM Win32_USBHub"
var result []struct {
Name string
}
err := wmi.Query(query, &result, connectParams)
if err != nil {
return 0, err
}
// Note: This counts logical hubs, not physical ports
return len(result), nil
}
Registry-Based Approach
The Windows registry contains some USB controller information, but it’s less reliable than the IOCTL approach for physical port counting.
Sources
- USBView – Windows USB Device Viewer Tool - Official documentation for USB topology visualization
- GitHub - microsoft/Windows-driver-samples - Source code for USBView and implementation examples
- Host controller interface (USB, Firewire) - Wikipedia - Technical background on USB architecture
- GitHub · Where software is built - Community discussion on USB enumeration techniques
- How Many USB Ports Does My Motherboard Have? Easy Guide - Practical guide to USB port identification
Conclusion
Counting physical USB ports on Windows using Go is achievable through the proper use of Windows APIs. The most reliable method involves:
- Enumerating USB host controllers using SetupAPI
- Accessing root hubs via device symbolic links
- Querying hub descriptors with IOCTL_USB_GET_NODE_INFORMATION
- Summing the port counts from all root hubs
This approach provides a stable count that doesn’t fluctuate with device connections, as it targets hardware topology rather than connected devices. The implementation requires careful handling of Windows APIs and proper error handling, but the result is a robust solution for physical USB port enumeration in Go applications.
By following the code example and troubleshooting guidance provided, developers can implement reliable physical USB port counting in their Go applications, ensuring consistent results regardless of what USB devices are currently connected to the system.