轉自:http://blog.chinaunix.net/u3/90973/showart.php?id=2061985
JNI其實是Java Native Interface的簡稱,也就是java本地接口。它提供了若幹的API實作了和Java和其他語言的通信(主要是C&C++)。也許不少人覺 得Java已經足夠強大,為什麼要需要JNI這種東西呢?我們知道Java是一種平台無關性的語言,平台對于上層的java代碼來說是透明的,是以在多數 時間我們是不需要JNI的,但是假如你遇到了如下的三種情況之一呢?
- 你的Java代碼,需要得到一個檔案的屬性。但是你找遍了JDK幫助文檔也找不到相關的API。
- 在本地還有一個别的系統,不過他不是Java語言實作的,這個時候你的老闆要求你把兩套系統整合到一起。
- 你的Java代碼中需要用到某種算法,不過算法是用C實作并封裝在動态連結庫檔案(DLL)當中的。
對于上述的三種情況,如果沒有JNI的話,那就會變得異常棘手了。就算找到解決方案了,也是費時費力。其實說到底還是會增加開發和維護的成本。
說了那麼多一通廢話,現在進入正題。看過JDK源代碼的人肯定會注意到在源碼裡有很多标記成native的方法。這些個方法隻有方法簽名但是沒有方 法體。其實這些naive方法就是我們說的 java native interface。他提供了一個調用(invoke)的接口,然後用C或者C++去實作。我們首先來編寫這個“橋梁”.我自己的開發環境是 j2sdk1.4.2_15 + eclipse 3.2 + VC++ 6.0,先在eclipse裡建立一個HelloFore的Java工程,然後編寫下面的代碼。
Java代碼
- package com.chnic.jni;
- public class SayHellotoCPP {
- public SayHellotoCPP(){
- }
- public native void sayHello(String name);
- }
一般的第一個程式總是HelloWorld。今天換換口味,把world換成一個名字。我的native本地方法有一個String的參數。會傳 遞一個name到背景去。本地方法已經完成,現在來介紹下javah這個方法,接下來就要用javah方法來生成一個相對應的.h頭檔案。
javah是一個專門為JNI生成頭檔案的一個指令。CMD打開控制台之後輸入javah回車就能看到javah的一些參數。在這裡就不多介紹 我們要用的是 -jni這個參數,這個參數也是預設的參數,他會生成一個JNI式的.h頭檔案。在控制台進入到工程的根目錄,也就是HelloFore這個目錄,然後輸 入指令。
Java代碼
- javah -jni com.chnic.jni.SayHellotoCPP
指令執行完之後在工程的根目錄就會發現com_chnic_jni_SayHellotoCPP.h 這個頭檔案。在這裡有必要多句嘴,在執行javah的時候,要輸入完整的包名+類名。否則在以後的測試調用過程中會發生java.lang.UnsatisfiedLinkError這個異常。
到這裡java部分算是基本完成了,接下來我們來編寫後端的C++代碼。(用C也可以,隻不過cout比printf用起來更快些,是以這裡俺偷下 懶用C++)打開VC++首先建立一個Win32 Dynamic-Link library工程,之後選擇An empty DLL project空工程。在這裡我C++的工程是HelloEnd,把剛剛生成的那個頭檔案拷貝到這個工程的根目錄裡。随便用什麼文本編輯器打開這個頭文 件,發現有一個如下的方法簽名。
Cpp代碼
- JNIEXPORT void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello
- (JNIEnv *, jobject, jstring);
仔細觀察一下這個方法,在注釋上标注類名、方法名、簽名(Signature),至于這個簽名是做什麼用的,我們以後再說。在這裡最重要的是 Java_com_chnic_jni_SayHellotoCPP_sayHello這個方法簽名。在Java端我們執行 sayHello(String name)這個方法之後,JVM就會幫我們喚醒在DLL裡的Java_com_chnic_jni_SayHellotoCPP_sayHello這個方 法。是以我們建立一個C++ source file來實作這個方法。
Cpp代碼
- #include <iostream.h>
- #include "com_chnic_jni_SayHellotoCPP.h"
- JNIEXPORT void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello
- (JNIEnv* env, jobject obj, jstring name)
- {
- const char* pname = env->GetStringUTFChars(name, NULL);
- cout << "Hello, " << pname << endl;
- }
因為我們生成的那個頭檔案是在C++工程的根目錄不是在環境目錄,是以我們要把尖括号改成單引号,至于VC++的環境目錄可以在 Tools->Options->Directories裡設定。F7編譯工程發現缺少jni.h這個頭檔案。這個頭檔案可以 在%JAVA_HOME%\include目錄下找到。把這個檔案拷貝到C++工程目錄,繼續編譯發現還是找不到。原來是因為在我們剛剛生成的那個頭檔案 裡,jni.h這個檔案是被 #include <jni.h>引用進來的,是以我們把尖括号改成雙引号#include "jni.h",繼續編譯發現少了jni_md.h檔案,接着在%JAVA_HOME%\include\win32下面找到那個頭檔案,放入到工程根目 錄,F7編譯成功。在Debug目錄裡會發現生成了HelloEnd.dll這個檔案。
這個時候後端的C++代碼也已經完成,接下來的任務就是怎麼把他們連接配接在一起了,要讓前端的java程式“認識并找到”這個動态連結庫,就必須把這個DLL放在windows path環境變量下面。有兩種方法可以做到:
- 把這個DLL放到windows下面的sysytem32檔案夾下面,這個是windows預設的path
- 複制你工程的Debug目錄,我這裡是C:\Program Files\Microsoft Visual Studio\MyProjects\HelloEnd\Debug這個目錄,把這個目錄配置到User variable的Path下面。重新開機eclipse,讓eclipse在啟動的時候重新讀取這個path變量。
比較起來,第二種方法比較靈活,在開發的時候不用來回copy dll檔案了,節省了很多工作量,是以在開發的時候推薦用第二種方法。在這裡我們使用的也是第二種,eclipse重新開機之後打開 SayHellotoCPP這個類。其實我們上面做的那些是不是是讓JVM能找到那些DLL檔案,接下來我們要讓我們自己的java代碼“認識”這個動态 連結庫。加入System.loadLibrary("HelloEnd");這句到靜态初始化塊裡。
Java代碼
- package com.chnic.jni;
- public class SayHellotoCPP {
- static{
- System.loadLibrary("HelloEnd");
- }
- public SayHellotoCPP(){
- }
- public native void sayHello(String name);
- }
這樣我們的代碼就能認識并加載這個動态連結庫檔案了。萬事俱備,隻欠測試代碼了,接下來編寫測試代碼。
Java代碼
- SayHellotoCPP shp = new SayHellotoCPP();
- shp.sayHello("World");
我們不讓他直接Hello,World。我們把World傳進去,執行代碼。發現控制台列印出來Hello, World這句話。就此一個最簡單的JNI程式已經開發完成。也許有朋友會對CPP代碼裡的
Cpp代碼
- const char* pname = env->GetStringUTFChars(name, NULL);
這句有疑問,這個GetStringUTFChars就是JNI給developer提供的API,我們以後再講。在這裡不得不多句嘴。
- 因為JNI有一個Native這個特點,一點有項目用了JNI,也就說明這個項目基本不能跨平台了。
- JNI調用是相當慢的,在實際使用的之前一定要先想明白是否有這個必要。
- 因為C++和C這樣的語言非常靈活,一不小心就容易出錯,比如我剛剛的代碼就沒有寫析構字元串釋放記憶體,對于java developer來說因為有了GC 垃圾回收機制,是以大多數人沒有寫析構函數這樣的概念。是以JNI也會增加程式中的風險,增大程式的不穩定性。
其實在Java代碼中,除了對本地方法标注native關鍵字和加上要加載動态連結庫之外, JNI基本上是對上層coder透明的,上層coder調用那些本地方法的時候并不知道這個方法的方法體究竟是在哪裡,這個道理就像我們用JDK所提供的API一樣。是以在Java中使用 JNI還是很簡單的,相比之下在C++中調用java,就比前者要複雜的多了。
現在來介紹下JNI裡的資料類型。在C++裡,編譯器會很據所處的平台來為一些基本的 資料類型來配置設定長度,是以也就造成了平台不一緻性,而這個問題在Java中則不存在,因為有JVM的緣故,是以Java中的基本資料類型在所有平台下得到 的都是相同的長度,比如int的寬度永遠都是32位。基于這方面的原因,java和c++的基本資料類型就需要實作一些mapping,保持一緻性。下面 的表可以概括:
Java類型 | 本地類型 | JNI中定義的别名 |
int | long | jint |
long | _int64 | jlong |
byte | signed char | jbyte |
boolean | unsigned char | jboolean |
char | unsigned short | jchar |
short | short | jshort |
float | float | jfloat |
double | double | jdouble |
Object | _jobject* | jobject |
上面的表格是我在網上搜的,放上來給大家對比一下。對于每一種映射的資料類型,JNI的設計者其實已經幫我們取好了相應的别名以友善記憶。如果想了解一些更加細緻的資訊,可以去看一些jni.h這個頭檔案,各種資料類型的定義以及别名就被定義在這個檔案中。
了解了JNI中的資料類型,下面就來看這次的例子。這次我們用Java來實作一個前端的market(以下就用Foreground代替)用CPP來實作一個後端factory(以下用backend代替)。我們首先還是來編寫包含本地方法的java類。
Java代碼
- package com.chnic.service;
- import com.chnic.bean.Order;
- public class Business {
- static{
- System.loadLibrary("FruitFactory");
- }
- public Business(){
- }
- public native double getPrice(String name);
- public native Order getOrder(String name, int amount);
- public native Order getRamdomOrder();
- public native void analyzeOrder(Order order);
- public void notification(){
- System.out.println("Got a notification.");
- }
- public static void notificationByStatic(){
- System.out.println("Got a notification in a static method.");
- }
- }
這個類裡面包含4個本地方法,一個靜态初始化塊加載将要生成的dll檔案。剩下的方法都是很普通的java方法,等會在backend中回調這些方法。這個類需要一個名為Order的JavaBean。
Java代碼
- package com.chnic.bean;
- public class Order {
- private String name = "Fruit";
- private double price;
- private int amount = 30;
- public Order(){
- }
- public int getAmount() {
- return amount;
- }
- public void setAmount(int amount) {
- this.amount = amount;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public double getPrice() {
- return price;
- }
- public void setPrice(double price) {
- this.price = price;
- }
- }
JavaBean中,我們為兩個私有屬性指派,友善後面的例子示範。到此為止除了測試代碼之外的Java端的代碼就全部高調了,接下來進行生成.h 頭檔案、建立C++工程的工作,在這裡就一筆帶過,不熟悉的朋友請回頭看第一篇。在工程裡我們建立一個名為Foctory的C++ source file 檔案,去實作那些native方法。具體的代碼如下。
Cpp代碼
- #include <iostream.h>
- #include <string.h>
- #include "com_chnic_service_Business.h"
- jobject getInstance(JNIEnv* env, jclass obj_class);
- JNIEXPORT jdouble JNICALL Java_com_chnic_service_Business_getPrice(JNIEnv* env,
- jobject obj,
- jstring name)
- {
- const char* pname = env->GetStringUTFChars(name, NULL);
- cout << "Before release: " << pname << endl;
- if (strcmp(pname, "Apple") == 0)
- {
- env->ReleaseStringUTFChars(name, pname);
- cout << "After release: " << pname << endl;
- return 1.2;
- }
- else
- {
- env->ReleaseStringUTFChars(name, pname);
- cout << "After release: " << pname << endl;
- return 2.1;
- }
- }
- JNIEXPORT jobject JNICALL Java_com_chnic_service_Business_getOrder(JNIEnv* env,
- jobject obj,
- jstring name,
- jint amount)
- {
- jclass order_class = env->FindClass("com/chnic/bean/Order");
- jobject order = getInstance(env, order_class);
- jmethodID setName_method = env->GetMethodID(order_class, "setName", "(Ljava/lang/String;)V");
- env->CallVoidMethod(order, setName_method, name);
- jmethodID setAmount_method = env->GetMethodID(order_class, "setAmount", "(I)V");
- env->CallVoidMethod(order, setAmount_method, amount);
- return order;
- }
- JNIEXPORT jobject JNICALL Java_com_chnic_service_Business_getRamdomOrder(JNIEnv* env,
- jobject obj)
- {
- jclass business_class = env->GetObjectClass(obj);
- jobject business_obj = getInstance(env, business_class);
- jmethodID notification_method = env->GetMethodID(business_class, "notification", "()V");
- env->CallVoidMethod(obj, notification_method);
- jclass order_class = env->FindClass("com/chnic/bean/Order");
- jobject order = getInstance(env, order_class);
- jfieldID amount_field = env->GetFieldID(order_class, "amount", "I");
- jint amount = env->GetIntField(order, amount_field);
- cout << "amount: " << amount << endl;
- return order;
- }
- JNIEXPORT void JNICALL Java_com_chnic_service_Business_analyzeOrder (JNIEnv* env,
- jclass cls,
- jobject obj)
- {
- jclass order_class = env->GetObjectClass(obj);
- jmethodID getName_method = env->GetMethodID(order_class, "getName", "()Ljava/lang/String;");
- jstring name_str = static_cast<jstring>(env->CallObjectMethod(obj, getName_method));
- const char* pname = env->GetStringUTFChars(name_str, NULL);
- cout << "Name in Java_com_chnic_service_Business_analyzeOrder: " << pname << endl;
- jmethodID notification_method_static = env->GetStaticMethodID(cls, "notificationByStatic", "()V");
- env->CallStaticVoidMethod(cls, notification_method_static);
- }
- jobject getInstance(JNIEnv* env, jclass obj_class)
- {
- jmethodID construction_id = env->GetMethodID(obj_class, "<init>", "()V");
- jobject obj = env->NewObject(obj_class, construction_id);
- return obj;
- }
可以看到,在我Java中的四個本地方法在這裡全部被實作,接下來針對這四個方法來解釋下,一些JNI相關的API的使用方法。先從第一個方法講起吧:
1.getPrice(String name)
這個方法是從foreground傳遞一個類型為string的參數到backend,然後backend判斷傳回相應的價格。在cpp的代碼中, 我們用GetStringUTFChars這個方法來把傳來的jstring變成一個UTF-8編碼的char型字元串。因為jstring的實際類型是 jobject,是以無法直接比較。
GetStringUTFChars方法包含兩個參數,第一參數是你要處理的jstring對象,第二個參數是否需要在記憶體中生成一個副本對象。将 jstring轉換成為了一個const char*了之後,我們用string.h中帶strcmp函數來比較這兩個字元串,如果傳來的字元串是“Apple”的話我們傳回1.2。反之傳回 2.1。在這裡還要多說一下ReleaseStringUTFChars這個函數,這個函數從字面上不難了解,就是釋放記憶體用的。有點像cpp裡的析構函 數,隻不過Sun幫我們已經封裝好了。由于在JVM中有GC這個東東,是以多數java coder并沒有寫析構的習慣,不過在JNI裡是必須的了,否則容易造成記憶體洩露。我們在這裡在release之前和之後分别打出這個字元串來看一下效果。
粗略的解釋完一些API之後,我們編寫測試代碼。
Java代碼
- Business b = new Business();
- System.out.println(b.getPrice("Apple"));
運作這段測試代碼,控制台上打出
Before release: Apple
After release: ??
1.2
在release之前列印出來的是我們“需要”的Apple,release之後就成了亂碼了。由于傳遞的是Apple,是以得到1.2。測試成功。
2. getOrder(String name, int amount)
在foreground中可以通過這個方法讓backend傳回一個你“指定”的Order。所謂“指定”,其實也就是指方法裡的兩個參數:name和amout,在cpp的代碼在中,會根據傳遞的兩個參數來構造一個Order。回到cpp的代碼裡。
Java代碼
- jclass order_class = env->FindClass("com/chnic/bean/Order");
是不是覺得這句代碼似曾相識?沒錯,這句代碼很像我們java裡寫的Class.forName(className)反射的代碼。其實在這裡 FindClass的作用和上面的forName是類似的。隻不過在forName中要用完整的類名,但是在這裡必須用"/"來代替“.”。這個方法會返 回一個jclass的對象,其實也就是我們在Java中說的類對象。
Java代碼
- jmethodID construction_id = env->GetMethodID(obj_class, "<init>", "()V");
- jobject obj = env->NewObject(obj_class, construction_id);
拿到"類對象"了之後,按照Java RTTI的邏輯我們接下來就要喚醒那個類對象的構造函數了。在JNI中,包括構造函數在内的所有方法都被看成Method。每個method都有一個特定的ID,我們通過GetMethodID這個方法就可以拿到我們想要的某一個java 方法的ID。GetMethodID需要傳三個參數,第一個是很顯然jclass,第二個參數是java方法名,也就是你想取的method ID的那個方法的方法名(有些繞口

),第三個參數是方法簽名。
在這裡有必要單獨來講一講這個方法簽名,為什麼要用這個東東呢?我們知道,在Java裡方法是可以被重載的,比如我一個類裡有public void a(int arg)和public void a(String arg)這兩個方法,在這裡用方法名來區分方法顯然就是行不通的了。方法簽名包括兩部分:參數類型和傳回值類型;具體的格式:(參數1類型簽名 參數2類型簽名)傳回值類型簽名。下面是java類型和年名類型的對照的一個表
Java類型 | 對應的簽名 |
boolean | Z |
byte | B |
char | C |
shrot | S |
int | I |
long | L |
float | F |
double | D |
void | V |
Object | L用/分割包的完整類名; Ljava/lang/String; |
Array | [簽名 [I [Ljava/lang/String; |
其實除了自己對照手寫之外,JDK也提供了一個很好用的生成簽名的工具javap,cmd進入控制台到你要生成簽名的那個類的目錄下。在這裡用 Order類打比方,敲入: javap -s -private Order。 所有方法簽名都會被輸出,關于javap的一些參數可以在控制台下面輸入 javap -help檢視。(做coder的 畢竟還是要認幾個單詞的)
啰嗦了一大堆,還是回到我們剛剛的getMethodID這個方法上。因為是調用構造函數,JNI規定調用構造函數的時候傳遞的方法名應該為<init> ,通過javap檢視 我們要的那個無參的構造函數的方法簽是()V。得到方法簽名,最後我們調用NewObject方法來生成一個新的對象。
拿到了對象,之後我們開始為對象jobject填充數值,還是首先拿到setXXX方法的Method ID,之後調用Call<Type>Method來調用java方法。這裡的<Type>所指的是方法的傳回類型,我們剛剛調用 的是set方法的傳回值是void,是以這裡的方法也就是CallVoidMethod,這個方法的參數除了前兩個要傳入jobject和 jmethodID之外還要傳入要調用的那個方法的參數,而且要順序必須一緻,這點和Java的反射一模一樣,在這裡就不多解釋。(看到這一步是不是對 java 反射又有了自己新的了解?)
終于介紹完了第二個方法,下來就是測試代碼測試。
Java代碼
- Order o = b.getOrder("Watermelom", 100);
- System.out.println("java: " + o.getName());
- System.out.println("java: " + o.getAmount());
控制台打出
java: Watermelom
java: 100
就此,我們完成了第二個方法的測試。
3.getRamdomOrder()
這個方法會從backend得到一個随機的Order對象(抱歉這裡“Random”拼錯了),然後再調用java中相應的通知方法來通知 foreground。getRamdomOrder方法沒有參數,但是所對應的C++方法裡卻有兩個參數,一定有人會不解。其實細心的朋友一定會發 現,JNI裡所有對應Java方法的C++ 方法都會比Java方法多兩個參數,第一個參數是我們很熟悉的JNIEnv*指針,第二個參數有時是jobject有時是個jclass。針對這第二個參 數在這裡有必要多廢話兩句。
其實第二個參數傳遞的是包含了native本地方法的對象或者類對象,我們知道非靜态的方法是屬于某一個對象的,而靜态方法是屬于類對象的,是以靜 态方法可以被所有對象共享。有這個對象/類對象,我們就可以很友善的操作包含了native方法的對象的一些函數了。(這句話有點繞口,沒看明白的建議多 讀兩遍)。
廢話完了言歸正傳,因為getRamdomOrder不是靜态的,是以C++相對應的參數中傳遞來的是一個jobject對象。
Cpp代碼
- jclass business_class = env->GetObjectClass(obj);
這一句不難了解,GetObjectClass方法可以得到一個對象的類對象,這句有點像Java中的Object.class。不熟悉的朋友建議 再去看一下Java反射機制。接下來的幾句C++代碼應該在之前的方法1和方法2中都解釋過。早backend端會發一個“消息”給 foreground,之後new一個新的Order類出來。接下來的三句有必要再廢話一下。
Cpp代碼
- jfieldID amount_field = env->GetFieldID(order_class, "amount", "I");
- jint amount = env->GetIntField(order, amount_field);
- cout << "amount: " << amount << endl;
之前我為Order這個Javabean的amount的屬性設定了一個初始值為30,其實就是為了在這裡示範如何在C++中拿一個Java對象的 屬性,拿的方法和我們之前說過的調用Java方法的程式差不多,也要先拿到一個jfieldID,之後調用Get<type>Field方法 來取得某一個對象中的某一個屬性的數值,最後cout把他列印出來。我們編寫測試代碼來看一下最終效果。
Java代碼
- Business b = new Business();
- Order o2 = b.getRamdomOrder();
- System.out.println(o2.getName());
運作上述的測試代碼之後,控制台上打出了
Got a notification.
amount: 30
Fruit
和我們想要的結果是一樣的,測試成功。
4.analyzeOrder(Order order)
這是一個靜态方法,foreground會通過這個方法傳一個Order的對象到backend去,然後再由CPP端進行“analyze”。在這 裡我們取出來傳遞過來的Order對象的name屬性,然後列印到控制台上。因為這個方法是靜态static方法,是以相對應的C++方法中的第二個參數 也變成了jclass對象,也就是Business.class這個類對象。第三個參數是一個jobject對象,很明顯就是我們傳遞過來的order對 象。
前5句代碼應該不難了解,就是調用getName這個方法,然後列印出來。因為JNI的API中并沒有提供CallStringMethod這個方 法,是以我們用CallObjectMethod這個方法來取得name這個字元串(String很明顯也是一個Object),然後再轉型成為 jstring。也就是下面這句代碼。
Cpp代碼
- jstring name_str = static_cast<jstring>(env->CallObjectMethod(obj, getName_method));
取到了name這個字元串之後cout列印出來,之後調用Business這個類對象中的靜态方法notificationByStatic來通知 foreground。調用的流程以及方法和非靜态都是一樣的,隻不過注意JNI中調用靜态方法的API所傳遞的一個參數是一個jclass而非 jobject(這個也不難了解,因為靜态方法是屬于class類對象的)
還是編寫測試代碼測試這個方法
Java代碼
- Business b = new Business();
- Order o = b.getOrder("Watermelom", 100);
- Business.analyzeOrder(o);
控制台上列印出
Name in Java_com_chnic_service_Business_analyzeOrder: Watermelom
Got a notification in a static method.
第一句是C++中cout列印出來的,第二句則是Java中的靜态方法列印出來的,和我們想要的結果是一緻的。
呼~好不容易介紹完了4個方法,最後總結一下吧。
- JNI中所提供的API遠遠不止這4個方法中所使用的API。上面介紹的都是比較常用的,本人也不可能羅列出所有的API。
- 了解了JNI程式設計更加有利于深入了解Java中的反射機制,反之亦然。
是以如果有對JNI程式設計有興趣或者有更深入的需要,可以參考一下sun的相關文檔。在這裡上傳sun提供的JNI的API手冊,還有上面例子中所用的示範代碼給大家參考。