JNI技术经验总结

JNI是Java Native Interface的缩写,为Java程序提供与本地程序交互的能力。使用JNI技术,能够使得Java程序充分利用本地代码的优势,如高性能,不必重复造轮子等,在生产中,有着诸多实用价值。

JNI工作流程

JNI的典型使用场景是:Java程序调用C,C++代码编译而成的动态库文件。动态库文件在Windows下是.dll文件,在Linux下为.so文件。其主要工作流程如图所示:

java nio系统调用 java jni调用过程_Java

  1. 编写.java源代码,其中用native关键字标明需要本地实现的函数。
  2. 使用命令 javac -h <输出位置> <源代码路径> 编译出.h头文件。
  3. 编写.cpp源代码实现.h文件中声明的native函数。
  4. 编译.h头文件和.cpp源文件生成.dll动态库,并移动到.java源代码中指定的位置。
  5. 使用命令 javac <源代码路径> 将.java源代码编译成.class字节码。
  6. 使用命令 java <类名> 执行类名.class文件,调用.dll库文件得到输出。

Hello, world!

这里实现一个简单的Windows版本的demo,带读者来熟悉上述流程。

  1. 编写Hello.java源代码
public class Hello {
    // native 关键字声明native函数
    native void sayHelloToC();
    // 加载 java2c.dll库文件
    static { System.loadLibrary("java2c");}
    
    public static void main(String []args){
        Hello hello = new Hello();
        hello.sayHelloToC();
    }
}
  1. 在Hello.java所在目录,打开cmd命令行,编译出Hello.h头文件。(也一并编译出了Hello.class文件)
javac -encoding utf-8 -h . Hello.java

utf-8防止中文注释乱码,. 表示输出到当前路径下。

  1. 为了简化较长的.dll编译命令,此处我们借助 Dev-C++ 开发工具来完成C++代码的编写和编译。
  1. 打开Dev-C++,新建dll项目,将自动生成dll.h和dllmain.cpp两个文件。
  2. 将dll.h的内容替换为Hello.h。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Hello */

#ifndef _Included_Hello
#define _Included_Hello
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Hello
 * Method:    sayHelloToC
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_Hello_sayHelloToC
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
  1. 清空dllmain.cpp中的内容,修改为如下
#include"dll.h"
#include<stdio.h>

// 实现dll.h中声明的函数 
JNIEXPORT void JNICALL Java_Hello_sayHelloToC
  (JNIEnv * env, jobject obj){
  	printf("Hello, C! I am Java.");
  }
  1. 编译生成.dll库文件。
    dll.h的第一行,引入了jni.h头文件。在java安装目录下的include文件夹,找到jni.h文件和jni_md.h。将它们复制到dev-c++项目的 工具->编译选项->目录->C++包含文件 中的任一目录下。
  2. java nio系统调用 java jni调用过程_Java_02

  3. 点击编译,生成hello_c++.dll文件(hello_c++为项目名),将其重命名为java2c.dll(与1中的java源代码保持一致),移动到Hello.java所在的文件夹。
  4. 编译Hello.java生成Hello.class字节码,其实在第2步已经顺带完成。
javac -encoding utf-8 Hello.java
  1. 在Hello.class目录下,打开cmd命令行,执行字节码程序。
java Hello

java nio系统调用 java jni调用过程_c++_03

以上实现一个简单的demo,完成Java程序调用C++编写的动态库的目标。通过JNI技术还可以实现Java程序和C/C++程序间的数据传递,下面将一一陈述。

基本数据

Java类型

JNI类型

描述

boolean

jboolean

unsigned 8 bits

byte

jbyte

signed 8 bits

char

jchar

unsigned 16 bits

short

jshort

signed 16 bits

int

jint

signed 32 bits

long

jlong

signed 64 bits

float

jfloat

32 bits

double

jdouble

64 bits

void

void

void

JNI的基本数据类型与Java类型的对照表如上,下面演示传递基本类型的数据。

Hello.java中添加:

native double average(int x, int y);

Hello.h中新增:

/*
 * Class:     Hello
 * Method:    average
 * Signature: (II)D
 */
JNIEXPORT jdouble JNICALL Java_Hello_average
  (JNIEnv *, jobject, jint, jint);

java编译程序自动添加了三行注释,Class表示类名,Method表示方法名,Signature是java的函数签名,告诉我们函数的参数类型和返回类型。参数是两个Interger,返回是Double。

函数体代码中,函数返回值为jdouble与java中的double对映,函数名为程序自动生成的Java _类名 _方法名形式,JNIEnv参数为jni环境,jobject为native函数所在的java对象,这两个是jni自带的。后面两个jint参数对应原本java方法的int参数。

在dllmain.cpp中实现average的逻辑:

JNIEXPORT jdouble JNICALL Java_Hello_average
  (JNIEnv * env, jobject obj, jint x, jint y){
  	return jdouble(x + y) / 2;
  }

执行输出:

System.out.println(hello.average(1, 2));
// 1.5

字符串

JNI的字符串处理函数丰富全面,此处演示传递字符串的简单例子,并以此引出JNI处理复杂数据类型时的一般流程。更多API文件见参考链接的官方文档。

Hello.java中添加:

native String sendMessage(String msg);

Hello.h中生成:

/*
 * Class:     Hello
 * Method:    sendMessage
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_Hello_sendMessage
  (JNIEnv *, jobject, jstring);

dllmain.cpp中实现:

JNIEXPORT jstring JNICALL Java_Hello_sendMessage
  (JNIEnv * env, jobject obj, jstring msg){
  	// 1.convert jstring to cstring 
  	const char* cMsg = env->GetStringUTFChars(msg, NULL);
  	if(NULL == cMsg) return NULL;
  	
  	// 2.use cstring 
  	printf("Java: %s\n", cMsg);
  	
  	// 3.release resources
  	env->ReleaseStringUTFChars(msg, cMsg);
  	
  	// 4.return
  	const char* cMsg2 = "Good afternoon! Miss.Java.";
  	return env->NewStringUTF(cMsg2);
  }

注意:JNI中需要手动显式地释放资源,否则会造成内存泄漏。

执行输出:

String msg = "Good morning! Mr.C++.";
System.out.println("C++: " + hello.sendMessage(msg));
// Java: Good morning! Mr.C++.
// C++: Good afternoon! Miss.Java.

对象

JNI同样可以传递对象,也就是说,JNI让C/C++代码能够访问Java类中的成员变量和方法。

从这里开始,我们使用Maven来管理我们的代码。

新建maven项目,其目录结构如下:

java nio系统调用 java jni调用过程_java_04

Hello.java的代码如下

package org.example;

public class Hello {
    static {
        System.loadLibrary("java2c");
    }

    private int num = 2021;
    private void printNum(){
        System.out.println("In Java, num is " + num);
    }

    // 演示访问成员变量和方法
    native void jniMethod();

    public static void main(String []args){
        Hello hello = new Hello();
        hello.jniMethod();
    }
}

此处引入了java包,且Hello中导入了Color类,所以如果直接在Hello.java同级文件夹下调用javac命令,会报找不到符号的错误,应在所有源文件的根目录,即maven项目的java文件夹下,调用javac命令。这样编译器能将包名和路径对应起来。

进入src\main\java路径,运行:

javac -encoding utf-8  -h . ./org/example/Hello.java

如果源文件还引入了其他位置的包,如test文件夹下的源文件引入了main文件夹下的包,可使用javac -cp <依赖路径>告知java编译器依赖包的位置。更多javac选项,可使用javac -help自行查看。

Hello.h中生成:

/*
 * Class:     org_example_Hello
 * Method:    jniMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_example_Hello_jniMethod
  (JNIEnv *, jobject);

dllmain.h中实现定义的函数:

JNIEXPORT void JNICALL Java_org_example_Hello_jniMethod
  (JNIEnv* env, jobject obj){
	jclass cls = env->GetObjectClass(obj);
	// fieldID is certain for a class
	jfieldID numField = env->GetFieldID(cls, "num", "I");
  	// get number
	jint num = env->GetIntField(obj, numField);
  	printf("In C++, num is %d\n", num);
  	// change number
  	env->SetIntField(obj, numField, 2035);
  	// call method 
  	jmethodID printNumField = env->GetMethodID(cls, "printNum", "()V");
  	env->CallVoidMethod(obj, printNumField);
  	return;
  }

执行编译,在maven项目中编译执行的路径不再是源文件所在的路径。为简单起见,我们将生成的dll文件放到系统环境变量中,以便java虚拟机能够找到。执行输出得到:

public static void main(String []args){
    Hello hello = new Hello();
    hello.jniMethod();
}

// In Java, num is 2035
// In C++, num is 2021

通过JNI,获取对象的成员变量或调用对象的主要过程如下:

  1. GetObjectClass通过jobject获得jclass,或者FindClass通过类名直接获得jclass
  2. 通过jclass获得jfieldID获得jmethodID。
  3. 使用jfieldID获取成员变量的值或使用jmethodID调用对象的方法。

dllmain.cpp中的"I"、"()V"是java的签名字符串,可以在终端通过javap命令获取。

数组

jni也可以传递基本类型的数组和对象数组,此处通过向量加法来演示此功能。

Hello.java中新增:

native void vectorAdd(int[] a, int[] b, int[] c);

public static void main(String []args){
    Hello hello = new Hello();
    int []a = {1, 2, 3};
    int []b = {4, 5, 6};
    int []c = new int[3];
    hello.vectorAdd(a, b, c);
    for(int i:c){
        System.out.print(i + " ");
    }

javac -h重新编译后,Hello.h中生成:

/*
 * Class:     org_example_Hello
 * Method:    vectorAdd
 * Signature: ([I[I[I)V
 */
JNIEXPORT void JNICALL Java_org_example_Hello_vectorAdd
  (JNIEnv *, jobject, jintArray, jintArray, jintArray);

在dllmain.h中实现上述函数:

JNIEXPORT void JNICALL Java_org_example_Hello_vectorAdd
  (JNIEnv *env, jobject obj, jintArray a, jintArray b, jintArray c){
  	// 1.convert JNI jintArray to C jint[]
	jint *cA = env->GetIntArrayElements(a, NULL);
  	jint *cB = env->GetIntArrayElements(b, NULL);
  	jint *cC = env->GetIntArrayElements(c, NULL);
  	jint len = env->GetArrayLength(a);
	// 2.use array
	for(jint i=0; i<len; i++){
		cC[i] = cA[i] + cB[i];
	}
	// 3.release resources
	env->ReleaseIntArrayElements(a, cA, JNI_ABORT);
	env->ReleaseIntArrayElements(b, cB, JNI_ABORT);
	env->ReleaseIntArrayElements(c, cC, 0);
	return;
  }

函数依旧分为:转化数据、使用数据、释放资源三步。

Release函数第三个参数为mode,有三个备选值如下:

mode

行为

0

copy back the content and free the elems buffer

JNI_COMMIT

copy back the content but do not free the elems buffer

JNI_ABORT

free the buffer without copying back the possible changes

编译后执行得输出如下:

5 7 9

对象数组与基本类型数组的方法相似,但是没有Get<PrimitiveType>ArrayElements对数组元素进行批量转化,究其原因,Java对象不能直接转化为C/C++的对象。

使用如下两个函数,可以操作基本数据类型数组的直接指针,极大的加快程序运行的效率。但同时,我们要保证在调用ReleasePrimitiveArrayCritical函数之前,不能进行任何可能导致线程阻塞的操作。

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);

工程经验

c语言宏定义

观察javac -h生成的函数声明,我们会发现都是如下结构:

Java_包名_方法名
JNIEXPORT void JNICALL Java_org_example_Hello_jniMethod(...)
JNIEXPORT void JNICALL Java_org_example_Hello_vectorAdd(...)

当程序越来越复杂,我们调整Java的包结构,每一个地方都要修改。可以使用C++的宏替换来减少这种冗余如下:

#define FULL_FUNC_NAME(SHORT_NAME) Java_org_example_Hello_##SHORT_NAME

JNIEXPORT void JNICALL FULL_FUNC_NAME(jniMethod)(...)
JNIEXPORT void JNICALL FULL_FUNC_NAME(vectorAdd)(...)

将dll动态库打入jar包

首先我们要明白Java加载动态库的方式:

  1. System.load
    System.load加载绝对路径下的库文件,如:
System.load("D:\\workplace\\java2c.dll");
  1. System.loadLibrary
    System.loadLibrary加载相对路径下的库文件,参数为库文件名,不包含库文件的扩展名,如:
System.loadLibrary("java2c");

这里java2c.dll必须在java.library.path这一jvm变量指向的路径中。

可以通过如下方法来获得该变量的值:

System.getProperty("java.library.path");

默认情况下,在Windows平台下,该值包含如下位置:
1)和jre相关的一些目录
2)程序当前目录
3)Windows目录
4)系统目录(system32)
5)系统环境变量path指定目录

上述两个函数都是以路径作为参数的,但是jar包中的文件没有路径,只能以文件流的形式获取。为了能将动态库文件打入jar包,并且能够顺利调用,我们的策略是将jar包中的动态库写入系统临时目录下,再调用System.load载入,参考代码如下:

static {
    // copy .so from jar to syetem tmp dir
    String libName = "java2c.dll";
    String nativeTempDir = System.getProperty("java.io.tmpdir");
    File extractedLibFile = new File(nativeTempDir + File.separator + libName);
    InputStream in = null;
    BufferedInputStream reader = null;
    FileOutputStream writer = null;
    if(!extractedLibFile.exists()){
        try{
            in = Level2.class.getClassLoader().getResourceAsStream(libName);
            reader = new BufferedInputStream(in);
            writer = new FileOutputStream(extractedLibFile);
            byte[] buffer = new byte[1024];
            while(reader.read(buffer) > 0){
                writer.write(buffer);
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally {
            if (in != null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (writer != null){
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    System.load(extractedLibFile.toString());
}

参考链接

JNI demo图文

JNI 完全指南文档

JNI 官方文档

JNI 从零开始详细教程