优秀的编程知识分享平台

网站首页 > 技术文章 正文

Java修炼终极指南:138 介绍Java本地访问(JNA)

nanyue 2024-10-27 11:22:04 技术文章 4 ℃

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

Tags:

最近发表
标签列表