使用user32.dll读取DualSense手柄输入时触发System.AccessViolationException
WPF中使用GetRawInputData读取PS5手柄触发内存访问异常的问题
问题背景
刚接触独立应用开发,4天前启动了一个WPF项目,通过user32.dll的原生方法GetRawInputData读取键盘、鼠标及游戏手柄输入,P/Invoke定义如下:
[DllImport("User32.dll", SetLastError = true)] internal static extern int GetRawInputData(IntPtr hRawInput, DataCommand command, [Out] out InputData buffer, [In, Out] ref int size, int cbSizeHeader); [DllImport("User32.dll", SetLastError = true)] internal static extern int GetRawInputData(IntPtr hRawInput, DataCommand command, [Out] IntPtr pData, [In, Out] ref int size, int sizeHeader);
键盘和鼠标输入处理完全正常,但注册窗口处理游戏手柄并插入PS5 DualSense控制器后,程序抛出System.AccessViolationException:“Attempted to read or write protected memory.”。
下载了多个使用GetRawInputData的VS输入处理项目,发现除了一个纯C编写的项目外,其余均存在相同问题。不介意用纯C编写DLL获取所需信息,但遇到无法将int数组作为输出参数返回输入值的问题。
目前希望找到任一可行方案:
- A. 找出触发
System.AccessViolationException的原因 - B. 实现从C DLL方法中传出int数组
补充定义代码
hRawInput为自定义窗口过程的lParam,相关枚举和结构体定义如下:
public enum DataCommand : uint { RID_HEADER = 0x10000005, // 获取RAWINPUT结构的头部信息 RID_INPUT = 0x10000003 // 获取RAWINPUT结构的原始数据 } [StructLayout(LayoutKind.Sequential)] public struct RawInputHeader { public uint dwType; // 原始输入类型(RIM_TYPEHID 2, RIM_TYPEKEYBOARD 1, RIM_TYPEMOUSE 0) public uint dwSize; // 整个输入数据包的字节大小,包含RAWINPUT及RAWHID变长数组中的额外输入报告 public IntPtr hDevice; // 生成原始输入数据的设备句柄 public IntPtr wParam; // 应用在前台时为RIM_INPUT 0,后台时为RIM_INPUTSINK 1 public override string ToString() { return string.Format("RawInputHeader\n dwType : {0}\n dwSize : {1}\n hDevice : {2}\n wParam : {3}", dwType, dwSize, hDevice, wParam); } } [StructLayout(LayoutKind.Sequential)] internal struct RawHid { public uint dwSizHid; public uint dwCount; public byte bRawData; public override string ToString() { return string.Format("Rawhib\n dwSizeHid : {0}\n dwCount : {1}\n bRawData : {2}\n", dwSizHid, dwCount, bRawData); } } [StructLayout(LayoutKind.Explicit)] internal struct RawMouse { [FieldOffset(0)] public ushort usFlags; [FieldOffset(4)] public uint ulButtons; [FieldOffset(4)] public ushort usButtonFlags; [FieldOffset(6)] public ushort usButtonData; [FieldOffset(8)] public uint ulRawButtons; [FieldOffset(12)] public int lLastX; [FieldOffset(16)] public int lLastY; [FieldOffset(20)] public uint ulExtraInformation; } [StructLayout(LayoutKind.Sequential)] internal struct RawKeyboard { public ushort Makecode; // 按键按下的扫描码 public ushort Flags; // RI_KEY_MAKE、RI_KEY_BREAK、RI_KEY_E0、RI_KEY_E1中的一个或多个 private readonly ushort Reserved; // 始终为0 public ushort VKey; // 虚拟键码 public uint Message; // 对应的Windows消息,例如WM_KEYDOWN、WM_SYSKEYDOWN等 public uint ExtraInformation; // 事件的设备特定附加信息(键盘通常为0) public override string ToString() { return string.Format("Rawkeyboard\n Makecode: {0}\n Makecode(hex) : {0:X}\n Flags: {1}\n Reserved: {2}\n VKeyName: {3}\n Message: {4}\n ExtraInformation {5}\n", Makecode, Flags, Reserved, VKey, Message, ExtraInformation); } } [StructLayout(LayoutKind.Explicit)] public struct RawData { [FieldOffset(0)] internal RawMouse mouse; [FieldOffset(0)] internal RawKeyboard keyboard; [FieldOffset(0)] internal RawHid hid; } [StructLayout(LayoutKind.Sequential)] public struct InputData { public RawInputHeader header; // 64位系统头部大小24字节,32位系统16字节 public RawData data; // 将剩余部分放在结构体中,确保32/64位系统头部对齐正确 }
解决方案
A. 排查内存访问异常的原因
异常的核心问题是**RawHid结构体的定义错误**:
- 原生Windows的
RAWHID结构体中,bRawData是一个可变长度的字节数组(长度为dwSizHid * dwCount),但当前C#定义中把它声明为单个byte。当读取PS5手柄这类HID设备的输入时,GetRawInputData会向bRawData的位置写入远超过1字节的数据,直接越界访问内存,触发AccessViolationException。
修复步骤:
- 改用IntPtr版本的
GetRawInputData:不要用out InputData buffer的重载,因为InputData的大小是固定的,无法容纳变长的HID数据。正确流程是:- 先调用
GetRawInputData,传入RID_HEADER命令,获取输入数据的总大小dwSize; - 用
Marshal.AllocHGlobal分配对应大小的非托管内存; - 再次调用
GetRawInputData,传入RID_INPUT命令,将数据读取到分配的内存中; - 手动解析内存中的
RawHid部分:先读取dwSizHid和dwCount,然后从bRawData的偏移位置读取dwSizHid * dwCount个字节,转换为实际的手柄输入数据。
- 先调用
- 修正
RawHid结构体定义:将bRawData改为IntPtr(或者直接不定义,手动计算偏移),避免固定长度限制:[StructLayout(LayoutKind.Sequential)] internal struct RawHid { public uint dwSizHid; public uint dwCount; // 移除bRawData,通过偏移手动读取 }
B. 纯C DLL返回int数组的实现
如果选择用C DLL处理手柄输入,可通过以下方式传出int数组:
C DLL代码
#include <windows.h> #include <stdlib.h> // 导出函数:获取手柄输入数组,返回1表示成功,0表示失败 __declspec(dllexport) int GetControllerInput(int** outputArray, int* arrayLength) { // 示例:返回4个手柄输入值(左摇杆X、Y,右摇杆X、Y) *arrayLength = 4; // 分配内存存储数组 *outputArray = (int*)malloc(*arrayLength * sizeof(int)); if (*outputArray == NULL) { return 0; } // 替换为实际读取PS5手柄HID数据的逻辑 (*outputArray)[0] = 127; // 左摇杆X(示例值) (*outputArray)[1] = -45; // 左摇杆Y(示例值) (*outputArray)[2] = 89; // 右摇杆X(示例值) (*outputArray)[3] = 0; // 右摇杆Y(示例值) return 1; } // 导出函数:释放C DLL分配的内存 __declspec(dllexport) void FreeInputArray(int* array) { if (array != NULL) { free(array); } }
C#调用代码
[DllImport("YourDllName.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int GetControllerInput(out IntPtr outputArrayPtr, out int arrayLength); [DllImport("YourDllName.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern void FreeInputArray(IntPtr arrayPtr); // 使用示例 void ReadControllerInput() { IntPtr arrayPtr; int length; if (GetControllerInput(out arrayPtr, out length)) { int[] inputArray = new int[length]; // 将非托管内存中的数据复制到托管数组 Marshal.Copy(arrayPtr, inputArray, 0, length); // 处理手柄输入数据 Console.WriteLine($"左摇杆X: {inputArray[0]}, Y: {inputArray[1]}"); Console.WriteLine($"右摇杆X: {inputArray[2]}, Y: {inputArray[3]}"); // 释放非托管内存,避免泄漏 FreeInputArray(arrayPtr); } }
内容的提问来源于stack exchange,提问作者The Great ReLLeRtR
相关产品推荐
相关产品推荐

