dahall / Vanara

A set of .NET libraries for Windows implementing PInvoke calls to many native Windows APIs with supporting wrappers.
MIT License
1.79k stars 193 forks source link

[Bug] GetRawInputDeviceInfo can not use CharSet = CharSet.Auto #439

Closed emako closed 7 months ago

emako commented 8 months ago

Error Code:

// CharSet  = CharSet.Auto will get the half pcbSize size
[DllImport(Lib.User32, SetLastError = true, CharSet = CharSet.Auto)]
[PInvokeData("winuser.h", MSDNShortId = "")]
public static extern uint GetRawInputDeviceInfo(HANDLE hDevice, uint uiCommand, IntPtr pData, ref uint pcbSize);

And then Marshal.PtrToStringAuto will get the error device name or some time will make application crash when in Chinese OS.

Like this issuse https://github.com/dahall/Vanara/issues/428

I use following code to fix it.

correct code:

[DllImport("User32.dll", SetLastError = true)]
internal static extern uint GetRawInputDeviceInfo(IntPtr hDevice, RawInputDeviceInfo command, IntPtr pData, ref uint size);
dahall commented 7 months ago

Will you share your code? I have run this under 32 and 64-bit, with Unicode character sets without problem.

uint nDev = 0;
Assert.That(GetRawInputDeviceList(null, ref nDev, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICELIST))), ResultIs.Not.Value(uint.MaxValue));
Assert.That(nDev, Is.GreaterThan(0));
RAWINPUTDEVICELIST[] devs = new RAWINPUTDEVICELIST[(int)nDev];
Assert.That(nDev = GetRawInputDeviceList(devs, ref nDev, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICELIST))), ResultIs.Not.Value(uint.MaxValue));
Assert.That(nDev, Is.GreaterThan(0));

for (int i = 0; i < nDev; i++)
{
   uint sz = 0;
   Assert.That(GetRawInputDeviceInfo(devs[i].hDevice, RIDI.RIDI_DEVICENAME, default, ref sz), ResultIs.Value(0));
   SafeLPTSTR data = new((int)sz + 1);
   Assert.That(GetRawInputDeviceInfo(devs[i].hDevice, RIDI.RIDI_DEVICENAME, data, ref sz), Is.GreaterThan(0));
   TestContext.WriteLine($"{data}");
}
emako commented 7 months ago

Sample codes here:

.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Vanara.PInvoke.Kernel32" Version="3.4.17" />
        <PackageReference Include="Vanara.PInvoke.User32" Version="3.4.17" />
    </ItemGroup>

</Project>

Program.cs

using System.Runtime.InteropServices;
using Vanara.PInvoke;

namespace ConsoleUser32;

internal class Program
{
    static void Main()
    {
        EnumerateDevices();
    }

    internal static void EnumerateDevices()
    {
        uint deviceCount = 0;
        int dwSize = Marshal.SizeOf(typeof(RawInputDeviceList));

        if (User32.GetRawInputDeviceList(null, ref deviceCount, (uint)dwSize) == 0)
        {
            User32.RAWINPUTDEVICELIST[] pRawInputDeviceList = new User32.RAWINPUTDEVICELIST[deviceCount];
            User32.GetRawInputDeviceList(pRawInputDeviceList, ref deviceCount, (uint)dwSize);

            for (int i = 0; i < deviceCount; i++)
            {
                uint pcbSize = 0;

                User32.RAWINPUTDEVICELIST rid = pRawInputDeviceList[i];
                User32.GetRawInputDeviceInfo(rid.hDevice, (uint)RawInputDeviceInfo.RIDI_DEVICENAME, IntPtr.Zero, ref pcbSize);

                if (pcbSize <= 0) continue;

                // --------------------------------------
                // All right
                //nint pDataX = Marshal.AllocHGlobal((int)pcbSize);
                //User32X.GetRawInputDeviceInfo((nint)rid.hDevice, RawInputDeviceInfo.RIDI_DEVICENAME, pDataX, ref pcbSize);
                //string? deviceNameX = Marshal.PtrToStringAnsi(pDataX);
                //Console.WriteLine(deviceNameX);
                //Marshal.FreeHGlobal(pDataX);
                // --------------------------------------

                // --------------------------------------
                // Application crash
                nint pData = Marshal.AllocHGlobal((int)pcbSize);
                User32.GetRawInputDeviceInfo(rid.hDevice, (uint)RawInputDeviceInfo.RIDI_DEVICENAME, pData, ref pcbSize);
                string? deviceName = Marshal.PtrToStringAuto(pData);
                Console.WriteLine(deviceName);
                Marshal.FreeHGlobal(pData);
                // --------------------------------------
            }
        }
    }
}

file static class User32X
{
    [DllImport("User32.dll", SetLastError = true)]
    public static extern uint GetRawInputDeviceInfo(nint hDevice, RawInputDeviceInfo command, nint pData, ref uint size);
}

[StructLayout(LayoutKind.Sequential)]
file struct RawInputDeviceList
{
    public nint Device;
    public uint Type;
}

file enum RawInputDeviceInfo : uint
{
    RIDI_DEVICENAME = 0x20000007,
    RIDI_DEVICEINFO = 0x2000000b,
    PREPARSEDDATA = 0x20000005
}
dahall commented 7 months ago

You have a small bug in the code due to an odd use by the API of a variable. The docs say:

For this (RIDI_DEVICENAME) uiCommand only, the value in pcbSize is the character count (not the byte count).

When calling Marshal.AllocHGlobal with the resulting value from the first call, you need to adjust for character size. This could be done by multiplying pData by Marshal.SystemDefaultCharSize or by using the SafeLPTSTR class in my sample code above. The DllImport statement then still accounts for the API having both an Ansi and a Unicode implementation.

emako commented 7 months ago

I ajust to nint pData = Marshal.AllocHGlobal((int)pcbSize * Marshal.SystemDefaultCharSize); and it works. Thank you