Marshal.PtrToStructure 抛出 System.ArgumentException 错误

13

我试图从键盘钩子的lParam中获取一个KBDLLHOOKSTRUCT。

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {

        KBDLLHOOKSTRUCT kbd = new KBDLLHOOKSTRUCT();
        Marshal.PtrToStructure(lParam, kbd); // Throws System.ArguementException
        ...

很遗憾,PtrToStructure 抛出了两个错误

A first chance exception of type 'System.ArgumentException' occurred in myprogram.exe

每次按键时都会出现错误。它也会阻止该方法的执行。

MSNDA说: http://msdn.microsoft.com/en-us/library/4ca6d5z7.aspx

ArgumentException when:

The structureType parameter layout is not sequential or explicit.

-or-

The structureType parameter is a generic type.

我应该怎样做才能让它工作?lParam 直接来自键盘钩子,所以我希望它是正确的。这两个错误在这里都有意义吗?我该怎么办才能修复它?


1
你能发布一下你的KBDLLHOOKSTRUCT结构定义吗? - wj32
是的,KBDLLHOOKSTRUCT 很可能被声明不正确,可能缺少 LayoutKind.Explicit 或 LayoutKind.Sequential 属性。 - EricLaw
在我的情况下,你的 KBDLLHOOKSTRUCT 被声明为一个 struct。将其转换为一个 class 后,该异常消失了。 - c00000fd
1个回答

37

这是我用的一些代码:

public struct KBDLLHOOKSTRUCT
{
  public Int32 vkCode;
  public Int32 scanCode;
  public Int32 flags;
  public Int32 time;
  public IntPtr dwExtraInfo;
}

private static IntPtr HookCallback(
    int nCode, IntPtr wParam, IntPtr lParam)
{
  if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
  {
    KBDLLHOOKSTRUCT kbd = (KBDLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
    Debug.WriteLine(kbd.vkCode);  // ***** your code here *****
  }
  return CallNextHookEx(_hookID, nCode, wParam, lParam);
}
您的代码与我关键之处不同的地方在于,我调用了 Marshal.PtrToStructure(IntPtr, Type) 的重载而不是 (IntPtr, object) 的重载。我认为这就是您出现问题的地方。因为如果您使用结构体调用 (IntPtr, object) 重载,则会出现以下错误:
System.ArgumentException: 结构体不能是值类。
显然,解决此错误的方法是将 KBDLLHOOKSTRUCT 更改为类(引用类型),而不是结构体(值类型)。
public class KBDLLHOOKSTRUCT    // not necessarily the right solution!

然而,这会导致“structureType参数布局不是顺序的或明确的”错误:

System.ArgumentException:指定的结构体必须是可平坦化的或具有布局信息。

我猜这就是你现在所面对的问题,KBDLLHOOKSTRUCT 声明为 class,并且遇到了“没有布局信息”的错误。有两种方法可以解决这个问题。

首先,根据 Eric Law 的评论,你可以保留 Marshal.PtrToStructure 调用,将 KBDLLHOOKSTRUCT 保持为 class,并向 KBDLLHOOKSTRUCT添加布局信息:

[StructLayout(LayoutKind.Sequential)]
public class KBDLLHOOKSTRUCT { ... }

其次,根据我上面的示例代码,你可以将 KBDLLHOOKSTRUCT 更改为 struct,而不是 class,并将 Marshal.PtrToStructure 调用更改为 (IntPtr、Type) 重载:

public struct KBDLLHOOKSTRUCT { ... }

KBDLLHOOKSTRUCT kbd = (KBDLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
在这种情况下,如果你愿意,仍然可以为KBDLLHOOKSTRUCT结构添加[StructLayout(LayoutKind.Sequential)]属性。虽然在技术上来说是多余的,但有助于你的代码读者将KBDLLHOOKSTRUCT识别为一个布局敏感的Interop类型。
这两种解决方案都对我起作用(虽然只是简单的测试)。从这两种解决方案中,我推荐第二种,因为在Win32 / C结构的P/Invoke场景中,通常会使用struct进行声明。此外,以STRUCT结尾的名称可能应该是一个结构体,而不是一个类!
最后,让我提到一种替代方法。
可以将LowLevelKeyboardProc的lParam声明为ref KBDLLHOOKSTRUCT(其中KBDLLHOOKSTRUCT是一个结构体,而不是类),而不是IntPtr。这也需要更改CallNextHookEx,但净结果是通过避免Marshal调用来简化对KBDLLHOOKSTRUCT信息的使用。使用ref参数还意味着你可以对结构体进行写入操作(我知道这是你的目标,来自其他问题),而无需在写入后进行marshal操作:
private delegate IntPtr LowLevelKeyboardProc(
    int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT kbd);

private static IntPtr HookCallback(
    int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT kbd)
{
  Debug.WriteLine(kbd.vkCode);  // look!  no marshalling!
  return CallNextHookEx(_hookID, nCode, wParam, ref kbd);
}

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
    IntPtr wParam, ref KBDLLHOOKSTRUCT kbd);

(我应该提醒您,当我尝试修改kbd.vkCode时,并没有实际影响文本框中出现的内容等。我对低级键盘钩子不了解,不知道为什么会这样,也不知道需要做些什么使其生效;抱歉。)


顺便提一下,关于 ref 参数解决方案,我有一种感觉,是我最先向你指出了 Marshal.PtrToStructure。回想起来,ref 参数解决方案可能是最简单的,我应该在最开始就建议它。如果你觉得我给了你错误的引导,请随意给予惩罚性的负评! - itowlson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接