技術轉載:Jni學習四:如何編寫jni方法

轉載:http://blog.chinaunix.net/u1/38994/showart_1099528.html

 

 


一、概述:

在這篇文章中將會簡單介紹如何編制一些簡單的JNI 方法。我們都知道JNI方法可以幫助我們調用用C/c++編寫的函數,這樣如果一項工作已經用C/c++語言實現的話,我們就可以不用花很大的力氣再用JAVA語言對這一工作進行再實現,只要編制相應的JNI函數,就可以輕鬆實現JAVA語言對C/c++函數的調用,從而大大減輕程序開發人員的工作量。

在這個項目中,我們編制了很多小實例,通過閱讀,運行這些小實例,你可以輕鬆的學會如何編制JNI方法。這篇文檔可以幫助你更好的理解及實現這些實例。

現在讓我們進入主題。首先,我們看一下這個項目的體系構架。該項目分爲兩部分,一部分用c語言是c語言的例子,另一部分是c++語言的例子。每部分都包含java,src源文件目錄,以及一個Makefile文件。java目錄中是需要調用JNI函數的JAVA源程序,含有後綴名.java。src 目錄中含有JNI函數的實現代碼,包括.c或.cpp文件和.h文件。Makefile文件是對 java 、src 目錄下的文件進行編譯組織進而生成可執行文件的文件。當Makefile文件執行以後還會生成以下子目錄:lib , class ,bin目錄 。lib 目錄中包含項目中生成的靜態函數庫文件libJNIExamples.so,java程序所調用的JNI方法都是通過這個庫來調用的。class 目錄中包含由java目錄下的.java 文件生成的.class文件。bin目錄中是一個可執行的shell腳本文件。在執行該腳本的時候,項目所有程序實例的運行結果都將一併顯示在屏幕上。


具體執行步驟爲:

make

cd bin

./run.sh


下面來介紹一下在這個項目中所實現的實例:

   1. 如何調用標準C/c++中的函數--例如:printf(...)
   2. 如何調用C/c++中自定義的函數
   3. 如何在jni函數中訪問java類中的對象實例域
   4. 如何在jni函數中訪問java類中的靜態實例域
   5. 如何在jni函數中調用java對象的方法
   6. 如何在jni函數中調用java類的靜態方法
   7. 如何在jni函數中傳遞基本數據類型參數
   8. 如何在jni函數中傳遞對象類型參數
   9. 如何在jni函數中處理字符串
  10. 如何在jni函數中處理數組
  11. 處理jni函數中的返回值情況
  12. 在jni中實現創建java類對象


二、基本步驟:

在介紹這些例子之前,讓我們先來看看編寫jni方法所需要的基本步驟,這些實例都是用c來實例來講解,至於c++的實例和c的實例區別不大,只要作稍微的修改即可,在文檔的末尾我們將介紹這些內容:

1、要想定義jni方法,首先得要在java語言中對這一方法進行聲明(自然這一聲明過程要在類中進行)

聲明格式如下:

publicnativevoid print();   System.loadLibrary(“JNIExamples”);   }  

jni 函數用關鍵字native方法聲明。

2、對該類的源文件進行編譯使用javac命令,生成相應的.class文件。
3、用javah -jni爲函數生成一個在java調用和實際的c函數之間的轉換存根,該存根通過從虛擬機棧中取出參數信息,並將其傳遞給已編譯的C函數來實現轉換。
4、建立一個特殊的共享庫,並從該共享庫到處這個存根,在上面的例子中使用了System.loadLibrary,來加載libJNIExamples共享庫。


三、配置運行環境:

在編寫一個簡單的jni函數之前我們必須配置相應的運行環境。jdk的配置在這裏就不作介紹,這裏主要說的是庫的路徑。當調用System.loadLibrary(..)時,編譯器會到我們系統設置的庫路徑中尋找該庫。修改路徑的方法和修改任何環境變量的方法基本相同,只要在/etc/bash.bashrc目錄下增加一行LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH)即可。也可以通過命令行export LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH)


四、運行實例分析:

1、實例一:在jni中調用標準c中自帶的函數printf():

下面以實例1爲例來詳細說明編寫jni方法的詳細過程。

(1)、定義包含jni函數的類Print.java:
{   /*********************************************************************** * the print() function will call the printf() funcion which is a ANSI c funciton * *************************************************************************/publicnativevoid print();       System.loadLibrary("JNIExamples");     }   }  


在上面的實例中,使用public native void print();語句來定義了一個Print類的jni方法。並用Sysgem.loadLibrary(“JNIExamples”)語句來加載libJNIExamples.so庫。注意:加載的語句一定要用static關鍵字聲明在靜態塊中,以保證引用該類時該庫始終被加載。

(2)、對該類進行編譯:javac Print.java。生成Print.class類,然後用javah 產生一個Print.h的頭文件:javah Print。長生的Print.h文件格式如下:
/* DO NOT EDIT THIS FILE - it is machine generated *//* Header for class Print */  JNIEXPORT void JNICALL Java_Print_print     (JNIEnv *, jobject);   }   

其中的加粗字體爲要實現的JNI函數生命部分。

(3)、編寫JNI函數的實現部分Print.c
JNIEXPORT void JNICALL Java_Print_print (JNIEnv *env, jobject obj)   {     printf("example1:in this example a printf() function in ANSI C is called\n");     printf("Hello,the output is generated by printf() function in ANSI C\n");   }  

在這個文件中實現了一個簡單的Jni方法。該方法調用ANSI C 中的printf()函數,輸出了兩個句子。

(4)、將本地函數編譯到libJNIExamples.so的庫中:
使用語句:gcc -fPIC -I/usr/jdk1.5/include -I/usr/jdk1.5/include/linux -shared -o libJNIExamples.so Print.c。

(5)、至此Jni函數已全部實現,可以在java代碼中調用拉。
在此我們使用一個簡單的類來對實現的jni方法進行測試,下面是PrintTest.java的源代碼部分:
publicstaticvoid main(String[] args) {       Print p = new Print();       p.print();     }   }  

(6)、對PrintTest.java進行編譯執行得到如下結果:
example1:in this example a printf() function in ANSI C is called
Hello,the output is generated by printf() function in ANSI C .

下面介紹的每個實例實現的步驟也都是按着上述步驟執行的。所以介紹時只介紹實現的關鍵部分。

2、實例二、調用c 語言用戶定義的函數(源程序爲:java/Cfunction.java java/C_functionTest.java src/Cfunction.c src/Cfunction.h )
當需要在java程序中調用用c所實現的函數是,需要在需要調用該c函數的類中定義一個jni方法,在該jni方法中去調用該c函數,相當於用java方法把c函數封裝起來,以供java程序調用。
在實例二中我們簡單定義了一個printHello()函數,該函數的功能只是輸出一句話,如果要在java程序中調用該函數,只需在jni函數中調用即可,和調用ANSI C中自帶的prinf()函數沒有任何區別。

3、實例三、在jni函數中訪問java類中的對象實例域(源程序爲:java/CommonField.java java/CommonFieldTest.java src/CommonField.c src/CommonField.h )
jni函數的實現部分是在c 語言中實現的,如果它想訪問java中定義的類對象的實例域需要作三步工作,
(1)調用GetObjectClass()函數得到該對像的類,該函數返回一個jclass類型值。
(2)調用GetFieldID()函數得到要訪問的實例域在該類中的id。
(3)調用GetXXXField()來得到要訪問的實例域的值。其中XXX和要訪問的實例域的類型相對應。
在jni中java 編程語言和c 語言數據類型的對應關係爲java原始數據類型前加 'j' 表示對應c語言的數據類型例如boolean 爲jboolean ,int 爲 jint,double 爲jdouble等。對象類型的對應類型爲jobject。
在本實例中,您可以看到我們在java/CommonField.java 中定義了類CommonField類,其中包含int a , int b 兩個實例域,我們要在jni函數getCommonField()中對這兩個域進行訪問和修改。你可以在 src/CommonField.c中找到該函數的實現部分。
以下語句是對該域的訪問(以下代碼摘自:src/CommonField.c):

jclass class_Field = (*env)->GetObjectClass(env,obj);   jfieldID fdA = (*env)->GetFieldID(env,class_Field,"a","I");   jfieldID fdB = (*env)->GetFieldID(env,class_Field,"b","I");   jint valueA = (*env)->GetIntField(env,obj,fdA);   jint valueB = (*env)->GetIntField(env,obj,fdB);  


在jni中對所有jni函數的調用都要用到env指針,該指針也是每一個本地方法的第一個參數,他是函數指針表的指針,所以,必須在每一個jni調用前面加上(*env)->GetObjectClass(env,obj)函數調用返回obj對像的類型,其中obj 參數表示要你想要得到類型的類對象。
jfieldID GetFieldID(JNIEnv *env,jclass cl, const char name[], const char sig[]) 該函數返回一個域的標識符name 表示域名,sig表示編碼的域簽名。所謂編碼的簽名即編碼類型的簽名在上例中類中的a實例域爲int 型,用"I”來表示,同理"B” 表示byte ,"C” 表示 char , “D”表示 double ,”F” 表示float,“J”表示long, “S” 表示short , “V” 表示void ,”Z”表示 boolean類型。
GetIntField(env,obj,fdA),用來訪問obj對象的fdA域,如果要訪問的域爲double類型,則要使用GetDoubleField(env,obj,fdA)來訪問,即類型對應GetXXXField中的XXX。

以下函數用來修改域的值:

(*env)->SetIntField(env,obj,fdA,109);   (*env)->SetIntField(env,obj,fdB,145);  

這和獲得域的值類似,只是該函數多了一個要設置給該域的值參數。
訪問對象實例域的相關函數如下:
jfieldID GetFieldID(JNIEnv *env, jclass cl, const char name[], const char sig[])
該函數返回一個域的標識符。各參數含義如下:
env JNI 接口指針;cl 類對象 ; name 域名; sig 編碼的域簽名

XXX GetXXXField(JNIEnv *env, jobject obj, jfieldID id)
該函數返回域的值。域類型XXX是Object, Boolean, byte, char , short, int ,long ,float, double 中類型之一。
參數 env JNI藉口指針;obj爲域所在對象;id爲域的標識符。
void SetXXXField(JNIEnv *env,jobject obj, jfieldID id, XXX value)

該函數用於設置域的值。XXX的含義同上,
參數中env, obj , id 的含義也同上,value 值爲將要設置的值。

4、實例四:在jni函數中訪問類的靜態實例域 (java/Field.java java/FieldTest.java src/Field.c src/Field.h)

因爲靜態實例域並不屬於某個對象,而是屬於一個類,所以在要訪問靜態實例域時,和訪問對象的實例域不同,它所調用的函數是(以實例四來說明,一下代碼摘自src/Field.c):
jclass class_Field = (*env)->FindClass(env,"Field");   jfieldID fdA = (*env)->GetStaticFieldID(env,class_Field,"a","I");   jint valueA = (*env)->GetStaticIntField(env,class_Field,fdA);   (*env)->SetStaticIntField(env,class_Field,fdA,111);  

由於沒有對象,必須使用FindClass代替GetObjectClass來獲得類引用。在FindClass()的第二個參數是類的編碼簽名,類的編碼簽名和基本類型的編碼簽名有所不同,如果類在當前包中,就直接是類的名稱,如果類不在當前包中則要加入該類的詳細路徑:例如String類在java.lang包中,則String的簽名要寫成( Ljava/lang/String;),其中的(L和;)是不可少的,其中(;)是表達是的終止符。其他三個函數和訪問對象數據域基本沒什麼區別。

5、實例五:在jni函數中調用java對象的方法(java/CommonMethod.java java/CommonMethodTest.java src/CommonMehod.c src/CommonMethod.h )

在jni函數中我們不僅要對java對象的數據域進行訪問,而且有時也需要調用java中類對象已經實現的方法,實例五就是關於這方面的實現的。在src/CommonMethod.c中我們可以找到下面的代碼:

JNIEXPORT void JNICALL Java_CommonMethod_callMethod   (JNIEnv *env, jobject obj, jint a, jstring s)   {     printf("example 5:in this example,a object's method will be called\n");     jclass class_CommonMethod = (*env)->GetObjectClass(env,obj);     jmethodID md = (*env)->GetMethodID(env,class_CommonMethod,"print","(ILjava/lang/String;)V");     (*env)->CallVoidMethod(env,obj,md,a,s);   }  


該代碼部分展示瞭如何實現對java類對象函數的調用過程。從以上代碼部分我們可以看到,要實現該調用需要有三個步驟,調用三個函數
jclass class_CommonMethod = (*env)->GetObjectClass(env,obj);   jmethodID md = (*env)->GetMethodID(env,class_CommonMethod,"print","(ILjava/lang/String;)V");   (*env)->CallVoidMethod(env,obj,md,a,s);  

GetObjectClass(...)函數獲得要調用對象的類;GetMethodID(...)獲得要調用的方法相對於該類的ID號;CallXXXMethod(...)調用該方法。
在編寫該調用過程的時候,需要注意的仍然是GetMethodID(...)函數中編碼簽名的問題,在該實例中,我們要做的是找到CommonMethod類的print(int a, String s)方法,該方法打印整數a,和字符串s 的直。在函數的編碼簽名部分(該部分以加粗、並加有下劃線)GetMethodID(env,class_CommonMethod,"print","(ILjava/lang/String;)V"); 從左往右可以查看,括號中的內容爲要調用方法的參數部分內容,I表示第一個參數爲int類型,“Ljava/lang/String;”表示第二個參數爲String類型,V表示返回值類型爲空void,如果返回值類型不爲空,則使用相應的類型簽名。返回值類型是和下面將要使用的調用該方法的函數CallXXXMethod(...)相關聯的,該函數的xxx要用相應的類型來替換,在此實例中爲void,如果返回值類型爲int類型則調用該方法的函數就爲CallIntMethod(...)。


6、實例六:在jni函數中調用java類的靜態方法(java/Method.java java/MethodTest.java src/Method.h src/Method.c)

實例五中介紹瞭如何調用類對象的方法,在此實例中我們將介紹如何調用java類的靜態方法在此實例中我們在/java/Method.java中定義了靜態方法:

public static void print() {
  System.out.println("this is a static method of class Method");
}

該函數的功能就是打印字符串“ this is a static method of class Method”;
我們在src/Method.c中實現了對該方法調用的jni函數:
JNIEXPORT void JNICALL Java_Method_callMethod   (JNIEnv *env, jobject obj)   {     printf("example 6:in this example, the class's static method will be called\n");     jclass class_Method = (*env)->FindClass(env,"Method");     jmethodID md = (*env)->GetStaticMethodID(env,class_Method,"print","()V");     (*env)->CallStaticVoidMethod(env,class_Method,md);   }  


和實例五不同的是,我們要調用的三個函數變爲:
FindClass(...)、GetStaticMethodID(...)、CallStaticVoidMethod(...)。
其中的機制和實例五是一樣的。再次就不做過多的介紹。

7、實例七:jni函數中傳遞基本數據類型參數(java/Basic.java java/BasicTest.java src/Basic.c src/Basic.h) 在java/Basic.java中,我們定義了一個public native void raiseValue(int a)函數,該函數將打印使value的值增加a,並打印原來的value和新的value值。
在src/Basic.c中給出了該jni函數的實現部分。
JNIEXPORT void JNICALL Java_Basic_raiseValue   (JNIEnv *env, jobject obj, jint a)   {     printf("example 7: in this example, a integer type parament will be passed to the jni method\n");     jclass class_Basic = (*env)->GetObjectClass(env,obj);     jfieldID fd = (*env)->GetFieldID(env,class_Basic,"value","I");     jint v = (*env)->GetIntField(env,obj,fd);     v = v+a;     (*env)->SetIntField(env,obj,fd,v);   }  

在此函數實現中,因爲要訪問Basic類中的value域,所以調用了GetObjectClass(...), GetFieldID(...), GetIntField(...)函數獲取value值,下面一步的 “ = v+a; ”說明,傳遞基本類型參數的處理方式和在c語言中的基本數據類型的處理無異。


8、實例八:在jni函數中傳遞對象類型參數(java/Book.java java/BookTest.java src/BookTest.c src/BookTest.h)

  在該實例中演示了在jni函數中傳遞對象函數的過程。

  我們在該實例中定義了一個類Book
    total_page = t;     }   publicint getTotalPage() {     }   publicint getCurrentPage() {     }       current_page++;     }   }  

然後我們在java/BookTest.java中定義jni函數
public native void bookCurrentStatus(Book b);
該函數需要一個Book類型的參數,並返回該參數的當前狀態,包括該書一共有多少頁的total_page,以及當前頁current_page。函數的實現部分爲(src/BookTest.c)
JNIEXPORT void JNICALL Java_BookTest_bookCurrentStatus   (JNIEnv *env, jobject this_obj, jobject obj)   {     printf("example 8: in this example, a object parament will be passed to the jni method。\n");     jclass class_book = (*env)->GetObjectClass(env,obj);     jmethodID id_getTotal = (*env)->GetMethodID(env,class_book,"getTotalPage","()I");     jmethodID id_getCurrent = (*env)->GetMethodID(env,class_book,"getCurrentPage","()I");     jint total_page = (*env)->CallIntMethod(env,obj,id_getTotal);     jint current_page = (*env)->CallIntMethod(env,obj,id_getCurrent);     printf("the total page is:%d and the current page is :%d\n",total_page,current_page);   }  

該函數包含三個參數(JNIEnv *env, jobject this_obj, jobject obj) ,第二個jobject this_obj參數表示當前的jni 函數所屬於的類對象,第三個jobject obj參數表示傳遞的參數Book類型的類對象。
對於實現部分,基本和實例五--調用java類對象的方法中的操作相同,就不作詳解。

9、實例九:在jni函數中處理字符串(java/Str.java java/StrTest.java src/Str.c src/Str.h)
在該實例中我們講解如何傳遞、處理字符串參數。
在java/Str.java中我們定義了一個 printString(String s) 的方法,用來處理字符串參數。
在src/Str.c中我們可以看到該函數的實現部分:
JNIEXPORT void JNICALL Java_Str_printString   (JNIEnv *env, jobject obj, jstring s)   {     printf("example 9: in this example, a String object parament will be passed to the jni method.\n");     string = (char*)(*env)->GetStringUTFChars(env,s,NULL);     printf("%s is put out in native method\n",string);     (*env)->ReleaseStringUTFChars(env,s,(jbyte*)string);   }  

實現過程中調用了兩個函數:GetStringUTFChars(...)、 ReleaseStringUTFChars(...)。
GetStringUTFChars(...)用來獲取String對象的字符串,並將其抓那還爲char*類型,這應該字符串就可以在c語言中進行處理拉。ReleaseStringUTFChars(...)用於當該字符串使用完成後,將其進行垃圾回收。記住,當使用完字符串時一定不要忘記調用該函數。

10、實例十:在jni函數中處理數組(java/Arr.java java/ArrTest.java src/Arr.c src/Arr.h)
java中所有的數組類型都有相對應的c語言類型,其中jarray類型表示一個泛型數組
boolean[] --jbooleanArray byte[]--jbyteArray char[]--jcharArary
int[]---jcharArray short[]---jshortArray long[]---jlongArray float[]--jfloatArray
double[]—-jdoubleArray Object[]--- jobjectArray。當訪問數組時,可以通過GetObjectAraryElement和SetObjectArrayElement方法訪問對象數組的元素。
而對於一般類型數組,你可以調用GetXXXAraryElements來獲取一個只想數組起始元素的指針,而當你不在使用該數組時,要記得調用ReleaseXXXArrayElements,這樣你所作的改變才能保證在原始數組裏得到反映。當然如果你需要得到數組的長度,可以調用GetArrayLength函數。
在本實例中,我們在Arr.java中定義一個本地方法:print(int intArry[]),該函數的功能爲對該數組進行輸出,在src/Arr.c中我們可以看到該方法的實現過程如下:
JNIEXPORT void JNICALL Java_Arr_print   (JNIEnv *env, jobject obj, jintArray intArray)   {     printf("example 10:in this example, a array parament will be passed to the jni method.\n");     arr = (*env)->GetIntArrayElements(env,intArray,NULL);   //n = (*env)->GetArrayLength(env,intArray);   printf("the native method output the int array\n");   for( i = 0;i<(*env)->GetArrayLength(env,intArray);i++)     {       printf("%d ",arr[i]);     }     (*env)->ReleaseIntArrayElements(env,intArray,arr,0);   }  

我們在此調用了GetIntArrayElements(...)來獲取一個指向intArray[]數組第一個元素的指針。
用getArrayLength(..)函數來得到數組的長度,以方便數組遍歷時使用。最後應用ReleaseArrayElements(...)函數來釋放該數組指針。

11、實例十一:在jni中的返回值問題(java/ReturnValue.java java/ReturnValueTest.java java/BookClass.java src/ReturnValue.c src/ReturnValue.h)
在java/ReturnValue類中定義了三個jni方法: returnInt(),returnString() ,returnObject()
三個方法,分別返回int , String , Object 類型的值。
其在src/ReturnValue.c中的實現分別爲:

JNIEXPORT jint JNICALL Java_ReturnValue_returnInt   (JNIEnv *env, jobject obj)   {     jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);     jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"value","I");     jint v = (*env)->GetIntField(env,obj,fd);   }      * Signature: ()Ljava/lang/String; JNIEXPORT jstring JNICALL Java_ReturnValue_returnString   (JNIEnv *env, jobject obj)   {     printf("example 11: in this example, the int and object of return value will be proceeding\n");     jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);     jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"name","Ljava/lang/String;");     jstring jstr = (jstring)(*env)->GetObjectField(env,obj,fd);   }      * * Method: returnObject JNIEXPORT jobject JNICALL Java_ReturnValue_returnObject   (JNIEnv *env, jobject obj)   {     jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);     jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"myBook","LBookClass;");     jobject jbook = (jstring)(*env)->GetObjectField(env,obj,fd);   }  

在這裏分別涉及到了對java類對象的一般參數,String參數,以及Object參數的訪問。


12、實例十二:在jni中創建java類對象:(java/Test.java src/CreateObj.c src/CreateObj.h)

如果想要在jni函數創建java類對象則要引用java 類的構造器方法,通過調用NewObject函數來實現。
NewObject函數的調用方式爲:
jobject obj_new = (*env)->NewObject(env,class, methodid, paraments);
在該實例中,我們在java/Test.java 中定義了Book1類,要在CreateObj類的modifyProperty() jni方法中創建該類對象。我們可以在src/CreateObj.c中看到該jni方法創建對象的過程:
jobject book;   jclass class_book;   jmethodID md_book;   class_book = (*env)->FindClass(env,"LBook1;");   md_book = (*env)->GetMethodID(env,class_book,"<init>","(IILjava/lang/String;)V");   book = (*env)->NewObject(env,class_book,md_book,100,1,"huanghe");  


在創建對象的過程中可以看到,要創建一個java類對象,首先需要得到得到使用FindClass函數得到該類,然後使用GetMethodID方法得到該類的構造器方法id,主義在此時構造器的函數名始終爲:"”,其後函數的簽名要符合函數簽名規則。在此我們的構造器有三個參數:int , int, String.
並且其返回值類型要永久爲空,所以函數簽名爲:"(IILjava/lang/String;)V"
然後我們調用NewObject()函數來創建該類的對象,在此之後就可以使用該對象拉。

以上內容介紹的是jni函數c語言的實現實例。如果想要使用c++的實例,我們只需要把其中的每一個函數調用過程作稍微的修改:
例如:(*env)->NewObject(env,class_book,md_book,100,1,”huanghe”);
修改爲:(env)->NewObject(class_book,md_book,100,1,”huanghe”);
即修改(*env)爲(env)再把參數中的env去掉。然後把所有c的函數改爲c++的函數就OK拉。
具體情況可以去查看我們的c++實例代碼.

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