通过使用JNA访问本地动态链接库

1. 概览

在本教程中,我们将看到如何使用Java本地访问库(简称JNA)而无需写任何JNI(Java Native Interface)代码。

2. 为什么JNA?

很多年以来,Java和其他的基于JVM的语言在一定程度上已经满足“编写一次,处处运行”的条件。然而,有时,我们必须使用本地代码去实现一些功能。

  • 重用原来C/C++写的代码或者其他语言创建的本地代码

  • 获得在标准Java运行时没有的系统功能

  • 对给定应用的特殊部分进行速度优化或者内存使用

最初,这种类型的需求意味着我们不得不重新诉诸于JNI-Java Native Interface。尽管它非常有效,然而这种方法有其自身的缺点并且最终被大众所抛弃,其原因如下:

  • 需要开发者去写C/C++“胶水代码”以建立起Java和本地代码之间的桥梁

  • 针对目标系统需要一个完整的编译和链接工具链

  • 在JVM与目标之间的转换是无聊的并且易于出错

  • 当混合Java和本地库时会对合法性和支持性产生过多的关注

JNA来源于解决在使用JNI时碰到的复杂性。特别是,当需要在动态库中使用本地库时,其实没必要去创建任何JNI代码,因此会使整个过程变得更加简单。

当然,也因此会带来以下的开销:

  • 我们不能直接使用静态库

  • 相比于手工编写的JNI代码,JNA编写的代码运行速度比较慢

尽管对大多数应用,JNA所带来的简易性优势远远大于其缺点。就如,公平地讲,除非我们有特别的需求,包括对于其他基于JVM的语言,JNA在今日可能是从Java访问本地代码最好的方式。

3. JNA项目安装

第一件我们必须去使用JNA的事情是在我们的pom.xml中添加对它的依赖:

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna-platform</artifactId>
    <version>5.6.0</version>
</dependency>

最新版本的jna-platform能够从Maven中心仓库中下载。

4. 使用JNA

使用JNA需要2步:

  • 首先,我们创建一个Java接口,该接口继承自JNA的Library接口,其目的是用于描述需要调用的本地代码的方法和类型

  • 然后,我们把这个接口传给JNA,而JNA会返回一个关于这个接口的实体类实现,我们使用这个实体类去调用本地方法

4.1. 从C标准库调用方法

作为我们的第一个例子,让我们使用JNA去调用C标准库中的cosh函数,这个函数在大多数系统中都有。这个方法用两个double参数计算双曲余弦函数。一个C程序能够通过包含<math.h>头文件使用这个函数:

#include <math.h>
#include <stdio.h>
int main(int argc, char** argv) {
    double v = cosh(0.0);
    printf("Result: %f\n", v);
}

让我们创建一个java接口,这个接口调用这个函数:

public interface CMath extends Library { 
    double cosh(double value);
}

下一步,我们使用JNA的Native类去创建一个这个接口的实体类,从而调用我们的API:

CMath lib = Native.load(Platform.isWindows()?"msvcrt":"c", CMath.class);
double result = lib.cosh(0);

真正有意思的部分就是调用load()方法。它使用2个参数:动态库名和一个Java接口,这个接口就是我们将要用的描述方法的接口。它返回了一个这个接口的实体实现,允许我们去调用它的任一方法。

现在,动态链接库名称通常是依赖系统的,并且C标准库也无非是:libc.so(在大部分基于Linux的系统中),msvcrt.dll(在Windows系统中)。这就是为什么我们在JNA中使用platform帮助类,该类用于检查我们运行的是哪个平台,然后选择合适的库。

注意,我们不必添加.so或者.dll的扩展名,因为它们是被默认添加的。同样,对于Linux系统,我们不必指定lib前缀,因为它是标准库使用的规定。

因为动态链接库的行为,从Java的角度来看就像单例模式,一个普通的模式就是生命一个INSTANCE字段作为接口声明的一部分:

public interface CMath extends Library {
    CMath INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CMath.class);
    double cosh(double value);
}

4.2. 基本类型映射

在我们前面的例子中,调用方法只是用了原始类型作为参数和返回类型。当映射C类型时,JNA会自动转换这些类型,通常使用它们在Java中的对应类型:

  • char => byte

  • short => short

  • wchar_t => char

  • int => int

  • long => com.sun.jna.NativeLong

  • long long => long

  • float => float

  • double => double

  • char * => String

有一个类型看起来比较奇怪,这就是native long类型。这是因为,在C/C++,long类型可能会以32或者64位的值形式,依赖于我们是运行在一个32位或者64位系统。

为了解决这个问题,JNA提供了NativeLong类型,这个类型使用依赖于系统架构的合适类型。

4.3. 结构体和联合体

一个常见的应用场景是处理本地代码API,这个API会期望一个指针或者一些结构体或者联合体。当创建Java接口去访问它,相对应的参数或者返回值必须是继承于结构体或者联合体的Java类型。

例如,给定一个C结构体:

struct foo_t {
    int field1;
    int field2;
    char *field3;
};

它对应的Java类型如下:

@FieldOrder({"field1","field2","field3"})
public class FooType extends Structure {
    int field1;
    int field2;
    String field3;
};

JNA需要@FieldOlder声明,从而可以在使用它当作目标函数的参数之前合适地在内存的缓存中序列化这些数据

相对地,我们能重写getFieldOrder()方法以起到同样的效果。当以某个架构或者平台作为目标时,前面的方法已经足够好了。我们能够用后者在处理跨平台时处理对齐的问题,因为有时需要添加一些额外的空白。

联合体的工作方式相似,除了一些额外需要注意的点:

  • 不需要使用@FieldOrder声明或者实现getFieldOrder()

  • 我们必须在调用本地方法之前调用setType()

让我们看看如何做下面这样一个简单的例子:

public class MyUnion extends Union {
    public String foo;
    public double bar;
};

现在,让我们使用MyUnion作为一个假设库:

MyUnion u = new MyUnion();
u.foo = "test";
u.setType(String.class);
lib.some_method(u);

如果foobar有同样的类型,我们则必须使用字段名:

u.foo = "test";
u.setType("foo");
lib.some_method(u);

4.4. 使用指针

JNA提供了一个指针抽象,该抽象能够帮助处理声明了无类型指针的API - 典型地,一个 *void。这个类提供了方法集合,这个方法集合能够提供对底层本地内存缓冲区的读写访问,当然,这明显是有风险的。

在使用这个类之前,我们必须确保我们明确理解每次谁在使用这段引用内存。 如果不能理解则很有可能产生难以调试的与内存泄漏或者非法访问的错误。

假设我们知道我们正在做什么(一般来说)。那让我们看看怎么在JNA中用著名的malloc()free()函数,用来分配和释放一个内存空间。首先,让我们再一次创建我们自己的封装接口:

public interface StdC extends Library {
    StdC INSTANCE = // ... instance creation omitted
    Pointer malloc(long n);
    void free(Pointer p);
}

现在,让我们用用它来分配内存并且对其进行操作:

StdC lib = StdC.INSTANCE;
Pointer p = lib.malloc(1024);
p.setMemory(0l, 1024l, (byte) 0);
lib.free(p);

setMemory()方法只是用一个字节值常量填充到内存中(0,在这个例子中)。注意,这个Pointer对象完全不知道究竟指向哪里,比它本身的大小要小。这也意味着我们很容易就用这个方法破坏我们的堆内存。

我们将在后面看到我们怎么用JNA的冲突保护特性消除这样的错误。

4.5. 处理错误

老版本的标准C库会使用全局errno变量区保存当一个特定调用失败后产生的原因。例如,这是一个典型的open调用会在C中使用到这个全局变量。

int fd = open("some path", O_RDONLY);
if (fd < 0) {
    printf("Open failed: errno=%d\n", errno);
    exit(1);
}

当然,在现代的多线程程序中,这个代码可能不会工作,难道不是吗?然而,谢谢C的预处理机制,开发者依然会写这样的代码,并且它也依然会工作。 现在,*error*已经是一个扩展到一个函数调用的宏:

// ... excerpt from bits/errno.h on Linux
#define errno (*__errno_location ())
​
// ... excerpt from <errno.h> from Visual Studio
#define errno (*_errno())

现在,这个方法在编译源码的时候用起来很好,但是当使用JNA的时候却没有这样的东西。我们能声明在我们封装的接口里声明这样的扩展函数并且小心地调用它,但是JNA提供了更好的解决方法: LastErrorException.

任何在封装接口中声明了的方法,只要用了throw LastErrorException都会自动包括一个对本地方法调用的错误检查。如果它报了一个错误,JNA会抛出一个LastErrorException,而这个LastErrorExceptioon包括了原始的错误码。

让我们添加一些函数到Std封装接口,这个接口会在实际中展现这些特性:

public interface StdC extends Library {
    // ... other methods omitted
    int open(String path, int flags) throws LastErrorException;
    int close(int fd) throws LastErrorException;
}

现在,我们能够使用open()在一个try/catch语句中了:

StdC lib = StdC.INSTANCE;
int fd = 0;
try {
    fd = lib.open("/some/path",0);
    // ... use fd
}
catch (LastErrorException err) {
    // ... error handling
}
finally {
    if (fd > 0) {
       lib.close(fd);
    }
}

catch块中,我们能够使用LastErrorException.getErrorCode()获得原始的errno值并且把它用作错误处理逻辑中的一部分。

4.6. 处理访问错误

正如前面所述,JNA不会保护我们错误地使用一个给定的API, 特别是处理从本地代码和内存中交换数据. 在正常的情况下,这样的错误会导致一个访问错误并停止JVM。

JNA在一定程度上支持允许Java代码区处理访问非法的错误。下面是处理这种情况的2种方法:

  • 设置jna.protected system propertytrue

  • 调用Native.setProtected(true)

一旦我们激活了这种保护模式,JNA将会捕获这种访问非法的错误,这种错误正常情况下会导致一个崩溃并且抛出一个java.lang.Error的异常。我们能够通过使用一个被非法地址初始化的Pointer来验证并且尝试去给它写一些数据:

Native.setProtected(true);
Pointer p = new Pointer(0l);
try {
    p.setMemory(0, 100*1024, (byte) 0);
}
catch (Error err) {
    // ... error handling omitted
}

然而,正如本文所陈述的那样,这种特性应该只被用于调试和开发目的。

5. 结论

在本文中,我们展示了,相比于JNI,怎么使用JNA去方便地访问本地代码。

跟平常一样, 所有的代码能够在 GitHub获得。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值