JNI 允許您從本機代碼內調用 Java 類方法。要做到這一點,通常必須使用 Invocation API 在本機代碼內創建和初始化一個 JVM。下列是您可能決定從 C/C++ 代碼調用 Java 代碼的典型情況:
-
希望實現的這部分代碼是平臺無關的,它將用於跨多種平臺使用的功能。
-
需要在本機應用程序中訪問用 Java 語言編寫的代碼或代碼庫。
- 希望從本機代碼利用標準 Java 類庫。
從 C/C++ 調用 Java 方法過程的四個步驟如下:
-
編寫 Java 代碼。這個步驟包含編寫一個或多個 Java 類,這些類實現(或調用其它方法實現)您想要訪問的功能。
-
編譯 Java 代碼。在能夠使用這些 Java 類之前,必須成功地將它們編譯成字節碼。
-
編寫 C/C++ 代碼。這個代碼將創建和實例化 JVM,並調用正確的 Java 方法。
- 運行本機 C/C++ 應用程序。將運行應用程序以查看它是否正常工作。我們還將討論一些用於處理常見錯誤的技巧。
我們從編寫一個或多個 Java 源代碼文件開始,這些文件將實現我們想要本機 C/C++ 代碼使用的功能。
下面顯示了一個 Java 代碼示例 Sample2.java:
1. public class Sample2 2. { 3. public static int intMethod(int n) { 4. return n*n; 5. } 6. 7. public static boolean booleanMethod(boolean bool) { 8. return !bool; 9. } 10. } |
注:Sample2.java 實現了兩個 static
Java 方法:intMethod(int n)
和 booleanMethod(boolean bool)
(分別在第 3 行和第 7 行)。static
方法是一種不需要與對象實例關聯的類方法。調用 static
方法要更容易些,因爲不必實例化對象來調用它們。
接下來,我們將 Java 代碼編譯成字節碼。完成這一步的方法之一是使用隨 SDK 一起提供的 Java 編譯器 javac
。 使用的命令是:
javac Sample1.java |
即使是在本機應用程序中運行,所有 Java 字節碼也必須在 JVM 中執行。因此 C/C++ 應用程序必須包含用來創建和初始化 JVM 的調用。爲了方便我們,SDK 包含了作爲共享庫文件(jvm.dll 或 jvm.so)的 JVM,這個庫文件可以嵌入到本機應用程序中。
讓我們先從瀏覽一下 C 和 C++ 應用程序的整個代碼開始,然後對兩者進行比較。
Sample2.c 是一個帶有嵌入式 JVM 的簡單的 C 應用程序:
1. #include <jni.h> 2. 3. #ifdef _WIN32 4. #define PATH_SEPARATOR ';' 5. #else 6. #define PATH_SEPARATOR ':' 7. #endif 8. 9. int main() 10. { 11. JavaVMOption options[1]; 12. JNIEnv *env; 13. JavaVM *jvm; 14. JavaVMInitArgs vm_args; 15. long status; 16. jclass cls; 17. jmethodID mid; 18. jint square; 19. jboolean not; 20. 21. options[0].optionString = "-Djava.class.path=."; 22. memset(&vm_args, 0, sizeof(vm_args)); 23. vm_args.version = JNI_VERSION_1_2; 24. vm_args.nOptions = 1; 25. vm_args.options = options; 26. status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); 27. 28. if (status != JNI_ERR) 29. { 30. cls = (*env)->FindClass(env, "Sample2"); 31. if(cls !=0) 32. { mid = (*env)->GetStaticMethodID(env, cls, "intMethod", "(I)I"); 33. if(mid !=0) 34. { square = (*env)->CallStaticIntMethod(env, cls, mid, 5); 35. printf("Result of intMethod: %d\n", square); 36. } 37. 38. mid = (*env)->GetStaticMethodID(env, cls, "booleanMethod", "(Z)Z"); 39. if(mid !=0) 40. { not = (*env)->CallStaticBooleanMethod(env, cls, mid, 1); 41. printf("Result of booleanMethod: %d\n", not); 42. } 43. } 44. 45. (*jvm)->DestroyJavaVM(jvm); 46. return 0; 47/ } 48. else 49. return -1; 50. } |
Sample2.cpp 是一個帶有嵌入式 JVM 的 C++ 應用程序:
1. #include <jni.h> 2. 3. #ifdef _WIN32 4. #define PATH_SEPARATOR ';' 5. #else 6. #define PATH_SEPARATOR ':' 7. #endif 8. 9. int main() 10. { 11. JavaVMOption options[1]; 12. JNIEnv *env; 13. JavaVM *jvm; 14. JavaVMInitArgs vm_args; 15. long status; 16. jclass cls; 17. jmethodID mid; 18. jint square; 19. jboolean not; 20. 21. options[0].optionString = "-Djava.class.path=."; 22. memset(&vm_args, 0, sizeof(vm_args)); 23. vm_args.version = JNI_VERSION_1_2; 24. vm_args.nOptions = 1; 25. vm_args.options = options; 26. status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); 27. 28. if (status != JNI_ERR) 29. { 30. cls = (*env)->FindClass(env, "Sample2"); 31. if(cls !=0) 32. { mid = (*env)->GetStaticMethodID(env, cls, "intMethod", "(I)I"); 33. if(mid !=0) 34. { square = (*env)->CallStaticIntMethod(env, cls, mid, 5); 35. printf("Result of intMethod: %d\n", square); 36. } 37. 38. mid = (*env)->GetStaticMethodID(env, cls, "booleanMethod", "(Z)Z"); 39. if(mid !=0) 40. { not = (*env)->CallStaticBooleanMethod(env, cls, mid, 1); 41. printf("Result of booleanMethod: %d\n", not); 42. } 43. } 44. 45. (*jvm)->DestroyJavaVM(jvm); 46. return 0; 47. } 48. else 49. return -1; 50. } |
C 和 C++ 代碼幾乎相同;唯一的差異在於用來訪問 JNI 函數的方法。在 C 中,爲了取出函數指針所引用的值,JNI 函數調用前要加一個 (*env)->
前綴。在 C++ 中,JNIEnv
類擁有處理函數指針查找的內聯成員函數。因此,雖然這兩行代碼訪問同一函數,但每種語言都有各自的語法,如下所示。
C 語法: | cls = (*env)->FindClass(env, "Sample2"); |
C++ 語法: | cls = env->FindClass("Sample2"); |
我們剛纔編寫了許多代碼,但它們都做些什麼呢? 在執行步驟 4 之前,讓我們更深入地研究一下 C 應用程序的代碼。我們將先瀏覽一些必要的步驟,包括準備本機應用程序以處理 Java 代碼、將 JVM 嵌入本機應用程序,然後從該應用程序內找到並調用 Java 方法。
我們從 C 應用程序中所包括的 jni.h C 頭文件開始,如下面的代碼樣本中所示:
#include <jni.h> |
jni.h 文件包含在 C 代碼中所需要的 JNI 的所有類型和函數定義。
接下來,聲明所有希望在程序中使用的變量。JavaVMOption options[]
具有用於 JVM 的各種選項設置。當聲明變量時,確保所聲明的 JavaVMOption options[]
數組足夠大,以便能容納您希望使用的所有選項。在本例中,我們使用的唯一選項就是類路徑選項。因爲在本示例中,我們所有的文件都在同一目錄中,所以將類路徑設置成當前目錄。可以設置類路徑,使它指向任何您希望使用的目錄結構。
以下代碼聲明瞭用於 Sample2.c 的變量:
JavaVMOption options[1]; JNIEnv *env; JavaVM *jvm; JavaVMInitArgs vm_args; |
注:
-
JNIEnv *env
表示 JNI 執行環境。 -
JavaVM jvm
是指向 JVM 的指針。我們主要使用這個指針來創建、初始化和銷燬 JVM。 -
JavaVMInitArgs vm_args
表示可以用來初始化 JVM 的各種 JVM 參數。
JavaVMInitArgs
結構表示用於 JVM 的初始化參數。在執行 Java 代碼之前,可以使用這些參數來定製運行時環境。正如您所見,這些選項是一個參數而 Java 版本是另一個參數。按如下所示設置了這些參數:
vm_args.version = JNI_VERSION_1_2; vm_args.nOptions = 1; vm_args.options = options; |
接下來,爲 JVM 設置類路徑,以使它能找到所需要的 Java 類。在這個特定示例中,因爲 Sample2.class 和 Sample2.exe 都位於同一目錄中,所以將類路徑設置成當前目錄。我們用來爲 Sample2.c 設置類路徑的代碼如下所示:
options[0].optionString = "-Djava.class.path=."; // same text as command-line options for the java.exe JVM |
在可以使用 vm_args
之前,必需爲它留出一些內存。一旦設置了內存,就可以設置版本和選項參數了,如下所示:
memset(&vm_args, 0, sizeof(vm_args)); // set aside enough memory for vm_args vm_args.version = JNI_VERSION_1_2; // version of Java platform vm_args.nOptions = 1; // same as size of options[1] vm_args.options = options; |
處理完所有設置之後,現在就準備創建 JVM 了。先從調用方法開始:
JNI_CreateJavaVM(JavaVM **jvm, void** env, JavaVMInitArgs **vm_args) |
如果成功,則這個方法返回零,否則,如果無法創建 JVM,則返回 JNI_ERR
。
一旦創建了 JVM 之後,就可以準備開始在本機應用程序中運行 Java 代碼。首先,需要使用 FindClass()
函數查找並裝入 Java 類,如下所示:
cls = (*env)->FindClass(env, "Sample2"); |
cls
變量存儲執行 FindClass()
函數後的結果。 如果找到該類,則 cls
變量表示該 Java 類的句柄。如果不能找到該類,則 cls
將爲零。
接下來,我們希望用 GetStaticMethodID()
函數在該類中查找某個方法。我們希望查找方法 intMethod
,它接收一個 int
參數並返回一個 int
。以下是查找 intMethod
的代碼:
mid = (*env)->GetStaticMethodID(env, cls, "intMethod", "(I)I"); |
mid
變量存儲執行 GetStaticMethodID()
函數後的結果。如果找到了該方法,則 mid
變量表示該方法的句柄。如果不能找到該方法,則 mid
將爲零。
請記住,在本示例中,我們正在調用 static
Java 方法。 那就是我們使用 GetStaticMethodID()
函數的原因。GetMethodID()
函數與GetStaticMethodID()
函數的功能一樣,但它用來查找實例方法。
如果正在調用構造器,則方法的名稱爲“<init>”。 要了解更多關於調用構造器的知識,請參閱錯誤處理。 要了解更多關於用來指定參數類型的代碼以及關於如何將 JNI 類型映射到 Java 原始類型的知識,請參閱附錄 。
最後,我們調用 Java 方法,如下所示:
square = (*env)->CallStaticIntMethod(env, cls, mid, 5); |
CallStaticIntMethod()
方法接受 cls
(表示類)、mid
(表示方法)以及用於該方法一個或多個參數。在本例中參數是 int
5。
您還會遇到 CallStaticXXXMethod()
和 CallXXXMethod()
之類的方法。這些方法分別調用靜態方法和成員方法,用方法的返回類型(例如,Object
、Boolean
、Byte
、Char
、Int
、Long
等等)代替變量 XXX
。
現在準備運行這個 C 應用程序,並確保代碼正常工作。當運行 Sample2.exe 時,應該可以得到如下結果:
PROMPT>Sample2 Result of intMethod: 25 Result of booleanMethod: 0 PROMPT> |
JNI 的 Invocation API 有點麻煩,因爲它是用 C 語言定義的,而 C 語言基本上不支持面向對象編程。結果是,它很容易遇到問題。下面是一份檢查表,它可能有助於您避免一些較常見的錯誤。
-
請總是確保正確設置了引用。例如,當使用
JNI_CreateJavaVM()
方法創建 JVM 時,確保它返回零。還請確保,在使用FindClass()
和GetMethodID()
方法之前,它們的引用設置不是零。 -
請檢查方法名是否拼寫正確以及是否適當地轉換了方法說明。還請確保,對靜態方法使用了
CallStaticXXXMethod()
以及對成員方法使用了CallXXXMethod()
。 -
確保使用任何 Java 類所需的特殊的參數或選項來初始化 JVM。例如,如果 Java 類需要大量內存,則可能需要增加堆的最大大小選項。
- 請總是確保正確設置了類路徑。使用嵌入式 JVM 的本機應用程序必須能夠找到 jvm.dll 或 jvm.so 共享庫。
儘管從 C 調用 Java 方法確實需要相當高級的“類面向對象編程”技術,但這對經驗豐富的 C 程序員而言相對比較簡單。儘管 JNI 支持 C 和 C++,但 C++ 接口要更清晰些,通常比 C 接口更可取。
要記住很重要的一點是:可以用單個 JVM 來裝入和執行多個類和方法。如果每次從本機代碼與 Java 交互都創建和銷燬 JVM,則會浪費資源並降低性能。