写点什么

java 高级用法之:JNA 中的 Structure

作者:程序那些事
  • 2022 年 4 月 18 日
  • 本文字数:2997 字

    阅读完需:约 10 分钟

java高级用法之:JNA中的Structure

简介

前面我们讲到了 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/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 23 小时前阅读数: 20
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
java高级用法之:JNA中的Structure_Java_程序那些事_InfoQ写作平台