Win32 Raw Input: Device Registration, WM_INPUT, and Hot-Plug
Register HID collections, read WM_INPUT with bounded buffers, track device changes, and choose raw or buffered Win32 input without breaking the message pump.
Win32 Raw Input is a device-oriented path for desktop applications that need data beyond translated keyboard and mouse messages. An application registers the Human Interface Device (HID) top-level collections it wants, then receives WM_INPUT records. This can distinguish two mice, expose keyboard scan codes, or deliver reports from a supported HID without opening the device directly.
Raw Input is not a general device-management API, a text-input framework, or a substitute for Plug and Play (PnP). Registration selects a usage page and usage, not an arbitrary USB endpoint or a permanent physical-device identity. Use it when an interactive application needs input records from selected device classes. Use PnP and device-interface APIs for installation, driver health, and broader device lifecycle questions.
Register a collection, not a friendly device name
Raw Input is opt-in. A RAWINPUTDEVICE entry identifies a top-level collection with a HID usage page and usage ID. Generic Desktop page 0x01 with usage 0x02 selects mice; usage 0x06 selects keyboards. The registration can be made even when a matching device is not currently attached, so an application can register once during startup and receive input when a matching device becomes available.
Only one window in a process can receive raw input for a given device class; a later registration changes the receiving window. Microsoft warns libraries not to register independently because they can take input away from the application. Make registration the responsibility of the application-level input owner, and query GetRegisteredRawInputDevices when diagnosing active flags and targets.
This example registers a window for generic mouse and keyboard input and asks for raw-device arrival/removal notifications:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
BOOL register_raw_input(HWND target)
{
RAWINPUTDEVICE devices[2] = {0};
devices[0].usUsagePage = 0x01; /* Generic Desktop */
devices[0].usUsage = 0x02; /* Mouse */
devices[0].dwFlags = RIDEV_DEVNOTIFY;
devices[0].hwndTarget = target;
devices[1].usUsagePage = 0x01;
devices[1].usUsage = 0x06; /* Keyboard */
devices[1].dwFlags = RIDEV_DEVNOTIFY;
devices[1].hwndTarget = target;
return RegisterRawInputDevices(
devices, 2, sizeof(devices[0]));
}
Check the Boolean result and capture GetLastError immediately on failure. RIDEV_DEVNOTIFY enables WM_INPUT_DEVICE_CHANGE; Microsoft documents that flag as unsupported on Windows XP and available beginning with Windows Vista. The underlying Raw Input APIs support desktop applications starting with Windows XP, but this specific notification-enabled example therefore has a Vista-or-later requirement. If XP compatibility is genuinely required, omit the notification flag and do not expect those arrival/removal messages.
Do not add RIDEV_NOLEGACY casually. For keyboard or mouse registrations it suppresses the corresponding legacy messages for the application, such as WM_KEYDOWN or WM_LBUTTONDOWN. A GUI framework may depend on those messages for controls, accelerators, accessibility, or text entry. Decide whether the application will consume raw events, translated events, or both, then test the combined behavior rather than assuming the two streams are interchangeable.
Read each WM_INPUT record with its reported size
For the standard read path, the window procedure receives an HRAWINPUT in lParam. Pass that handle to GetRawInputData with RID_INPUT. A common pattern first asks for the required size with a null data pointer, allocates an aligned buffer, and then copies the record. Microsoft says the output buffer should be DWORD-aligned; memory returned by HeapAlloc is suitably aligned for the Win32 structure.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stddef.h>
static BOOL read_raw_record(HRAWINPUT handle)
{
UINT bytes = 0;
UINT probe = GetRawInputData(
handle, RID_INPUT, NULL, &bytes, sizeof(RAWINPUTHEADER));
if (probe != 0 || bytes < sizeof(RAWINPUTHEADER))
return FALSE;
RAWINPUT *raw = HeapAlloc(GetProcessHeap(), 0, bytes);
if (raw == NULL)
return FALSE;
UINT capacity = bytes;
UINT copied = GetRawInputData(
handle, RID_INPUT, raw, &capacity, sizeof(RAWINPUTHEADER));
BOOL valid = copied != (UINT)-1 &&
copied >= sizeof(RAWINPUTHEADER) && copied <= bytes &&
raw->header.dwSize >= sizeof(RAWINPUTHEADER) &&
raw->header.dwSize <= copied;
if (valid) {
switch (raw->header.dwType) {
case RIM_TYPEMOUSE:
valid = raw->header.dwSize >=
offsetof(RAWINPUT, data) + sizeof(RAWMOUSE);
if (valid) {
/* Consume raw->data.mouse according to the app's input model. */
}
break;
case RIM_TYPEKEYBOARD:
valid = raw->header.dwSize >=
offsetof(RAWINPUT, data) + sizeof(RAWKEYBOARD);
if (valid) {
/* Consume raw->data.keyboard; this is not a text string. */
}
break;
case RIM_TYPEHID: {
size_t data_offset = offsetof(RAWINPUT, data) +
offsetof(RAWHID, bRawData);
if (raw->header.dwSize < data_offset + sizeof(BYTE)) {
valid = FALSE;
break;
}
const RAWHID *hid = &raw->data.hid;
size_t payload_bytes = raw->header.dwSize - data_offset;
valid = hid->dwSizeHid != 0
? hid->dwCount <= payload_bytes / hid->dwSizeHid
: hid->dwCount == 0;
if (valid) {
/* Consume only the validated reports in raw->data.hid. */
}
break;
}
default:
valid = FALSE;
break;
}
}
HeapFree(GetProcessHeap(), 0, raw);
return valid;
}
The reader checks the copied/header extent, then validates the fixed mouse or keyboard member size. For HID it checks the variable payload before consumption using division, avoiding an unchecked dwSizeHid * dwCount multiplication. Application-specific parsing must still validate supported report IDs and layouts; reject unexpected report lengths rather than reading past the record.
RAWKEYBOARD exposes fields such as MakeCode, flags, a virtual-key code, and a corresponding legacy message. It does not hand the application a ready-to-display Unicode string. Text interpretation depends on keyboard layout and state and should use the appropriate Windows keyboard-input APIs; do not log raw key events as though they were harmless diagnostics. RAWMOUSE also carries flags that distinguish relative from absolute movement, button transitions, and wheel data. Treat those fields according to their flags: an absolute coordinate is not a relative delta, and wheel direction/value is encoded separately from movement.
For foreground input (RIM_INPUT), Microsoft requires the application to call DefWindowProc so the system can perform cleanup. A robust window procedure does this even if reading or parsing the record fails. Background input (RIM_INPUTSINK) is a separate registration choice; only request it when the product needs to receive device input without foreground focus.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
LRESULT CALLBACK input_window_proc(
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_INPUT:
(void)read_raw_record((HRAWINPUT)lParam);
if (GET_RAWINPUT_CODE_WPARAM(wParam) == RIM_INPUT)
return DefWindowProcW(hwnd, message, wParam, lParam);
return 0;
case WM_INPUT_DEVICE_CHANGE:
if (wParam == GIDC_ARRIVAL) {
/* Query lParam with GetRawInputDeviceInfo while it is present. */
} else if (wParam == GIDC_REMOVAL) {
/* Retire per-device state keyed by the removed handle. */
}
return 0;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
The device-change branch requires RIDEV_DEVNOTIFY. Arrival/removal notifications expose a raw-device handle in lParam; on arrival, GetRawInputDeviceInfo can retrieve a name or RID_DEVICE_INFO. Retire caches on removal. Treat the handle as a live-session key, not a stable reconnect identity; use a separate device-interface identity when long-lived identity matters.
Choose standard or buffered reads deliberately
The standard path processes the HRAWINPUT for one WM_INPUT message with GetRawInputData. It is the simplest option and is adequate for most applications. A high-rate device can produce more events than a UI loop should parse synchronously; in that case, measure queue latency and consider GetRawInputBuffer, which retrieves an array of queued records. Walk that array with NEXTRAWINPUTBLOCK, not by assuming every record has a fixed size. For WOW64 callers, follow the API’s additional buffer-size and eight-byte alignment guidance rather than assuming native-process layout rules apply.
There is a subtle boundary when mixing the two paths. GetMessage removes the current WM_INPUT from the raw-input queue before returning it, so a later GetRawInputBuffer call does not include that event. Microsoft documents the safe sequence as: first read the current message’s lParam with GetRawInputData, then call GetRawInputBuffer in a loop to drain additional records that arrived meanwhile. Calling only the buffered API after retrieving a message can silently skip the event represented by that message.
There is also a message-pump constraint for a buffered-only design. Microsoft warns against combining RIDEV_DEVNOTIFY with a loop that filters WM_INPUT out of dispatch: device-change messages share the raw-input queue and can be stranded by those filters. If the application needs hot-plug callbacks, keep a loop that dispatches them or use the standard message-driven pattern.
Keep the window procedure bounded. Parse enough to validate and timestamp the event, then enqueue a small owned record for heavier application work. Do not retain the HRAWINPUT after its message handling or pass pointers into the temporary HeapAlloc buffer to another thread. Copy the fields or report bytes the consumer needs, with an explicit ownership rule and a bounded queue. This keeps device bursts from blocking painting or ordinary input dispatch.
Diagnose registration and hot-plug as separate layers
If no WM_INPUT arrives, verify registration, target-window lifetime, the usage pair, and whether another registration replaced the target. Inspect registrations with GetRegisteredRawInputDevices; enumerate attached devices with GetRawInputDeviceList and query handles with GetRawInputDeviceInfo, following its per-command size and buffer contract.
Arrival/removal messages are not a PnP health report. WM_INPUT_DEVICE_CHANGE tells a registered Raw Input client that a matching input device arrived or was removed. It does not explain driver installation, device problem codes, or whether another application can open a related interface. Correlate it with PnP instance state only when the incident needs that broader lifecycle view. Keep hot-plug cleanup idempotent: a removal should discard device-specific state without assuming that the rest of the input subsystem or the process is shutting down.
Raw keyboard collection also has a privacy consequence. Foreground-only collection should be the default for an interactive feature. RIDEV_INPUTSINK explicitly allows the app to receive input while it is not foreground; do not use it to collect keystrokes or other sensitive input outside the visible task that requires it. Minimize registered collections, avoid raw-input logging by default, redact device-interface names where they can reveal peripherals, and make background capture explicit and reviewable.
Test the input contract, not just successful registration
Test two devices of the same class to verify input is not merged. Exercise arrival, removal during input, disconnects during metadata lookup, and rejected report lengths. Cover keyboard modifiers and non-US layouts, mouse relative/absolute movement and wheel flags, high-rate bursts, and expected legacy-message behavior when RIDEV_NOLEGACY is absent.
Profile before adopting buffered reads. Test message filters, nested loops, shutdown with input pending, registration/read failures, and ensure the current message is processed before draining later records. The application still owns input semantics, privacy, queue bounds, and per-device state retirement.
Related:
- Windows PnP Runtime Diagnostics: Devnodes, Problem Codes, and Device State
- Windows Thread Message Queues: Posting Work Without Breaking the Pump
Sources:
- Raw Input Overview - Microsoft Learn
- RegisterRawInputDevices function - Microsoft Learn
- RAWINPUTDEVICE structure - Microsoft Learn
- GetRawInputData function - Microsoft Learn
- WM_INPUT message - Microsoft Learn
- WM_INPUT_DEVICE_CHANGE message - Microsoft Learn
- GetRawInputBuffer function - Microsoft Learn
- RAWINPUT structure - Microsoft Learn
- RAWHID structure - Microsoft Learn
- RAWKEYBOARD structure - Microsoft Learn
- ToUnicodeEx function - Microsoft Learn
- RAWMOUSE structure - Microsoft Learn
- GetRawInputDeviceInfoW function - Microsoft Learn
- GetRawInputDeviceList function - Microsoft Learn
- GetRegisteredRawInputDevices function - Microsoft Learn
- Using Raw Input - Microsoft Learn
- Plug and Play Manager - Microsoft Learn
- SetupDiGetDeviceInterfaceDetailW function - Microsoft Learn