從 Java 程序內調用本機代碼破壞了 Java 程序的可移植性和安全性。儘管已編譯的 Java 字節碼保持了很好的可移植性,但必須爲您打算用來運行該應用程序的每個平臺重新編譯本機代碼。另外,由於本機代碼在 JVM 之外執行,所以約束它的安全性協議不必和 Java 代碼的相同。
從本機程序調用 Java 代碼也很複雜。因爲 Java 語言是面向對象的,所以從本機應用程序調用 Java 代碼通常涉及面向對象技術。有些本機語言不支持面向對象編程或只是有限地支持面向對象編程(譬如 C),使用這些語言調用 Java 方法可能會產生問題。在本節中,我們將討論使用 JNI 所帶來的若干複雜性,並研究解決它們的方法。
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 數組和 C 數組在內存中的表示不同。幸運的是,一組 JNI 函數可以提供指向數組中元素的指針。下圖顯示瞭如何將 Java 數組映射到 JNI C 類型。
C 類型 jarray
表示通用數組。在 C 語言中,所有數組類型實際上只是 jobject
的同義類型。但是,在 C++ 語言中,所有的數組類型都繼承了 jarray
,jarray
又依次繼承了 jobject
。有關所有
C 類型對象的繼承圖,請參閱附錄 A:JNI 類型。
通常,處理數組時,首先想到要做的是確定其大小。爲了做到這一點,應該使用 GetArrayLength()
函數,它返回一個表示數組大小的 jsize
。
接下來,會想要獲取一個指向數組元素的指針。可以使用 GetXXXArrayElement()
和 SetXXXArrayElement()
函數(根據數組的類型替換方法名中的 XXX
:Object
、Boolean
、Byte
、Char
、Int
、Long
等等)來訪問數組中的元素。
當本機代碼完成了對 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); |
當使用 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 的異常處理函數。
有兩種方法用來在本機代碼中拋出異常:可以調用 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 語句,以便在出錯點退出本機方法。
當從 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. |
確保本機方法同步的另一種方法是:當在 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 代碼中相同的方式來使用它們,以實現線程同步。
JNI 使用幾種映射到 Java 類型的本機定義的 C 類型。這些類型可以分成兩類:原始類型和僞類(pseudo-classes)。在 C 中,僞類作爲結構實現,而在 C++ 中它們是真正的類。
Java 原始類型直接映射到 C 依賴於平臺的類型,如下所示:
C 類型 jarray
表示通用數組。在 C 中,所有的數組類型實際上只是 jobject
的同義類型。但是,在 C++ 中,所有的數組類型都繼承了 jarray
,jarray
又依次繼承了 jobject
。下列表顯示了
Java 數組類型是如何映射到 JNI C 數組類型的。
這裏是一棵對象樹,它顯示了 JNI 僞類是如何相關的。
用下表指定的編碼將本機 Java 方法參數類型表示或轉換成本機代碼。
注:
-
類類型 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