簡介
前面我們講到了JNA中JAVA程式碼和native程式碼的對映,雖然可以通過TypeMapper來將JAVA中的型別和native中的型別進行對映,但是native中的資料型別都是基礎型別,如果native中的資料型別是複雜的struct型別該如何進行對映呢?
不用怕,JNA提供了Structure類,來幫助我們進行這些對映處理。
native中的struct
什麼時候會用到struct呢?一般情況下,當我們需要自定義一個資料類的時候,一般情況下,在JAVA中需要定義一個class(在JDK17中,可以使用更加簡單的record來進行替換),但是為一個資料結構定義class顯然有些臃腫,所以在native語言中,有一些更簡單的資料結構叫做struct。
我們先看一個struct的定義:
typedef struct _Point {
int x, y;
} Point;
上面的程式碼中,我們定義了一個Pointer的struct資料類下,在其中定義了int的x和y值表示Point的橫縱座標。
struct的使用有兩種情況,一種是值傳遞,一種是引用傳遞。先來看下這兩種情況在native方法中是怎麼使用的:
引用傳遞:
Point* translate(Point* pt, int dx, int dy);
值傳遞:
Point translate(Point pt, int dx, int dy);
Structure
那麼對於native方法中的struct資料型別的使用方式,應該如何進行對映呢? JNA為我們提供了Structure類。
預設情況下如果Structure是作為引數或者返回值,那麼對映的是struct*,如果表示的是Structure中的一個欄位,那麼對映的是struct。
當然你也可以強制使用Structure.ByReference 或者 Structure.ByValue 來表示是傳遞引用還是傳值。
我們看下上面的native的例子中,如果使用JNA的Structure來進行對映應該怎麼實現:
指標對映:
class Point extends Structure { public int x, y; }
Point translate(Point pt, int x, int y);
...
Point pt = new Point();
Point result = translate(pt, 100, 100);
傳值對映:
class Point extends Structure {
public static class ByValue extends Point implements Structure.ByValue { }
public int x, y;
}
Point.ByValue translate(Point.ByValue pt, int x, int y);
...
Point.ByValue pt = new Point.ByValue();
Point result = translate(pt, 100, 100);
Structure內部提供了兩個interface,分別是ByValue和ByReference:
public abstract class Structure {
public interface ByValue { }
public interface ByReference { }
要使用的話,需要繼承對應的interface。
特殊型別的Structure
除了上面我們提到的傳值或者傳引用的struct,還有其他更加複雜的struct用法。
結構體陣列作為引數
首先來看一下結構體陣列作為引數的情況:
void get_devices(struct Device[], int size);
對應結構體陣列,可以直接使用JNA中對應的Structure陣列來進行對映:
int size = ...
Device[] devices = new Device[size];
lib.get_devices(devices, devices.length);
結構體陣列作為返回值
如果native方法返回的是一個指向結構體的指標,其本質上是一個結構體陣列,我們應該怎麼處理呢?
先看一下native方法的定義:
struct Display* get_displays(int* pcount);
void free_displays(struct Display* displays);
get_displays方法返回的是一個指向結構體陣列的指標,pcount是結構體的個數。
對應的JAVA程式碼如下:
Display get_displays(IntByReference pcount);
void free_displays(Display[] displays);
對於第一個方法來說,我們只返回了一個Display,但是可以通過Structure.toArray(int) 方法將其轉換成為結構體陣列。傳入到第二個方法中,具體的呼叫方式如下:
IntByReference pcount = new IntByReference();
Display d = lib.get_displays(pcount);
Display[] displays = (Display[])d.toArray(pcount.getValue());
...
lib.free_displays(displays);
結構體中的結構體
結構體中也可以嵌入結構體,先看下native方法的定義:
typedef struct _Point {
int x, y;
} Point;
typedef struct _Line {
Point start;
Point end;
} Line;
對應的JAVA程式碼如下:
class Point extends Structure {
public int x, y;
}
class Line extends Structure {
public Point start;
public Point end;
}
如果是下面的結構體中的指向結構體的指標:
typedef struct _Line2 {
Point* p1;
Point* p2;
} Line2;
那麼對應的程式碼如下:
class Point extends Structure {
public static class ByReference extends Point implements Structure.ByReference { }
public int x, y;
}
class Line2 extends Structure {
public Point.ByReference p1;
public Point.ByReference p2;
}
或者直接使用Pointer作為Structure的屬性值:
class Line2 extends Structure {
public Pointer p1;
public Pointer p2;
}
Line2 line2;
Point p1, p2;
...
line2.p1 = p1.getPointer();
line2.p2 = p2.getPointer();
結構體中的陣列
如果結構體中帶有固定大小的陣列:
typedef struct _Buffer {
char buf1[32];
char buf2[1024];
} Buffer;
那麼我們在JAVA中需要指定資料的大小:
class Buffer extends Structure {
public byte[] buf1 = new byte[32];
public byte[] buf2 = new byte[1024];
}
如果結構體中是動態大小的陣列:
typedef struct _Header {
int flags;
int buf_length;
char buffer[1];
} Header;
那麼我們需要在JAVA的結構體中定義一個建構函式,傳入bufferSize的大小,並分配對應的記憶體空間:
class Header extends Structure {
public int flags;
public int buf_length;
public byte[] buffer;
public Header(int bufferSize) {
buffer = new byte[bufferSize];
buf_length = buffer.length;
allocateMemory();
}
}
結構體中的可變欄位
預設情況下結構體中的內容和native memory的內容是一致的。JNA會在函式呼叫之前將Structure的內容寫入到native memory中,並且在函式呼叫之後,將 native memory中的內容回寫到Structure中。
預設情況下是將結構體中的所有欄位都進行寫入和寫出。但是在某些情況下,我們希望某些欄位不進行自動更新。這個時候就可以使用volatile關鍵字,如下所示:
class Data extends com.sun.jna.Structure {
public volatile int refCount;
public int value;
}
...
Data data = new Data();
當然,你也可以強制使用Structure.writeField(String)來將欄位資訊寫入記憶體中,或者使用Structure.read() 來更新整個結構體的資訊或者使用data.readField("refCount")來更新具體欄位資訊。
結構體中的只讀欄位
如果不想從JAVA程式碼中對Structure的內容進行修改,則可以將對應的欄位標記為final。在這種情況下,雖然JAVA程式碼不能直接對其進行修改,但是仍然可以呼叫read方法從native memory中讀取對應的內容並覆蓋Structure中對應的值。
來看下JAVA中如何使用final欄位:
class ReadOnly extends com.sun.jna.Structure {
public final int refCount;
{
// 初始化
refCount = -1;
// 從記憶體中讀取資料
read();
}
}
注意所有的欄位的初始化都應該在建構函式或者靜態方法塊中進行。
總結
結構體是native方法中經常會使用到的一種資料型別,JNA中對其進行對映的方法是我們要掌握的。
本文已收錄於 http://www.flydean.com/08-jna-structure/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!