我该如何将一个F#委托传递给一个期望函数指针的P/Invoke方法?

11
我试图在F#应用程序中使用P/Invoke设置低级键盘挂钩。Win32函数 SetWindowsHookEx 接受一个类型为 HOOKPROC 的参数作为其第二个参数,我将其表示为一个委托 (int * IntPtr * IntPtr) -> IntPtr,类似于在C#中处理此问题的方式。调用该方法时,我遇到了一个 MarshalDirectiveException 异常,指出委托参数无法进行封送,因为:

通用类型不能进行封送

我不确定泛型是如何涉及其中的,因为所有类型都是具体指定的。有人能解释一下吗?以下是代码。

编辑

这可能与F#编译器处理类型签名的方式有关。Reflector指示委托 LowLevelKeyboardProc 是实现为接受类型为 Tuple<int, IntPtr, IntPtr> 的参数的方法,并且会有无法封送的通用类型。是否有某种方法可以解决这个问题,还是说 F# 函数根本就无法封送到本地函数指针?

let WH_KEYBOARD_LL = 13

type LowLevelKeyboardProc = delegate of (int * IntPtr * IntPtr) -> IntPtr

[<DllImport("user32.dll")>]
extern IntPtr SetWindowsHookEx(int idhook, LowLevelKeyboardProc proc, IntPtr hMod, UInt32 threadId)

[<DllImport("kernel32.dll")>]
extern IntPtr GetModuleHandle(string lpModuleName)

let SetHook (proc: LowLevelKeyboardProc) =
    use curProc = Process.GetCurrentProcess ()
    use curMod = curProc.MainModule

    SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curMod.ModuleName), 0u)

尝试从F#中使用p/invoke并传递委托,这是值得赞赏的。但我不确定是否可以实现。 - Frédéric Hamidi
我认为结果类型应该简单地是 int,而不是 IntPtr。但我猜这不会对错误消息产生影响...建议:添加调用 SetHook 的代码,也许有问题? - wmeyer
@wmeyer:返回类型为int而不是nativeint将在64位平台上失败。 - ildjarn
@Frédéric Hamidi:在所有.NET语言中,将回调传递给本地代码的方式都是标准的;使用F#也没有什么特别之处。 - ildjarn
2
@Frédéric Hamidi:我承认,一开始并不明显。delegate of (int * int) -> int 定义了一个接受实际 Tuple<int, int> 的一元委托,而 delegate of int * int -> int 则定义了一个接受两个 int 的二元函数。但是,即使它看起来使用了元组语法,后者也必须与柯里化函数一起使用。无论如何,前者版本的非明显语义是导致Ben错误的原因,因为 Tuple<int, nativeint, nativeint> 明显是一个泛型。 - ildjarn
显示剩余2条评论
2个回答

16
你的LowLevelKeyboardProc定义是错误的。请更改为:
type LowLevelKeyboardProc = delegate of (int * IntPtr * IntPtr) -> IntPtr

to

type LowLevelKeyboardProc = delegate of int * IntPtr * IntPtr -> IntPtr

或者更好的是

type LowLevelKeyboardProc = delegate of int * nativeint * nativeint -> nativeint

或者更好的是

[<StructLayout(LayoutKind.Sequential)>]
type KBDLLHOOKSTRUCT =
    val vkCode      : uint32
    val scanCode    : uint32
    val flags       : uint32
    val time        : uint32
    val dwExtraInfo : nativeint

type LowLevelKeyboardProc =
    delegate of int * nativeint * KBDLLHOOKSTRUCT -> nativeint

在所有情况下,proc需要使用柯里化形式而不是元组形式。
还要注意,对于那些文档说明在失败时调用GetLastErrorextern函数(如GetModuleHandleSetWindowsHookExUnhookWindowsHookEx),应添加SetLastError = true。这样,如果有任何失败(并且您应该检查返回值...),您可以简单地引发一个Win32Exception或调用Marshal.GetLastWin32Error以获取适当的诊断信息。 编辑:仅为了清晰起见,以下是我在本地成功测试的所有P/Invoke签名:
[<Literal>]
let WH_KEYBOARD_LL = 13

[<StructLayout(LayoutKind.Sequential)>]
type KBDLLHOOKSTRUCT =
    val vkCode      : uint32
    val scanCode    : uint32
    val flags       : uint32
    val time        : uint32
    val dwExtraInfo : nativeint

type LowLevelKeyboardProc = delegate of int * nativeint * KBDLLHOOKSTRUCT -> nativeint

[<DllImport("kernel32.dll")>]
extern uint32 GetCurrentThreadId()

[<DllImport("kernel32.dll", SetLastError = true)>]
extern nativeint GetModuleHandle(string lpModuleName)

[<DllImport("user32.dll", SetLastError = true)>]
extern bool UnhookWindowsHookEx(nativeint hhk)

[<DllImport("user32.dll", SetLastError = true)>]
extern nativeint SetWindowsHookEx(int idhook, LowLevelKeyboardProc proc, nativeint hMod, uint32 threadId)

此外需要注意的是,如果您更喜欢对 KBDLLHOOKSTRUCT 进行值语义,那么这同样有效:

[<Struct; StructLayout(LayoutKind.Sequential)>]
type KBDLLHOOKSTRUCT =
    val vkCode      : uint32
    val scanCode    : uint32
    val flags       : uint32
    val time        : uint32
    val dwExtraInfo : nativeint

type LowLevelKeyboardProc = delegate of int * nativeint * byref<KBDLLHOOKSTRUCT> -> nativeint

你通常如何测试你的P/Invokes? - GregC
1
@GregC: 这取决于复杂程度。在这种情况下,我编写了一个小的C++控制台应用程序来确定预期行为,然后验证F#应用程序是否具有相同的行为。在更复杂的情况下,我通常会编写一个C++ .dll,该.dll导出与我最终希望调用的函数相同的签名,并且这些函数只是盲目地将数据来回转发到底层WinAPI函数,并在此过程中记录。然后,我可以查看日志输出以确保编组行为是合理的。 - ildjarn

1

你尝试过使用托管C++吗?它可以使许多翻译变得非常无缝。那么你就不需要使用P/Invoke了。

编辑:我想指出一件相当重要的事情:编译器将为您执行更多的类型检查。我相信你喜欢你的类型检查,因为你希望在应用程序的其余部分中使用F#(希望如此)。


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