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 <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 代码 `src/cfile/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) 编译生成本地库(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 <jni.h>
#include <iostream>
#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 <jni.h>
#include <iostream>
#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 基本类型: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);
				
			

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 传递引用类型(字符串、数组、对象)

1) 字符串(UTF-8 编码与释放)

				
					// Java
public class JNIStringDemo {
    static { System.loadLibrary("string_demo"); }
    public native String shout(String s);
}
				
			
				
					// C
#include <jni.h>
#include <string.h>
#include "JNIStringDemo.h"

JNIEXPORT jstring JNICALL Java_JNIStringDemo_shout(JNIEnv* env, jobject self, jstring js) {
    if (js == NULL) return (*env)->NewStringUTF(env, "<null>");
    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 <jni.h>
#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 <jni.h>
#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` 有助于发现误用。

引用