通過第一篇文章,大家明白了呼叫native方法之前,首先要呼叫System.loadLibrary介面載入一個實現了native方法的動態庫才能正常訪問,否則就會丟擲java.lang.UnsatisfiedLinkError異常,找不到XX方法的提示。現在我們想想,在Java中呼叫某個native方法時,JVM是通過什麼方式,能正確的找到動態庫中C/C++實現的那個native函式呢?
JVM查詢native方法有兩種方式:
1> 按照JNI規範的命名規則
2> 呼叫JNI提供的RegisterNatives函式,將本地函式註冊到JVM中。(後面會詳細介紹)
本文通過第一篇文章HelloWorld示例中的Java_com_study_jnilearn_HelloWorld_sayHello函式來詳細介紹第一種方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_HelloWorld */ #ifndef _Included_com_study_jnilearn_HelloWorld #define _Included_com_study_jnilearn_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_HelloWorld * Method: sayHello * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello (JNIEnv *, jclass, jstring); #ifdef __cplusplus } #endif #endif |
JNIEXPORT 和 JNICALL的作用:
在上篇文章中,我們在將HelloWorld.c編譯成動態庫的時候,用-I引數包含了JDK安裝目錄下的兩個標頭檔案目錄:
1 |
gcc -dynamiclib -o /Users/yangxin/Library/Java/Extensions/libHelloWorld.jnilib jni/HelloWorld.c -framework JavaVM -I/$JAVA_HOME/include -I/$JAVA_HOME/include/darwin |
其中第一個目錄為jni.h標頭檔案所在目錄,第二個是跨平臺標頭檔案目錄(mac os x系統下的目錄名為darwin,在windows下目錄名為win32,linux下目錄名為linux),用於定義與平臺相關的巨集,其中用於標識函式用途的兩個巨集JNIEXPORT 和 JNICALL,就定義在darwin目錄下的jni_md.h標頭檔案中。在Windows中編譯dll動態庫規定,如果動態庫中的函式要被外部呼叫,需要在函式宣告中新增__declspec(dllexport)標識,表示將該函式匯出在外部可以呼叫。在Linux/Unix系統中,這兩個巨集可以省略不加。這兩個平臺的區別是由於各自的編譯器所產生的可執行檔案格式不一樣。這裡有篇文章詳細介紹了兩個平臺編譯的動態庫區別:http://www.cnblogs.com/chio/archive/2008/11/13/1333119.html。JNICALL在windows中的值為__stdcall,用於約束函式入棧順序和堆疊清理的規則。
Windows下jni_md.h標頭檔案內容:
1 2 3 4 5 6 7 8 9 10 11 12 |
#ifndef _JAVASOFT_JNI_MD_H_ #define _JAVASOFT_JNI_MD_H_ #define JNIEXPORT __declspec(dllexport) #define JNIIMPORT __declspec(dllimport) #define JNICALL __stdcall typedef long jint; typedef __int64 jlong; typedef signed char jbyte; #endif |
Linux下jni_md.h標頭檔案內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#ifndef _JAVASOFT_JNI_MD_H_ #define _JAVASOFT_JNI_MD_H_ #define JNIEXPORT #define JNIIMPORT #define JNICALL typedef int jint; #ifdef _LP64 /* 64-bit Solaris */ typedef long jlong; #else typedef long long jlong; #endif typedef signed char jbyte; #endif |
從Linux下的jni_md.h標頭檔案可以看出來,JNIEXPORT 和 JNICALL是一個空定義,所以在Linux下JNI函式宣告可以省略這兩個巨集。
函式的命名規則:
用javah工具生成函式原型的標頭檔案,函式命名規則為:Java_類全路徑_方法名。如Java_com_study_jnilearn_HelloWorld_sayHello,其中Java_是函式的字首,com_study_jnilearn_HelloWorld是類名,sayHello是方法名,它們之間用 _(下劃線) 連線。
函式引數:
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello(JNIEnv *, jclass, jstring);
第一個引數:JNIEnv* 是定義任意native函式的第一個引數(包括呼叫JNI的RegisterNatives函式註冊的函式),指向JVM函式表的指標,函式表中的每一個入口指向一個JNI函式,每個函式用於訪問JVM中特定的資料結構。
第二個引數:呼叫java中native方法的例項或Class物件,如果這個native方法是例項方法,則該引數是jobject,如果是靜態方法,則是jclass
第三個引數:Java對應JNI中的資料型別,Java中String型別對應JNI的jstring型別。(後面會詳細介紹JAVA與JNI資料型別的對映關係)
函式返回值型別:
夾在JNIEXPORT和JNICALL巨集中間的jstring,表示函式的返回值型別,對應Java的String型別
總結:
當我們熟悉了JNI的native函式命名規則之後,就可以不用通過javah命令去生成相應java native方法的函式原型了,只需要按照函式命名規則編寫相應的函式原型和實現即可。
比如com.study.jni.Utils類中還有一個計算加法的native例項方法add,有兩個int引數和一個int返回值:public native int add(int num1, int num2),對應JNI的函式原型就是:JNIEXPORT jint JNICALL Java_com_study_jni_Utils_add(JNIEnv *, jobject, jint,jint);