相对C++调用java而言,使用JNI实现Java调用C++相对比较简单点,因为不用自己启动和管理一个JVM。
最简单的流程:
1. 编写一份简单的HelloWorld.java
class HelloWorld {
public native void displayHelloWorld();
static {
System.loadLibrary("hello");
}
public static void main(String[] args) {
new HelloWorld().displayHelloWorld();
}
}
2. 执行javah –jni HelloWorld生成一份C++的头文件HelloWorld.h
/* DO NOT EDIT THIS FILE - it is machinegenerated */
#include <jni.h>
/* Header for class HelloWorld */
#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
*Class: HelloWorld
*Method: displayHelloWorld
*Signature: ()V
*/
JNIEXPORT void JNICALLJava_HelloWorld_displayHelloWorld
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
3.根据已经生成的头文件,编写一份.cpp文件HelloWorld.cpp(其中函数名跟.h文件中自动生成的保持一致),并且要include jni.h文件,(该文件可以在%JAVA_HOME%/include文件夹下面找到)文件引入,因为在程序中的JNIEnv、jobject等类型都是在该头文件中定义的
#include <jni.h>
#include "HelloWorld.h"
#include <stdio.h>
JNIEXPORT void JNICALLJava_HelloWorld_displayHelloWorld(JNIEnv *env, jobject obj){
printf("Helloworld!\n");
return;
}
4. 对C++文件进行编译生成目标文件HelloWorld.o:g++ -c -fPICHelloWorld.cpp 生成HelloWorld.o
5.生成动态链接文件libhello.so: g++ -fpic -shared -oHelloWorld.so libhello.o
--千万要注意.so文件的命名规则,一定要以lib+hello+.so的格式,其中hello是必须跟System.loadLibrary("hello");中的hello保持一致。尤其是lib作为前缀千万不能忘记。
6. 要把生成的libhello.so拷贝到java.library.path目录下,这个目录不知怎么在linux查询,有个笨办法就是写一个简单java程序,执行System.out.println(System.getProperty("java.library.path"));就可以得到目录路径。或者可以export LD_LIBRARY_PATH=(libhello.so所在的目录)
7.执行javaHelloWorld,正常输出为:
Hello world!
--注意第6步,如果没有很好解决java.library.path路径问题的话,执行HelloWorld时就会报错:
Exception in thread "main"java.lang.UnsatisfiedLinkError: no hello in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at HelloWorld.<clinit>(HelloWorld.java:5)
8. 静态注册和动态注册
在java调用C++的过程中,有一个很重要的步骤就是:java运行时如何去查找到某个native方法,这就涉及到native方法要注册到JVM中,这就分成了动态(手工)注册和静态(自动)注册的概念了,为什么上述流程没有看到这一步呢?因为我们偷懒,选择了静态(自动)注册这一方式。我们用javah xxxx去生成.h文件,这样native的函数名就按标准格式生成了,这样JVM在加载动态库后就可以根据标准格式去找到对应的native函数调用。至于动态注册的过程后续再补充。
补充:
动态注册的流程
动态注册和静态注册的区别就是动态注册不需要使用javah生成JNI标准规范的.h头文件,可以根据自己的需要去编写C++的头文件和源文件,函数名也可以自己定义,比如HelloWorld.c(这里为了简单,用C语言的文件,其实也可以使用C++)
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <jni.h>
#include <assert.h>
/*自己定义函数名,而非javah所生成的复杂的标准的函数名*/
JNIEXPORT jstring JNICALL native_hello(JNIEnv *env, jclass clazz)
{
printf("hello in c native code./n");
return (*env)->NewStringUTF(env, "hello world returned.");
}
// 指定要注册的类,把
#define JNIREG_CLASS "HelloWorld"
// 定义一个JNINativeMethod数组,其中的成员就是Java代码中对应的native方法,下面代码其实使用了构造函数初始化了JNINativeMethod数组中的第一个对象
static JNINativeMethodgMethods[] = {
{ "hello", "()Ljava/lang/String;", (void*)native_hello},
};
其中JNINativeMethod是一个struct结构体:
typedefstruct {
constchar* name;
constchar* signature;
void* fnPtr;
} JNINativeMethod;
//调用JNIEnv上RegisterNatives来真正实现本地函数名和java方法名的映射关系注册到JVM中
staticint registerNativeMethods(JNIEnv* env, constchar* className,
JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
clazz =(*env)->FindClass(env, className);
if (clazz == NULL) {
returnJNI_FALSE;
}
if ((*env)->RegisterNatives(env, clazz,gMethods, numMethods) < 0) {
returnJNI_FALSE;
}
returnJNI_TRUE;
}
//实现JNI_OnLoad函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)-> GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
if (!registerNativeMethods(env, JNIREG_CLASS, gMethods,sizeof(gMethods) / sizeof(gMethods[0]))) { //注册
return -1;
}
result = JNI_VERSION_1_4;
return result;
}
--代码流程并不复杂,简单来说就是当java代码中调用System.loadLibrary(hello.so)时,JVM会先去找到JNI_OnLoad函数,在该该函数中调用了registerNativeMethods(env,JNIREG_CLASS, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))---à(*env)->RegisterNatives(env, clazz, gMethods,numMethods),最终调到了RegisterNatives真正把本地函数名和java方法名的映射关系注册到JVM中,后续java调用这些本地方法时就能根据映射关系找到。