相對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調用這些本地方法時就能根據映射關係找到。