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++结构体、联合体和指针。