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 JNI调用C代码
运行环境:
- Windows 64位系统
- Java 开发环境 (Java 8及以上)
- C\C++ 编译器 (比如 MinGW-w64 (Windows))
项目结构:
jni-with-c
│
├── src
│ ├── cfile
│ │ └── Hello.c
│ │ └── CCodeCaller.h
│ └── java
│ └── CCodeCaller.java
└── out
步骤1:编写一个调用C代码的Java类CCodeCaller
public class CCodeCaller {
static {
System.loadLibrary("hello_from_c");
}
public native void hello();
public static void main(String[] args) {
new CCodeCaller().hello();
}
}
静态块中的 System.loadLibrary("hello_from_c")
在Windows上执行会加载系统中的“hello_from_c.dll” 文件;在Linux上执行则会加载系统中的 “libhello_from_c.so” 文件。
步骤2:编译Java程序并生成C/C++头文件
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 程序 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:编译 C 程序
对于Windows 64位系统,执行如下命令:
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
:用于设置输出文件名 。
步骤5:打包Java类文件到JAR
jar cfe JNIWithC.jar CCodeCaller -C out .
步骤6:运行Java程序
java -jar JNIWithC.jar
2.3 JNI调用C++代码
运行环境:
- Windows 64位系统
- Java 开发环境 (Java 8及以上)
- C\C++ 编译器 (比如 MinGW-w64 (Windows))
上面的例子中,除了C代码,我们还可以使用C++代码。其他部分和使用 C 代码没有太大差别,将 C 类替换为 C++ 类 Hello.cpp
:
#include
#include
#include "CPPCodeCaller.h"
JNIEXPORT void JNICALL Java_CPPCodeCaller_hello(JNIEnv *, jobject) {
std::cout << "Hello from C++ using JNI!" << std::endl;
}
编译C++代码并生成本地链接库需要使用MinGW的 g++.exe
程序:
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.String
jthrowable
表示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);
C语言实现
在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 传递引用类型
[Todo]
完整的代码可以在我的GitHub仓库上获取。
引用
- 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