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

基于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字段(虚拟按键码)。

修改步骤及代码

  1. 定义KBDLLHOOKSTRUCT结构体的内存布局:对应Windows原生结构体的字段,创建Java Foreign API的内存布局。
  2. 修改钩子回调的参数类型:将long lParam改为MemorySegment lParam,以便访问指针指向的内存。
  3. 读取结构体中的按键码:通过VarHandle读取vkCode字段。
  4. 完善消息循环:添加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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 20:24:55