Search code examples
cwinapiraw-inputdeviceiocontrol

How to use DeviceIoControl on a rawinput device handle


So I have a program that is using multiple keyboards as input using raw input and I want to mess with the indicator lights of caps lock, scroll lock, and num lock per keyboard.

So my approach was to RegisterRawInputDevices for keyboards using the RIDEV_DEVNOTIFY flag. Then when I get a WM_INPUT_DEVICE_CHANGE message with a GIDC_ARRIVAL wParam. I would save off the lParam like HANDLE hDevice = (HANDLE)Message.lParam; then later when I get a WM_INPUT I can act on the key to mess with the indicator lights using DeviceIoControl with the handle in the RAWINPUT struct from GetRawInputData on the lParam. However DeviceIoControl does not like the header.hDevice in the RAWINPUT struct. How would I use DeviceIoControl per keyboard with the raw input handles?

Here is a program that demos the above trying to make the caps lock indicator light per keyboard

#ifndef NOMINMAX
#define NOMINMAX
#endif

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

//windows
#include <windows.h>
#include <dbt.h>
#include <Ntddkbd.h>

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>

typedef uint8_t  u8;
typedef uint32_t u32;
typedef int64_t  s64;
typedef float    f32;

#define DEFAULT_SCREEN_WIDTH 1280
#define DEFAULT_SCREEN_HEIGHT 720

#define MINIMUM_SCREEN_WIDTH 300
#define MINIMUM_SCREEN_HEIGHT 300

typedef struct GameState
{
    u8 hwIsRunning;
} GameState;

typedef struct Renderer
{
    //ScreenGraphics State
    u8 hwIsMinimized;

    //Win32 Screen Variables
    u32 dwScreenWidth = DEFAULT_SCREEN_WIDTH;
    u32 dwScreenHeight = DEFAULT_SCREEN_HEIGHT;
    HWND MainWindowHandle;
    const char *szWindowName = "FPS Camera Basic";
} Renderer;

Renderer sRENDERER;
GameState sGAMESTATE;

u32 max( u32 a, u32 b )
{
    return a > b ? a : b;
}

int logWindowsError(const char* msg)
{
    LPVOID lpMsgBuf;
    DWORD dw = GetLastError();

    FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, dw, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPTSTR)&lpMsgBuf, 0, NULL );
    OutputDebugStringA( msg );
    OutputDebugStringA( (LPCTSTR)lpMsgBuf );

    LocalFree( lpMsgBuf );
    return -1;
}

inline
void CloseProgram()
{
    sGAMESTATE.hwIsRunning = 0;
}

LRESULT CALLBACK
Win32MainWindowCallback(
    HWND Window,  
    UINT Message,
    WPARAM WParam, 
    LPARAM LParam)
{
    LRESULT Result = 0;
    switch (Message)
    {

    case WM_SYSCHAR:
    break;

    case WM_SIZE:
    {
        u32 dwTempScreenWidth = LOWORD( LParam );
        u32 dwTempScreenHeight = HIWORD( LParam );
        if( WParam == SIZE_MINIMIZED || dwTempScreenWidth == 0 || dwTempScreenHeight == 0 )
        {
            sRENDERER.hwIsMinimized = 1;
        }
        else
        {
            sRENDERER.hwIsMinimized = 0;
        }
        sRENDERER.dwScreenWidth =  max( 1, dwTempScreenWidth );
        sRENDERER.dwScreenHeight = max( 1, dwTempScreenHeight );

    }break;


    case WM_GETMINMAXINFO:
    {
        LPMINMAXINFO lpMMI = (LPMINMAXINFO)LParam;
        lpMMI->ptMinTrackSize.x = MINIMUM_SCREEN_WIDTH;
        lpMMI->ptMinTrackSize.y = MINIMUM_SCREEN_HEIGHT;
    }break;

    case WM_CLOSE: //when user clicks on the X button on the window
    {
        CloseProgram();
    } break;

    case WM_ACTIVATE:
    {
        switch(WParam)
        {
            //WM_MOUSEACTIVATE 
            case WA_ACTIVE:
            case WA_CLICKACTIVE:
            case WA_INACTIVE:
            default:
            {
                break;
            }
        }
    } break;

    default:
        Result = DefWindowProc( Window, Message, WParam, LParam ); //call windows to handle default behavior of things we don't handle
    }

    return Result;
}


inline
void InitRawInput( HWND WindowHandle )
{ 
    RAWINPUTDEVICE Rid[2];
    Rid[0].usUsagePage = (USHORT) 0x01; 
    Rid[0].usUsage = (USHORT) 0x02; 
    Rid[0].dwFlags = RIDEV_INPUTSINK|RIDEV_DEVNOTIFY;   //RIDEV_INPUTSINK: If set, this enables the caller to receive the input even when the caller is not in the foreground
    Rid[0].hwndTarget = WindowHandle;

    Rid[1].usUsagePage = (USHORT) 0x01; 
    Rid[1].usUsage = (USHORT) 0x06; 
    Rid[1].dwFlags =  RIDEV_INPUTSINK | RIDEV_DEVNOTIFY | RIDEV_NOLEGACY;   //RIDEV_INPUTSINK: If set, this enables the caller to receive the input even when the caller is not in the foreground
    Rid[1].hwndTarget = WindowHandle;

    RegisterRawInputDevices( Rid, 2, sizeof( Rid[0] ) );

}

inline
void InitWin32Window()
{
    WNDCLASSEX WindowClass;
    WindowClass.cbSize = sizeof( WNDCLASSEX );
    WindowClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW; //https://devblogs.microsoft.com/oldnewthing/20060601-06/?p=31003
    WindowClass.lpfnWndProc = Win32MainWindowCallback;
    WindowClass.cbClsExtra = 0;
    WindowClass.cbWndExtra = 0;
    WindowClass.hInstance = GetModuleHandle( NULL );
    WindowClass.hIcon = LoadIcon( 0, IDI_APPLICATION ); //IDI_APPLICATION: Default application icon, 0 means use a default Icon
    WindowClass.hCursor = LoadCursor( 0, IDC_ARROW ); //IDC_ARROW: Standard arrow, 0 means used a predefined Cursor
    WindowClass.hbrBackground = NULL; 
    WindowClass.lpszMenuName = NULL;    // No menu 
    WindowClass.lpszClassName = "WindowTestClass"; //name our class
    WindowClass.hIconSm = NULL; //can also do default Icon here? will NULL be default automatically?

    if ( !RegisterClassEx( &WindowClass ) )
    {
        logWindowsError( "Failed to Register Window Class:\n" );
        sRENDERER.MainWindowHandle = 0;
        return;
    }

    HWND WindowHandle = CreateWindowEx( 0, WindowClass.lpszClassName, sRENDERER.szWindowName,
        WS_OVERLAPPEDWINDOW | WS_VISIBLE,  CW_USEDEFAULT, CW_USEDEFAULT, sRENDERER.dwScreenWidth, sRENDERER.dwScreenHeight, //if fullscreen get monitor width and height
        0, 0, WindowClass.hInstance, NULL );

    if ( !WindowHandle )
    {
        logWindowsError( "Failed to Instantiate Window Class:\n" );
        sRENDERER.MainWindowHandle = 0;
        return;   
    }

    sRENDERER.MainWindowHandle = WindowHandle;
    InitRawInput( sRENDERER.MainWindowHandle  );
}

inline
void InitStartingGameState()
{
    sGAMESTATE.hwIsRunning = 1;
}

u32 dwNumHandles = 0;
typedef struct DoubleHandle
{
    HANDLE hHandle0;
    HANDLE hHandle1;
} DoubleHandle;
DoubleHandle handles[32];

//Use subsystem console when compiling
int main()
{
    InitStartingGameState();

    InitWin32Window();

    if( !sRENDERER.MainWindowHandle )
    {
        return -1;
    }

    while( sGAMESTATE.hwIsRunning )
    {
        MSG Message;
        while( PeekMessage( &Message, 0, 0, 0, PM_REMOVE ) )
        {
            switch( Message.message )
            {
                case WM_QUIT:
                {
                    CloseProgram();
                    break;
                }
                case WM_SYSKEYDOWN:
                case WM_SYSKEYUP:
                case WM_KEYDOWN:
                case WM_KEYUP:
                {
                    break;
                }
                case WM_INPUT_DEVICE_CHANGE:
                {
                    HANDLE hDevice = (HANDLE)Message.lParam;
                    RID_DEVICE_INFO deviceInfo;
                    deviceInfo.cbSize = sizeof(RID_DEVICE_INFO);
                    u32 dwSize = sizeof(RID_DEVICE_INFO);
                    GetRawInputDeviceInfo(hDevice,RIDI_DEVICEINFO,&deviceInfo,&dwSize);

                    u32 dwNameSize = 0;
                    u32 res = GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, nullptr, &dwNameSize);

                    char *pName = new char[dwNameSize+1];
                    res = GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, pName, &dwNameSize);
                    pName[dwNameSize] = 0;

                    switch(Message.wParam)
                    {
                        case GIDC_ARRIVAL:
                        {
                            switch(deviceInfo.dwType)
                            {
                                case RIM_TYPEKEYBOARD:
                                {
                                    u32 dwKeyBoard = dwNumHandles++;
                                    handles[dwKeyBoard].hHandle0 = hDevice;
                                } break;
                                default:
                                {

                                } break;
                            }
                            printf("GIDC_ARRIVAL %p %d %s\n",hDevice,deviceInfo.dwType,pName);
                        } break;
                        case GIDC_REMOVAL:
                        {
                            switch(deviceInfo.dwType)
                            {
                                case RIM_TYPEKEYBOARD:
                                {

                                } break;
                                default:
                                {

                                } break;
                            }
                            printf("GIDC_REMOVAL %p %d %s\n",hDevice,deviceInfo.dwType,pName);
                        } break;
                        default:
                        {

                        } break;
                    }
                    delete [] pName;
                } break;
                case WM_INPUT:
                {
                    UINT dwSize = sizeof( RAWINPUT );
                    static BYTE lpb[sizeof( RAWINPUT )];

                    GetRawInputData( (HRAWINPUT)Message.lParam, RID_INPUT, lpb, &dwSize, sizeof( RAWINPUTHEADER ) );
                
                    RAWINPUT* raw = (RAWINPUT*)lpb;
                
                    if(raw->header.dwType == RIM_TYPEKEYBOARD )
                    {
                        bool bIsUp = (raw->data.keyboard.Flags & RI_KEY_BREAK) != 0;

                        u32 dwScanCode = raw->data.keyboard.MakeCode;

                        //capslock
                        if( 0x3A == dwScanCode)
                        {
                            for( u32 dwKeyBoard = 0; dwKeyBoard < dwNumHandles; ++dwKeyBoard )
                            {
                                if( hDevice != handles[dwKeyBoard].hHandle0)
                                {
                                    KEYBOARD_INDICATOR_PARAMETERS InputBuffer;    // Input buffer for DeviceIoControl
                                    KEYBOARD_INDICATOR_PARAMETERS OutputBuffer;   // Output buffer for DeviceIoControl
                                    UINT                LedFlagsMask;               
                                    BOOL                Toggle;                         
                                    ULONG               DataLength = sizeof(KEYBOARD_INDICATOR_PARAMETERS);                     
                                    ULONG               ReturnedLength; // Number of bytes returned in output buffer                                
                                    InputBuffer.UnitId = 0;
                                    OutputBuffer.UnitId = 0;

                                    UINT LedFlag = KEYBOARD_CAPS_LOCK_ON;
                                    if (DeviceIoControl(handles[dwKeyBoard].hHandle0, IOCTL_KEYBOARD_QUERY_INDICATORS,
                                                &InputBuffer, DataLength,
                                                &OutputBuffer, DataLength,
                                                &ReturnedLength, NULL))
                                    {
                                        LedFlagsMask = (OutputBuffer.LedFlags & (~LedFlag));
                                        Toggle = (OutputBuffer.LedFlags & LedFlag);
    
    
                                        Toggle ^= 1;
                                        InputBuffer.LedFlags = (LedFlagsMask | (LedFlag * Toggle));
    
                                         DeviceIoControl(handles[dwKeyBoard].hHandle0, IOCTL_KEYBOARD_SET_INDICATORS,
                                                        &InputBuffer, DataLength,
                                                        NULL,   0,  &ReturnedLength, NULL);
    
                                            
                                    }
                                    else
                                    {
                                        logWindowsError("failed to get indicators\n");
                                    }
                                }
                            }
                        }

                    }
                    else
                    {
                        TranslateMessage( &Message );
                        DispatchMessage( &Message );
                    }
                    break;
                }
                default:
                {
                    TranslateMessage( &Message );
                    DispatchMessage( &Message );
                    break;
                }
            }
        }       
    }

    return 0;
}

Solution

  • RAWINPUT.header.hDevice cannot be directly used with DeviceIoControl.

    You have to do a call to GetRawInputDeviceInfo with a RIDI_DEVICENAME to obtain device interface path. After that you need to call to CreateFile to open this interface and obtain its handle. This handle can be used with DeviceIoControl.

    Here is some code from my test repo:

    inline ScopedHandle OpenDeviceInterface(const std::string& deviceInterface, bool readOnly = false)
    {
        DWORD desired_access = readOnly ? 0 : (GENERIC_WRITE | GENERIC_READ);
        DWORD share_mode = FILE_SHARE_READ | FILE_SHARE_WRITE;
    
        HANDLE handle = ::CreateFileW(utf8::widen(deviceInterface).c_str(), desired_access, share_mode, 0, OPEN_EXISTING, 0, 0);
    
        return ScopedHandle(handle);
    }
    
    bool RawInputDevice::QueryRawInputDeviceInfo()
    {
        DCHECK(IsValidHandle(m_Handle));
    
        UINT size = 0;
    
        UINT result = ::GetRawInputDeviceInfoW(m_Handle, RIDI_DEVICENAME, nullptr, &size);
        if (result == static_cast<UINT>(-1))
        {
            //PLOG(ERROR) << "GetRawInputDeviceInfo() failed";
            return false;
        }
        DCHECK_EQ(0u, result);
    
        std::wstring buffer(size, 0);
        result = ::GetRawInputDeviceInfoW(m_Handle, RIDI_DEVICENAME, buffer.data(), &size);
        if (result == static_cast<UINT>(-1))
        {
            //PLOG(ERROR) << "GetRawInputDeviceInfo() failed";
            return false;
        }
        DCHECK_EQ(size, result);
    
        m_InterfacePath = utf8::narrow(buffer);
    
        m_InterfaceHandle = OpenDeviceInterface(m_InterfacePath);
    
        if (!IsValidHandle(m_InterfaceHandle.get()))
        {
            /* System devices, such as keyboards and mice, cannot be opened in
            read-write mode, because the system takes exclusive control over
            them.  This is to prevent keyloggers.  However, feature reports
            can still be sent and received.  Retry opening the device, but
            without read/write access. */
            m_InterfaceHandle = OpenDeviceInterface(m_InterfacePath, true);
            if (IsValidHandle(m_InterfaceHandle.get()))
                m_IsReadOnlyInterface = true;
        }
    
        return !m_InterfacePath.empty();
    }
    
    bool RawInputDeviceKeyboard::ExtendedKeyboardInfo::QueryInfo(const ScopedHandle& interfaceHandle)
    {
        // https://docs.microsoft.com/windows/win32/api/ntddkbd/ns-ntddkbd-keyboard_extended_attributes
    
        KEYBOARD_EXTENDED_ATTRIBUTES extended_attributes{ KEYBOARD_EXTENDED_ATTRIBUTES_STRUCT_VERSION_1 };
        DWORD len = 0;
    
        if (!DeviceIoControl(interfaceHandle.get(), IOCTL_KEYBOARD_QUERY_EXTENDED_ATTRIBUTES, nullptr, 0, &extended_attributes, sizeof(extended_attributes), &len, nullptr))
            return false;
    
        DCHECK_EQ(len, sizeof(extended_attributes));
    
        FormFactor = extended_attributes.FormFactor;
        KeyType = extended_attributes.IETFLanguageTagIndex;
        PhysicalLayout = extended_attributes.PhysicalLayout;
        VendorSpecificPhysicalLayout = extended_attributes.VendorSpecificPhysicalLayout;
        IETFLanguageTagIndex = extended_attributes.IETFLanguageTagIndex;
        ImplementedInputAssistControls = extended_attributes.ImplementedInputAssistControls;
    
        return true;
    }
    

    I want to mess with the indicator lights of caps lock, scroll lock, and num lock per keyboard

    You cannot open keyboard handle for writing and do a DeviceIoControl(..., IOCTL_KEYBOARD_SET_INDICATORS, ...) from a user mode without some kind of hacks described here. It is undocumented and may break at any time.

    Bonus chatter: Windows organizes devices as virtual files almost like Linux doing it under /dev/ filesystem path. These virtual files cannot be listed via usual APIs. But there is WinObj tool exist that uses undocumented APIs and can show the inner workings. It can help to understand how Windows works under the hood.