JNI入門篇

JNI編程 —— 讓C++和Java相互調用

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代碼
/*
* Class: com_chnic_jni_SayHellotoCPP
* Method: sayHello
* Signature: (Ljava/lang/String;)V
*/
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手冊,還有上面例子中所用的演示代碼給大家參考。


本文轉載自:http://www.360doc.com/content/11/0427/10/3700464_112638437.shtml


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