本文原名《使用JNA方便地調用原生函數》發表於2009年3月的“程序員”雜誌上。感謝程序員雜誌的許可,使這篇文章能夠成爲免費的電子版,發佈於網絡上。
程序員雜誌發表此文時,略有裁剪,因此本文比程序員上的文章內容更多。
JNA的API參考手冊和最新版本的pdf文檔,可以在如下地址下載:
http://code.google.com/p/shendl/downloads/list
PDF格式文檔可在http://download.csdn.net/source/1503487免費下載。
和許多解釋執行的語言一樣,Java提供了調用原生函數的機制,以加強Java平臺的能力。Java™ Native Interface (JNI)就是Java調用原生函數的機制。
事實上,很多Java核心代碼內部就是使用JNI實現的。這些Java功能實際上是通過原生函數提供的。
但是,使用JNI對Java開發者來說簡直是一場噩夢。
如果你已經有了原生函數,使用JNI,你必須使用C語言再編寫一個動態鏈接庫,這個動態鏈接庫的唯一功能就是使用Java能夠理解的C代碼來調用目標原生函數。
這個沒什麼實際用途的動態鏈接庫的編寫過程令人沮喪。同時編寫Java和C代碼使開發難度大大增加。
因此,在Java開發社區中,人們一直都視JNI爲禁地,輕易不願涉足。
缺少原生函數的協助使Java的使用範圍大大縮小。
反觀.NET陣營,其P/Invoke技術調用原生函數非常方便,不需要編寫一行C代碼,只需要寫Annotation就可以快速調用原生函數。因此,與硬件有關的很多開發領域都被.NET所佔據。
介紹
JNA(Java NativeAccess)框架是一個開源的Java框架,是SUN公司主導開發的,建立在經典的JNI的基礎之上的一個框架。
JNA項目地址:https://jna.dev.java.net/
JNA使Java調用原生函數就像.NET上的P/Invoke一樣方便、快捷。
JNA的功能和P/Invoke類似,但編寫方法與P/Invoke截然不同。JNA沒有使用Annotation,而是通過編寫一般的Java代碼來實現。
P/Invoke是.NET平臺的機制。而JNA是Java平臺上的一個開源類庫,和其他類庫沒有什麼區別。只需要在classpath下加入jna.jar包,就可以使用JNA。
JNA使Java平臺可以方便地調用原生函數,這大大擴展了Java平臺的整合能力。
實現原理
JNI 是Java調用原生函數唯一的機制。JNA也是建立在JNI技術之上的。它簡化了Java調用原生函數的過程。
JNA提供了一個動態的C語言編寫的轉發器,可以自動實現Java和C的數據類型映射。你不再需要編寫那個煩人的C動態鏈接庫。
當然,這也意味着,使用JNA技術比使用JNI技術調用動態鏈接庫會有些微的性能損失。可能速度會降低幾倍。但對於絕大部分項目來說,影響不大。
調用原生函數
讓我們先看一個JNA調用原生函數的例子。
使用JNA調用原生函數
假設我們有一個動態鏈接庫,發佈了這樣一個C函數:
void say(wchar_t* pValue){
std::wcout.imbue(std::locale("chs"));
std::wcout<<L"原生函數說:"<<pValue<<std::endl;
}
它需要傳入一個Unicode編碼的字符數組。然後在控制檯上打印出一段中文字符。
爲了調用這個原生函數,使用JNA,我們需要編寫這樣的Java代碼:
publicinterface TestDll1 extends Library {
TestDll1 INSTANCE =(TestDll1)Native.loadLibrary("TestDll1", TestDll1.class);
public void say(WString value);
}
這裏,如果動態鏈接庫是以stdcall方式輸出函數,那麼就繼承StdCallLibrary。
然後就可以像普通的Java程序那樣調用這個接口:
public static void main(String[] args) {
TestDll1.INSTANCE.say(newWString("Hello World!"));
System.out.println("Java輸出。");
}
執行,可以看到控制檯下如下輸出:
原生函數說:Hello World!
Java輸出。
調用原生函數的模式
JNA不使用native關鍵字。
JNI使用native關鍵字,使用一個個Java方法來代表外部的原生函數。
而JNA使用一個Java接口來代表一個動態鏈接庫發佈的所有函數。
對於不需要的原生函數,你可以不在Java接口中聲明Java方法原型。
如果使用JNI,你需要使用System.loadLibrary方法,把我們專爲JNI編寫的動態鏈接庫載入進來。這個動態鏈接庫實際上是我們真正需要的動態鏈接庫的代理。
上例中使用JNA類庫的Native類的loadLibrary方法 ,是直接把我們需要的動態鏈接庫載入進來。使用JNA,我們不需要編寫作爲代理的動態鏈接庫,不需要編寫一行原生代碼。
上面的JNA代碼使用了單例,接口的靜態變量返回的是接口的唯一實例,這個Java對象是JNA通過反射動態創建的。通過這個對象,我們可以調用動態鏈接庫發佈的函數。
和原生代碼的類型映射
跨平臺、跨語言調用的最大難點,就是不同語言之間數據類型不一致造成的問題。絕大部分跨平臺調用的失敗,都是這個問題造成的。
JNA使用的數據類型是Java的數據類型。而原生函數中使用的數據類型是原生函數的編程語言使用的數據類型。可能是C,Delphi,彙編等語言的數據類型。因此,不一致是在所難免的。
JNA提供了Java和原生代碼的類型映射。
和操作系統數據類型的對應表
Java 類型 |
C 類型 |
原生表現 |
boolean |
int |
32位整數 (可定製) |
byte |
char |
8位整數 |
char |
wchar_t |
平臺依賴 |
short |
short |
16位整數 |
int |
int |
32位整數 |
long |
long long, __int64 |
64位整數 |
float |
float |
32位浮點數 |
double |
double |
64位浮點數 |
|
pointer |
平臺依賴(32或 64位指針) |
<T>[] (基本類型的數組) |
pointer |
32或 64位指針(參數/返回值) |
支持常見的數據類型的映射
Java 類型 |
C 類型 |
原生表現 |
char* |
/0結束的數組 (native encoding or |
|
wchar_t* |
/0結束的數組(unicode) |
|
char** |
/0結束的數組的數組 |
|
wchar_t** |
/0結束的寬字符數組的數組 |
|
struct* |
指向結構體的指針 (參數或返回值) (或者明確指定是結構體指針) |
|
union |
等同於結構體 |
|
struct[] |
結構體的數組,鄰接內存 |
|
<T> (*fp)() |
Java函數指針或原生函數指針 |
|
varies |
依賴於定義 |
|
long |
平臺依賴(32或64位整數) |
|
pointer |
和 |
:
儘量使用基本、簡單的數據類型;
儘量少跨平臺、跨語言傳遞數據!
如果有複雜的數據類型需要在Java和原生函數中傳遞,那麼我們就必須在Java中模擬大量複雜的原生類型。這將大大增加實現的難度,甚至無法實現。
如果在Java和原生函數間存在大量的數據傳遞,那麼一方面,性能會有很大的損失。更爲重要的是,Java調用原生函數時,會把數據固定在內存中,這樣原生函數纔可以訪問這些Java數據。這些數據,JVM的GC不能管理,會造成內存碎片。
如果在你需要調用的動態鏈接庫中,有複雜的數據類型和龐大的跨平臺數據傳遞。那麼你應該另外寫一些原生函數,把需要傳遞的數據類型簡化,把需要傳遞的數據量簡化。
模擬結構體
在原生代碼中,結構體是經常使用的複雜數據類型。這裏我們研究一下怎樣使用JNA模擬結構體。
使用JNA調用使用Struct的C函數
假設我們現在有這樣一個C語言結構體
struct UserStruct{
long id;
wchar_t* name;
int age;
};
使用上述結構體的函數
#define MYLIBAPI extern "C" __declspec( dllexport )
MYLIBAPI void sayUser(UserStruct* pUserStruct);
對應的Java程序中,在例1的接口中添加下列代碼:
publicstatic classUserStruct extendsStructure{
publicNativeLongid;
publicWStringname;
publicint age;
publicstatic class ByReferenceextends UserStructimplements Structure.ByReference { }
publicstatic class ByValue extends UserStructimplements Structure.ByValue
{ }
}
publicvoid sayUser(UserStruct.ByReference struct);
Java中的調用代碼:
UserStruct userStruct=new UserStruct ();
userStruct.id=newNativeLong(100);
userStruct.age=30;
userStruct.name=newWString("奧巴馬"); TestDll1.INSTANCE.sayUser(userStruct);
說明
現在,我們就在Java中實現了對C語言的結構體的模擬。
這裏,我們繼承了Structure類,用這個類來模擬C語言的結構體。
必須注意,Structure子類中的公共字段的順序,必須與C語言中的結構的順序一致。否則會報錯!
因爲,Java調用動態鏈接庫中的C函數,實際上就是一段內存作爲函數的參數傳遞給C函數。
動態鏈接庫以爲這個參數就是C語言傳過來的參數。
同時,C語言的結構體是一個嚴格的規範,它定義了內存的次序。因此,JNA中模擬的結構體的變量順序絕對不能錯。
如果一個Struct有2個int變量。 Int a, int b
如果JNA中的次序和C中的次序相反,那麼不會報錯,但是數據將會被傳遞到錯誤的字段中去。
Structure類代表了一個原生結構體。當Structure對象作爲一個函數的參數或者返回值傳遞時,它代表結構體指針。當它被用在另一個結構體內部作爲一個字段時,它代表結構體本身。
另外,Structure類有兩個內部接口Structure.ByReference和Structure.ByValue。這兩個接口僅僅是標記,如果一個類實現Structure.ByReference接口,就表示這個類代表結構體指針。如果一個類實現Structure.ByValue接口,就表示這個類代表結構體本身。
使用這兩個接口的實現類,可以明確定義我們的Structure實例表示的是結構體的指針還是結構體本身。
上面的例子中,由於Structure實例作爲函數的參數使用,因此是結構體指針。所以這裏直接使用了UserStruct userStruct=new UserStruct ();
也可以使用UserStruct userStruct=new UserStruct.ByReference ();
明確指出userStruct對象是結構體指針而不是結構體本身。
模擬複雜結構體
C語言最主要的數據類型就是結構體。結構體可以內部可以嵌套結構體,這使它可以模擬任何類型的對象。
JNA也可以模擬這類複雜的結構體。
結構體內部可以包含結構體對象的數組
structCompanyStruct{
long id;
wchar_t* name;
UserStruct users[100];
int count;
};
JNA中可以這樣模擬:
publicstatic classCompanyStruct extendsStructure{
public NativeLongid;
public WString name;
public UserStruct.ByValue[]users=new UserStruct.ByValue[100];
publicint count;
}
這裏,必須給users字段賦值,否則不會分配100個UserStruct結構體的內存,這樣JNA中的內存大小和原生代碼中結構體的內存大小不一致,調用就會失敗。
結構體內部可以包含結構體對象的指針的數組
structCompanyStruct2{
long id;
wchar_t* name;
UserStruct* users[100];
int count;
};
JNA中可以這樣模擬:
publicstatic classCompanyStruct2extendsStructure{
public NativeLongid;
public WString name;
public UserStruct.ByReference[]users=newUserStruct.ByReference[100];
publicint count;
}
測試代碼:
CompanyStruct2.ByReference companyStruct2=new CompanyStruct2.ByReference();
companyStruct2.id=new NativeLong(2);
companyStruct2.name=new WString("Yahoo");
companyStruct2.count=10;
UserStruct.ByReference pUserStruct=new UserStruct.ByReference();
pUserStruct.id=new NativeLong(90);
pUserStruct.age=99;
pUserStruct.name=new WString("楊致遠");
// pUserStruct.write();
for(inti=0;i<companyStruct2.count;i++){
companyStruct2.users[i]=pUserStruct;
}
TestDll1.INSTANCE.sayCompany2(companyStruct2);
執行測試代碼,報錯了。這是怎麼回事?
考察JNI技術,我們發現Java調用原生函數時,會把傳遞給原生函數的Java數據固定在內存中,這樣原生函數纔可以訪問這些Java數據。對於沒有固定住的Java對象,GC可以刪除它,也可以移動它在內存中的位置,以使堆上的內存連續。如果原生函數訪問沒有被固定住的Java對象,就會導致調用失敗。
固定住哪些java對象,是JVM根據原生函數調用自動判斷的。而上面的CompanyStruct2結構體中的一個字段是UserStruct對象指針的數組,因此,JVM在執行時只是固定住了CompanyStruct2對象的內存,而沒有固定住users字段引用的UserStruct數組。因此,造成了錯誤。
我們需要把users字段引用的UserStruct數組的所有成員也全部固定住,禁止GC移動或者刪除。
如果我們執行了pUserStruct.write();這段代碼,那麼就可以成功執行上述代碼。
Structure類的write()方法會把結構體的所有字段固定住,使原生函數可以訪問。
代碼
JNI技術是雙向的,既可以從Java代碼中調用原生函數,也可以從原生函數中直接創建Java虛擬機,並調用Java代碼。
但是,這樣做要寫大量C代碼,對於廣大Java程序員來說是很頭疼的。
使用JNA,我們就可以不寫一行C代碼,照樣實現原生代碼調用Java代碼!
JNA可以模擬函數指針,通過函數指針,就可以實現在原生代碼中調用Java函數。
讓我們先看一個模擬函數指針的JNA例子:
通過回調函數實現原生代碼調用Java代碼
intgetValue(int (*fp)(intleft,int right),intleft,int right){
returnfp(left,right);
}
C函數中通過函數指針調用外部傳入的函數,執行任務。
JNA中這樣模擬函數指針:
publicstatic interfaceFp extendsCallback {
int invoke(int left,int right);
}
C函數用如下Java方法聲明代表:
publicint getValue(Fp fp,int left,int right);
現在,我們有了代表函數指針int(*fp)(int left,intright)的接口Fp,但是還沒有Fp的實現類。
publicstatic classFpAdd implementsFp{
@Override
publicintinvoke(intleft,intright) {
return left+right;
}
}
回調函數說明
原生函數可以通過函數指針實現函數回調,調用外部函數來執行任務。這就是策略模式。
JNA可以方便地模擬函數指針,把Java函數作爲函數指針傳遞給原生函數,實現在原生代碼中調用Java代碼。
模擬指針
JNA可以模擬原生代碼中的指針。Java和原生代碼的類型映射表中的指針映射是這樣的:
Java 類型 |
C 類型 |
原生表現 |
|
pointer |
平臺依賴(32或 64位指針) |
<T>[] (基本類型的數組) |
pointer |
32或 64位指針(參數/返回值) |
pointer |
和 |
原生代碼中的數組,可以使用JNA中對應類型的數組來表示。
原生代碼中的指針,可以使用Pointer類型,或者PointerType類型及它們的子類型來模擬。
Pointer代表原生代碼中的指針。其屬性peer就是原生代碼中指針的地址。
我們不可以直接創建Pointer對象,但可以用它表示原生函數中的任何指針。
Pointer類有2個子類:Function, Memory。
Function類代表原生函數的指針,可以通過invoke(Class,Object[],Map)這一系列的方法調用原生函數。
Memory類代表的是堆中的一段內存,它也是我們可以創建的Pointer子類。創建一個Memory類的實例,就是在原生代碼的內存區中分配一塊指定大小的內存。這塊內存會在GC釋放這個Java對象時被釋放。Memory類在指針模擬中會被經常用到。
PointerType類代表的是一個類型安全的指針。ByReference類是PointerType類的子類。ByReference類代表指向堆內存的指針。ByReference類非常簡單。
publicabstract class ByReference extends PointerType {
protected ByReference(int dataSize) {
setPointer(new Memory(dataSize));
}
}
ByReference類有很多子類,這些類都非常有用。
ByteByReference, DoubleByReference,FloatByReference, IntByReference, LongByReference, NativeLongByReference,PointerByReference, ShortByReference, W32API.HANDLEByReference,X11.AtomByReference, X11.WindowByReference
ByteByReference等類故名思議,就是指向原生代碼中的字節數據的指針。
PointerByReference類表示指向指針的指針。
在JNA中模擬指針,最常用到的就是Pointer類和PointerByReference類。Pointer類代表指向任何東西的指針,PointerByReference類表示指向指針的指針。Pointer類更加通用,事實上PointerByReference類內部也持有Pointer類的實例。
PointerByReference類可以嵌套使用,它所指向的指針,本身可能也是指向指針的指針。PointerByReference類的源代碼:
publicclass PointerByReferenceextends ByReference {
public PointerByReference() {
this(null);
}
public PointerByReference(Pointervalue) {
super(Pointer.SIZE);
setValue(value);
}
publicvoid setValue(Pointer value) {
getPointer().setPointer(0,value);
}
public Pointer getValue() {
returngetPointer().getPointer(0);
}
}
可以看到,PointerByReference類的構造器做了如下工作:
1, 首先在堆中分配一個指針大小的內存,並用一個Pointer對象代表。PointerByReference類的實例持有這個Pointer對象。
2, 然後,這個堆上新創建的指針的值被設置爲傳入的參數的地址,也就是指向傳入的Pointer對象。這樣,新創建的Pointer對象就是指針的指針。
使用PointerByReference模擬指向指針的指針
假設我們有一個結構體UserStruct的實例userStruct,現在又有了一個指向userStruct對象的指針pUser。
爲了得到UserStruct**指針在Java中的對等體,我們可以執行如下代碼:
PointerByReferenceppUser=newPointerByReference(pUser);
這會在堆中創建一個指針pointer,然後把pUser指針的地址複製到pointer對象中,這樣pointer也就是指向pUser的指針。Pointer對象就是代表UserStruct**類型的指針。可以使用ppUser.getPointer()方法返回pointer對象。
我們在Java和原生代碼的類型映射表中曾經指出,PointerType和Pointer類型相同,都可以表示指針。PointerByReference類是PointerType類的子類,因此,ppUser對象也可以代表UserStruct**類型的指針。
模擬指針
下面,給大家展示一個完整的例子,展示如何使用Pointer和PointerByReference類型模擬各類原生指針。
C代碼:
void sayUser(UserStruct* pUserStruct){
std::wcout.imbue(std::locale("chs"));
std::wcout<<L"ID:"<<pUserStruct->id<<std::endl;
std::wcout<<L"姓名:"<<pUserStruct->name<<std::endl;
std::wcout<<L"年齡:"<<pUserStruct->age<<std::endl;
}
void sayUser2(UserStruct** ppUserStruct){
//UserStruct** ppUserStruct=*pppUserStruct;
UserStruct* pUserStruct=*ppUserStruct;
sayUser(pUserStruct);
}
void sayUser3(UserStruct*** pppUserStruct){
//UserStruct**ppUserStruct=*pppUserStruct;
UserStruct** ppUserStruct=*pppUserStruct;
sayUser2(ppUserStruct);
}
然後發佈這3個函數。
JNA中模擬:
在接口中添加方法:
publicvoidsayUser(UserStruct.ByReference struct);
publicvoid sayUser2(PointerByReference ppUserStruct);
publicvoid sayUser3(PointerpppUserStruct);
JNA中調用:
UserStruct pUserStruct2=new UserStruct();
pUserStruct2.id=new NativeLong(90);
pUserStruct2.age=99;
pUserStruct2.name=new WString("喬布斯");
pUserStruct2.write();
PointerpPointer=pUserStruct2.getPointer();
PointerByReferenceppUserStruct=new PointerByReference(pPointer);
System.out.println("使用ppUserStruct!!!!");
TestDll1.INSTANCE.sayUser2(ppUserStruct);
System.out.println("使用pppUserStruct!!!!");
PointerByReference pppUserStruct=newPointerByReference(ppUserStruct.getPointer());
TestDll1.INSTANCE.sayUser3(pppUserStruct.getPointer());
可以看到,我們能夠使用Pointer或者PointerByReference來表示指向指針的指針。sayUser3中,我們使用了PointerByReference類的getPointer()方法返回了代表UserStruct***類型的指針。
事實上,如果publicvoid sayUser3(Pointer pppUserStruct);定義成
publicvoid sayUser3(PointerByReference pppUserStruct);也是可以的,只是調用時提供的參數變爲pppUserStruct對象本身即可。
通過使用Pointer和PointerByReference類,我們可以模擬任何原生代碼的指針。
類詳解
setPointer()方法相當於pTr2=&ptr1;
setLong()方法相當於ptr2=&long;
getPointer(0)相當於 (void*) *ptr2;
取指針指向的值,返回的還是指針。
getLong(0)相當於 (long)*ptr2;
取指針指向的值,返回的是long類型的數據。
JNA打破了Java和原生代碼原本涇渭分明的界限,實現了Java和原生代碼的強強聯合,在各自擅長的領域分工合作,快速解決問題。
Java可以方便地利用原生代碼的優勢:執行速度快,可以直接操作硬件,機器碼不容易被破解等。
原生代碼可以通過回調Java函數,利用Java的優勢:開發效率高,自動內存管理,跨平臺,類庫豐富,網絡功能強大,支持多種腳本語言等。
JNA爲Java開發者打開了一扇通向廣袤的原生代碼世界的大門。
本文出自:http://blog.csdn.net/shendl/article/details/4362495