Node ffi调用SetWindowsHookExW的KeyboardProc回调在32位系统失效求助
问题根因
- 核心错误是
LowLevelKeyboardProc回调的参数类型定义不符合Windows API规范:该回调的第三个参数是LPARAM类型(本质是指向KBDLLHOOKSTRUCT的内存地址指针),而你直接将其声明为KBDLLHOOKSTRUCT结构体类型,导致ffi在x86架构下解析栈参数时完全错位,永远只能读到固定的栈内存值。 - 次要问题是
CallNextHookEx调用时第四个参数传值错误,你错误传递了结构体实例的引用地址,而非系统原生传入的LPARAM值。 - 原有代码的消息循环存在逻辑缺陷,
GetMessageW返回值只赋值一次,无法正常处理后续消息。
修复方案
1. 修正回调参数定义
将ffi.Callback的第三个参数类型改为LPARAM,在回调内部手动将LPARAM指针转换为KBDLLHOOKSTRUCT实例读取数据。
2. 修正系统API传参
CallNextHookEx直接传入系统返回的原生LPARAM值即可,无需手动取地址;同时修复消息循环的逻辑缺陷。
修正后的完整代码
const os = require("os"); const ffi = require("ffi-napi"); const ref = require("ref-napi"); const Struct = require("ref-struct-di")(ref); const is64bit = os.arch() === "x64"; // Types const LONG = is64bit ? ref.types.long : ref.types.int32; const ULONG_PTR = is64bit ? ref.types.ulong : ref.types.uint32; // 明确对应Windows API的ULONG_PTR类型 const INT = ref.types.int; const UINT = ref.types.uint; const DWORD = ref.types.uint32; // DWORD始终为无符号32位整数 const BOOL = ref.types.bool; const HANDLE = is64bit ? ref.types.uint64 : ref.types.uint32; const HHOOK = HANDLE; const HWND = HANDLE; const HINSTANCE = HANDLE; const WPARAM = is64bit ? ref.types.uint64 : ref.types.uint32; const LPARAM = is64bit ? ref.types.int64 : ref.types.int32; const LRESULT = is64bit ? ref.types.int64 : ref.types.int32; const HOOKPROC = "pointer"; // Structures const POINT = Struct({ x: LONG, y: LONG, }); const MSG = Struct({ hwnd: HWND, message: UINT, wParam: WPARAM, lParam: LPARAM, time: DWORD, pt: POINT, lPrivate: DWORD, }); const KBDLLHOOKSTRUCT = Struct({ vkCode: DWORD, scanCode: DWORD, flags: DWORD, time: DWORD, dwExtraInfo: ULONG_PTR, // 修正字段类型匹配API定义 }); // User32 bindings const user32 = ffi.Library("user32", { SetWindowsHookExW: [HHOOK, [INT, HOOKPROC, HINSTANCE, DWORD]], UnhookWindowsHookEx: [BOOL, [HHOOK]], CallNextHookEx: [LRESULT, [HHOOK, INT, WPARAM, LPARAM]], GetMessageW: [BOOL, [MSG, HWND, UINT, UINT]], TranslateMessage: [BOOL, [MSG]], DispatchMessageW: [LRESULT, [MSG]], }); // Constants const WH_KEYBOARD_LL = 13; const WM_KEYDOWN = 0x0100; let hHook = 0; // 修正回调参数类型,第三个参数为LPARAM而非结构体 const keyboardProc = ffi.Callback( LRESULT, [INT, WPARAM, LPARAM], (nCode, wParam, lParam) => { // 手动将LPARAM指针转换为KBDLLHOOKSTRUCT实例 const kbdData = ref.reinterpret(lParam, KBDLLHOOKSTRUCT.size, 0); const kbdStruct = KBDLLHOOKSTRUCT(kbdData); console.log( `input event callback: nCode:${nCode} wParam:${wParam} vkCode:${kbdStruct.vkCode} scanCode:${kbdStruct.scanCode}` ); if (wParam === WM_KEYDOWN) { const message = { event: "key-down", data: { vkCode: kbdStruct.vkCode, scanCode: kbdStruct.scanCode, }, }; // 此处添加你的业务逻辑 } // 直接传原生lParam值即可,无需手动取地址 return user32.CallNextHookEx(hHook, nCode, wParam, lParam); } ); /* 设置钩子 */ hHook = user32.SetWindowsHookExW(WH_KEYBOARD_LL, keyboardProc, 0, 0); /* 修复消息循环逻辑 */ const msg = new MSG(); let res = user32.GetMessageW(msg.ref(), 0, 0, 0); while (res) { user32.TranslateMessage(msg.ref()); user32.DispatchMessageW(msg.ref()); res = user32.GetMessageW(msg.ref(), 0, 0, 0); }
额外注意事项
需要在全局作用域持续持有keyboardProc回调的引用,避免被Node.js垃圾回收导致钩子运行一段时间后崩溃。
内容的提问来源于stack exchange,提问作者Elendiar
相关产品推荐
相关产品推荐

