Java以JNI形式调用C/C++动态库实现回调方法

步骤一:

Java生成.h头文件(以Test.java为例)

javac -encoding utf8 -h . Test.java

若Test.java有依赖,依赖类需要提前编译,如Test.java里依赖了Callback.java。

javac Callback.java

并把生成的.class放置在对应包结构层次的文件目录里,如com.hw.Callback.java则放置在/com/hw目录

执行后生成com_hw_Test.h

扩展:如其有包层次结构,执行java Test时需要带上与其包层次结构一致的文件目录

java com/hw/Test

Java代码:

调用接口主类 Test.java

public class Test {

	public static String driverLib = "";
	static {
		driverLib = "/xxx/xxx/xxx.so";
		System.load(driverLib);
	}

	public native int test1();

	public native int test2();

	public native void recvCallback(Callback callback);

	public static void main(String[] args) throws InterruptedException {
		
		Test test= new Test();
		test.recvCallback(new Callback() {
			@Override
			public void recv(String message) {
				System.out.println(message);
			}
		});

		Thread.sleep(50000);
	}
}

注册回调对象接口类 Callback.java

public interface Callback {

	void recv(String message);
	
}

步骤二:

C/C++根据头文件实现接口,生成动态库(该代码例子存在问题,下文详解)

#include 

#include "com_hw_test.h"

int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReasion,PVOID pvReserved)
{
    return TRUE;
}

JNIEXPORT void JNICALL Java_com_hw_Test_recvCallback(JNIEnv * env, jobject obj, jobject log)
{
    jclass cls = (*env)->GetObjectClass(env, log);
    jmethodID jmid = (*env)->GetMethodID(env, cls, "recv", "(Ljava/lang/String;)V");
    jstring info = (*env)->NewStringUTF(env, "i am a error!");
    (*env)->CallVoidMethod(env,log, jmid,info);
    (*env)->ReleaseStringUTFChars(env,info,(*env)->GetStringUTFChars(env, info, FALSE));
}

以上代码实现后发现一个问题,首次调用回调成功,再调用出现崩溃。

例子中我们把JNI接口的指针JNIEnv *env,和jobject obj保存在DLL中的变量里。一段时间后,DLL中的消息接收线程接收到服务器发来的消息,并试图通过保存过的env和obj来调用先前的java对象的方法来处理此消息。


然而JNI文档上说,JNI接口的指针JNIEnv*不能在c++的线程间共享,在我的程序中,如果接收线程试图调用java对象的方法,程序会突然退出。

不知道有没有方法突破JNI接口的指针不能在多个c++线程中共享的限制?

在 Oracle Java Technologies | Oracle 提到,
JNI接口指针不可为多个线程共用,但是java虚拟机的JavaVM指针是整个jvm公用的. 于是,在DLL中可以调用:

static JavaVM* gs_jvm;
env->GetJavaVM(&gs_jvm); //来获取JavaVM指针.获取了这个指针后,在DLL中的另一个线程里,可以调用:
JNIEnv *env;
gs_jvm->AttachCurrentThread((void **)&env, NULL);

来将DLL中的线程 "attached to the virtual machine"(不知如何翻译...),同时获得了这个线程在jvm中的  JNIEnv指针.
    由于我需要做的是在DLL中的一个线程里改变某个java对象的值,所以,还必须获取那个java对象的jobject指针.同 JNIEnv 指针一样,jobject指针也不能在多个线程中共享. 就是说,不能直接在保存一个线程中的jobject指针到全局变量中,然后在另外一个线程中使用它.幸运的是,可以用
gs_object=env->NewGlobalRef(obj);
来将传入的obj保存到gs_object中,从而其他线程可以使用这个gs_object来操纵那个java对象了.
示例代码如下:

//DLL代码:Test.cpp:
#include "test.h"
#include
#include
static JavaVM *gs_jvm=NULL;
static jobject gs_object=NULL;
static int gs_i=10;
void WINAPI ThreadFun(PVOID argv)
{ 
 JNIEnv *env;
 gs_jvm->AttachCurrentThread((void **)&env, NULL);
 jclass cls = env->GetObjectClass(gs_object);
 jfieldID fieldPtr = env->GetFieldID(cls,"value","I");
 while(1)
 {
 Sleep(100);
 //在DLL中改变外面的java对象的value变量的值.
 env->SetIntField(gs_object,fieldPtr,(jint)gs_i++);
 }
}
JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)
{
 printf("come into test.dll/n");
 //Returns “0” on success; returns a negative value on failure. 
 int retGvm=env->GetJavaVM(&gs_jvm);
 //直接保存obj到DLL中的全局变量是不行的,应该调用以下函数:
 gs_object=env->NewGlobalRef(obj);
 HANDLE ht=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadFun,0,NULL,NULL);
 printf("the Handle ht is:%d/n",ht);
}

你可能感兴趣的:(java,c++,JNI)