1.概述
在本教程中,我们将看到如何使用Java Native Access库(简称JNA)来访问本机库,而无需编写任何JNI(Java本机接口)代码。
2.为什么选择JNA?
多年来,Java和其他基于JVM的语言在很大程度上实现了“一次编写,随处运行”的座右铭。但是,有时我们需要使用本机代码来实现某些功能:
- 重用用C / C ++或任何其他能够创建本机代码的语言编写的遗留代码
- 访问标准Java运行时中不可用的特定于系统的功能
- 为给定应用程序的特定部分优化速度和/或内存使用率。
最初,这种要求意味着我们不得不采用JNI – Java Native Interface。这种方法虽然有效,但也有其缺点,由于一些问题,通常可以避免使用:
- 要求开发人员编写C / C ++“胶水代码”以桥接Java和本机代码
- 需要每个目标系统都可用的完整编译和链接工具链
- 与JVM来回编组和解组值是一项繁琐且容易出错的任务
- 混合Java库和本机库时的法律和支持问题
JNA解决了与使用JNI相关的大多数复杂性。特别是,无需创建任何JNI代码即可使用位于动态库中的本机代码,这使整个过程变得更加容易。
当然,需要权衡以下几点:
- 我们不能直接使用静态库
- 与手工制作的JNI代码相比更慢
但是,对于大多数应用程序而言,JNA的简单性优势远远超过了这些劣势。因此,可以说,除非我们有非常具体的要求,否则今天的JNA可能是从Java(或任何其他基于JVM的语言)访问本机代码的最佳选择。
3.JNA项目设置
使用JNA要做的第一件事是将其依赖项添加到我们项目的pom.xml
:
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.6.0</version>
</dependency>
可以从Maven Central下载最新版本的jna-platform
4.使用JNA
使用JNA分为两个步骤:
- 首先,我们创建一个Java接口,该接口扩展了JNA的
Library
接口,以描述调用目标本机代码时使用的方法和类型。 - 接下来,我们将此接口传递给JNA,JNA返回此接口的具体实现,用于调用本机方法
4.1 C标准库中的调用方法
对于我们的第一个示例,让我们使用JNA cosh
函数,该函数在大多数系统中都可用。此方法采用double
精度参数并计算其双曲余弦值。 AC程序只需包含<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()
方法的调用。它有两个参数:动态库名称和描述我们将使用的方法的Java接口。它返回此接口的具体实现,允许我们调用其任何方法。
现在,动态库名称通常取决于系统,C标准库也不例外:在大多数基于Linux的系统中, libc.so
在Windows中为msvcrt.dll
这就是为什么我们使用Platform
helper类来检查我们在哪个平台上运行并选择正确的库名称的原因。
请注意,我们不必添加.so
或.dll
扩展名(暗含)。同样,对于基于Linux的系统,我们不需要指定共享库标准的“ lib”前缀。
因为从Java角度来看,动态库的行为类似于Singletons,所以一种常见的做法是声明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
一种看起来很奇怪的映射是用于本机long
类型的映射。这是因为在C / C ++中, long
类型可能表示32位或64位值,具体取决于我们是在32位还是64位系统上运行。
为了解决此问题,JNA提供了NativeLong
类型,该类型根据系统的体系结构使用适当的类型。
4.3 struct和union
另一个常见的情况是处理本机代码API,这些API需要指向某种struct
或union
类型.
创建Java接口以访问它时,相应的参数或返回值必须是分别Structure or Union
例如,给定此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需要@FieldOrder
批注,因此它可以在将数据用作目标方法的参数之前正确地将数据序列化到内存缓冲区中。
另外,我们可以重写getFieldOrder()
方法以达到相同的效果。当针对单个体系结构/平台时,前一种方法通常就足够了。我们可以使用后者来处理跨平台的对齐问题,有时需要添加一些额外的填充字段。
Unions
工作方式类似,除了以下几点:
- 无需使用
@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);
如果foo
和bar
的类型相同,则必须改用字段名:
u.foo = "test";
u.setType("foo");
lib.some_method(u);
4.4 使用指针
JNA提供了一个Pointer
抽象,可以帮助处理用无类型指针声明的API,通常是void *
。此类提供的方法允许对底层本机内存缓冲区进行读写访问,这有明显的风险。
在开始使用此类之前,我们必须确保我们清楚地了解每次是谁“拥有”了所引用的内存。否则,可能会产生难以调试的错误,这些错误与内存泄漏和/或无效访问有关。
假设我们知道自己在做什么(一如既往),让我们看看如何将著名的malloc()
和free()
函数与JNA一起使用,用于分配和释放内存缓冲区。首先,让我们再次创建包装器接口:
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()
方法只是用一个恒定的字节值(在这种情况下为零)填充基础缓冲区。注意, 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的预处理程序,开发人员仍然可以编写这样的代码,并且可以正常工作。事实证明,如今, errno
是一个扩展为函数调用的宏:
// ... excerpt from bits/errno.h on Linux
#define errno (*__errno_location ())
// ... excerpt from <errno.h> from Visual Studio
#define errno (*_errno())
现在,这种方法在编译源代码时很好用,但是在使用JNA时就没有这种东西了。我们可以在包装器接口中声明扩展功能并显式调用它,但是JNA提供了更好的替代方法: LastErrorException
。
在包装器接口中声明的所有带有throws LastErrorException
都将在本机调用之后自动包括错误检查。如果报告错误,则JNA将抛出LastErrorException
,其中包括原始错误代码。
让我们在以前用来显示此功能StdC
包装器接口中添加一些方法:
public interface StdC extends Library {
// ... other methods omitted
int open(String path, int flags) throws LastErrorException;
int close(int fd) throws LastErrorException;
}
现在,我们可以在try / catch子句中open()
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代码处理访问冲突错误的方法。有两种激活它的方法:
- 将
jna.protected
系统属性true
- 调用
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轻松访问本机代码。
0 评论