Skip to content

Windows Implementation

Manuel Bl edited this page Feb 25, 2024 · 6 revisions

The Windows implementation is by far the most complex implementation, mainly due to the complex model of how composite USB devices are presented in APIs. A particular style of APIs using variable length structs and functions with many arguments are not helpful either.

Also see Windows reference code.

Device enumeration

For device enumeration, the Setup API is used. As the name already implies, it was built for a different purpose, and it isn't obvious from the documentation how to get information about USB devices.

In order to get details about a USB device, a DeviceIoControl request is made using IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX. The request is not issued to the device but rather to the USB hub device where the device is connected to. So the USB hub must be opened first. That way the USB device and configuration descriptors can be received without directly opening the USB device, which is not possible for devices already in use or devices not using the WinUSB driver.

Composite devices are represented as a parent and multiple child devices. The code assumes that a device is a composite device if usbccgp (USB Generic Parent Driver) is used as the device's driver (Windows calls it "device service").

Enumerating the child devices is not too difficult but determining the device path is. The implemented solution is to open the registry key for device configuration information (SetupDiOpenDiRegKey), lookup the registry value DeviceInterfaceGUIDs, split it into a list of GUIDs and finally use the Setup API for each one to look up the device path until the first one succeeds.

A further challenges is to figure out which USB interfaces the child device is responsible for as this information will be needed to use the WinUSB APIs. A child device can be responsible for a single USB interface, or for multiple ones. In the case of multiple ones, they must have consecutive numbers (given how interface associations work). The implemented solution determines uses the list of hardware IDs and scans them for the pattern USB\VID_nnnn&PID_nnnn&MI_nn. The last 2 digits (nn) in this pattern represent the number of the child device's first interface. The pattern is documented by Microsoft in Multiple-Interface USB Devices. But it is unclear if this will always stay like this.

In order to open an interface of a composite device, the child device of the particular composite function must be opened first (using CreateFileW) and then the first interface of the function (using WinUsb_Initialize). If the interface to be opened is not the first interface of the function, then it must be additionally claimed using WinUsb_GetAssociatedInterface.

Device monitoring

Notifications about connected and disconnected devices are registered with the Kernel32 function RegisterDeviceNotification, and not with a Setup API function as one would expect. In line with the original Windows architecture, notification messages are sent to a window. To receive them, a so called Message-Only Window is created. It never appears on screen and is excluded from most window enumerations to not confuse UI applications.

An upcall stub of the Foreign Function And Memory API is used to register Java code as the message handler for the window.

The background process waits in a window message loop for the next event.

For composite devices, the notification about a connected device refers to the parent device. When it is received, the child devices are likely not yet ready and the Setup API will return an empty or incomplete list of child instance IDs. Thus, retrieving information about child devices is delayed until the first interface is claimed and the child's device path is actually needed. Even then, the device path information might not be available yet. This case can be detected (composite device and missing child information). So claiming the interface is retried several times until it succeeds or 3 seconds have passed.

Communication with the device

As described in Composite Devices and Interface Associations, a composite USB device consists of multiple functions, and each function is represented as a separate Windows device in Windows. Before communicating with an endpoint, the correct child device and the correct interface must be opened.

There are no APIs to retrieve the list of functions. So the configuration descriptor (including the interface association descriptors) is parsed. The result is a list of functions. Each function consists of the interface number of the first interface and the count of interfaces. This is matched with the details of the child devices.

Managing the device and interface handles (including a reference count for device handles) requires additional code not necessary on Linux and macOS.

This complexity also affects control transfers. If the recipient is an interface or endpoint, they must be sent using the interface handle of the particular interface or endpoint. If the recipient is the device, they can be sent using any open interface handle.

Aside from the challenges with composite devices and multiple interface, the WinUSB APIs are straightforward to use from Java. They have limitations though as they do not support changing the device configuration or using USB 3 streams.

Asynchronous task completion thread

Asynchronous operations use so called Overlapped I/O and an OVERLAPPED structure. The completion will be reported to an I/O completion port.

The background thread waits for completion packets on the I/O completion port. Each USB device is associated with the completion port.

Additionally, the background thread maintains a pool of OVERLAPPED instances and manages their (temporary) relationship to a Transfer instance.

Timeouts on transfer operations are implemented using Object.wait() and Object.notify() on the Transfer instance.