杰瑞科技汇

Java如何用JNA调用DLL?

  1. JNA 简介:它是什么,为什么用它。
  2. 核心概念Library 接口、Native 类、Structure 类。
  3. 完整实践步骤:从零开始,创建一个 DLL 并用 JNA 调用它。
  4. 处理复杂数据类型:结构体、指针、回调函数等。
  5. 常用技巧和注意事项

JNA 简介

什么是 JNA? JNA (Java Native Access) 是一个开源的 Java 框架,由 Sun Microsystems (现 Oracle) 开发,它允许 Java 代码直接调用本地共享库(如 Windows 的 .dll、Linux 的 .so、macOS 的 .dylib)中的函数,而无需编写任何本地代码(如 C/C++)。

Java如何用JNA调用DLL?-图1
(图片来源网络,侵删)

为什么使用 JNA?

  • 简单易用:相比 JNI,JNA 极大地简化了过程,你只需要定义一个 Java 接口,JNA 会自动处理底层的 JNI 调用、参数类型转换和内存管理。
  • 高性能:虽然比不上直接调用本地代码,但其性能对于大多数应用场景已经足够。
  • 跨平台:JNA 本身是跨平台的,你只需要为不同平台提供对应的本地库即可。

JNA 的工作原理 JNA 的核心思想是动态代理,你定义一个 Java 接口,继承自 Library,JNA 在运行时会为这个接口创建一个动态代理实例,当你在 Java 代码中调用接口方法时,这个代理实例会负责将 Java 数据类型转换为 C 数据类型,然后通过 JNI 机制调用本地库中的对应函数,最后将返回值从 C 类型转换回 Java 类型。


核心概念

a. Library 接口

这是 JNA 的核心,你需要创建一个 Java 接口,继承自 com.sun.jna.Library,在这个接口中,声明你想要调用的 DLL 中的函数(作为 Java 方法)。

关键点

Java如何用JNA调用DLL?-图2
(图片来源网络,侵删)
  • 函数名必须与 DLL 中的导出函数名完全一致(不区分大小写,但最好保持一致)。
  • JNA 会自动处理基本数据类型的映射(如 int, double, String)。

b. Native

这是 JNA 的入口点,它负责加载本地库并创建 Library 接口的实例。

常用方法

  • Native.load(String libraryName, Class<T> libraryInterface): 加载库并返回接口实例。
    • libraryName: 库的名字,不带 .dll.so 后缀。
    • libraryInterface: 你定义的 Library 子接口。
  • Native.register(String libraryName, Class<T> libraryInterface): 与 load 类似,但会尝试在类路径中查找库文件。

c. Structure

当需要传递或接收 C 语言中的结构体时,就需要使用 Structure,你需要创建一个继承自 com.sun.jna.Structure 的 Java 类,并用 @FieldOrder 注解来声明结构体中字段的顺序和类型。


完整实践步骤:从零开始

我们将创建一个简单的 C++ DLL,它提供一个函数,用于计算两个整数的和,然后用 Java 和 JNA 来调用它。

Java如何用JNA调用DLL?-图3
(图片来源网络,侵删)

步骤 1:创建 C/C++ DLL

  1. 环境准备:你需要一个 C++ 编译器,MinGW (GCC) 或 Visual Studio。

  2. 编写 C++ 代码 (MyMathUtils.cpp)

    // MyMathUtils.cpp
    #include <windows.h>
    // 使用 extern "C" 来避免 C++ 的名称修饰(Name Mangling)
    // 这样导出的函数名就是原样的 "add_numbers"
    extern "C" __declspec(dllexport) int add_numbers(int a, int b) {
        return a + b;
    }
    • __declspec(dllexport):告诉编译器这个函数需要被导出,以便其他程序(如我们的 Java 程序)可以调用。
    • extern "C":非常重要!它告诉编译器使用 C 语言的链接规则,防止 C++ 编译器对函数名进行“修饰”(add_numbers 可能会变成 _add_numbers@8),导致 Java 找不到函数。
  3. 编译 DLL 使用 MinGW 的 g++ 命令进行编译:

    g++ -shared -o MyMathUtils.dll MyMathUtils.cpp -Wl,--kill-at
    • -shared: 生成共享库(DLL)。
    • -o MyMathUtils.dll: 指定输出的 DLL 文件名。
    • -Wl,--kill-at: 用于移除导出函数名后的 后缀,确保导出名是 add_numbers

    编译成功后,你会得到 MyMathUtils.dll 文件,将它放到你的 Java 项目的根目录下(或者 java.library.path 指定的路径中)。

步骤 2:编写 Java 代码

  1. 添加 JNA 依赖 如果你使用 Maven,在 pom.xml 中添加:

    <dependency>
        <groupId>net.java.dev.jna</groupId>
        <artifactId>jna-platform</artifactId>
        <version>5.12.1</version> <!-- 使用最新版本 -->
    </dependency>

    如果使用 Gradle:

    implementation 'net.java.dev.jna:jna-platform:5.12.1' // 使用最新版本
  2. 定义 Library 接口 (MyMathUtilsLibrary.java)

    import com.sun.jna.Library;
    public interface MyMathUtilsLibrary extends Library {
        // 声明一个方法,对应 DLL 中的 add_numbers 函数
        int add_numbers(int a, int b);
    }
  3. 编写主程序调用 (JnaDemo.java)

    import com.sun.jna.Native;
    public class JnaDemo {
        public static void main(String[] args) {
            // 1. 加载 DLL 文件
            // JNA 会在 java.library.path 中查找 MyMathUtils.dll
            // 你也可以指定完整路径,"C:/path/to/MyMathUtils.dll"
            MyMathUtilsLibrary mathUtils = Native.load("MyMathUtils", MyMathUtilsLibrary.class);
            // 2. 调用 DLL 中的函数
            int num1 = 10;
            int num2 = 20;
            int sum = mathUtils.add_numbers(num1, num2);
            // 3. 打印结果
            System.out.println("The sum of " + num1 + " and " + num2 + " is: " + sum);
        }
    }

步骤 3:运行 Java 程序

确保 MyMathUtils.dll 和你的 Java 类文件在正确的位置,运行 JnaDemomain 方法。

预期输出

The sum of 10 and 20 is: 30

恭喜!你已经成功使用 JNA 调用了 DLL 函数。


处理复杂数据类型

现实世界中,DLL 函数的参数往往比 intString 复杂。

a. 结构体

假设我们的 DLL 中有一个函数,接收一个包含 xy 坐标的结构体,并返回其距离。

C++ 代码 (PointUtils.cpp)

#include <windows.h>
#include <math.h>
typedef struct {
    double x;
    double y;
} Point;
extern "C" __declspec(dllexport) double calculate_distance(Point p1, Point p2) {
    double dx = p1.x - p2.x;
    double dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
}

编译成 PointUtils.dll

Java 代码 你需要创建一个 Structure 子类来映射 Point 结构体。

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
// 1. 定义结构体
@FieldOrder({"x", "y"})
public static class Point extends Structure {
    public double x;
    public double y;
    public Point() {}
    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }
}
// 2. 定义 Library 接口
public interface PointUtilsLibrary extends Library {
    double calculate_distance(Point p1, Point p2);
}
// 3. 主程序
public class JnaStructureDemo {
    public static void main(String[] args) {
        PointUtilsLibrary pointUtils = Native.load("PointUtils", PointUtilsLibrary.class);
        Point p1 = new Point(3.0, 4.0);
        Point p2 = new Point(7.0, 1.0);
        double distance = pointUtils.calculate_distance(p1, p2);
        System.out.println("The distance between the points is: " + distance); // 预期输出: 5.0
    }
}

b. 指针 和 Pointer 类型

当 DLL 函数需要接收一个指向缓冲区的指针时(用于写入结果),JNA 提供了 Pointer 类型。

示例:一个函数,接收一个整数指针,并将结果写入其中。 C++ 代码

extern "C" __declspec(dllexport) void get_square(int number, int* result_ptr) {
    *result_ptr = number * number;
}

Java 调用

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
public interface SquareLibrary extends Library {
    void get_square(int number, Pointer result_ptr);
}
public class JnaPointerDemo {
    public static void main(String[] args) {
        SquareLibrary squareLib = Native.load("SquareLibrary", SquareLibrary.class);
        // 创建一个指向足够大内存的 Pointer
        Pointer resultPointer = new Pointer(Native.malloc(4)); // 分配 4 字节(int 大小)
        squareLib.get_square(10, resultPointer);
        // 从 Pointer 中读取 int 值
        int squareValue = resultPointer.getInt(0);
        System.out.println("The square of 10 is: " + squareValue); // 预期输出: 100
        // 释放内存
        Native.free(resultPointer.getPointer());
    }
}

c. 回调函数

回调函数(DLL 调用 Java 方法)稍微复杂一些,JNA 通过 Callback 接口实现。

示例:一个 DLL 函数,遍历数组,并对每个元素调用一个回调函数。 C++ 代码

#include <windows.h>
// 定义回调函数指针类型
typedef void (*ArrayCallback)(int element);
extern "C" __declspec(dllexport) void process_array(int* array, int size, ArrayCallback callback) {
    for (int i = 0; i < size; ++i) {
        callback(array[i]);
    }
}

Java 调用

import com.sun.jna.Callback;
import com.sun.jna.Library;
import com.sun.jna.Native;
// 1. 定义回调接口
public interface ArrayCallback extends Callback {
    void apply(int element);
}
// 2. 定义 Library 接口
public interface ArrayProcessorLibrary extends Library {
    void process_array(int[] array, int size, ArrayCallback callback);
}
// 3. 主程序
public class JnaCallbackDemo {
    public static void main(String[] args) {
        ArrayProcessorLibrary processor = Native.load("ArrayProcessor", ArrayProcessorLibrary.class);
        int[] numbers = {1, 2, 3, 4, 5};
        // 创建回调实例
        ArrayCallback callback = new ArrayCallback() {
            @Override
            public void apply(int element) {
                System.out.println("Processing element: " + element * 2); // 对每个元素进行操作
            }
        };
        System.out.println("Starting array processing...");
        processor.process_array(numbers, numbers.length, callback);
        System.out.println("Array processing finished.");
    }
}

预期输出:

Starting array processing...
Processing element: 2
Processing element: 4
Processing element: 6
Processing element: 8
Processing element: 10
Array processing finished.

常用技巧和注意事项

  1. java.library.path

    • JNA 默认会从 java.library.path 系统属性中查找 DLL,这个路径通常包括:
      • JVM 启动目录 (-Djava.library.path=.)。
      • System32 目录。
      • JDK/JRE 的 bin 目录。
    • 如果你的 DLL 不在标准路径中,可以通过 -Djava.library.path 参数指定:
      java -Djava.library.path="C:/my/dll/path" -jar myapp.jar
  2. 数据类型映射表: 了解 JNA 如何映射 C/Java 类型非常重要。 | C 类型 | JNA 映射类型 | 备注 | | :--- | :--- | :--- | | char | byte | | | short | short | | | int | int | | | long | com.sun.jna.NativeLong | 在 32/64 位系统上大小可能不同,NativeLong 会自动适配。 | | long long | long | | | float | float | | | double | double | | | char* | String | JNA 会自动处理内存分配和释放。 | | void* | Pointer | 通用指针类型。 | | HANDLE | PointerWinNT.HANDLE | Windows 句柄通常用 Pointer 表示。 |

  3. 内存管理

    • 对于 JNA 自动分配的内存(如 String 参数),你不需要手动释放。
    • 对于通过 Native.malloc()Pointer 接收到的指针,你必须手动调用 Native.free() 来释放内存,否则会导致内存泄漏。
  4. 错误处理

    • 如果找不到 DLL 或函数,JNA 会抛出 UnsatisfiedLinkError,建议使用 try-catch 包裹 Native.load 调用,以便友好地处理错误。
    • DLL 内部的错误(如除以零)可能会导致 JVM 崩溃,编写健壮的本地代码至关重要。
  5. 查看 JNA 日志: JNA 提供了详细的日志功能,对于调试非常有用,你可以通过设置系统属性来开启:

    java -Djna.debug_load=true -Djna.debug_caching=true ...

    这会打印出库加载、函数查找等详细信息。

  6. 使用 JNA Platformjna-platform 依赖包不仅包含了 jna,还包含了许多针对特定平台(如 Windows)的接口定义(如 Kernel32, User32 等),当你需要调用 Windows API 时,直接继承这些接口即可,非常方便。

分享:
扫描分享到社交APP
上一篇
下一篇