1. 程式人生 > >Java JNA (三)—— 結構體使用及簡單示例

Java JNA (三)—— 結構體使用及簡單示例

JNA簡介

JNA全稱Java Native Access,是一個建立在經典的JNI技術之上的Java開源框架(https://github.com/twall/jna)。JNA提供一組Java工具類用於在執行期動態訪問系統本地庫(native library:如Window的dll)而不需要編寫任何Native/JNI程式碼。開發人員只要在一個java介面中描述目標native library的函式與結構,JNA將自動實現Java介面到native function的對映。

JNA包:

1,dll和so是C函式的集合和容器,這與Java中的介面概念吻合,所以JNA把dll檔案和so檔案看成一個個介面。在JNA中定義一個介面就是相當於了定義一個DLL/SO檔案的描述檔案,該介面代表了動態連結庫中釋出的所有函式。而且,對於程式不需要的函式,可以不在介面中宣告。

2,JNA定義的介面一般繼承com.sun.jna.Library介面,如果dll檔案中的函式是以stdcall方式輸出函式,那麼,該介面就應該繼承com.sun.jna.win32.StdCallLibrary介面。

3,Jna難點:程式語言之間的資料型別不一致。

Java和C的資料型別對照

Java和C的資料型別對照表

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位浮點數

 Buffer/Pointer

 pointer

 平臺依賴(32或64位指標)

 <T>[] (基本型別的陣列)

 pointer/array

32或64位指標(引數/返回值)

鄰接記憶體(結構體成員)

 String

 char*

/0結束的陣列 (native encoding or jna.encoding)

 WString

 wchar_t*

 /0結束的陣列(unicode)

 String[]

 char**

 /0結束的陣列的陣列

 WString[]

 wchar_t**

 /0結束的寬字元陣列的陣列

 Structure

 struct*/struct

指向結構體的指標(引數或返回值) (或者明確指定是結構體指標)結構體(結構體的成員) (或者明確指定是結構體)

 Union

union 

 等同於結構體

 Structure[]

 struct[]

 結構體的陣列,鄰接記憶體

 Callback

 <T> (*fp)()

 Java函式指標或原生函式指標

 NativeMapped

 varies

 依賴於定義

 NativeLong

 long

 平臺依賴(32或64位整數)

 PointerType

 pointer

 和Pointer相同

通用入門案例

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
 
/** Simple example of JNA interface mapping and usage. */
public class HelloWorld {
 
    // This is the standard, stable way of mapping, which supports extensive
    // customization and mapping of Java to native types.
 
    public interface CLibrary extends Library {
        CLibrary INSTANCE = (CLibrary)
            Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"),
                               CLibrary.class);
 
        void printf(String format, Object... args);
    }
 
    public static void main(String[] args) {
        CLibrary.INSTANCE.printf("Hello, World\n");
        for (int i=0;i < args.length;i++) {
            CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
        }
    }
}

執行程式,如果沒有帶引數則只打印出“Hello, World”,如果帶了引數,則會打印出所有的引數。

很簡單,不需要寫一行C程式碼,就可以直接在Java中呼叫外部動態連結庫中的函式!

下面來解釋下這個程式。

(1)需要定義一個介面,繼承自Library StdCallLibrary

預設的是繼承Library ,如果動態連結庫裡的函式是以stdcall方式輸出的,那麼就繼承StdCallLibrary,比如眾所周知的kernel32庫。比如上例中的介面定義:

public interface CLibrary extends Library {
 
}

(2)介面內部定義

介面內部需要一個公共靜態常量:INSTANCE,通過這個常量,就可以獲得這個介面的例項,從而使用介面的方法,也就是呼叫外部dll/so的函式。

該常量通過Native.loadLibrary()這個API函式獲得,該函式有2個引數:

  • 第一個引數是動態連結庫dll/so的名稱,但不帶.dll或.so這樣的字尾,這符合JNI的規範,因為帶了字尾名就不可以跨作業系統平臺了。搜尋動態連結庫路徑的順序是:先從當前類的當前資料夾找,如果沒有找到,再在工程當前資料夾下面找win32/win64資料夾,找到後搜尋對應的dll檔案,如果找不到再到WINDOWS下面去搜索,再找不到就會拋異常了。比如上例中printf函式在Windows平臺下所在的dll庫名稱是msvcrt,而在其它平臺如Linux下的so庫名稱是c。

  • 第二個引數是本介面的Class型別。JNA通過這個Class型別,根據指定的.dll/.so檔案,動態建立介面的例項。該例項由JNA通過反射自動生成。

CLibrary INSTANCE = (CLibrary)
            Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"),
                               CLibrary.class);

介面中只需要定義你要用到的函式或者公共變數,不需要的可以不定義,如上例只定義printf函式:

void printf(String format, Object... args);

注意引數和返回值的型別,應該和連結庫中的函式型別保持一致。

(3)呼叫連結庫中的函式

定義好介面後,就可以使用介面中的函式即相應dll/so中的函數了,前面說過呼叫方法就是通過介面中的例項進行呼叫,非常簡單,如上例中:

CLibrary.INSTANCE.printf("Hello, World\n");
        for (int i=0;i < args.length;i++) {
            CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
        }

這就是JNA使用的簡單例子,可能有人認為這個例子太簡單了,因為使用的是系統自帶的動態連結庫,應該還給出一個自己實現的庫函式例子。其實我覺得這個完全沒有必要,這也是JNA的方便之處,不像JNI使用使用者自定義庫時還得定義一大堆配置資訊,對於JNA來說,使用使用者自定義庫與使用系統自帶的庫是完全一樣的方法,不需要額外配置什麼資訊。比如我在Windows下建立一個動態庫程式:

#include "stdafx.h"
 
extern "C"_declspec(dllexport) int add(int a, int b);
 
int add(int a, int b) {
    return a + b;
}

然後編譯成一個dll檔案(比如CDLL.dll),放到當前目錄下,然後編寫JNA程式呼叫即可:

public class DllTest {
 
    public interface CLibrary extends Library {
        CLibrary INSTANCE = (CLibrary)Native.loadLibrary("CDLL", CLibrary.class);
 
        int add(int a, int b);
    }
 
    public static void main(String[] args) {
        int sum = CLibrary.INSTANCE.add(3, 6);
 
        System.out.println(sum);
    }
}

簡單案例

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;

interface HelloInter extends Library {
	int toupper(int ch);

	double pow(double x, double y);

	void printf(String format, Object... args);
}

public class HelloWorld {

	public static void main(String[] args) {
		HelloInter INSTANCE = (HelloInter) Native.loadLibrary(Platform.isWindows() ? "msvcrt" : "c", HelloInter.class);
		INSTANCE.printf("Hello, Worldn");
		String[] strs = new String[] { "芙蓉", "如花", "鳳姐" };
		for (int i = 0; i < strs.length; i++) {
			INSTANCE.printf("人物 %d: %sn", i, strs[i]);
		}
		System.out.println("pow(2d,3d)==" + INSTANCE.pow(2d, 3d));
		System.out.println("toupper('a')==" + (char) INSTANCE.toupper((int) 'a'));
	}

}

顯示結果:

pow(2d,3d)==8.0
toupper('a')==A
Hello, Worldn人物 0: 芙蓉n人物 1: 如花n人物 2: 鳳姐n

說明:

HelloInter介面中定義的3個函式全是C語言函式庫中的函式,其定義格式如下:

int toupper(int ch)
double pow( double x, double y )
int printf(const char* format, ...)

C語言函式庫中有很多個函式,但是我們只用到了這3個函式,所以其他的函式不需要宣告在介面中。

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 的介面中新增下列程式碼:

public static class UserStruct extends Structure{
    public NativeLong id;
    public WString name;
    public int age;
    public static class ByReference extends UserStruct implements Structure.ByReference {}
    public static class ByValue extends UserStruct implements Structure.ByValue {}
    @Override
    protected List getFieldOrder() {
	return Arrays.asList(new String[] { "id", "name", "age"});
    }

}
public void sayUser(UserStruct.ByReference struct);

Java中的程式碼

   UserStruct userStruct=new UserStruct ();
   userStruct.id=new NativeLong(100);
   userStruct.age=30;
   userStruct.name=new WString("奧巴馬");
   TestDll1.INSTANCE.sayUser(userStruct);

Structure說明

現在,我們就在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.ByReferenceStructure.ByValue。這兩個介面僅僅是標記,如果一個類實現Structure.ByReference 介面,就表示這個類代表結構體指標。

如果一個類實現Structure.ByValue 介面,就表示這個類代表結構體本身。使用這兩個介面的實現類,可以明確定義我們的Structure 例項表示的是結構體的指標還是結構體本身。上面的例子中,由於Structure 例項作為函式的引數使用,因此是結構體指標。所以這裡直接使用了UserStruct userStruct=new UserStruct ();也可以使用UserStruct userStruct=new UserStruct.ByReference ();明確指出userStruct 物件是結構體指標而不是結構體本身。

JNA模擬複雜結構體C 語言最主要的資料型別就是結構體。結構體可以內部可以巢狀結構體,這使它可以擬任何型別的物件。JNA 也可以模擬這類複雜的結構體,結構體內部可以包含結構體物件的指標的陣列

struct CompanyStruct{
long id;
wchar_t* name;
UserStruct users[100];
int count;
};

JNA 中可以這樣模擬:

public static class CompanyStruct extends Structure{
    public NativeLong id;
    public WString name;
    public UserStruct.ByValue[] users=new UserStruct.ByValue[100];
    public int count;
    @Override
    protected List getFieldOrder() {
	return Arrays.asList(new String[] { "id", "name",,"users" "count"});
    }
}

這裡,必須給users 欄位賦值,否則不會分配100 個UserStruct 結構體的記憶體,這樣JNA中的記憶體大小和原生程式碼中結構體的記憶體大小不一致,呼叫就會失敗。

測試程式碼:

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(int i=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()方法會把結構體的所有欄位固定住,使原生函式可以訪問。

總結



    使用JNA的過程中也不一定會一帆風順,比如會丟擲”非法記憶體訪問”,這時候檢查一下變數是否==null。還有記憶體對齊的問題,當從記憶體中獲取圖片資訊進行儲存的時候,如果記憶體對齊處理不好,就會丟擲很嚴重的異常,導致JVM異常退出,JNA提供了四種記憶體對齊的方式,分別是:ALIGN_DEFAULT、ALIGN_NONE、ALIGN_GNUC和ALIGN_MSVC。ALIGN_DEFAULT採用平臺預設的對齊方式(推薦);ALIGN_NONE是不採用對齊方式;ALIGN_GNUC為針對linux/gcc作業系統的對齊方式。ALIGN_MSVC為針對win32/msvc架構的記憶體對齊方式。

    JNA也提供了一種保護機制.比如防止JNA出現異常不會導致JVM異常退出,預設是開啟這個功能的,開啟方式為System.setProperty(“jna.protected”,”true”); 記得要在JNA載入dll檔案之前呼叫,然後try {...} catch(Throwable e)異常,不過你也不要期望過高,不要以為加上這個就萬事大吉,出現”非法記憶體訪問”的時候還是會束手無策。JNA也提供了一種保護機制.比如防止JNA 出現異常不會導致JVM異常退出,預設是開啟這個功能的,開啟方式為 System.setProperty(“jna.protected”,”true”); 記得要在JNA載入dll檔案之前呼叫,然後try {...} catch(Throwable e)異常,不過你也不要期望過高,不要以為加上這個就萬事大吉,出現”非法記憶體訪問”的時候還是會束手無策。