Java本地访问(JNA)详解:成为Java修炼高手

发表时间: 2024-05-17 02:51

Java本地访问(JNA)是一个勇敢的开源尝试,通过一个更直观和易于使用的API来解决JNI的复杂性。作为一个第三方库,JNA必须作为依赖项添加到我们的项目中:

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

接下来,让我们尝试调用问题137中相同的sumTwoInt()方法。这个函数定义在一个名为math.dll的C本地共享库中,并存储在我们的项目中的jna/cpp文件夹中。我们首先编写一个扩展JNA的Library接口的Java接口。这个接口包含我们计划从Java调用并在本地代码中定义的方法和类型的声明。我们编写包含sumTwoInt()声明的SimpleMath接口如下:

public interface SimpleMath extends Library {  long sumTwoInt(int x, int y);}

接下来,我们必须指导JNA加载math.dll库并生成这个接口的具体实现,这样我们就可以调用它的的方法。为此,我们需要jna.library.path系统属性和JNA的Native类如下:

package modern.challenge;public class Main {  public static void main(String[] args) {    System.setProperty("jna.library.path", "./jna/cpp");    SimpleMath math = Native.load(Platform.isWindows()      ? "math" : "NOT_WINDOWS", SimpleMath.class);    long result = math.sumTwoInt(3, 9);    System.out.println("Result: " + result);  }}

在这里,我们指导JNA通过System.setProperty()从jna/cpp加载math.dll,但您也可以从终端通过-Djna.library.path=jna/cpp来完成。接下来,我们调用Native.load(),它接受两个参数。首先,它接受原生库的名称,在我们的情况下是math(不带.dll扩展名)。其次,它接受包含方法声明的Java接口,在我们的情况下是SimpleMath.class。load()方法返回一个SimpleMath的具体实现,我们用它来调用sumTwoInt()方法。

JNA Platform助手允许我们提供特定于当前操作系统的原生库的名称。我们只有Windows的math.dll。

实现.cpp和.h文件 这次,.cpp和.h文件没有命名约定,所以让我们将它们命名为Arithmetic.cpp和Arithmetic.h(头文件是可选的)。Artihmetic.cpp的源代码基本上是纯C代码:

#include <iostream>#include "Arithmetic.h"long sumTwoInt(int x, int y) {  std::cout << "C++: The received arguments are : " << x <<     " and " << y << std::endl;  return (long)x + (long)y;}

正如您所看到的,使用JNA,我们不需要用JNI特定的桥接代码来修补我们的代码。它只是纯C代码。Arithmetic.h是可选的,我们可以这样写:

#ifndef FUNCTIONS_H_INCLUDED#define FUNCTIONS_H_INCLUDED  long sumTwoInt(int x, int y); #endif

接下来,我们可以编译我们的代码。

编译C源代码 通过G++编译器和下图所示的命令完成C源代码的编译:


图7.5 - 编译C++代码

或者,作为纯文本:

C:\SBPBP\GitHub\Java-Coding-Problems-Second-Edition\Chapter07\P138_EngagingJNA>g++ -c                         "-I%JAVA_HOME%\include" "-I%JAVA_HOME%\include\win32" src/main/java/modern/challenge/cpp/Arithmetic.cpp –o jna/cpp/Arithmetic.o                        

接下来,我们可以生成适当的本地库。

生成本地共享库 是时候创建本地共享库math.dll了。为此,我们再次使用G++,如下图所示:


图7.6 - 生成math.dll

或者,作为纯文本:

C:\SBPBP\GitHub\Java-Coding-Problems-Second-Edition\Chapter07\P138_EngagingJNA>g++ -shared –o jna/cpp/math.dll jna/cpp/Arithmetic.o –static –m64             –Wl,--add-stdcall-alias

在这一点上,您应该在jna/cpp文件夹中有math.dll。

最后,运行代码 最后,我们可以运行代码。如果一切顺利,那么您就完成了。否则,如果您得到一个异常,如
java.lang.UnsatisfiedLinkError: Error looking up function 'sumTwoInt': The specified procedure could not be found,那么我们必须修复它。但是,发生了什么?最有可能的是,G++编译器应用了一种称为名称混淆(或名称装饰)的技术 -
https://en.wikipedia.org/wiki/Name_mangling。换句话说,G++编译器将sumTwoInt()方法重命名为了JNA不知道的其他名称。

解决这个问题可以分两步进行。首先,我们需要使用DLL依赖项查看器(例如这个)检查math.dll,
https://github.com/lucasg/Dependencies。正如下图所示,G++已将sumTwoInt重命名为_Z9sumTwoIntii(当然,在您的计算机上可能是另一个名称):


图7.7 - G++已将sumToInt重命名为_Z9sumTwoIntii

其次,我们必须告诉JNA这个名称(_Z9sumTwoIntii)。基本上,我们需要定义一个包含名称对应映射的Map,并将这个map传递给接受这个map作为最后一个参数的Native.load()的一个变体。代码很直接:

public class Main {  private static final Map MAPPINGS;             static {    MAPPINGS = Map.of(      Library.OPTION_FUNCTION_MAPPER,      new StdCallFunctionMapper() {      Map<String, String> methodNames        = Map.of("sumTwoInt", "_Z9sumTwoIntii");      @Override      public String getFunctionName(             NativeLibrary library, Method method) {        String methodName = method.getName();        return methodNames.get(methodName);      }    });  }     public static void main(String[] args) {           System.setProperty("jna.library.path", "./jna/cpp");    SimpleMath math = Native.load(Platform.isWindows()      ? "math" : "NOT_WINDOWS", SimpleMath.class, MAPPINGS);           long result = math.sumTwoInt(3, 9);    System.out.println("Result: " + result);  }}

完成!现在,您应该得到3+9的结果。请随时进一步探索JNA,并尝试使用C/C++结构体、联合体和指针。