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