Skip to content
WindowsDeep Dive Published Updated 4 min readViews unavailable

WinUSB Overlapped Pipe I/O: Completion, Cancellation, and Teardown

Handle WinUSB endpoint selection, interface-specific result queries, request or pipe aborts, and unplug recovery without confusing pipe IDs with interface handles.

WinUsb_ReadPipe() submits a transfer to a USB endpoint; a FALSE return with GetLastError() == ERROR_IO_PENDING means the read has not completed. WinUSB’s endpoint and interface rules are easy to mix up: PipeID is an endpoint address, while submission and result collection can require different interface handles. This guide focuses on those WinUSB-specific boundaries, pipe-wide abort, and device removal. Microsoft’s WinUsb_ReadPipe and WinUsb_GetOverlappedResult documentation define the API contract.

Resolve the endpoint and interface handles

The read’s PipeID is the endpoint descriptor’s bEndpointAddress: bit 7 marks direction (1 for IN, 0 for OUT). It is not an interface number or an arbitrary application ID. WinUsb_ReadPipe() uses the handle returned by WinUsb_Initialize() for the first interface, or the associated-interface handle returned by WinUsb_GetAssociatedInterface() for another interface.

Use a distinct manual-reset event for each in-flight request. Microsoft warns that another waiter can consume an auto-reset event before a later blocking WinUsb_GetOverlappedResult() call observes it. Keep each request’s buffer and OVERLAPPED stable until its WinUSB completion result has been collected.

Distinguish submission from the WinUSB result query

WinUsb_GetOverlappedResult() requires the first-interface handle returned by WinUsb_Initialize(), even when WinUsb_ReadPipe() was submitted through an associated-interface handle. Microsoft also documents Win32 GetOverlappedResult() with the original file handle from CreateFile() as an alternative; do not pass that raw file handle to WinUsb_GetOverlappedResult().

OVERLAPPED request = {0};
request.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); /* manual reset */
if (request.hEvent == NULL) {
    DWORD eventError = GetLastError();
    /* release the request record; preserve the event-creation error */
    SetLastError(eventError);
    /* Do not submit I/O with a null event. */
    return FALSE;
}

WINUSB_INTERFACE_HANDLE pipeInterface = firstInterfaceHandle;
/* Select an associated-interface handle here when needed. */
ULONG transferred = 0;
BOOL returned = WinUsb_ReadPipe(pipeInterface, pipeId,
                                buffer, bufferCapacity,
                                &transferred, &request);
DWORD submitError = returned ? ERROR_SUCCESS : GetLastError();

if (returned) {
    /* transferred is the completed byte count for immediate success */
} else if (submitError == ERROR_IO_PENDING) {
    /* Query only the operation that was actually pending. */
    ULONG completedBytes = 0;
    BOOL completed = WinUsb_GetOverlappedResult(
        firstInterfaceHandle, &request, &completedBytes, TRUE);
    if (completed) {
        transferred = completedBytes;
    } else {
        /* capture GetLastError(); completion failed */
    }
} else {
    /* immediate failure: no pending completion to consume */
}

The example assumes firstInterfaceHandle came from WinUsb_Initialize(); set pipeInterface to the associated handle when the endpoint belongs to another interface. A successful immediate return is already a completed submission, FALSE with ERROR_IO_PENDING needs a result query, and another error is an immediate failure. Capture GetLastError() immediately after FALSE. For a pending request, obtain the final byte count from WinUsb_GetOverlappedResult(); the original LengthTransferred output is not the completed count. With bWait == FALSE, an unfinished request returns FALSE and ERROR_IO_INCOMPLETE. Keep a blocking wait off the UI thread.

Choose targeted cancellation or pipe-wide abort

For one transfer, CancelIoEx() takes the original file handle from CreateFile() and that request’s OVERLAPPED; it does not take the WINUSB_INTERFACE_HANDLE. Cancellation can race with ordinary completion, so collect the WinUSB result before reusing the request state (CancelIoEx).

WinUsb_AbortPipe() synchronously aborts all pending transfers on one endpoint. It is pipe-wide, unlike targeted CancelIoEx(), and is not the same as resetting a stalled pipe; use the documented reset operation only when the endpoint’s state calls for it.

On unplug, stop submitting to the affected device generation, cancel its requests or abort the relevant pipes, and collect their terminal results before releasing buffers, events, interface handles, and the device handle. When the device is re-enumerated, rebuild the interface and endpoint mapping and associate new completions with a new generation so late results cannot be attributed to the replacement device.

Diagnose the endpoint or device boundary

Log the device generation, interface, endpoint address and direction, requested and transferred lengths, immediate submission result, and final WinUSB completion error. Correlate them with the selected endpoint descriptor and exact device instance; a friendly name is insufficient when identical devices are attached.

  • If submission fails immediately, verify the interface handle, endpoint PipeID, and immediate GetLastError() before changing pipe policy.
  • An IN read can remain pending while the device has no data; compare the endpoint’s expected behavior and timeout policy before treating that state as a failure.
  • If completion reports an error or short transfer, preserve the final error and actual byte count from the result query rather than relying on the submission output.

Test unplug during an active transfer, a timed-out IN read, a short transfer, cancellation racing with completion, repeated re-enumeration, and multiple outstanding requests on a disposable device or fixture. Tune pipe policies only after endpoint selection and completion handling are correct.

The C fragment was not compiled, and no Windows host, USB device, or hardware-in-loop test was run here. WinUSB behavior also depends on endpoint descriptors, device firmware, and pipe policy; validate the exact device protocol before shipping.

Related:

Sources:

Comments