使用java.lang.foreign替代JNativeHook:读取KBDLLHOOKSTRUCT遇越界异常
解决JDK23 java.lang.foreign实现键盘钩子的IndexOutOfBoundsException异常
我尝试用JDK23的java.lang.foreign替代JNativeHook实现键盘钩子。根据资料,SetWindowsHookEx的第三个参数lParam指向KBDLLHOOKSTRUCT结构体,C语言定义如下:
typedef struct tagKBDLLHOOKSTRUCT { DWORD vkCode; DWORD scanCode; DWORD flags; DWORD time; ULONG_PTR dwExtraInfo; } KBDLLHOOKSTRUCT, *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;
我编写了读取该结构体的代码片段:
public static MemorySegment hookProc(int code, MemorySegment wParam, MemorySegment lParam) { if (code >= 0) { System.out.println("wParam address: " + wParam.address() + " lParam address: " + lParam.address()); long vkCode = lParam.get(ValueLayout.JAVA_INT, 0); long scanCode = lParam.get(ValueLayout.JAVA_INT, 4); long flags = lParam.get(ValueLayout.JAVA_INT, 8); long time = lParam.get(ValueLayout.JAVA_INT, 12); long dwExtraInfo = lParam.get(ValueLayout.JAVA_LONG, 16);
但运行时抛出java.lang.IndexOutOfBoundsException,错误信息如下:
Hook set successfully wParam address: 256 lParam address: 400736383368 java.lang.IndexOutOfBoundsException: Out of bound access on segment MemorySegment{ address: 0x5d4dbff188, byteSize: 0 }; new offset = 0; new length = 4 at java.base/jdk.internal.foreign.AbstractMemorySegmentImpl.outOfBoundException(AbstractMemorySegmentImpl.java:439) at java.base/jdk.internal.foreign.AbstractMemorySegmentImpl.apply(AbstractMemorySegmentImpl.java:420) at java.base/jdk.internal.foreign.AbstractMemorySegmentImpl.apply(AbstractMemorySegmentImpl.java:70) at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:98) at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:124) at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:448) at java.base/jdk.internal.foreign.AbstractMemorySegmentImpl.checkBounds(AbstractMemorySegmentImpl.java:409) at java.base/jdk.internal.foreign.AbstractMemorySegmentImpl.checkAccess(AbstractMemorySegmentImpl.java:369) at java.base/jdk.internal.foreign.LayoutPath.checkEnclosingLayout(LayoutPath.java:288) at java.base/jdk.internal.foreign.AbstractMemorySegmentImpl.get(AbstractMemorySegmentImpl.java:777) at hook_test.hookProc(hook_test.java:46) at hook_test.main(hook_test.java:38) Unrecoverable uncaught exception encountered. The VM will now exit
完整代码如下:
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 MemorySegment hook; private static MethodHandle callNextHookEx; public static void main(String[] args) throws Throwable { System.loadLibrary("user32"); Linker linker = Linker.nativeLinker(); SymbolLookup user32Lookup = SymbolLookup.loaderLookup(); MethodHandle setWindowsHookEx = linker.downcallHandle(user32Lookup.find("SetWindowsHookExA").orElseThrow(), FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); callNextHookEx = linker.downcallHandle(user32Lookup.find("CallNextHookEx").orElseThrow(), FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, 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.ADDRESS)); MethodHandle hookProcHandle = MethodHandles.lookup().findStatic(hook_test.class, "hookProc", MethodType.methodType(MemorySegment.class, int.class, MemorySegment.class, MemorySegment.class)); MemorySegment hookProcAddress = linker.upcallStub(hookProcHandle, FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS), Arena.ofAuto()); hook = (MemorySegment) setWindowsHookEx.invoke(WH_KEYBOARD_LL, hookProcAddress, MemorySegment.NULL, 0); if (hook.equals(MemorySegment.NULL)) { System.out.println("Failed to set hook"); return; } else System.out.println("Hook set successfully"); Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { if (!hook.equals(MemorySegment.NULL)) 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 MemorySegment hookProc(int code, MemorySegment wParam, MemorySegment lParam) { if (code >= 0) { System.out.println("wParam address: " + wParam.address() + " lParam address: " + lParam.address()); long vkCode = lParam.get(ValueLayout.JAVA_INT, 0); long scanCode = lParam.get(ValueLayout.JAVA_INT, 4); long flags = lParam.get(ValueLayout.JAVA_INT, 8); long time = lParam.get(ValueLayout.JAVA_INT, 12); long dwExtraInfo = lParam.get(ValueLayout.JAVA_LONG, 16); System.out.printf("vkCode: %d, scanCode: %d, flags: %d, time: %d, dwExtraInfo: %d\n", vkCode, scanCode, flags, time, dwExtraInfo); if (wParam != MemorySegment.NULL && wParam.byteSize() >= ValueLayout.JAVA_INT.byteSize()) { long wParamValue = wParam.get(ValueLayout.JAVA_INT, 0); if (wParamValue == WM_KEYDOWN) System.out.printf("int code:%s long wParam:%s\n", code, wParamValue); } else System.err.println("Invalid wParam segment size or null segment."); } try { return (MemorySegment) callNextHookEx.invoke(hook, code, wParam, lParam); } catch (Throwable t) { System.err.println(t.getMessage()); return MemorySegment.NULL; } } }
环境信息:
Oracle OpenJDK 23.0.1 23(Preview) IntelliJ IDEA 2024.2.1 (Community Edition) Build #IC-242.21829.142, built on August 29, 2024 Runtime version: 21.0.3+13-b509.11 amd64 (JCEF 122.1.9) VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o. Toolkit: sun.awt.windows.WToolkit Windows 11.0 GC: G1 Young Generation, G1 Concurrent GC, G1 Old Generation Memory: 2014M Cores: 12 Registry: debugger.attach.dialog.enabled=true documentation.show.toolbar=true ide.experimental.ui=true i18n.locale= terminal.new.ui.show.promotion=false org.toml.json.schema=false Non-Bundled Plugins: color.scheme.Eclipse Dark Theme (1.4) Lombook Plugin (242.20224.331) org.teavm.idea (0.10.1) Kotlin: 242.21829.142-IJ
问题原因
错误核心是:Windows API传递给钩子回调的wParam和lParam是64位地址值(64位系统上),但当前upcall函数描述符把它们声明为ValueLayout.ADDRESS,JDK Foreign API会将其包装成一个MemorySegment,但这个Segment没有关联任何内存大小(byteSize=0),读取时直接越界。另外,wParam本质是消息ID(比如WM_KEYDOWN),不是指针,不需要当作MemorySegment处理。
修复方案
- 修改upcall函数描述符:将
wParam和lParam的类型从ValueLayout.ADDRESS改为ValueLayout.JAVA_LONG,直接接收地址数值。 - 调整hookProc方法参数:对应改成
int code, long wParam, long lParam。 - 手动创建合法MemorySegment:用
MemorySegment.ofAddress(lParam, structLayout.byteSize(), Arena.global())从lParam地址值创建Segment,同时提前定义KBDLLHOOKSTRUCT的内存布局,确保内存大小正确。 - 直接用wParam数值判断消息类型:不需要转成Segment。
修改后的完整代码
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 MemorySegment hook; private static MethodHandle callNextHookEx; // 定义KBDLLHOOKSTRUCT的内存布局,匹配C结构体字段和Windows默认对齐 private static final StructLayout 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") ).withByteAlignment(8); public static void main(String[] args) throws Throwable { System.loadLibrary("user32"); Linker linker = Linker.nativeLinker(); SymbolLookup user32Lookup = SymbolLookup.loaderLookup(); // 初始化Windows API的MethodHandle MethodHandle setWindowsHookEx = linker.downcallHandle( user32Lookup.find("SetWindowsHookExA").orElseThrow(), FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT) ); callNextHookEx = linker.downcallHandle( user32Lookup.find("CallNextHookEx").orElseThrow(), // 同步修改CallNextHookEx的参数类型为LONG FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, 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.ADDRESS) ); // 调整hookProc的MethodType,参数改为int, long, long MethodHandle hookProcHandle = MethodHandles.lookup().findStatic( hook_test.class, "hookProc", MethodType.methodType(MemorySegment.class, int.class, long.class, long.class) ); // 调整upcall的函数描述符,匹配参数类型 MemorySegment hookProcAddress = linker.upcallStub( hookProcHandle, FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG), Arena.ofAuto() ); // 设置钩子 hook = (MemorySegment) setWindowsHookEx.invoke(WH_KEYBOARD_LL, hookProcAddress, MemorySegment.NULL, 0); if (hook.equals(MemorySegment.NULL)) { System.out.println("Failed to set hook"); return; } else { System.out.println("Hook set successfully"); } // 注册钩子关闭的 shutdown hook Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { if (!hook.equals(MemorySegment.NULL)) { unhookWindowsHookEx.invoke(hook); } } catch (Throwable t) { System.err.println(t.getMessage()); } })); // 消息循环 try (Arena arena = Arena.ofAuto()) { MemorySegment msg = arena.allocate(28); // MSG结构体大小 while ((int) getMessage.invoke(msg, MemorySegment.NULL, 0, 0) != 0) { // 可选:添加TranslateMessage和DispatchMessage处理消息,避免系统消息阻塞 } } } public static MemorySegment hookProc(int code, long wParam, long lParam) { if (code >= 0) { System.out.printf("wParam: %d, lParam address: 0x%x\n", wParam, lParam); // 从lParam地址创建合法的MemorySegment,指定结构体大小 try (SegmentScope scope = SegmentScope.global()) { MemorySegment kbdStruct = MemorySegment.ofAddress(lParam, KBDLLHOOKSTRUCT_LAYOUT.byteSize(), scope); // 通过布局读取字段,避免手动计算偏移出错 long vkCode = kbdStruct.get(ValueLayout.JAVA_INT, KBDLLHOOKSTRUCT_LAYOUT.sequenceElementOffset(0)); long scanCode = kbdStruct.get(ValueLayout.JAVA_INT, KBDLLHOOKSTRUCT_LAYOUT.sequenceElementOffset(1)); long flags = kbdStruct.get(ValueLayout.JAVA_INT, KBDLLHOOKSTRUCT_LAYOUT.sequenceElementOffset(2)); long time = kbdStruct.get(ValueLayout.JAVA_INT, KBDLLHOOKSTRUCT_LAYOUT.sequenceElementOffset(3)); long dwExtraInfo = kbdStruct.get(ValueLayout.JAVA_LONG, KBDLLHOOKSTRUCT_LAYOUT.sequenceElementOffset(4)); System.out.printf("vkCode: %d, scanCode: %d, flags: %d, time: %d, dwExtraInfo: %d\n", vkCode, scanCode, flags, time, dwExtraInfo); // 直接用wParam数值判断按键消息 if (wParam == WM_KEYDOWN) { System.out.printf("Key down detected, code: %d\n", code); } } catch (Exception e) { System.err.println("Failed to read KBDLLHOOKSTRUCT: " + e.getMessage()); } } // 调用CallNextHookEx,参数类型匹配 try { return (MemorySegment) callNextHookEx.invoke(hook, code, wParam, lParam); } catch (Throwable t) { System.err.println("CallNextHookEx failed: " + t.getMessage()); return MemorySegment.NULL; } } }
关键修改说明
- 结构体布局定义:用
StructLayout明确字段和对齐方式,避免手动计算偏移出错,同时确保Segment大小合法。 - upcall参数类型调整:将
wParam和lParam改为JAVA_LONG,直接接收地址数值,避免生成空Segment。 - 合法Segment创建:用
MemorySegment.ofAddress指定内存大小,保证读取操作不会越界。 - CallNextHookEx参数同步:修改其函数描述符的参数类型,保证调用时类型一致。
内容的提问来源于stack exchange,提问作者halu86x
相关产品推荐
相关产品推荐

