01. 引言
在某些场景中,我们可能需要Java程序使用针对特定架构原生编译的代码,比如:
- 程序需要跟硬件交互。
- 提高对性能敏感的程序的性能。
- 与非Java环境交互,重用其他语言编写的库,节省用 Java 重写的工作。
我们可以使用JNI (Java Native Interface)来调用本地代码。JNI是 Java 提供的允许 Java 应用程序调用和被调用本地(Native)代码的机制,通常是使用 C 或 C++ 编写的代码。使得 Java 能够与底层的本地库进行交互,调用系统级别的功能,或者与其他编程语言进行集成。
“Talk is cheap, show me the code”,下面我们来动手实践在Java中怎么使用JNI。
02. Hello World
2.1 准备
- 操作系统:Windows 64 位(文中也给出 Linux/macOS 命令)
- JDK:8 及以上(需要 `javac -h`)
- C/C++ 编译器:MinGW-w64(Windows)/ gcc(Linux)/ clang(macOS)或 MSVC
- 架构需匹配:64 位 JVM 对应 64 位本地库(DLL/.so/.dylib)
建议的项目结构(示意):
jni-with-c
│
├── src
│ ├── cfile
│ │ └── Hello.c
│ │ └── CCodeCaller.h
│ └── java
│ └── CCodeCaller.java
└── out
2.2 JNI 调用 C 代码
1) 编写一个调用C代码的Java类`CCodeCaller`
public class CCodeCaller {
static {
// Windows: hello_from_c.dll; Linux: libhello_from_c.so; macOS: libhello_from_c.dylib
System.loadLibrary("hello_from_c");
}
public native void hello();
public static void main(String[] args) {
new CCodeCaller().hello();
}
}
2) 编译并生成本地头文件
javac -h src/cfile -d out src/java/CCodeCaller.java
选项`-h <directory>`会生成 本地头文件,并将其放置在指定的目录中。在 JDK 8 之前,您需要使用专门的 `javah` 工具生成 C/C++ 头文件。JDK 10 中不再提供 `javah`命令。让我们来看看生成的头文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class CCodeCaller */
#ifndef _Included_CCodeCaller
#define _Included_CCodeCaller
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: CCodeCaller
* Method: hello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_CCodeCaller_hello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
头文件声明了一个 C 函数Java_CCodeCaller_hello。
C 函数的命名约定是 Java_{package_and_classname}_{function_name}(JNI_arguments)。包名中的点号用下划线代替。
参数为:
JNIEnv*:对 JNI 环境的引用,通过它可以访问所有 JNI 函数jobject:对调用本地代码的 Java 对象的引用。
本示例中没有使用这些参数,但稍后会用到。extern "C"只能被 C++ 编译器识别。它通知 C++ 编译器,这些函数将使用 C 的函数命名协议而不是 C++ 的命名协议进行编译。C 和 C++ 有不同的函数命名协议,因为 C++ 支持函数重载,并使用名称混淆方案来区分重载函数。
3) 编写 C 代码 `src/cfile/Hello.c`
#include // JDK内置的JNI头文件
#include // C标准I/O头文件
#include "CCodeCaller.h" // 生成的头文件
// 实现本地方法hello()
JNIEXPORT void JNICALL Java_CCodeCaller_hello(JNIEnv *env, jobject thisObj) {
printf("Hello from C using JNI!");
return;
}
JDK 提供的 JNI 头文件 jni.h 可在”<JAVA_HOME>\include “和”<JAVA_HOME>\include\win32″(适用于 Windows)或”<JAVA_HOME>\include\linux”(适用于 Linux)目录下找到, <JAVA_HOME> 表示 JDK 安装目录。
上面的 C 函数只包含向控制台打印 “Hello from C using JNI!”的简单动作。
4) 编译生成本地库(Windows + MinGW)
gcc -shared -o hello_from_c.dll -I $env:JAVA_HOME/include -I $env:JAVA_HOME/include/win32 src/cfile/Hello.c
对于上面的编译器选项:
- `-I <headerDir>`:用于指定头文件目录。
- `-shared`:生成共享库。
- `-o outputFilename`:用于设置输出文件名 。
Linux 与 macOS 的命令见后文“多平台编译”。
5) 打包与运行
jar cfe JNIWithC.jar CCodeCaller -C out .
# 确保能找到本地库:
java -Djava.library.path=. -jar JNIWithC.jar
# 或者使用绝对路径加载:System.load("C:\\abs\\path\\hello_from_c.dll")
2.3 JNI 调用 C++ 代码
上面的例子中,除了C代码,我们还可以使用C++代码。其他部分和使用 C 代码没有太大差别,将 C 类替换为 C++ 类 Hello.cpp:
1) Java 类 `CPPCodeCaller`
#include
#include
#include "CPPCodeCaller.h"
JNIEXPORT void JNICALL Java_CPPCodeCaller_hello(JNIEnv *, jobject) {
std::cout << "Hello from C++ using JNI!" << std::endl;
}
2) 生成头文件
javac -h src/cfile -d out src/java/CPPCodeCaller.java
3) C++ 代码 `src/cfile/Hello.cpp`
#include
#include
#include "CPPCodeCaller.h"
extern "C" { // 避免 C++ 名字修饰
JNIEXPORT void JNICALL Java_CPPCodeCaller_hello(JNIEnv *, jobject) {
std::cout << "Hello from C++ using JNI!" << std::endl;
}
}
4) 编译生成本地库(Windows + MinGW)
g++ -shared -o hello_from_cpp.dll -I $env:JAVA_HOME/include -I $env:JAVA_HOME/include/win32 src/cfile/Hello.cpp
03. JNI 设计简述
在 JNI 框架中,本地函数是在单独的 .c 或 .cpp 文件中实现的。JVM 调用函数时,会传递一个 JNIEnv 指针、一个 jobject 指针和 Java 方法声明的任何 Java 参数。
JNI 在本地系统中定义了以下与 Java 类型相对应的 JNI 类型:
- Java 基本类型:
jint、jbyte、jshort、jlong、jfloat、jdouble、jchar、jboolean分别表示int、byte、short、long、float、double、char和boolean。 - Java 引用类型:
jobject用于映射java.lang.Object。它还定义了以下子类型:jclass表示java.lang.Class。jstring表示java.lang.Stringjthrowable表示java.lang.Throwable。jarray表示 Java 数组。Java 数组是一种引用类型,包括8个基本类型数组jintArray、jbyteArray、jshortArray、jlongArray、jfloatArray、jdoubleArray、jcharArray和jbooleanArray,以及1个Object数组jobjectArray。
04. 本地程序传递参数和返回值
4.1 传递基本参数
下面的代码演示Java程序传递两个int类型的数值到本地方法中计算平均值。首先定义一个Java类 src/java/JNIPrimitiveTest.java:
public class JNIPrimitiveTest {
static {
System.loadLibrary("dealing_primitive_with_c");
}
public native double avg(int n1, int n2);
public static void main(String[] args) {
int n1 = Integer.parseInt(args[0]),
n2 = Integer.parseInt(args[1]);
System.out.printf("The average is: %f\n",
new JNIPrimitiveTest().avg(n1, n2));
}
}
编译Java类并生成C/C++头文件:
javac -h src/cfile -d out src/java/JNIPrimitiveTest.java
生成的头文件中的JNI函数声明如下:
JNIEXPORT jdouble JNICALL Java_JNIPrimitiveTest_avg
(JNIEnv *, jobject, jint, jint);
在DealingPrimitive.c 文件中编写C实现,代码如下:
#include
#include
#include "CallingNativeAvg.h"
JNIEXPORT jdouble JNICALL Java_CallingNativeAvg_avg
(JNIEnv *env, jobject thisObj, jint n1, jint n2) {
jdouble avg;
avg = ((jdouble) n1 + n2) / 2.0;
return avg;
}
4.2 传递引用类型(字符串、数组、对象)
1) 字符串(UTF-8 编码与释放)
// Java
public class JNIStringDemo {
static { System.loadLibrary("string_demo"); }
public native String shout(String s);
}
// C
#include
#include
#include "JNIStringDemo.h"
JNIEXPORT jstring JNICALL Java_JNIStringDemo_shout(JNIEnv* env, jobject self, jstring js) {
if (js == NULL) return (*env)->NewStringUTF(env, "");
const char* utf = (*env)->GetStringUTFChars(env, js, NULL); // modified UTF-8
if (utf == NULL) return NULL; // OOM
char buf[512];
snprintf(buf, sizeof(buf), "[%s]", utf);
(*env)->ReleaseStringUTFChars(env, js, utf);
return (*env)->NewStringUTF(env, buf);
}
要点:`GetStringUTFChars`/`ReleaseStringUTFChars` 成对出现;返回 `NULL` 需立刻返回以传播异常。
2) 基本类型数组(长度、元素访问与释放)
// Java
public class JNIArrayDemo {
static { System.loadLibrary("array_demo"); }
public native int sum(int[] a);
}
// C
#include
#include "JNIArrayDemo.h"
JNIEXPORT jint JNICALL Java_JNIArrayDemo_sum(JNIEnv* env, jobject self, jintArray arr) {
if (arr == NULL) return 0;
jsize len = (*env)->GetArrayLength(env, arr);
jint* elems = (*env)->GetIntArrayElements(env, arr, NULL); // 可能是拷贝或直映射
if (elems == NULL) return 0; // OOM
long s = 0; for (jsize i = 0; i < len; ++i) s += elems[i];
(*env)->ReleaseIntArrayElements(env, arr, elems, 0); // 0: 写回并释放
return (jint)s;
}
批量读写可用 `Get<Primitive>ArrayRegion/Set…Region`。`GetPrimitiveArrayCritical`/`Release…Critical` 延迟 GC,时间窗口需尽量短且避免阻塞。
3) 对象字段与方法(访问与回调)
// Java
public class Counter { int value; }
public class JNIObjectDemo {
static { System.loadLibrary("object_demo"); }
public native int incCounter(Counter c);
}
// C
#include
#include "JNIObjectDemo.h"
JNIEXPORT jint JNICALL Java_JNIObjectDemo_incCounter(JNIEnv* env, jobject self, jobject counter) {
jclass cls = (*env)->GetObjectClass(env, counter);
jfieldID fid = (*env)->GetFieldID(env, cls, "value", "I");
jint v = (*env)->GetIntField(env, counter, fid);
(*env)->SetIntField(env, counter, fid, v + 1);
return v + 1;
}
回调 Java 方法:使用 `Get/Call<type>Method`,静态方法对应 `Get/CallStatic<type>Method`。
05. 多平台编译与兼容
Windows(MinGW):
gcc -shared -o hello_from_c.dll -I $env:JAVA_HOME/include -I $env:JAVA_HOME/include/win32 src/cfile/Hello.c
g++ -shared -o hello_from_cpp.dll -I $env:JAVA_HOME/include -I $env:JAVA_HOME/include/win32 src/cfile/Hello.cpp
Windows(MSVC):
cl /LD /I %JAVA_HOME%\include /I %JAVA_HOME%\include\win32 src\cfile\Hello.c /Fe:hello_from_c.dll
Linux(gcc):
gcc -shared -fPIC -o libhello_from_c.so -I "$JAVA_HOME/include" -I "$JAVA_HOME/include/linux" src/cfile/Hello.c
macOS(clang):
clang -dynamiclib -o libhello_from_c.dylib -I "$JAVA_HOME/include" -I "$JAVA_HOME/include/darwin" src/cfile/Hello.c
说明:Linux 需 `-fPIC`;macOS 生成 `.dylib`;类名/函数名需与头文件一致;C++ 需 `extern “C”`。
06 性能与最佳实践
- 减少 JNI 往返次数,尽量批处理(如一次传递数组而非逐元素)。
- 缓存 `jclass`、`jmethodID`、`jfieldID`(在 `JNI_OnLoad` 或首次调用时初始化)。
- 字符串/数组按需复制;大块二进制数据优先使用 `DirectByteBuffer`。
- `GetPrimitiveArrayCritical` 窗口尽量短,避免阻塞/进入 JVM。
- 避免在本地方法内做可能长时间阻塞的 I/O 或锁等待。
– 启用运行时检查:`-Xcheck:jni` 有助于发现误用。
引用
- Java本地接口规范文档 @ https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
- JNI提示 @ https://developer.android.com/training/articles/perf-jni?hl=zh-cn
- Java Native Interface @ https://www3.ntu.edu.sg/home/ehchua/programming/java/javanativeinterface.html
- Java Native Interface Wiki @ https://en.wikipedia.org/wiki/Java_Native_Interface