JNI官方規範中文版——基本類型、字符串、數組

開發者使用JNI時最常問到的是JavaC/C++之間如何傳遞數據,以及數據類型之間如何互相映射。本章我們從整數等基本類型和數組、字符串等普通的對象類型開始講述。至於如何傳遞任意對象,我們將在下一章中進行講述。

3.1 一個簡單的本地方法

JAVA端源代碼如下:

class Prompt {

     // native method that prints a prompt and reads a line

     private native String getLine(String prompt);

 

     public static void main(String args[]) {

         Prompt p = new Prompt();

         String input = p.getLine("Type a line: ");

         System.out.println("User typed: " + input);

     }

     static {

         System.loadLibrary("Prompt");

     }

 }

3.1.1 本地方法的C函數原型

Prompt.getLine方法可以用下面這個C函數來實現:

JNIEXPORT jstring JNICALL 

 Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);

其中,JNIEXPORT和JNICALL這兩個宏(被定義在jni.h)確保這個函數在本地庫外可見,並且C編譯器會進行正確的調用轉換。C函數的名字構成有些講究,在11.3中會有一個詳細的解釋。

3.1.2 本地方法參數

第一個參數JNIEnv接口指針,指向一個個函數表,函數表中的每一個入口指向一個JNI函數。本地方法經常通過這些函數來訪問JVM中的數據結構。圖3.1演示了JNIEnv這個指針:

3.1 JNIEnv接口指針

第二個參數根據本地方法是一個靜態方法還是實例方法而有所不同。本地方法是一個靜態方法時,第二個參數代表本地方法所在的類;本地方法是一個實例方法時,第二個參數代表本地方法所在的對象。我們的例子當中,Java_Prompt_getLine是一個實例方法,因此jobject參數指向方法所在的對象。

3.1.3 類型映射

本地方法聲明中的參數類型在本地語言中都有對應的類型。JNI定義了一個C/C++類型的集合,集合中每一個類型對應於JAVA中的每一個類型。

JAVA中有兩種類型:基本數據類型(int,float,char等)和引用類型(類,對象,數組等)。

JNI對基本類型和引用類型的處理是不同的。基本類型的映射是一對一的。例如JAVA中的int類型直接對應C/C++中的jint(定義在jni.h中的一個有符號 32位整數)。12.1.1包含了JNI中所有基本類型的定義。

JNIJAVA中的對象當作一個C指針傳遞到本地方法中,這個指針指向JVM中的內部數據結構,而內部數據結構在內存中的存儲方式是不可見的。本地代碼必須通過在JNIEnv中選擇適當的JNI函數來操作JVM中的對象。例如,對於java.lang.String對應的JNI類型是jstring,但本地代碼只能通過GetStringUTFChars這樣的JNI函數來訪問字符串的內容。

所有的JNI引用都是jobject類型,對了使用方便和類型安全,JNI定義了一個引用類型集合,集合當中的所有類型都是jobject的子類型。這些子類型和JAVA中常用的引用類型相對應。例如,jstring表示字符串,jobjectArray表示對象數組。

3.2 訪問字符串

Java_Prompt_getLine接收一個jstring類型的參數promptjstring類型指向JVM內部的一個字符串,和常規的C字符串類型char*不同。你不能把jstring當作一個普通的C字符串。

3.2.1 轉換爲本地字符串

本地代碼中,必須使用合適的JNI函數把jstring轉化爲C/C++字符串。JNI支持字符串在UnicodeUTF-8兩種編碼之間轉換。Unicode字符串代表了16-bit的字符集合。UTF-8字符串使用一種向上兼容7-bit ASCII字符串的編碼協議。UTF-8字符串很像NULL結尾的C字符串,在包含非ASCII字符的時候依然如此。所有的7-bitASCII字符的值都在1~127之間,這些值在UTF-8編碼中保持原樣。一個字節如果最高位被設置了,意味着這是一個多字節字符(16-bitUnicode值)。

函數Java_Prompt_getLine通過調用JNI函數GetStringUTFChars來讀取字符串的內容。GetStringUTFChars可以把一個jstring指針(指向JVM內部的Unicode字符序列)轉化成一個UTF-8格式的C字符串。如何你確信原始字符串數據只包含7-bit ASCII字符,你可以把轉化後的字符串傳遞給常規的C庫函數使用,如printf。我們會在8.2中討論如何處理非ASCII字符串。

JNIEXPORT jstring JNICALL 

 Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)

 {

     char buf[128];

     const jbyte *str;

     str = (*env)->GetStringUTFChars(env, prompt, NULL);

     if (str == NULL) {

         return NULL; /* OutOfMemoryError already thrown */

     }

     printf("%s", str);

     (*env)->ReleaseStringUTFChars(env, prompt, str);

     /* We assume here that the user does not type more than

      * 127 characters */

     scanf("%s", buf);

     return

不要忘記檢查GetStringUTFChars。因爲JVM需要爲新誕生的UTF-8字符串分配內存,這個操作有可能因爲內存太少而失敗。失敗時,GetStringUTFChars會返回NULL,並拋出一個OutOfMemoryError異常(對異常的處理在第6章)。這些JNI拋出的異常與JAVA中的異常是不同的。一個由JNI拋出的未決的異常不會改變程序執行流,因此,我們需要一個顯示的return語句來跳過C函數中的剩餘語句。Java_Prompt_getLine函數返回後,異常會在Prompt.main(Prompt.getLine這個發生異常的函數的調用者)中拋出,

3.2.2 釋放本地字符串資源

從GetStringUTFChars中獲取的UTF-8字符串在本地代碼中使用完畢後,要使用ReleaseStringUTFChars告訴JVM這個UTF-8字符串不會被使用了,因爲這個UTF-8字符串佔用的內存會被回收。

3.2.3 構造新的字符串

你可以通過JNI函數NewStringUTF在本地方法中創建一個新的java.lang.String字符串對象。這個新創建的字符串對象擁有一個與給定的UTF-8編碼的C類型字符串內容相同的Unicode編碼字符串。

如果一個VM不能爲構造java.lang.String分配足夠的內存,NewStringUTF會拋出一個OutOfMemoryError異常,並返回一個NULL。在這個例子中,我們不必檢查它的返回值,因爲本地方法會立即返回。如果NewStringUTF失敗,OutOfMemoryError這個異常會被在Prompt.main(本地方法的調用者)中拋出。如果NeweStringUTF成功,它會返回一個JNI引用,這個引用指向新創建的java.lang.String對象。這個對象被Prompt.getLine返回然後被賦值給Prompt.main中的本地input。

3.2.4 其它JNI字符串處理函數

JNI支持許多操作字符串的函數,這裏做個大致介紹。

GetStringChars和ReleaseStringChars獲取以Unicode格式編碼的字符串。當操作系統支持Unicode編碼的字符串時,這些方法很有用。

UTF-8字符串以\0結尾,而Unicode字符串不是。如果jstring指向一個Unicode編碼的字符串,爲了得到這個字符串的長度,可以調用GetStringLength。如果一個jstring指向一個UTF-8編碼的字符串,爲了得到這個字符串的字節長度,可以調用標準C函數strlen。或者直接對jstring調用JNI函數GetStringUTFLength,而不用管jstring指向的字符串的編碼格式。

GetStringChars和GetStringUTFChars函數中的第三個參數需要更進一步的解釋:

const jchar *

 GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);

當從JNI函數GetStringChars中返回得到字符串B時,如果B是原始字符串java.lang.String的拷貝,則isCopy被賦值爲JNI_TRUE。如果B和原始字符串指向的是JVM中的同一份數據,則isCopy被賦值爲JNI_FALSE。當isCopy值爲JNI_FALSE時,本地代碼決不能修改字符串的內容,否則JVM中的原始字符串也會被修改,這會打破JAVA語言中字符串不可變的規則。

通常,因爲你不必關心JVM是否會返回原始字符串的拷貝,你只需要爲isCopy傳遞NULL作爲參數。

JVM是否會通過拷貝原始Unicode字符串來生成UTF-8字符串是不可以預測的,程序員最好假設它會進行拷貝,而這個操作是花費時間和內存的。一個典型的JVM會在heap上爲對象分配內存。一旦一個JAVA字符串對象的指針被傳遞給本地代碼,GC就不會再碰這個字符串。換言之,這種情況下,JVM必須pin這個對象。可是,大量地pin一個對象是會產生內存碎片的,因爲,虛擬機會隨意性地來選擇是複製還是直接傳遞指針。

當你不再使用一個從GetStringChars得到的字符串時,不管JVM內部是採用複製還是直接傳遞指針的方式,都不要忘記調用ReleaseStringChars。根據方法GetStringChars是複製還是直接返回指針,ReleaseStringChars會釋放複製對象時所佔的內存,或者unpin這個對象。

3.2.5 JDK1.2中關於字符串的新JNI函數

爲了提高JVM返回字符串直接指針的可能性,JDK1.2中引入了一對新函數,Get/ReleaseStringCritical。表面上,它們和Get/ReleaseStringChars函數差不多,但實際上這兩個函數在使用有很大的限制。

使用這兩個函數時,你必須兩個函數中間的代碼是運行在"critical region"(臨界區)的,即,這兩個函數中間的本地代碼不能調用任何會讓線程阻塞或等待JVM中的其它線程的本地函數或JNI函數。

有了這些限制, JVM就可以在本地方法持有一個從GetStringCritical得到的字符串的直接指針時禁止GC。當GC被禁止時,任何線程如果觸發GC的話,都會被阻塞。而Get/ReleaseStringCritical這兩個函數中間的任何本地代碼都不可以執行會導致阻塞的調用或者爲新對象在JVM中分配內存。否則,JVM有可能死鎖,想象一下這樣的場景中:

1、 只有當前線程觸發的GC完成阻塞並釋放GC時,由其它線程觸發的GC纔可能由阻塞中釋放出來繼續運行。

2、 在這個過程中,當前線程會一直阻塞。因爲任何阻塞性調用都需要獲取一個正被其它線程持有的鎖,而其它線程正等待GC

Get/ReleaseStringCritical的交迭調用是安全的,這種情況下,它們的使用必須有嚴格的順序限制。而且,我們一定要記住檢查是否因爲內存溢出而導致它的返回值是NULL。因爲JVM在執行GetStringCritical這個函數時,仍有發生數據複製的可能性,尤其是當JVM內部存儲的數組不連續時,爲了返回一個指向連續內存空間的指針,JVM必須複製所有數據。

總之,爲了避免死鎖,在Get/ReleaseStringCritical之間不要調用任何JNI函數。Get/ReleaseStringCritical和 Get/ReleasePrimitiveArrayCritical這兩個函數是可以的。

下面代碼演示了這對函數的正確用法:

jchar *s1, *s2;

 s1 = (*env)->GetStringCritical(env, jstr1);

 if (s1 == NULL) {

     ... /* error handling */

 }

 s2 = (*env)->GetStringCritical(env, jstr2);

 if (s2 == NULL) {

     (*env)->ReleaseStringCritical(env, jstr1, s1);

     ... /* error handling */

 }

 ...     /* use s1 and s2 */

 (*env)->ReleaseStringCritical(env, jstr1, s1);

 (*env)->ReleaseStringCritical(env, jstr2, s2);

JNI不支持Get/ReleaseStringUTFCritical,因爲這樣的函數在進行編碼轉換時很可能會促使JVM對數據進行復制,因爲JVM內部表示字符串一般都是使用Unicode的。

JDK1.2還一對新增的函數:GetStringRegion和GetStringUTFRegion。這對函數把字符串複製到一個預先分配的緩衝區內。Prompt.getLine這個本地方法可以用GetStringUTFRegion重新實現如下:

JNIEXPORT jstring JNICALL 

 Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)

 {

     /* assume the prompt string and user input has less than 128

        characters */

     char outbuf[128], inbuf[128];

     int len = (*env)->GetStringLength(env, prompt);

     (*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf);

     printf("%s", outbuf);

     scanf("%s", inbuf);

     return (*env)->NewStringUTF(env, inbuf);

 }

GetStringUTFRegion這個函數會做越界檢查,如果必要的話,會拋出異常StringIndexOutOfBoundsException。這個方法與GetStringUTFChars比較相似,不同的是,GetStringUTFRegion不做任何內存分配,不會拋出內存溢出異常。

3.2.6 JNI字符串操作函數總結

對於小字符串來說,Get/SetStringRegion和Get/SetString-UTFRegion這兩對函數是最佳選擇,因爲緩衝區可以被編譯器提前分配,而且永遠不會產生內存溢出的異常。當你需要處理一個字符串的一部分時,使用這對函數也是不錯的,因爲它們提供了一個開始索引和子字符串的長度值。另外,複製少量字符串的消耗是非常小的。

在使用GetStringCritical時,必須非常小心。你必須確保在持有一個由GetStringCritical獲取到的指針時,本地代碼不會在JVM內部分配新對象,或者做任何其它可能導致系統死鎖的阻塞性調用。

下面的例子演示了使用GetStringCritical時需要注意的一些地方:

/* This is not safe! */

 const char *c_str = (*env)->GetStringCritical(env, j_str, 0);

 if (c_str == NULL) {

     ... /* error handling */

 }

 fprintf(fd, "%s\n", c_str);

 (*env)->ReleaseStringCritical(env, j_str, c_str);

上面代碼的問題在於,GC被當前線程禁止的情況下,向一個文件寫數據不一定安全。例如,另外一個線程T正在等待從文件fd中讀取數據。假設操作系統的規則是fprintf會等待線程T完成所有對文件fd的數據讀取操作,這種情況下就可能會產生死鎖:線程T從文件fd中讀取數據是需要緩衝區的,如果當前沒有足夠內存,線程T就會請求GC來回收一部分,GC一旦運行,就只能等到當前線程運行ReleaseStringCritical時纔可以。而ReleaseStringCritical只有在fprintf調用返回時纔會被調用。而fprintf這個調用,會一直等待線程T完成文件讀取操作。

3.3 訪問數組

JNI在處理基本類型數組和對象數組上面是不同的。對象數組裏面是一些指向對象實例或者其它數組的引用。

本地代碼中訪問JVM中的數組和訪問JVM中的字符串有些相似。看一個簡單的例子。下面的程序調用了一個本地方法sumArray,這個方法對一個int數組裏面的元素進行累加:

class IntArray {

     private native int sumArray(int[] arr);

     public static void main(String[] args) {

         IntArray p = new IntArray();

         int arr[] = new int[10];

         for (int i = 0; i < 10; i++) {

             arr[i] = i;

         }

         int sum = p.sumArray(arr);

         System.out.println("sum = " + sum);

     }

     static {

         System.loadLibrary("IntArray");

     }

 }

3.3.1 在本地代碼中訪問數組

數組的引用類型是一般是jarray或者或者jarray的子類型jintArray。就像jstring不是一個C字符串類型一樣,jarray也不是一個C數組類型。所以,不要直接訪問jarray。你必須使用合適的JNI函數來訪問基本數組元素:

JNIEXPORT jint JNICALL 

 Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)

 {

     jint buf[10];

     jint i, sum = 0;

     (*env)->GetIntArrayRegion(env, arr, 0, 10, buf);

     for (i = 0; i < 10; i++) {

         sum += buf[i];

     }

     return sum;

 }

3.3.2 訪問基本類型數組

上一個例子中,使用GetIntArrayRegion函數來把一個int數組中的所有元素複製到一個C緩衝區中,然後我們在本地代碼中通過C緩衝區來訪問這些元素。

JNI支持一個與GetIntArrayRegion相對應的函數SetIntArrayRegion。這個函數允許本地代碼修改所有的基本類型數組中的元素。

JNI支持一系列的Get/Release<Type>ArrayElement函數,這些函數允許本地代碼獲取一個指向基本類型數組的元素的指針。由於GC可能不支持pin操作,JVM可能會先對原始數據進行復制,然後返回指向這個緩衝區的指針。我們可以重寫上面的本地方法實現:

JNIEXPORT jint JNICALL 

 Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)

 {

     jint *carr;

     jint i, sum = 0;

     carr = (*env)->GetIntArrayElements(env, arr, NULL);

     if (carr == NULL) {

         return 0; /* exception occurred */

     }

     for (i=0; i<10; i++) {

         sum += carr[i];

     }

     (*env)->ReleaseIntArrayElements(env, arr, carr, 0);

     return sum;

 }

GetArrayLength這個函數返回數組中元素的個數,這個值在數組被首次分配時確定下來。

JDK1.2引入了一對函數:Get/ReleasePrimitiveArrayCritical。通過這對函數,可以在本地代碼訪問基本類型數組元素的時候禁止GC的運行。但程序員使用這對函數時,必須和使用Get/ReleaseStringCritical時一樣的小心。在這對函數調用的中間,同樣不能調用任何JNI函數,或者做其它可能會導致程序死鎖的阻塞性操作。

3.3.3 操作基本類型數組的JNI函數的總結

如果你想在一個預先分配的C緩衝區和內存之間交換數據,應該使用Get/Set</Type>ArrayRegion系列函數。這些函數會進行越界檢查,在需要的時候會有可能拋出ArrayIndexOutOfBoundsException異常。

對於少量的、固定大小的數組,Get/Set<Type>ArrayRegion是最好的選擇,因爲C緩衝區可以在Stack(棧)上被很快地分配,而且複製少量數組元素的代價是很小的。這對函數的另外一個優點就是,允許你通過傳入一個索引和長度來實現對子字符串的操作。

如果你沒有一個預先分配的C緩衝區,並且原始數組長度未定,而本地代碼又不想在獲取數組元素的指針時阻塞的話,使用Get/ReleasePrimitiveArrayCritical函數對。就像Get/ReleaseStringCritical函數對一樣,這對函數很小心地使用,以避免死鎖。

Get/Release<type>ArrayElements系列函數永遠是安全的。JVM會選擇性地返回一個指針,這個指針可能指向原始數據也可能指向原始數據複製。

3.3.5 訪問對象數組

JNI提供了一個函數對來訪問對象數組。GetObjectArrayElement返回數組中指定位置的元素,而SetObjectArrayElement修改數組中指定位置的元素。與基本類型的數組不同的是,你不能一次得到所有的對象元素或者一次複製多個對象元素。字符串和數組都是引用類型,你要使用Get/SetObjectArrayElement來訪問字符串數組或者數組的數組。

下面的例子調用了一個本地方法來創建一個二維的int數組,然後打印這個數組的內容:

class ObjectArrayTest {

     private static native int[][] initInt2DArray(int size);

     public static void main(String[] args) {

         int[][] i2arr = initInt2DArray(3);

         for (int i = 0; i < 3; i++) {

             for (int j = 0; j < 3; j++) {

                  System.out.print(" " + i2arr[i][j]);

             }

             System.out.println();

         }

     }

     static {

         System.loadLibrary("ObjectArrayTest");

     }

 }

靜態本地方法initInt2DArray創建了一個給定大小的二維數組。執行分配和初始化數組任務的本地方法可以是下面這樣子的:

JNIEXPORT jobjectArray JNICALL

 Java_ObjectArrayTest_initInt2DArray(JNIEnv *env,

                                    jclass cls,

                                    int size)

 {

     jobjectArray result;

     int i;

     jclass intArrCls = (*env)->FindClass(env, "[I");

     if (intArrCls == NULL) {

         return NULL; /* exception thrown */

     }

     result = (*env)->NewObjectArray(env, size, intArrCls,

                                     NULL);

     if (result == NULL) {

         return NULL; /* out of memory error thrown */

     }

     for (i = 0; i < size; i++) {

         jint tmp[256];  /* make sure it is large enough! */

         int j;

         jintArray iarr = (*env)->NewIntArray(env, size);

         if (iarr == NULL) {

             return NULL; /* out of memory error thrown */

         }

         for (j = 0; j < size; j++) {

             tmp[j] = i + j;

         }

         (*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);

         (*env)->SetObjectArrayElement(env, result, i, iarr);

         (*env)->DeleteLocalRef(env, iarr);

     }

     return result;

 }

函數newInt2DArray首先調用JNI函數FindClass來獲得一個int型二維數組類的引用,傳遞給FindClass的參數“[I”是JNI class descriptor(JNI類型描述符),它對應着JVM中的int[]類型。如果類加載失敗的話,FindClass會返回NULL,然後拋出一個異常。

接下來,NewObjectArray會分配一個數組,這個數組裏面的元素類型用intArrCls類引用來標識。函數NewObjectArray只能分配第一維,JVM沒有與多維數組相對應的數據結構。一個二維數組實際上就是一個簡單的數組的數組。

創建第二維數據的方式非常直接,NewInt-Array爲每個數組元素分配空間,然後SetIntArrayRegion把tmp[]緩衝區中的內容複製到新分配的一維數組中去。

在循環最後調用DeleteLocalRef,確保JVM釋放掉iarr這個JNI引用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章