JNI技术经验总结

JNI技术经验总结

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

JNI工作流程

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

  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();
        }
    }
    
  2. 在Hello.java所在目录,打开cmd命令行,编译出Hello.h头文件。(也一并编译出了Hello.class文件)

    javac -encoding utf-8 -h . Hello.java
    

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

  3. 为了简化较长的.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
      
    3. 清空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.");
        }
      
  4. 编译生成.dll库文件。

    dll.h的第一行,引入了jni.h头文件。在java安装目录下的include文件夹,找到jni.h文件和jni_md.h。将它们复制到dev-c++项目的 工具->编译选项->目录->C++包含文件 中的任一目录下。
    请添加图片描述

    点击编译,生成hello_c++.dll文件(hello_c++为项目名),将其重命名为java2c.dll(与1中的java源代码保持一致),移动到Hello.java所在的文件夹。

  5. 编译Hello.java生成Hello.class字节码,其实在第2步已经顺带完成。

    javac -encoding utf-8 Hello.java
    
  6. 在Hello.class目录下,打开cmd命令行,执行字节码程序。

    java Hello
    

    请添加图片描述

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

基本数据

Java类型JNI类型描述
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidvoid

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项目,其目录结构如下:

在这里插入图片描述

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行为
0copy back the content and free the elems buffer
JNI_COMMITcopy back the content but do not free the elems buffer
JNI_ABORTfree 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");
    
  2. 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 从零开始详细教程

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值