通过实现全局键盘监听来学习JNI的使用

引言

我是Java程序员,只用C++打过算法比赛(只熟悉STL容器),所以对C++不了解。在用C++实现全局监听时,也走了不少弯路,但现在我已成功实现这些功能,并对JNI有进一步的了解。

JNI

方法注册

C++实现native方法后,需要注册才能被JVM识别。这里有两种注册方式(静态注册和动态注册),但我感觉静态注册几乎没有优点,所以只讲动态注册的实现。

jstring xf1(JNIEnv* env, jclass jcls) {
    return env->NewStringUTF("HELLO, WORLD!");
};
void xf2(JNIEnv *env, jclass jcls, jstring jstr) {
    cout<<jstring2string(env,jstr)<<endl;		//	这是我自定义的函数,此处不再展示其函数体
};

static JNINativeMethod methods[] = {        //java中的方法名称,方法签名,cpp中的对应的函数名称
        {"hello1", "()Ljava/lang/String;", (void*) func1},
        {"hello2", "(Ljava/lang/String;)V", (void*) func2},
};

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_21) != JNI_OK)
        return JNI_FALSE;

    jclass jcls = env->FindClass(JAVA_CLASS);
    return env->RegisterNatives(jcls, methods, sizeof(methods)/sizeof(JNINativeMethod)) < 0?JNI_FALSE:JNI_VERSION_21;
}

这里需要关注几个点:

  • func被强转为(void*):这应该是将其转为函数指针了(我是Java程序员),任何函数的任何返回值都是被强转为这个值。
  • 方法签名一定要正确
  • JNI_VERSION_XX一定要对:有些版本的JDK复用了上一代JDK的JNI版本

类型签名

Java类型JNI的签名
intI
shortS
longJ
booleanZ
byteB
charC
ObjectL包名/类名;
数组[类型签名
无返回值V

注意:

  • 包名以/分割,且有;
  • 数组有几个维度就有几个[,维度拆完之后的类型签名按照之前的规则不变
  • 任何类的任何构造器的名字都是<init>,只不过该构造器方法的签名不一样

实例:

  • 获取System.out的字段ID:env->GetStaticFieldID(System,“out”,“Ljava/io/PrintStream;”)
  • 获取【无返回值,参数为(int,boolean[],String)】静态方法的方法ID:env->GetStaticMethodID(Main,“handler”,“(I[ZLjava/lang/String;)V”);
  • 获取Main的内部类Point:env->FindClass(“com/jni/Main$Point”);(这里只是路径而已,但如果是签名则也不要忘了$L;

编译运行

如果是Windows系统,那么肯定要编译成DLL格式;如果是Linux系统,那么就要编译成SO格式
C++项目规范
基于此,本人在.bat文件中编写了一个脚本,存放两条编译命令:

  • g++ -c Main.cpp -o ..\\out\\output.o -I C:\你的C++项目路径\C_C++\.h_lib
  • g++ ..\\out\\output.o -o ..\\out\\result.dll -shared -lgdi32(-lgdi32是因为有报错,所以我加上了)



好了我们可以来看看C++的代码了

C++

#include <jni.h>
#include <time.h>
#include <conio.h>
#include <windows.h>
#include <bits/stdc++.h>

using namespace std;

static JNIEnv* env;
static jclass Main;
static jmethodID callback;
static HHOOK hKbHook,hMsHook;
static const char* const JAVA_CLASS="com/jni/Main";


LRESULT CALLBACK kbProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode == HC_ACTION) {
        PKBDLLHOOKSTRUCT hs = (PKBDLLHOOKSTRUCT)lParam;     //  hs->flags;      LLKHF_EXTENDED
        env->CallStaticVoidMethod(Main,callback,hs->vkCode,hs->time,(wParam==WM_KEYDOWN?JNI_TRUE:JNI_FALSE));       //  GetTickCount():hs->time,即为系统运行时间
    }
    return CallNextHookEx(hKbHook, nCode, wParam, lParam);
}
LRESULT CALLBACK msProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode == HC_ACTION) {
        PMSLLHOOKSTRUCT hs = (PMSLLHOOKSTRUCT)lParam;
//  WM_XBUTTONDBLCLK:鼠标侧键的双击,WM_MOUSEWHEEL鼠标中键的滚动
        
//	这里将code复用到极致(反正坐标只用了一个int中的几个位而已)
        int code = ((hs->pt.x)<<16)|(hs->pt.y)|0x80000000;
        switch (wParam) {
            case WM_LBUTTONDOWN:
                code|=0x8000;
                break;
            case WM_RBUTTONDOWN:
                code|=0x4000;
                break;
            case WM_MBUTTONDOWN:
                code|=0x40000000;
                break;
            case WM_LBUTTONUP:
                code|=0x8000;
                break;
            case WM_RBUTTONUP:
                code|=0x4000;
                break;
            case WM_MBUTTONUP:
                code|=0x40000000;
                break;
            case WM_MOUSEWHEEL:
                code|=((((hs->mouseData)&0x80000000)==0)?0x3800:0x38000000);
                break;
            default:
                // cout<<"exec default logic"<<endl;
                return CallNextHookEx(hMsHook, nCode, wParam, lParam);
        }
        jboolean ud = (wParam==WM_LBUTTONDOWN||wParam==WM_RBUTTONDOWN||wParam==WM_MBUTTONDOWN);
        env->CallStaticVoidMethod(Main,callback,code,hs->time,ud);       //  GetTickCount():hs->time,即为系统运行时间
    }
    return CallNextHookEx(hMsHook, nCode, wParam, lParam);
}

jobject func1(JNIEnv* env, jclass jcls) {     //g++ Main.o -o Main.dll -shared -lgdi32(-lgdi32必须加,不然这一步报错)
    jclass pCls =    env->FindClass("pc/jni/Main$Point");
    jmethodID pMtd = env->GetMethodID(pCls,"<init>","(IIIII)V");

    POINT point;
    GetCursorPos(&point);
	HDC hdc = GetDC(NULL);
	COLORREF color = GetPixel(hdc,point.x,point.y);
	ReleaseDC(NULL,hdc);

    return env->NewObject(pCls,pMtd,point.x,point.y,GetRValue(color),GetGValue(color),GetBValue(color));
};
jboolean xf4(JNIEnv *env, jclass jcls, jint k) {
    return GetKeyState(k)>0?JNI_TRUE:JNI_FALSE;
};
void func2(JNIEnv* _env, jclass jcls) {
    env = _env;
    Main = env->FindClass(JAVA_CLASS);  //  或者直接把Main赋值为jcls
    callback = env->GetStaticMethodID(Main,"handler","(IIZ)V");

    hKbHook = SetWindowsHookEx(WH_KEYBOARD_LL, kbProc, GetModuleHandle(NULL), 0);       //  KeyboardProc
    hMsHook = SetWindowsHookEx(WH_MOUSE_LL, msProc, GetModuleHandle(NULL), 0);
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    UnhookWindowsHookEx(hMsHook);
    UnhookWindowsHookEx(hKbHook);
}
jlong func3(JNIEnv* env, jclass jcls) {
    return time(NULL)*1000-GetTickCount();
}

static JNINativeMethod methods[] = {        //java中的方法名称,方法签名(需要查看方法签名规则),cpp中的对应的方法名称
		{"getPoint", "()Lpc/jni/Main$Point;", (void*) func1},
        {"initHook", "()V", (void*) func2},
        {"getRunStamp", "()J", (void*) func3}
};


JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_21) != JNI_OK)		//注意看你的JDK版本(但有些JDK版本没有对应的JNI版本,此时降级即可(JDK-16,用JNI-10))
        return JNI_FALSE;

    jclass jcls = env->FindClass(JAVA_CLASS);
    return env->RegisterNatives(jcls, methods, sizeof(methods)/sizeof(JNINativeMethod)) < 0?JNI_FALSE:JNI_VERSION_21;
}
public class Main {
	private static native String hello1();

	private static native void hello2(String msg);

	private static native Point getPoint();

	private static native long getRunStamp();

	private static native boolean getKeyState(int k);

	private static native void initHook();

	private static void handler(int code, int stamp, boolean isPress) {
		if (code != (code &= Integer.MAX_VALUE)) {          //  鼠标
			switch (code & 0x4000_C000) {
				case 0x4000_0000 -> {//中键
					System.out.println("坐标:" + ((code >> 16) & 0x07FF) + "," + (code & 0x07FF) + ",时刻:" + new DateTime(SysRunTime.MOMENT + stamp) + (isPress ? ",左键点击" : ",左键释放"));
				}
				case 0x0000_8000 -> {//左键
					System.out.println("坐标:" + ((code >> 16) & 0x07FF) + "," + (code & 0x07FF) + ",时刻:" + new DateTime(SysRunTime.MOMENT + stamp) + (isPress ? ",右键点击" : ",右键释放"));
				}
				case 0x0000_4000 -> {//右键
					System.out.println("坐标:" + ((code >> 16) & 0x07FF) + "," + (code & 0x07FF) + ",时刻:" + new DateTime(SysRunTime.MOMENT + stamp) + (isPress ? ",中键点击" : ",中键释放"));
				}
				case 0x0000_0000 -> {//滚动
					System.out.println("坐标:" + ((code >> 16) & 0x07FF) + "," + (code & 0x07FF) + ",时刻:" + new DateTime(SysRunTime.MOMENT + stamp) + (((code&0x3800)==0)?"向上滚动":"向下滚动"));
				}
			}
		} else {                                            //  键盘
			System.out.println("code:" + KeyEvent.getKeyText(code) + ",time:" + new DateTime(SysRunTime.MOMENT + stamp) + ",isPress:" + isPress);
		}
	}


	public static void main(String[] args) throws Exception {
		System.load("C:\\.....................\\out\\result.dll");		//	System.load:调用指定文件,System.loadlibray:调用库名(此库的路径要求严格)

		System.out.println(getPoint());
		System.out.println(getRunStamp());
		
		initHook();
	}

	private static final class Point {
		public final int x, y;
		public final int r, g, b;

		public Point(int x, int y, int r, int g, int b) {
			this.x = x;
			this.y = y;
			this.r = r;
			this.g = g;
			this.b = b;
		}

		@Override
		public String toString() {
			return "坐标{x=" + x + ", y=" + y + ",色素{r=" + r + ", g=" + g + ", b=" + b + '}';
		}

		@Override
		public boolean equals(Object o) {
			if (this == o)
				return true;
			if (o instanceof Point point)
				return x == point.x && y == point.y && r == point.r && g == point.g && b == point.b;
			return false;
		}

		@Override
		public int hashCode() {
			return Objects.hash(x, y, r, g, b);
		}
	}

	private static final class SysRunTime {
		public final int run;
		public final long sys;

		public final long moment;
		public static final long MOMENT = Main.getRunStamp();

		public SysRunTime(int run, long sys) {
			this.run = run;
			this.sys = sys;

			this.moment = sys - run;
		}

		@Override
		public boolean equals(Object o) {
			if (this == o)
				return true;
			else if (o instanceof SysRunTime stamp)
				return run == stamp.run && sys == stamp.sys;
			else
				return false;
		}

		@Override
		public int hashCode() {
			return Objects.hash(run, sys);
		}

		@Override
		public String toString() {
			return "TimeStamp{run=" + run + ", sys=" + sys + '}';
		}
	}
}

注意:

  • System.loadlibray和System.load有区别,其中前者是相对路径后者是绝对路径(后者只需指定即可,前者可能无法被JVM找到(前者只需写入不含后缀的文件名即可))
  • 时间戳:并非OS时间戳,而是系统已运行的时间戳。所以我通过这种方式获取了事件的实际时间戳
  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值