引言
我是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的签名 |
---|---|
int | I |
short | S |
long | J |
boolean | Z |
byte | B |
char | C |
Object | L包名/类名; |
数组 | [类型签名 |
无返回值 | 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格式
基于此,本人在.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时间戳,而是系统已运行的时间戳。所以我通过这种方式获取了事件的实际时间戳