Using JNA to Access Native Dynamic Libraries – 使用JNA来访问本地动态库

最后修改: 2020年 10月 4日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll see how to use the Java Native Access library (JNA for short) to access native libraries without writing any JNI (Java Native Interface) code.

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

2. Why JNA?

2.为什么是JNA?

For many years, Java and other JVM-based languages have, to a large extent, fulfilled its “write once, run everywhere” motto. However, sometimes we need to use native code to implement some functionality:

多年来,Java和其他基于JVM的语言在很大程度上实现了其 “一次编写,到处运行 “的宗旨。然而,有时我们需要使用本地代码来实现一些功能

  • Reusing legacy code written in C/C++ or any other language able to create native code
  • Accessing system-specific functionality not available in the standard Java runtime
  • Optimizing speed and/or memory usage for specific sections of a given application.

Initially, this kind of requirement meant we’d have to resort do JNI – Java Native Interface. While effective, this approach has its drawbacks and was generally avoided due to a few issues:

最初,这种要求意味着我们不得不求助于JNI – Java Native Interface。这种方法虽然有效,但也有其缺点,由于以下几个问题,我们一般都避免使用这种方法:

  • Requires developers to write C/C++ “glue code” to bridge Java and native code
  • Requires a full compile and link toolchain available for every target system
  • Marshaling and unmarshalling values to and from the JVM is a tedious and error-prone task
  • Legal and support concerns when mixing Java and native libraries

JNA came to solve most of the complexity associated with using JNI. In particular, there’s no need to create any JNI code to use native code located in dynamic libraries, which makes the whole process much easier.

JNA的出现解决了大部分与使用JNI有关的复杂性。特别是,不需要创建任何JNI代码来使用位于动态库中的本地代码,这使得整个过程更加容易。

Of course, there are some trade-offs:

当然,也有一些权衡的结果。

  • We can’t directly use static libraries
  • Slower when compared to handcrafted JNI code

For most applications, though, JNA’s simplicity benefits far outweigh those disadvantages. As such, it is fair to say that, unless we have very specific requirements, JNA today is probably the best available choice to access native code from Java – or any other JVM-based language, by the way.

不过,对于大多数应用来说,JNA的简单性优势远远超过了这些缺点。因此,可以说,除非我们有非常特殊的要求,否则今天的JNA可能是从Java–或任何其他基于JVM的语言–访问本地代码的最佳选择。

3. JNA Project Setup

3.JNA项目设置

The first thing we have to do to use JNA is to add its dependencies to our project’s pom.xml:

为了使用JNA,我们要做的第一件事是把它的依赖性添加到我们项目的pom.xml中。

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

The latest version of jna-platform can be downloaded from Maven Central.

jna-platform的最新版本可以从Maven中心下载。

4. Using JNA

4.使用JNA

Using JNA is a two-step process:

使用JNA是一个两步的过程。

  • First, we create a Java interface that extends JNA’s Library interface to describe the methods and types used when calling the target native code
  • Next, we pass this interface to JNA which returns a concrete implementation of this interface that we use to invoke native methods

4.1. Calling Methods from the C Standard Library

4.1.从C语言标准库中调用方法

For our first example, let’s use JNA to call the cosh function from the standard C library, which is available in most systems. This method takes a double argument and computes its hyperbolic cosine. A-C program can use this function just by including the <math.h> header file:

对于我们的第一个例子,让我们用JNA来调用标准C库中的cosh函数,该函数在大多数系统中都可用。这个方法接收一个double参数并计算其双曲余弦。A-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);
}

Let’s create the Java interface needed to call this method:

让我们来创建调用这个方法所需的Java接口。

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

Next, we use JNA’s Native class to create a concrete implementation of this interface so we can call our API:

接下来,我们使用JNA的Native类来创建这个接口的具体实现,以便我们可以调用我们的API。

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

The really interesting part here is the call to the load() method. It takes two arguments: the dynamic library name and a Java interface describing the methods that we’ll use. It returns a concrete implementation of this interface, allowing us to call any of its methods.

这里真正有趣的部分是对load()方法的调用。它需要两个参数:动态库的名称和一个描述我们将使用的方法的Java接口。它返回该接口的具体实现,允许我们调用其任何方法。

Now, dynamic library names are usually system-dependent, and C standard library is no exception: libc.so in most Linux-based systems, but msvcrt.dll in Windows. This is why we’ve used the Platform helper class, included in JNA, to check which platform we’re running in and select the proper library name.

现在,动态库的名称通常与系统有关,C标准库也不例外。libc.so在大多数基于Linux的系统中,但msvcrt.dll在Windows中。这就是为什么我们使用JNA中包含的Platform辅助类来检查我们在哪个平台上运行并选择合适的库名。

Notice that we don’t have to add the .so or .dll extension, as they’re implied. Also, for Linux-based systems, we don’t need to specify the “lib” prefix that is standard for shared libraries.

注意,我们不必添加.so.dll扩展名,因为它们是隐含的。另外,对于基于Linux的系统,我们不需要指定 “lib “前缀,这是共享库的标准。

Since dynamic libraries behave like Singletons from a Java perspective, a common practice is to declare an INSTANCE field as part of the interface declaration:

由于从Java的角度来看,动态库的行为类似于Singletons,所以通常的做法是声明一个INSTANCE字段作为接口声明的一部分:

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

4.2. Basic Types Mapping

4.2.基本类型映射

In our initial example, the called method only used primitive types as both its argument and return value. JNA handles those cases automatically, usually using their natural Java counterparts when mapping from C types:

在我们最初的例子中,被调用的方法只使用原始类型作为其参数和返回值。JNA自动处理这些情况,当从C类型映射时,通常使用它们的自然Java对应物。

  • char => byte
  • short => short
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • long long => long
  • float => float
  • double => double
  • char * => String

A mapping that might look odd is the one used for the native long type. This is because, in C/C++, the long type may represent a 32- or 64-bit value, depending on whether we’re running on a 32- or 64-bit system.

一个可能看起来很奇怪的映射是用于本地long类型的映射。这是因为,在C/C++中,long类型可能代表一个32位或64位的值,这取决于我们是在32位还是64位系统上运行。

To address this issue, JNA provides the NativeLong type, which uses the proper type depending on the system’s architecture.

为了解决这个问题,JNA提供了NativeLong类型,它根据系统的架构使用适当的类型。

4.3. Structures and Unions

4.3.结构和联盟

Another common scenario is dealing with native code APIs that expect a pointer to some struct or union type. When creating the Java interface to access it, the corresponding argument or return value must be a Java type that extends Structure or Union, respectively.

另一个常见的情况是处理本机代码 API,这些 API 期待一个指向某个结构联合类型的指针。在创建访问它的 Java 接口时,相应的参数或返回值必须是分别扩展了 Structure 或 Union 的 Java 类型。

For instance, given this C struct:

例如,给定这个C结构。

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

Its Java peer class would be:

它的Java同伴类将是。

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

JNA requires the @FieldOrder annotation so it can properly serialize data into a memory buffer before using it as an argument to the target method.

JNA需要@FieldOrder注解,这样它就可以在将数据作为目标方法的参数之前正确地将其序列化到内存缓冲区。

Alternatively, we can override the getFieldOrder() method for the same effect. When targeting a single architecture/platform, the former method is generally good enough. We can use the latter to deal with alignment issues across platforms, that sometimes require adding some extra padding fields.

另外,我们可以覆盖getFieldOrder()方法以达到同样的效果。当针对单一架构/平台时,前一种方法通常已经足够好。我们可以使用后者来处理跨平台的对齐问题,这有时需要添加一些额外的填充字段。

Unions work similarly, except for a few points:

除了几点之外,Unions的工作方式与此类似。

  • No need to use a @FieldOrder annotation or implement getFieldOrder()
  • We have to call setType() before calling the native method

Let’s see how to do it with a simple example:

让我们通过一个简单的例子来看看如何做到这一点。

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

Now, let’s use MyUnion with a hypothetical library:

现在,让我们将MyUnion与一个假想的库一起使用。

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

If both foo and bar where of the same type, we’d have to use the field’s name instead:

如果foobar都是同一类型,我们就必须使用字段的名称来代替。

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

4.4. Using Pointers

4.4.使用指针

JNA offers a Pointer abstraction that helps to deal with APIs declared with untyped pointer – typically a void *. This class offers methods that allow read and write access to the underlying native memory buffer, which has obvious risks.

JNA提供了一个Pointer抽象,它有助于处理用未定型的指针声明的API–通常是一个void *该类提供了允许读写底层本地内存缓冲区的方法,这有明显的风险。

Before start using this class, we must be sure we clearly understand who “owns” the referenced memory at each time. Failing to do so will likely produce hard to debug errors related to memory leaks and/or invalid accesses.

在开始使用这个类之前,我们必须确保我们清楚地了解在每个时间段谁 “拥有 “被引用的内存。如果不能做到这一点,很可能会产生与内存泄漏和/或无效访问有关的难以调试的错误。

Assuming we know what we’re doing (as always), let’s see how we can use the well-known malloc() and free() functions with JNA, used to allocate and release a memory buffer. First, let’s again create our wrapper interface:

假设我们知道自己在做什么(像往常一样),让我们看看如何用JNA来使用著名的malloc() free() 函数,用来分配和释放内存缓冲区。首先,让我们再次创建我们的封装接口。

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

Now, let’s use it to allocate a buffer and play with it:

现在,让我们用它来分配一个缓冲区,并玩玩它。

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

The setMemory() method just fills the underlying buffer with a constant byte value (zero, in this case). Notice that the Pointer instance has no idea to what it is pointing to, much less its size. This means that we can quite easily corrupt our heap using its methods.

setMemory()方法只是用一个恒定的字节值(本例中为零)来填充底层缓冲区。请注意,Pointer实例不知道它指向什么,更不知道它的大小。这意味着我们可以很容易地使用其方法破坏我们的堆。

We’ll see later how we can mitigate such errors using JNA’s crash protection feature.

我们稍后会看到我们如何使用JNA的崩溃保护功能来减轻这种错误。

4.5. Handling Errors

4.5 处理错误

Old versions of the standard C library used the global errno variable to store the reason a particular call failed. For instance, this is how a typical open() call would use this global variable in C:

旧版本的标准C库使用全局errno变量来存储某个特定调用失败的原因。例如,在C语言中,一个典型的open()调用是这样使用这个全局变量的。

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

Of course, in modern multi-threaded programs this code would not work, right? Well, thanks to C’s preprocessor, developers can still write code like this and it will work just fine. It turns out that nowadays, errno is a macro that expands to a function call:

当然,在现代的多线程程序中,这段代码将无法工作,对吗?好吧,多亏了C的预处理器,开发人员仍然可以写出这样的代码,而且会很好地工作。事实证明,如今,errno是一个可以扩展为函数调用的宏:

// ... excerpt from bits/errno.h on Linux
#define errno (*__errno_location ())

// ... excerpt from <errno.h> from Visual Studio
#define errno (*_errno())

Now, this approach works fine when compiling source code, but there’s no such thing when using JNA. We could declare the expanded function in our wrapper interface and call it explicitly, but JNA offers a better alternative: LastErrorException.

现在,这种方法在编译源代码时很好用,但在使用JNA时就没有这种东西了。我们可以在我们的封装接口中声明扩展的函数并显式地调用它,但JNA提供了一个更好的选择。LastErrorException

Any method declared in wrapper interfaces with throws LastErrorException will automatically include a check for an error after a native call. If it reports an error, JNA will throw a LastErrorException, which includes the original error code.

任何在封装接口中声明的带有throws LastErrorException的方法都会在本地调用后自动包含一个错误检查。如果它报告了一个错误,JNA将抛出一个LastErrorException,其中包括原始错误代码。

Let’s add a couple of methods to the StdC wrapper interface we’ve used before to show this feature in action:

让我们为我们之前使用过的StdC封装接口添加几个方法,以展示这个功能的作用。

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

Now, we can use open() in a try/catch clause:

现在,我们可以在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);
    }
}

In the catch block, we can use LastErrorException.getErrorCode() to get the original errno value and use it as part of the error handling logic.

catch块中,我们可以使用LastErrorException.getErrorCode()来获取原始errno值,并将其作为错误处理逻辑的一部分。

4.6. Handling Access Violations

4.6.处理违反访问规定的行为

As mentioned before, JNA does not protect us from misusing a given API, especially when dealing with memory buffers passed back and forth native code. In normal situations, such errors result in an access violation and terminate the JVM.

如前所述,JNA并不能保护我们不滥用给定的API,尤其是在处理来回传递的内存缓冲区的时候本机代码。在正常情况下,这种错误会导致访问违规并终止JVM。

JNA supports, to some extent, a method that allows Java code to handle access violation errors. There are two ways to activate it:

JNA在某种程度上支持一种方法,允许Java代码处理访问违规错误。有两种方法可以激活它。

  • Setting the jna.protected system property to true
  • Calling Native.setProtected(true)

Once we’ve activated this protected mode, JNA will catch access violation errors that would normally result in a crash and throw a java.lang.Error exception. We can verify that this works using a Pointer initialized with an invalid address and trying to write some data to it:

一旦我们激活了这个保护模式,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
}

However, as the documentation states, this feature should only be used for debugging/development purposes.

然而,正如文档所述,这一功能只应用于调试/开发目的。

5. Conclusion

5.总结

In this article, we’ve shown how to use JNA to access native code easily when compared to JNI.

在这篇文章中,我们已经展示了与JNI相比,如何使用JNA来轻松访问本地代码。

As usual, all code is available over on GitHub.

像往常一样,所有的代码都可以在GitHub上找到