用JNI進行Java編程---高級主題及附錄

概述

從 Java 程序內調用本機代碼破壞了 Java 程序的可移植性和安全性。儘管已編譯的 Java 字節碼保持了很好的可移植性,但必須爲您打算用來運行該應用程序的每個平臺重新編譯本機代碼。另外,由於本機代碼在 JVM 之外執行,所以約束它的安全性協議不必和 Java 代碼的相同。

從本機程序調用 Java 代碼也很複雜。因爲 Java 語言是面向對象的,所以從本機應用程序調用 Java 代碼通常涉及面向對象技術。有些本機語言不支持面向對象編程或只是有限地支持面向對象編程(譬如 C),使用這些語言調用 Java 方法可能會產生問題。在本節中,我們將討論使用 JNI 所帶來的若干複雜性,並研究解決它們的方法。

Java 字符串 vs. C 字符串

Java 字符串是作爲 16 位 Unicode 字符存儲的,而 C 字符串是作爲一組 8 位且以空字符爲結束的字符存儲的。JNI 提供了幾個有用的函數,它們用於在 Java 字符串和 C 字符串之間進行轉換並操作這兩種字符串。下面的代碼片段演示瞭如何將 C 字符串轉換成 Java 字符串:

 1. /* Convert a C string to a Java String. */
 2. char[]  str  = "To be or not to be.\n";
 3. jstring jstr = (*env)->NewStringUTF(env, str);

接下來,我們研究將 Java 字符串轉換成 C 字符串的代碼。請注意第 5 行對 ReleaseStringUTFChars() 函數的調用。當您不再使用 Java 字符串時,應該使用這個函數來釋放它們。當本機代碼不再需要引用字符串時,請總是確保釋放它們。不這樣做可能導致內存泄漏。

 1. /* Convert a Java String into a C string. */
 2. char buf[128; 
 3. const char *newString = (*env)->GetStringUTFChars(env, jstr, 0);
 4. ...
 5. (*env)->ReleaseStringUTFChars(env, jstr, newString);

Java 數組 vs. C 數組

與字符串類似,Java 數組和 C 數組在內存中的表示不同。幸運的是,一組 JNI 函數可以提供指向數組中元素的指針。下圖顯示瞭如何將 Java 數組映射到 JNI C 類型。

C 類型 jarray 表示通用數組。在 C 語言中,所有數組類型實際上只是 jobject 的同義類型。但是,在 C++ 語言中,所有的數組類型都繼承了 jarrayjarray 又依次繼承了 jobject 。有關所有 C 類型對象的繼承圖,請參閱附錄 A:JNI 類型

使用數組

通常,處理數組時,首先想到要做的是確定其大小。爲了做到這一點,應該使用 GetArrayLength() 函數,它返回一個表示數組大小的 jsize

接下來,會想要獲取一個指向數組元素的指針。可以使用 GetXXXArrayElement() 和 SetXXXArrayElement() 函數(根據數組的類型替換方法名中的 XXXObjectBooleanByteCharIntLong 等等)來訪問數組中的元素。

當本機代碼完成了對 Java 數組的使用時,必須調用函數 ReleaseXXXArrayElements() 來釋放它。否則,可能導致內存泄漏。下面的代碼段顯示瞭如何循環遍歷一個整型數組的所有元素:

 1. /* Looping through the elements in an array. */
 2. int* elem = (*env)->GetIntArrayElements(env, intArray, 0);
 3. for (i=0; I < (*env)->GetIntArrayLength(env, intArray); i++)
 4.    sum += elem[i]
 5. (*env)->ReleaseIntArrayElements(env, intArray, elem, 0);

局部引用 vs. 全局引用

當使用 JNI 編程時,會需要使用對 Java 對象的引用。缺省情況下,JNI 創建局部引用以確保它們可以被垃圾收集。由於這一點,您可能會因爲嘗試存儲一個本地引用,以便稍後重用它而無意間編寫出非法代碼,如下面的代碼樣本所示:

 1. /* This code is invalid! */
 2. static jmethodID mid;
 3. 
 4. JNIEXPORT jstring JNICALL
 5. Java_Sample1_accessMethod(JNIEnv *env, jobject obj)
 6. {
 7.    ...
 8.	 cls = (*env)->GetObjectClass(env, obj);
 9.    if (cls != 0)
10.       mid = (*env)->GetStaticMethodID(env, cls, "addInt", "(I)I");
11.    ...
12. }

因爲第 10 行的錯誤,所以這個代碼段是非法的。mid 是 methodID,並且 GetStaticMethodID() 返回 methodID。但是,返回的methodID 是一個局部引用,而您不應該將一個局部引用賦給全局引用。而 mid 是一個全局引用。

在 Java_Sample1_accessMethod() 返回之後,mid 引用就不再有效,因爲賦給它現在超出作用域以外的局部引用。嘗試使用 mid 將導致錯誤結果或 JVM 崩潰。

創建全局引用

要糾正這個問題,需要創建和使用全局引用。全局引用將在顯式釋放之前一直有效,您必須記住去顯式地釋放它。沒有釋放引用可能導致內存泄漏。

使用 NewGlobalRef() 創建全局引用,並用 DeleteGlobalRef() 刪除它,如下面的代碼樣本所示:

 1. /* This code is valid! */
 2. static jmethodID mid;
 3. 
 4. JNIEXPORT jstring JNICALL
 5. Java_Sample1_accessMethod(JNIEnv *env, jobject obj)
 6. {
 7.    ...
 8.	 cls = (*env)->GetObjectClass(env, obj);
 9.    if (cls != 0)
10.    {
11.       mid1 = (*env)->GetStaticMethodID(env, cls, "addInt", "(I)I");
12.       mid = (*env)->NewGlobalRef(env, mid1);
13.    ...
14. }

錯誤處理

在 Java 程序中使用本機方法,就以某種基本的方式破壞了 Java 安全性模型。因爲 Java 程序在一個受控的運行時系統(JVM)中運行,所以 Java 平臺設計師決定通過檢查常見運行時系統錯誤(如數組下標、越界錯誤、空指針錯誤)來幫助程序員。從另一方面講,由於 C 和 C++ 不使用此類錯誤檢查,所以本機方法程序員必須自己處理所有錯誤情況,而在運行時,這些錯誤可以在 JVM 中被捕獲。

例如,對於 Java 程序而言,通過拋出一個異常來向 JVM 報告出錯是常見和正確的操作。C 沒有異常,因此必須使用 JNI 的異常處理函數。

JNI 的異常處理函數

有兩種方法用來在本機代碼中拋出異常:可以調用 Throw() 函數或 ThrowNew() 函數。在調用 Throw() 之前,首先需要創建一個Throwable 類型的對象。可以通過調用 ThrowNew() 跳過這一步,因爲這個函數爲您創建了該對象。在下面的示例代碼片段中,我們使用這兩個函數拋出 IOException

 1. /* Create the Throwable object. */
 2. jclass cls = (*env)->FindClass(env, "java/io/IOException");
 3. jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
 4. jthrowable e = (*env)->NewObject(env, cls, mid);
 5. 
 6. /* Now throw the exception */
 7. (*env)->Throw(env, e);
 8. ...
 9. 
10. /* Here we do it all in one step and provide a message*/
11. (*env)->ThrowNew(env,
12.                  (*env)->FindClass("java/io/IOException"),
13.                  "An IOException occurred!");

Throw() 和 ThrowNew() 函數並不中斷本機方法中的控制流。直到本機方法返回,在 JVM 中才會將異常實際拋出。在 C 中,一旦碰到錯誤條件,不能使用 Throw() 和 ThrowNew() 函數立即退出方法,而在 Java 中,這可以使用 throw 語句來退出方法。相反,需要在Throw() 和 ThrowNew() 函數之後立即使用 return 語句,以便在出錯點退出本機方法。

JNI 的異常捕獲函數

當從 C 或 C++ 調用 Java 時,也可能需要捕獲異常。 許多 JNI 函數都能拋出希望捕獲的異常。ExceptionCheck() 函數返回 jboolean以表明是否拋出了異常,而 ExceptionOccured() 方法返回指向當前異常的 jthrowable 引用(或者返回 NULL,如果未拋出異常的話)。

如果正在捕獲異常,可能要處理異常,在這種情況下需要在 JVM 中清除該異常。可以使用 ExceptionClear() 函數來進行這個操作。ExceptionDescribed() 函數用來顯示異常的調試消息。

本機方法中的多線程

在使用 JNI 工作時,您將遇到的更高級的問題之一是在本機方法中使用多線程。即使是在不需要支持多線程的系統上運行時,Java 平臺也是作爲多線程系統來實現的;因此您有責任確保本機函數是線程安全的。

在 Java 程序中,可以通過使用 synchronized 語句實現線程安全的代碼。synchronized 語句的語法使您能夠獲取對象上的鎖。 只要在 synchronized 塊中,就可以執行任何數據操作,而不必擔心其它線程會悄悄進入並訪問您鎖定的對象。

JNI 使用 MonitorEnter() 和 MonitorExit() 函數提供類似的結構。對於傳遞到 MonitorEnter() 函數中的對象,您會得到一個用於該對象的監視器(鎖),並在使用 MonitorExit() 函數釋放它之前一直持有該鎖。對於您鎖定的對象而言,MonitorEnter() 和MonitorExit() 函數之間的所有代碼保證是線程安全的。

本機方法中的同步

下表顯示瞭如何在 Java、C 和 C++ 中同步一塊代碼。正如您所見,這些 C 和 C++ 函數類似於 Java 代碼中的 synchronized 語句。

XML error: The image is not displayed because the width is greater than the maximum of 580 pixels. Please decrease the image width.

隨本機方法一起使用 synchronized

確保本機方法同步的另一種方法是:當在 Java 類中聲明 native 方法時使用 synchronized 關鍵字。

使用 synchronized 關鍵字將確保任何時候從 Java 程序調用 native 方法,它都將是 synchronized。 儘管用 synchronized 關鍵字來標記線程安全的本機方法是個好想法,但通常最好總是在本機方法實現中實現同步。這樣做的主要原因如下:

  • C 或 C++ 代碼和 Java 本機方法聲明不同,因此,如果方法聲明有變動(即,如果一旦除去了 synchronized 關鍵字),此方法可能馬上不再是線程安全的了。

  • 如果有人對使用該函數的其它本機方法(或其它 C 或 C++ 函數)進行編碼,他們可能並沒有意識到該本機實現不是線程安全的。

  • 如果將函數作爲普通的 C 函數在 Java 程序之外使用,則它不是線程安全的。

其它同步技術

Object.wait()Object.notify() 和 Object.notifyAll() 方法也支持線程同步。因爲所有 Java 對象都將 Object 類作爲父類,所以所有 Java 對象都有這些方法。您可以象調用其它方法一樣,從本機代碼調用這些方法,並以 Java 代碼中相同的方式來使用它們,以實現線程同步。

附錄 A:JNI 類型

JNI 使用幾種映射到 Java 類型的本機定義的 C 類型。這些類型可以分成兩類:原始類型和僞類(pseudo-classes)。在 C 中,僞類作爲結構實現,而在 C++ 中它們是真正的類。

Java 原始類型直接映射到 C 依賴於平臺的類型,如下所示:

C 類型 jarray 表示通用數組。在 C 中,所有的數組類型實際上只是 jobject 的同義類型。但是,在 C++ 中,所有的數組類型都繼承了 jarrayjarray 又依次繼承了 jobject。下列表顯示了 Java 數組類型是如何映射到 JNI C 數組類型的。

這裏是一棵對象樹,它顯示了 JNI 僞類是如何相關的。

附錄 B:JNI 方法說明編碼

用下表指定的編碼將本機 Java 方法參數類型表示或轉換成本機代碼。

JNI 對象樹

  • 類類型 L 表達式結尾的分號是類型表達式的終止符,而不是多個表達式之間的分隔符。

  • 必須用正斜槓(/)而不是點(.)來將包和類名稱隔開。要指定數組類型,用左方括號([)。 例如,Java 方法:
    boolean print(String[] parms, int n)
    

    的轉換說明如下:
    ([Ljava/lang/Sting;I)Z

FYI

http://www.ibm.com/developerworks/cn/education/java/j-jni/index.html

發佈了29 篇原創文章 · 獲贊 20 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章