杰瑞科技汇

Java JNI如何调用DLL?

  1. 编写 Java 代码:声明 native 方法。
  2. 生成 C/C++ 头文件:使用 javacjavah(或现代工具)生成 JNI 函数声明。
  3. 编写 C/C++ 实现代码:实现 JNI 函数,调用你的本地逻辑。
  4. 编译生成 DLL 文件:使用 MinGW (GCC) 或 MSVC 将 C/C++ 代码编译成 Windows DLL。
  5. 运行 Java 程序:配置 JVM 以找到并加载 DLL 文件。

环境准备

在开始之前,请确保你已经安装了以下工具:

  1. JDK (Java Development Kit):包含 javac, java, javah 等工具。
  2. C/C++ 编译器
    • 推荐 (跨平台)MinGW-w64 (GCC 的 Windows 移植版),安装后,将 bin 目录(如 C:\mingw64\bin)添加到系统 PATH 环境变量中。
    • 备选 (Windows)Visual Studio 自带的 MSVC 编译器,在安装 VS 时,务必勾选 "使用 C++ 的桌面开发" 工作负载。

完整示例:一个简单的加法运算

我们将创建一个 Java 类,它包含一个 native 方法 add,该方法会调用一个 C++ 函数来完成两个整数的加法。

步骤 1:编写 Java 代码

创建一个名为 JNITest.java 的文件,关键点是:

  • 使用 native 关键字声明一个方法,表示其实现不在 Java 中。
  • 加载一个包含本地实现的 DLL 文件。System.loadLibrary("MyNativeLib"); 会尝试加载 MyNativeLib.dll
// JNITest.java
public class JNITest {
    // 声明一个 native 方法
    public native int add(int a, int b);
    static {
        // 加载 DLL 文件
        // 注意:这里只写库名,不带 "lib" 前缀和 ".dll" 后缀
        // JVM 会在系统库路径 (PATH) 和 java.library.path 中查找
        System.loadLibrary("MyNativeLib");
    }
    public static void main(String[] args) {
        JNITest test = new JNITest();
        int result = test.add(10, 25);
        System.out.println("10 + 25 = " + result); // 预期输出: 10 + 25 = 35
    }
}

步骤 2:编译 Java 代码并生成头文件

编译 Java 文件:

javac JNITest.java

使用 javah 工具生成 C/C++ 头文件。注意javah 在 JDK 10 之后被移除了,如果你使用的是较新版本的 JDK (JDK 10+),需要使用 javac-h 选项。

对于 JDK 9 及以下版本:

javah -jni JNITest

这会生成一个名为 JNITest.h 的头文件。

对于 JDK 10 及以上版本 (推荐方式):

javac -h . JNITest.java

-h . 表示在当前目录生成头文件。

生成的头文件 JNITest.h 内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNITest */
#ifndef _Included_JNITest
#define _Included_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JNITest
 * Method:    add
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_JNITest_add(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif

解读头文件:

  • #include <jni.h>:包含了所有 JNI 的数据类型和函数定义。
  • JNIEXPORTJNICALL:是 JNI 函数所需的修饰符,确保函数能被正确导出和调用。
  • jint JNICALL Java_JNITest_add(...):这是 C/C++ 函数的签名。
    • Java_JNITest_add:函数名由 包名_类名_方法名 组成,由于我们这里没有包,所以是 Java_JNITest_add
    • (JNIEnv *, jobject, jint, jint):这是参数列表。
      • JNIEnv *:一个指向 JNI 环境的指针,是 JNI 函数与 JVM 交互的入口。
      • jobject:指向 Java 对象本身的指针(这里是 JNITest 的一个实例)。
      • jintint 类型的 JNI 表示,对应 Java 的 int
    • I:返回值类型,I 代表 int,在方法签名 (II)I 中,括号内是参数类型,括号外是返回值类型。

步骤 3:编写 C/C++ 实现代码

创建一个 MyNativeLib.cpp 文件(使用 .cpp 以便支持 C++ 特性,如 extern "C"),实现 Java_JNITest_add 函数。

// MyNativeLib.cpp
#include "JNITest.h" // 包含生成的头文件
// 实现 Java_JNITest_add 函数
// 注意函数名必须与头文件中声明的一致
JNIEXPORT jint JNICALL Java_JNITest_add(JNIEnv *, jobject, jint a, jint b) {
    // 在这里执行你的本地代码逻辑
    return a + b;
}

重要说明:extern "C" 当 C++ 编译器编译代码时,会对函数名进行“名称修饰”(Name Mangling),以便支持函数重载,但 C 语言没有这个机制,JNI 要求函数名必须是 C 风格的。

  • 如果你的实现文件是 .c (纯 C),则不需要 extern "C"
  • 如果是 .cpp (C++),则需要将所有 JNI 函数包裹在 extern "C" { ... } 块中,以告诉 C++ 编译器使用 C 的链接方式。

修改后的 MyNativeLib.cpp

// MyNativeLib.cpp
#include "JNITest.h"
// 使用 extern "C" 确保函数名不被 C++ 修饰
extern "C" {
    // 实现 Java_JNITest_add 函数
    JNIEXPORT jint JNICALL Java_JNITest_add(JNIEnv *, jobject, jint a, jint b) {
        printf("C++: 正在计算 %d + %d\n", a, b);
        return a + b;
    }
}

步骤 4:编译生成 DLL 文件

这是最关键的一步,需要使用 C++ 编译器,我们以 MinGW (GCC) 为例。

打开命令行(CMD 或 PowerShell),确保你的编译器在 PATH 中。

使用 MinGW (g++) 编译:

g++ -shared -IC:\path\to\jdk\include -IC:\path\to\jdk\include\win32 MyNativeLib.cpp -o MyNativeLib.dll

命令参数解释:

  • g++: MinGW 的 C++ 编译器。
  • -shared: 生成共享库(在 Windows 上就是 DLL)。
  • -I<path>: 指定头文件搜索路径。你需要替换成你自己的 JDK 安装路径下的 include 目录C:\Program Files\Java\jdk-17.0.2\include,如果代码中引用了 Windows 特有的 JNI 头文件(如 jni_md.h),还需要加上 -I<path>\win32
  • MyNativeLib.cpp: 你的 C++ 源文件。
  • -o MyNativeLib.dll: 指定输出的 DLL 文件名。

使用 Visual Studio (cl.exe) 编译:

  1. 打开 "Developer Command Prompt for VS"(在开始菜单中可以找到)。
  2. 运行以下命令:
cl -LD -IC:\path\to\jdk\include -IC:\path\to\jdk\include\win32 MyNativeLib.cpp -Fe:MyNativeLib.dll

命令参数解释:

  • cl: MSVC 的编译器。
  • -LD: 与 -shared 作用相同,生成 DLL。
  • -I<path>: 指定头文件搜索路径。
  • -Fe:<filename>: 指定输出文件名。

编译成功后,你会在同一目录下得到 MyNativeLib.dll 文件。

步骤 5:运行 Java 程序

你有 JNITest.classMyNativeLib.dll,为了让 Java 虚拟机找到 MyNativeLib.dll,你需要:

  1. 将 DLL 放在 java.library.path 指定的目录中

    • 最简单的方法:将 MyNativeLib.dll 复制到 JNITest.class 所在的目录中。
    • 或者,将 DLL 所在目录添加到系统 PATH 环境变量中。
    • 或者,在运行 Java 程序时通过 -Djava.library.path 参数指定:
      java -Djava.library.path=. JNITest
  2. 执行 Java 程序

java JNITest

预期输出:

C++: 正在计算 10 + 25
10 + 25 = 35

高级主题与常见问题

传递和返回复杂数据类型

JNI 提供了丰富的 API 来处理数组、字符串和对象。

  • 字符串 (jstring):

    • Java -> C: const char* GetStringUTFChars(JNIEnv *env, jstring str, jboolean *isCopy);
    • C -> Java: jstring NewStringUTF(JNIEnv *env, const char *bytes);
    • 别忘了释放: (*env)->ReleaseStringUTFChars(env, str, ...);
  • 数组 (jintArray, jdoubleArray):

    • 获取数组指针: jint *GetIntArrayElements(JNIEnv *env, jintArray array, jboolean *isCopy);
    • 释放数组指针: (*env)->ReleaseIntArrayElements(env, array, ...);
    • 创建新数组: jintArray NewIntArray(JNIEnv *env, jsize len);
  • 对象 (jobject):

    • 可以通过 GetObjectClass 获取对象的 jclass
    • 使用 GetMethodID 获取方法ID。
    • 使用 CallObjectMethod, CallIntMethod 等来调用 Java 对象的方法。

内存管理

JNI 中有两类内存:

  • 本地代码内存:由 C/C++ 的 malloc/new 分配,必须由 C/C++ 的 free/delete 释放。
  • JVM 内存:由 JNI 函数(如 NewStringUTF)创建的对象,属于 JVM 堆,必须使用对应的 Delete... 函数(如 DeleteLocalRef)释放,以避免内存泄漏。

调试

调试 JNI 程序比较复杂,因为涉及两个不同的运行时。

  • C/C++ 端调试:使用 GDB (MinGW) 或 Visual Studio 调试器,你需要将调试器附加到运行 Java 程序的进程上,或者直接在 VS 中配置 C++ 项目并启动调试。
  • Java 端调试:使用 jdb 或 IDE 的调试功能。
  • 日志:在 C/C++ 代码中大量使用 printfstd::cout 来打印信息,这是最简单有效的调试手段。

现代替代方案:JavaCPP

对于新项目,可以考虑使用 JavaCPP 等库,它通过预处理器生成 JNI 代码,极大地简化了 JNI 的使用,你只需要在 Java 代码中写 @Platform(...)@Library(...) 注解即可,无需手动编写 C/C++ 代码和头文件。


步骤 任务 关键点
1 Java 代码 声明 native 方法,static 块中 System.loadLibrary()
2 生成头文件 javac -h . MyClass.java (JDK 10+)
3 C/C++ 实现 实现 Java_PackageName_ClassName_MethodName 函数,使用 extern "C"
4 编译 DLL 使用 g++ -sharedcl -LD,并指定 JDK 的 include 路径
5 运行 Java 确保 JVM 能找到 DLL(放在类路径、PATH 或用 -Djava.library.path 指定)

遵循以上步骤,你就可以成功地在 Java 中调用 C/C++ 编写的 DLL 了,虽然 JNI 功能强大,但开发体验相对繁琐,因此通常只在必要时使用。

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