javawindowshooknative

Native Hook using java.lang.foreign in JDK23


I wanted to try using NativeHook, which can be written entirely in Java, instead of JNativeHook, so I wrote some test code. However, the output is always the same no matter which key I press. Can someone tell me how to get the correct key code?

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;
        }
    }
}

I expected that even if the correct key code was not obtained, a different value would be output from lParam for each key input. However, when I tried to run it, all the values ​​were the same and they changed every time I launched it.

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

Solution

  • As greg-449 suggested in his comment, the two parameters shouldn't be longs. This is what my MinGW includes show:

    typedef LRESULT (CALLBACK *HOOKPROC)(int code,WPARAM wParam,LPARAM lParam);
    
    typedef UINT_PTR WPARAM;
    typedef LONG_PTR LPARAM;
    

    In other words - one is a pointer to an int, the other a pointer to a long. In your code you should use MemoryLayout as types, and get their int and long values.


    Note that LRESULT also isn't a long, it's a pointer to a long:

    typedef LONG_PTR LRESULT;
    

    That means you have to change the return type of both callNextHookEx and hookProc. The latter can return the MemorySegment returned by callNextHookEx or MemorySegment.NULL on failure.