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

使用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处理。

修复方案

  1. 修改upcall函数描述符:将wParam和lParam的类型从ValueLayout.ADDRESS改为ValueLayout.JAVA_LONG,直接接收地址数值。
  2. 调整hookProc方法参数:对应改成int code, long wParam, long lParam。
  3. 手动创建合法MemorySegment:用MemorySegment.ofAddress(lParam, structLayout.byteSize(), Arena.global())从lParam地址值创建Segment,同时提前定义KBDLLHOOKSTRUCT的内存布局,确保内存大小正确。
  4. 直接用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 22:15:55