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 <jni.h>
/* 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 <jni.h>         // JDK内置的JNI头文件
#include <stdio.h>       // 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 <jni.h>
#include <iostream>
#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 基本类型:jintjbytejshortjlongjfloatjdoublejcharjboolean 分别表示 intbyteshortlongfloatdoublecharboolean
  • Java 引用类型:jobject 用于映射 java.lang.Object。它还定义了以下子类型:
    • jclass 表示 java.lang.Class
    • jstring 表示 java.lang.String
    • jthrowable 表示 java.lang.Throwable
    • jarray 表示 Java 数组。Java 数组是一种引用类型,包括8个基本类型数组 jintArrayjbyteArrayjshortArrayjlongArrayjfloatArrayjdoubleArrayjcharArrayjbooleanArray,以及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 <jni.h>
#include <stdio.h>
#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仓库上获取。

引用