基于JDK23 java.lang.foreign的Native Hook按键码获取异常求助
问题:如何获取Native Hook的正确按键码?
尝试用Java Foreign API编写Native Hook替代JNativeHook,但无论按下哪个按键,输出的lParam值始终相同(仅每次启动程序时变化),无法获取正确的按键码。测试代码及输出如下:
测试代码
import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class hook_test { private static final int WH_KEYBOARD_LL = 13; private static final int WM_KEYDOWN = 0x0100; private static long hook; private static MethodHandle callNextHookEx; void main() throws Throwable { System.loadLibrary("user32"); Linker linker = Linker.nativeLinker(); SymbolLookup user32Lookup = SymbolLookup.loaderLookup(); MethodHandle setWindowsHookEx = linker.downcallHandle(user32Lookup.find("SetWindowsHookExA").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); callNextHookEx = linker.downcallHandle(user32Lookup.find("CallNextHookEx").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG)); MethodHandle getMessage = linker.downcallHandle(user32Lookup.find("GetMessageA").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); MethodHandle unhookWindowsHookEx = linker.downcallHandle(user32Lookup.find("UnhookWindowsHookEx").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG)); MethodHandle hookProcHandle = MethodHandles.lookup().findStatic(hook_test.class, "hookProc", MethodType.methodType(long.class, int.class, long.class, long.class)); MemorySegment hookProcAddress = linker.upcallStub(hookProcHandle, FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG), Arena.ofAuto()); hook = (long) setWindowsHookEx.invoke(WH_KEYBOARD_LL, hookProcAddress, MemorySegment.NULL, 0); if (hook == 0) { System.out.println("Failed to set hook"); return; } Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { if (hook != 0) unhookWindowsHookEx.invoke(hook); } catch (Throwable t) { System.err.println(t.getMessage()); } })); try (Arena arena = Arena.ofAuto()) { MemorySegment msg = arena.allocate(28); while ((int) getMessage.invoke(msg, MemorySegment.NULL, 0, 0) != 0) { } } } public static long hookProc(int code, long wParam, long lParam) { if (code >= 0) { if (wParam == WM_KEYDOWN) System.out.printf("int code:%s\tlong wParam:%s\tlong lParam:%s\n", code, wParam, lParam); } try { return (long) callNextHookEx.invoke(hook, code, wParam, lParam); } catch (Throwable t) { System.err.println(t.getMessage()); return 0; } } }
输出示例
int code:0 long wParam:256 long lParam:1059701387608 int code:0 long wParam:256 long lParam:1059701387608 int code:0 long wParam:256 long lParam:1059701387608 int code:0 long wParam:256 long lParam:1059701387608
解决方案
问题核心
你现在打印的lParam是**KBDLLHOOKSTRUCT结构体的指针地址**,而非结构体内容。Windows系统会复用该结构体的内存块,所以每次按键时指针地址不变。要获取按键码,必须解析lParam指向的结构体,读取其中的vkCode字段(虚拟按键码)。
修改步骤及代码
- 定义
KBDLLHOOKSTRUCT结构体的内存布局:对应Windows原生结构体的字段,创建Java Foreign API的内存布局。 - 修改钩子回调的参数类型:将
long lParam改为MemorySegment lParam,以便访问指针指向的内存。 - 读取结构体中的按键码:通过
VarHandle读取vkCode字段。 - 完善消息循环:添加
TranslateMessage和DispatchMessage,保证系统消息正常处理。
修改后的完整代码:
import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class hook_test { private static final int WH_KEYBOARD_LL = 13; private static final int WM_KEYDOWN = 0x0100; private static long hook; private static MethodHandle callNextHookEx; // 定义KBDLLHOOKSTRUCT结构体布局 private static final GroupLayout KBDLLHOOKSTRUCT_LAYOUT = MemoryLayout.structLayout( ValueLayout.JAVA_INT.withName("vkCode"), ValueLayout.JAVA_INT.withName("scanCode"), ValueLayout.JAVA_INT.withName("flags"), ValueLayout.JAVA_INT.withName("time"), ValueLayout.JAVA_LONG.withName("dwExtraInfo") ); private static final VarHandle VK_CODE = KBDLLHOOKSTRUCT_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("vkCode")); public static void main(String[] args) throws Throwable { System.loadLibrary("user32"); Linker linker = Linker.nativeLinker(); SymbolLookup user32Lookup = SymbolLookup.loaderLookup(); // 初始化Native方法句柄 MethodHandle setWindowsHookEx = linker.downcallHandle( user32Lookup.find("SetWindowsHookExA").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT) ); callNextHookEx = linker.downcallHandle( user32Lookup.find("CallNextHookEx").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) ); MethodHandle getMessage = linker.downcallHandle( user32Lookup.find("GetMessageA").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) ); MethodHandle unhookWindowsHookEx = linker.downcallHandle( user32Lookup.find("UnhookWindowsHookEx").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG) ); MethodHandle translateMessage = linker.downcallHandle( user32Lookup.find("TranslateMessage").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS) ); MethodHandle dispatchMessage = linker.downcallHandle( user32Lookup.find("DispatchMessageA").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) ); // 创建钩子回调的upcall stub MethodHandle hookProcHandle = MethodHandles.lookup().findStatic( hook_test.class, "hookProc", MethodType.methodType(long.class, int.class, long.class, MemorySegment.class) ); MemorySegment hookProcAddress = linker.upcallStub( hookProcHandle, FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), Arena.ofAuto() ); // 设置钩子 hook = (long) setWindowsHookEx.invoke(WH_KEYBOARD_LL, hookProcAddress, MemorySegment.NULL, 0); if (hook == 0) { System.out.println("Failed to set hook"); return; } // 注册钩子卸载的 shutdown hook Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { if (hook != 0) unhookWindowsHookEx.invoke(hook); } catch (Throwable t) { System.err.println(t.getMessage()); } })); // 消息循环 try (Arena arena = Arena.ofAuto()) { MemorySegment msg = arena.allocate(28); while ((int) getMessage.invoke(msg, MemorySegment.NULL, 0, 0) != 0) { translateMessage.invoke(msg); dispatchMessage.invoke(msg); } } } public static long hookProc(int code, long wParam, MemorySegment lParam) { if (code >= 0 && wParam == WM_KEYDOWN) { // 读取虚拟按键码 int vkCode = (int) VK_CODE.get(lParam); System.out.printf("按键码:%d (十六进制:0x%X)\n", vkCode, vkCode); } try { return (long) callNextHookEx.invoke(hook, code, wParam, lParam); } catch (Throwable t) { System.err.println(t.getMessage()); return 0; } } }
说明
vkCode是Windows虚拟按键码,你可以通过它判断具体按键,比如0x41对应A键,0x31对应数字1键。- 消息循环中添加的
TranslateMessage和DispatchMessage是为了确保系统能正常处理其他窗口消息,避免钩子影响系统操作。
内容的提问来源于stack exchange,提问作者halu86x
相关产品推荐
相关产品推荐

