You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 14:39:02