- 编写 Java 代码:声明
native方法。 - 生成 C/C++ 头文件:使用
javac和javah(或现代工具)生成 JNI 函数声明。 - 编写 C/C++ 实现代码:实现 JNI 函数,调用你的本地逻辑。
- 编译生成 DLL 文件:使用 MinGW (GCC) 或 MSVC 将 C/C++ 代码编译成 Windows DLL。
- 运行 Java 程序:配置 JVM 以找到并加载 DLL 文件。
环境准备
在开始之前,请确保你已经安装了以下工具:
- JDK (Java Development Kit):包含
javac,java,javah等工具。 - C/C++ 编译器:
- 推荐 (跨平台):MinGW-w64 (GCC 的 Windows 移植版),安装后,将
bin目录(如C:\mingw64\bin)添加到系统PATH环境变量中。 - 备选 (Windows):Visual Studio 自带的 MSVC 编译器,在安装 VS 时,务必勾选 "使用 C++ 的桌面开发" 工作负载。
- 推荐 (跨平台):MinGW-w64 (GCC 的 Windows 移植版),安装后,将
完整示例:一个简单的加法运算
我们将创建一个 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 的数据类型和函数定义。JNIEXPORT和JNICALL:是 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的一个实例)。jint:int类型的 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) 编译:
- 打开 "Developer Command Prompt for VS"(在开始菜单中可以找到)。
- 运行以下命令:
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.class 和 MyNativeLib.dll,为了让 Java 虚拟机找到 MyNativeLib.dll,你需要:
-
将 DLL 放在
java.library.path指定的目录中。- 最简单的方法:将
MyNativeLib.dll复制到JNITest.class所在的目录中。 - 或者,将 DLL 所在目录添加到系统
PATH环境变量中。 - 或者,在运行 Java 程序时通过
-Djava.library.path参数指定:java -Djava.library.path=. JNITest
- 最简单的方法:将
-
执行 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, ...);
- Java -> C:
-
数组 (
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++ 代码中大量使用
printf或std::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++ -shared 或 cl -LD,并指定 JDK 的 include 路径 |
| 5 | 运行 Java | 确保 JVM 能找到 DLL(放在类路径、PATH 或用 -Djava.library.path 指定) |
遵循以上步骤,你就可以成功地在 Java 中调用 C/C++ 编写的 DLL 了,虽然 JNI 功能强大,但开发体验相对繁琐,因此通常只在必要时使用。
