JNI 是什么
JNI 全称为 Java Native Interface,即 Java 本地接口,是 Java 调用 Native 语言的一种特性。通过 JNI 可以使得 Java 与 C/C++ 机型交互,即可以在 Java 代码中调用 C/C++ 等语言的代码或者在 C/C++ 代码中调用 Java 代码。
由于 JNI 是 JVM 规范的一部分,因此可以将我们写的 JNI 的程序在任何实现了 JNI 规范的 Java 虚拟机中运行。同时,这个特性使我们可以复用以前用 C/C++ 写的大量代码。
JNI 是一种在 Java 虚拟机机制下的执行代码的标准机制。代码被编写成汇编程序或者 C/C++ 程序,并组装为动态库,也就允许非静态绑定用法。这提供了一个在 Java 平台上调用 C/C++ 的一种途径。
下面分别介绍在 Windows 平台和 Linux 平台使用 JNI 编程的常规流程,包括从创建工程项目、代码编写到编译运行。
在 Windows 平台上进行 JNI 编程
首先确保在 Windows 装有 JDK(Java 语言的软件开发工具包)。本文的代码例子适配的 Windows JDK 版本是 jdk-8u181-windows-x64。本章节示范在 Windows 平台上使用 IntelliJ IDEA 配合 VisualStudio 2013 进行 JNI 程序开发。
新建 Java 工程
1. 使用 IntelliJ IDEA 2018 新建一个 Java 工程,命名为 WinJniDemo,如下图:

2. 选择命令行程序模板。

3. 填写工程名、路径以及包名。

新建 Environment 类和 HelloJNI 类
在包 com.developerworks 下新建 Environment 类和 HelloJNI 类。Environment 类的作用在于统一加载 Java 程序所调用的 C/C++ 编写的动态库,HelloJNI 类是对应的 C/C++ 动态库的 Java 接口类,代码如下:
Environment 类代码
//Environment.java
package com.developerworks;
public class Environment {
public static boolean initEnvironment() {
String OS = System.getProperty("os.name").toLowerCase();
boolean isLinux = false;
if (OS.indexOf("windows") > -1) {
String strLibPath = "D:\\JavaCodeLib\\JNI_Learn\\hello\\OutDir\\Release_x64\\hello.dll";
System.load(strLibPath);
return true;
} else if (OS.indexOf("linux") > -1) {
String strLibPath = "/home/work/JavaCodeLib/JNI_Learn/hello/hello/hello.so";
System.load(strLibPath);
return true;
} else if (OS.indexOf("linux") > -1) {
String strLibPath = "/home/work/JavaCodeLib/JNI_Learn/hello/hello/hello.so";
System.load(strLibPath);
return true;
} else if (OS.indexOf("unix") > -1) {
String strLibPath = "/home/work/JavaCodeLib/JNI_Learn/hello/hello/hello.so";
System.load(strLibPath);
return true;
} else if (OS.indexOf("solaris") > -1) {
String strLibPath = "/home/work/JavaCodeLib/JNI_Learn/hello/hello/hello.so";
System.load(strLibPath);
return true;
} else if (OS.indexOf("mac") > -1) {
String strLibPath = "/Users/wang/javaPrj/hello.dylib";
System.load(strLibPath);
return true;
} else {
return false;
}
}
}
HelloJNI 类代码
//HelloJNI.java
packagecom.developerworks;
public class HelloJNI {
public native void sayHello();
}
入口主函数调用代码
// 入口主函数调用代码
packagecom.developerworks;
public class Main {
public static void main(String[] args) {
// write your code here
Environment.initEnvironment();
// invoke the native method
newHelloJNI().sayHello();
}
}
在上面代码中函数 System.loadLibrary() 是加载 dll(Windows)或 so(Linux)库,只需名称即可,无需加入文件名后缀(.dll 或 .so)。native 关键字将函数 sayHello() 声明为本地函数,由 C/C++实现。具体的实现就在 hello.dll(Windows 平台)或 hello.so(Linux 平台)中。
生成头文件
接下来我们将根据 HelloJNI类来生成 C/C++ 程序所需要的头文件。JNI 生成头文件是通过 JDK 中提供的 javah 工具来完成,javah 工具在 JAVA_HOME 环境变量所指向的目录下的 bin 文件夹中。使用 javah 工具生成头文件的步骤如下:
1. 打开 Windows 的控制台程序,使用 cd 命令进入到 WinJniDemo 工程的 src 目录下:

2. 执行javah命令生成头文件:
javah -classpath . -jni -encoding UTF-8 com.developerworks.HelloJNI

命令成功执行后,在 src 文件夹下会有一个 com_developerworks_HelloJNI.h。命令中 -classpath . 表示在当前目录下搜索类,-encoding UTF-8 表示使用 UTF-8 作为生成的头文件的编码,com.developerworks.HelloJNI 为指定的 Java 接口类。
com_developerworks_HelloJNI.h 代码
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_developerworks_HelloJNI */
#ifndef _Included_com_developerworks_HelloJNI
#define _Included_com_developerworks_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_developerworks_HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_developerworks_HelloJNI_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
从上面代码可以看出 C/C++ 动态库的接口头文件已经生成了。
创建动态库工程
接下来我们使用 VS2013 创建一个 Win32 的动态库工程。
1. 创建一个 Win32 的动态库工程。

2. 将 com_developerworks_HelloJNI.h 加到工程,在工程里添加一个 com_developerworks_HelloJNI.cpp,目录结构如下图:

com_developerworks_HelloJNI.cpp 代码
#include "com_developerworks_HelloJNI.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_com_developerworks_HelloJNI_sayHello
(JNIEnv* env, jobject obj)
{
printf("Hello,JNI!\n");
}
3. 在工程的附加包含目录添加:$(JAVA_HOME)\include;$(JAVA_HOME)\include\win32,如下图:

编译运行工程
在 IntelliJ IDEA 2018 编译运行 Java 工程 WinJniDemo,生成 dll 文件,得到下面的运行效果:

在 Linux 平台上进行 JNI 编程
首先确保在 Linux 装有 GCC 和 JDK(Java 语言的软件开发工具包)。本文的代码例子适配的 Linux JDK 版本是 jdk-8u144-linux-x64。
1. 先用 GCC 编译 C/C++ 程序生成 so 库,执行下面的编译脚本:
gcc com_developerworks_HelloJNI.cpp -std=c++11 -fPIC -shared -I/usr/local/java/jdk1.8.0_144/include -I/usr/local/java/jdk1.8.0_144/include/linux -lm -o ./hello.so
2. 然后使用 Eclipse 编译运行之前建好的 Java 工程,运行结果如下:

JNI 程序如何传递常用参数和返回值
JNI 程序开发中一个注意事项是参数传递和返回值处理。
Java 数据类型与 JNI 类型映射
Java 中的基本类型包括 boolean,byte,char,short,int,long,float,double,如果你用这几种类型做 native 方法的参数,当你通过 javah 工具生成 .h 文件的时候,这些基本类型分别对应的类型是 jboolean,jbyte,jchar,jshort,jint,jlong,jfloat,jdouble。因此基本类型的传递是可以直接传递的。下面是详细的 Java 数据类型与 JNI 类型映射表:
Java 数据类型与 JNI 类型映射表
Java 类型 | 本地类型 | 描述 |
boolean | jboolean | C/C++8 位整型 |
byte | jbyte | C/C++ 带符号的 8 位整型 |
char | jchar | C/C++ 无符号的 16 位整型 |
short | jshort | C/C++带符号的 16 位整型 |
int | jint | C/C++带符号的 32 位整型 |
long | jlong | C/C++带符号的 64 位整型 |
float | jfloat | C/C++32 位浮点型 |
double | jdouble | C/C++64 位浮点型 |
Object | jobject | 任何 Java 对象,或者没有对应 java 类型的对象 |
Class | jclass | Class 对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
byte[] | jbyteArray | 比特型数组 |
char[] | jcharArray | 字符型数组 |
short[] | jshortArray | 短整型数组 |
int[] | jintArray | 整型数组 |
long[] | jlongArray | 长整型数组 |
float[] | jfloatArray | 浮点型数组 |
double[] | jdoubleArray | 双浮点型数组 |
String 参数的传递和返回
Java 的 String 和 C++ 的 String 是不能对等起来的,所以需要额外进行处理。具体就是使用 GetStringUTFChars 函数将 jstring 类型变成一个 char* 类型,返回的时候,要生成一个 jstring 类型的对象,也必须通过 NewStringUTF 函数来构造。下面是示例代码:
Java_com_developerworks_HelloJNI_getLine 函数代码
JNIEXPORT jstring JNICALL Java_com_developerworks_HelloJNI_getLine
(JNIEnv* env, jobject obj, jstring prompt)
{
const char* str;
str = env->GetStringUTFChars(prompt, false);
if (str == NULL)
{
return NULL; /* OutOfMemoryError already thrown */
}
std::cout<<str<<std::endl;
env->ReleaseStringUTFChars(prompt, str);
char* tmpstr = "return string succeeded";
jstring rtstr = env->NewStringUTF(tmpstr);
return rtstr;
}
数组类型的传递
和 String 一样,JNI 为 Java 基本类型的数组提供了 j*Array 类型,比如 int[] 对应的就是 jintArray。来看一个传递 int 数组的例子,具体是用 GetArrayLength 函数来获取数组的长度,使用 GetIntArrayElements 函数将 jintArray 转换为 jint* 类型。下面是示例代码:
Java_com_developerworks_HelloJNI_sumArray 函数代码
JNIEXPORT jint JNICALL Java_com_developerworks_HelloJNI_sumArray
(JNIEnv *env, jobject obj, jintArray arr)
{
jint* carr = NULL;
carr = env->GetIntArrayElements(arr, false);
if (carr == NULL)
{
return 0; /* exception occurred */
}
int size = env->GetArrayLength(arr);
jint sum = 0;
for (int i = 0; i < size; i++)
{
sum += carr[i];
}
env->ReleaseIntArrayElements(arr, carr, 0);
return sum;
}
如何调试 JNI 程序
下面分别介绍如何在 Windows 和 Linux 平台上调试程序。
在 Windows 平台上调试程序
在 Windows 平台上调试 JNI 程序的流程如下所示。
1. 在 Idea 上以 Debug 方式运行 Java 程序,在 String input = hello.getLine(“this is a prompt”);这句代码设置一个断点,运行到该断点时停下来,如下图:

2. 在 VS 2013 上选择附加的 Java 进程,如下图:

然后,选择对应的 Java 进程并进行附加。

3. 在 C++ 程序中的函数中加上断点,继续运行 Java 程序,一会儿就可以看到程序已经运行到了 C++ 代码中,如下图:

在 Linux 平台上调试程序
在 Linux 平台调试 JNI 程序的流程如下所示。
1. 给 Eclipse 安装 CDT 插件,如下图:

2. 使用 Eclipse 创建一个 C++ Shared Library 工程(如下图),然后把 com_developerworks_HelloJNI.h 和 com_developerworks_HelloJNI.cpp 加到工程,编译生成。

3. 以 Debug 方式运行 Java 程序,在 String input = hello.getLine(“this is a prompt”); 这句代码设置一个断点,运行到该断点时停下来。
4. 设置 Eclipse C++ 工程调试配置。右键选择 C++ Shared Library 工程,在右键菜单中选择 Debug > Debug Configurations,在 Debug Configurations 窗口里选择 C/C++ Attach to Application,在 Debugger 选项卡里勾选 Non-stop mode,单击 Debug 按钮,如下图:

然后在 Eclipse 上选择附加相应的 Java 程序的进程。

5. 在 C++ 程序中的函数中加上断点,继续运行 Java 程序,一会儿就可以看到程序已经运行到了 C++ 代码中,如下图:

通过本文希望大家能大概了解在 Windows 和 Linux 使用 JNI 开发的常规流程。本文只是 JNI 开发的一个快速入门,如果要使用 JNI 进行更高级的开发,还会涉及到 JNI 的异常处理、JNI 中的引用等知识点,大家可以自行展开进一步学习。