JNI--有点小烦

290 阅读19分钟

JNI实战教程

概述

jni全称java native interface。出于某些原因我们需要在java程序中调用C/C++程序,一般情况下我们通过jni调用。

前提

我们需要以下工具:

  • JDK11: 笔者采用了JDK11,其实其他版本也可以。主要是在命令上有些许不同。
  • MinGW: C/C++工具链采用的MinGW,主要用来把C/C++文件编译成DLL。

下面为非必须:

  • IntelliJ IDEA: 笔者版本为2021.
  • CLion: 笔者版本为2021.2.

工作流程

图片来自csdn

SayHello

秉承计算机届的光荣传统,第一个程序就输出"Hello world"。

  • Java端

    可以很明显的发现,我们这里使用了native关键词,它就是用来标识这个借口是用来调用C++代码的。下面代码的静态代码块在这个类被类加载器加载的时候调用了System.loadLibrary()方法来加载一个native库“hello”(这个库中实现了sayHello函数)。这个库在windows品台上对应了“hello.dll”,而在类UNIX平台上对应了“libhello.so”。这个库应该包含在Java的库路径(使用java.library.path系统变量表示)上,否则这个上面的程序会抛出UnsatisfiedLinkError错误。你应该使用VM的参数-Djava.library.path=path_to_lib来指定包含native库的路径。

public class HelloJni {
    static {
        System.loadLibrary("hello");
    }
    private native void sayHello();
    public static void main(String[] args) {
        new HelloJni().sayHello();
    }
}

接下来我们使用javac -h命令来生成 .h 文件。使用如下 -h 后面的点表示生成的文件存放在当前目录。

javac -h. .\HelloJni.java

如果不出意外我们就会生成一个.h文件,内容如下。可以看到就是一个普普通通的头文件。注意开头引入的jni.h文件在你java安装目录的 inlcude文件夹下面。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJni */
#ifndef _Included_HelloJni
#define _Included_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJni
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJni_sayHello(JNIEnv *, jobject);
    #ifdef __cplusplus
}
#endif
#endifJNIEXPORT void JNICALL Java_HelloJni_sayHello(JNIEnv *, jobject);
  • 将java的native方法转换成C函数声明的规则是这样的:Java {package_and_classname} {function_name}(JNI arguments)。包名中的点换成单下划线。需要说明的是生成函数中的两个参数:

    1. JNIEnv *:这是一个指向JNI运行环境的指针,后面我们会看到,我们通过这个指针访问JNI函数
    1. jobject:这里指代java中的this对象下面我们给出的例子中没有使用上面的两个参数,不过后面我们的例子会使用的。到目前为止,你可以先忽略JNIEXPORT和JNICALL这两个玩意。上面头文件中有一个extern “C”,同时上面还有C++的条件编译语句,这么一来大家就明白了,这里的函数声明是要告诉C++编译器:这个函数是C函数,请使用C函数的签名协议规则去编译!因为我们知道C++的函数签名协议规则和C的是不一样的,因为C++支持重写和重载等面向对象的函数语法。
  • C++端

新建一个C++文件然后实现如上的头文件即可。代码如下

#include "HelloJni.h"
#include <iostream>
using namespace std;
JNIEXPORT void JNICALL Java_HelloJni_sayHello(JNIEnv *, jobject) {
    cout << "hello world" << endl;
}

然后我们使用如下命令编译C++文件:

g++ -I "D:\JDK11\include" -I "D:\JDK11\include\win32" -shared -o hello.dll
.\HelloJNI.cpp

执行完上述命令之后,我们便会dll文件。注意上述命令以及命令中的文件皆在%JAVA_HOME%\JDK11\include下执行的。得到dll文件我们可以选择就放在此目录下,然后在java程序启动的时候添加虚拟机参数如下即可:

-Djava.library.path=D:\JDK11\include

笔者直接在idea的启动配置中添加了此参数。

image-20220822170632975

然后我们直接运行HelloJNI.java,输出如下:

image-20220822170648740

至此我们算是成功完成了第一步,走通了这个流程。接下来笔者会用各种工具自动化这个流程。

IDEA,CLion配置JNI开发环境

  • IDEA配置

添加External Tool

File-->Setting-->tools-->External Tool

点击添加,然后填入如下参数:

Program:D:\JDK11\bin\javac.exe

Arguments:-h .\header FileNameFileName

Working directory: FileDirFileDir

image-20220822170824851

接着我们右击想要生成.h文件的java文件,这样我们就可以一键生成且生成文件保存在当前目录的header文件下:

image-20220822170850997

  • CLion配置

首先C/C++工具链的设置

image-20220822170919635

接着工程目录结构如下:

image-20220822170931988

然后设置Cmake文件,最外层配置如下

cmake_minimum_required(VERSION 3.20)
project(jni_test) 
set(CMAKE_CXX_STANDARD 20)
//这里加载jni.h需要配置好JAVA_HOME
find_package(JNI REQUIRED) 
include_directories(${JNI_INCLUDE_DIRS})
//头文件存储的地方
include_directories(header) 
//子目录
add_subdirectory(src)

src下Cmake配置如下:

//jni.cpp为我们接下来编写例子的文件 
add_library(jni SHARED jni.cpp)

上述流程可能存在不详细的地方,详细过程参考这篇文章,笔者也是按照此文章配置的工程。

JNI基础知识

上面我们简单演示了怎么使用JNI,现在我们来系统梳理一下JNI中涉及的基本知识。JNI定义了以下数据类型,这些类型和Java中的数据类型是一致的:

Java原始类型:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean这些分别对应这java的int, byte,short, long, float, double, char and boolean。

Java引用类型:jobject用来指代java.lang.Object,除此之外,还定义了以下子类型:

  • jclass for java.lang.Class.
  • jstring for java.lang.String.
  • jthrowable for java.lang.Throwable.
  • jarray对java的array。java的array是一个指向 8 个基本类型array的引用类型。于是,JNI中就有 8 个基本类型的array:jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray,jcharArray 和 jbooleanArray,还有一个就是指向Object的jobjectarray。Native函数会接受上面类型的参数,并且也会返回上面类型的返回值。然而,本地函数(C/C++)是需要按照它们自己的S方式处理类型的(比如C中的string,就是char *)。因此,需要在JNI类型和本地类型之间进行转换。通常来讲,本地函数需要:

    • 加收JNI类型的参数(从java代码中传来)
    • 对于JNI类型参数,需要讲这些数据转换或者拷贝成本地数据类型,比如讲jstring转成char *,jintArray转成C的int[]。需要注意的是,原始的JNI类型,诸如jint,jdouble之类的不用进行转换,可以直接使用,参与计算。
    • 进行数据操作,以本地的方式
    • 创建一个JNI的返回类型,然后讲结果数据拷贝到这个JNI数据中
    • returnJNI类型数据

这其中最麻烦的事莫过于在JNI类型(如jstring, jobject, jintArray, jobjectArray)和本地类型(如C-string, int[])之间进行转换这件事情了。不过所幸的是,JNI环境已经为我们定义了很多的接口函数来做这种烦人的转换。(译者注:这里就需要使用上面我们提到的JNIEnv*那个参数了!)

类型转换

Java数据分为基本数据类型和引用数据类型,JNI层也是区别对待这两种类型的。下面是基本数据类型的

类型转换

JavaNative类型符号类型字长
booleanjboolean无符号8位
bytejbyte无符号8位
charjchar无符号16位
shortjshort有符号16位
intjint有符号32位
longjlong有符号64位
floatjfloat有符号32位
doublejdouble有符号64位

Java引用类型转换

Java引用类型Native类型Java引用类型Native类型
All objectjobjectchar[ ]jcharArray
java.lang.Class 实例jclassshort[ ]jshortArray
java.lang.String 实例jstringint[ ]jintArray
Object[ ]jobjectArraylong[ ]jlongArray
boolean[ ]jbooleanArrayfloat[ ]jfloatArray
byte[ ]jbyteArraydouble[ ]jboubleArray
java.lang.Throwable 实例jthrowable

JNI 签名

因为Java支持方法重载,所以native层调用Java层方法时需要方法的签名,这样来唯一找到目标函数。

类型标识示意表

image-20220822172551598

几个函数签名例子

image-20220822172614120

Java和Native代码之间传递参数和返回值

传递int 返回double

java代码

//java
private native double average(int n1, int n2);
//main中代码
System.out.println("transfer int :In Java, the average is " + new
TestJNIPrimitive().average( 3 , 2 ));

头文件

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

这里解释一下头文件注释。

  • testprimitive_TestJNIPrimitive : TestJNIPrimitive对应java中的类名 testprimitive表示包名
  • average: java中的方法名
  • (II)D: II表示入参为两个int类型,D表示返回为double类型。

C++实现代码

JNIEXPORT jdouble JNICALL Java_testprimitive_TestJNIPrimitive_average(JNIEnv *env, jobject obj, jint a, jint b) {
    cout << "In C++ " << a << "--" << b << endl;
    //注意这里我们将类型转为jdoule然后返回,java只能接受再类型转换中对应的类型。
    return ((jdouble)a + b) / 2.0;
}

由于上面配置了Clion因此我们直接使用Clion即可完成编译工作,将dll放到java项目中:

image-20220822172846442

运行效果如下:

image-20220822172904428

传递字符串

java代码

private native String transString(String str);
//main中代码
System.out.println("transfer string :In Java, the transString is " + new
TestJNIPrimitive().transString("hello native"));

头文件

/*
* Class: testprimitive_TestJNIPrimitive
* Method: transString
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_testprimitive_TestJNIPrimitive_transString(JNIEnv *, jobject, jstring);

C++实现:

/*
* Class: testprimitive_TestJNIPrimitive
* Method: transString
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_testprimitive_TestJNIPrimitive_transString(JNIEnv *env, jobject obj, jstring str) {
    const char *inCStr = env->GetStringUTFChars(str, nullptr);【 1if (nullptr == inCStr) {
        return nullptr;
    }
    cout << "In c,the received string is: " << inCStr << endl;【 2// 释放内存
    env->ReleaseStringUTFChars(str, inCStr);【 3string outCppStr;
    cout << "Enter a String";
    cin >> outCppStr;
    return env->NewStringUTF(outCppStr.c_str());
}

执行效果如下:

In c,the received string is: 0xe8130ff
Enter a String
transfer stringIn Java, the transString is 1231

【 1 】:将jstring转为char*

【 2 】:打印字符串

【 3 】:释放内存

JNI定义了jstring类型应对java的String类型。上面声明中的最后一个参数jstring就是来自Java代码中的String参数,同时,返回值也是一个jstring类型。传递一个字符串比传递基本类型要复杂的多,因为java的String是一个对象,而C++的string是一个NULL结尾的char数组。因此,我们需要将Java的String对象转换成C的字符串表示形式:char*。前面我们提到,JNI环境指针JNIEnv * 调用const char*GetStringUTFChars(JNIEnv , jstring, jboolean ) 来将JNI的jstring转换成C的char 调用 jstringNewStringUTF(JNIEnv , char)* 来将C的char *转换成JNI的jstring因此我们的C程序基本过程如下:使用GetStringUTFChars()函数来将jstring转换成char *,然后进行需要的数据处理使用NewStringUTF()函数来将char *转换成jstring,并且返回

JNI中的string转换函数

上面我们展示了两个函数,现在我们全面梳理下JNI为我们提供的函数。JNI支持Unicode(16bit字符)和UTF-8(使用1~3字节的编码)转化。一般而言,我们应该在C/C++中使用UTF-8的编码方式。JNI系统提供了如下关于字符串处理的函数(一共两组,UTF8和Unicode):

// UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)
// Can be mapped to null-terminated char-array C-string
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
// Returns a pointer to an array of bytes representing the string in modifiedUTF-8 encoding.
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
// Informs the VM that the native code no longer needs access to utf.
jstring NewStringUTF(JNIEnv *env, const char *bytes);
// Constructs a new java.lang.String object from an array of characters inmodified UTF-8 encoding.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
// Returns the length in bytes of the modified UTF-8 representation of astring.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length,char *buf);
// Translates len number of Unicode characters beginning at offset start intomodified UTF-8 encoding
// and place the result in the given buffer buf.
    // Unicode Strings (16-bit character)
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
// Returns a pointer to the array of Unicode characters
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
// Informs the VM that the native code no longer needs access to chars.
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);
// Constructs a new java.lang.String object from an array of Unicodecharacters.
jsize GetStringLength(JNIEnv *env, jstring string);
// Returns the length (the count of Unicode characters) of a Java string.
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar*buf);
// Copies len number of Unicode characters beginning at offset start to thegiven buffer buf

GetStringUTFChars()函数可以将jstring转成char *,这个函数会返回NULL,如果系统的内容分配失败的话。因此,好的做法是检查这个函数的返回是不是NULL。第三个参数是isCopy,这个参数是一个in-out参数,传进去的是一个指针,函数结束的时候指针的内容会被修改。如果内容是JNI_TRUE的话,那么代表返回的数据是jstring数据的一个拷贝,反之,如果是JNI_FALSE的话,就说明返回的字符串就是直接指向那个String对象实例的。在这种情况下,本地代码不应该随意修改string中的内容,因为修改会代码Java中的修改。JNI系统会尽量保证返回的是直接引用,如果不能的话,那就返回一个拷贝。通常,我们很少关心修改这些string ,因此我们这里一般传递NULL给isCopy参数。必须要注意的是,当你不在需要GetStringUTFChars返回的字符串的时候,一定记得调用ReleaseStringUTFChars()函数来将内存资源释放!否则会内存泄露!并且上层java中的GC也不能进行!另外,在GetStringUTFChars和ReleaseStringUTFChars不能block!NewStringUTF()函数可以从char *字符串得到jstring。关于更详细的描述,请参考Java Native Interface Specification:docs.oracle.com/javase/7/do…

传递基本类型的数组

java代码

private native double[] sumAndAverage(int[] numbers);
//main中代码
final double[] sumAndAverage = new TestJNIPrimitive().sumAndAverage(new int[]
{ 1 , 2 , 3 , 4 });
System.out.println("transfer double[] :In Java, the sumAndAverage sum: " +
sumAndAverage[ 0 ] + " average: " + sumAndAverage[ 1 ]);

头文件

/*
* Class: testprimitive_TestJNIPrimitive
* Method: sumAndAverage
* Signature: ([I)[D
*/
JNIEXPORT jdoubleArray JNICALL Java_testprimitive_TestJNIPrimitive_sumAndAverage
(JNIEnv *, jobject, jintArray);

C++实现

JNIEXPORT jdoubleArray JNICALL Java_testprimitive_TestJNIPrimitive_sumAndAverage(JNIEnv *env, jobject obj, jintArray jarry) {
    jint *inCarry = (*env).GetIntArrayElements(jarry, nullptr);
    if (nullptr == inCarry) {
   		return nullptr;
    }
    jsize length = (*env).GetArrayLength(jarry);
    jint sum = 0 ;
    for (int i = 0 ; i < length; i++) {
    	sum += inCarry[i];
    }
    jdouble avg = (jdouble)sum / length;
    (*env).ReleaseIntArrayElements(jarry, inCarry, 0 );
    jdouble outCArray[] = {(jdouble)sum, avg};
    jdoubleArray p_array = (*env).NewDoubleArray( 2 );
    if (p_array == nullptr) {
    	return nullptr;
    }
    (*env).SetDoubleArrayRegion(p_array, 0 , 2 , outCArray);
    return p_array;
}

执行效果如下:

transfer double[] :In Java, the sumAndAverage sum: 10.0 average: 2.

在Java中,array是指一种类型,类似于类。一共有 9 种java的array, 8 个基本类型的array和一个object的array。JNI针对java的基本类型都定义了相应的array:jintArray, jbyteArray, jshortArray, jlongArray,jfloatArray, jdoubleArray, jcharArray, jbooleanArray,并且也有面向object的jobjectArray。同样地,你需要在JNIarray和Native array之间进行转换,JNI系统已经为我们提供了一系列的接口函数:

  1. 使用jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)将jintarray转换成C的jint[]
  1. 使用jintArray NewIntArray(JNIEnv *env, jsize len)函数来分配一个len字节大小的空间,然后再使用void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)函数讲jint[]中的数据拷贝到jintArray中去。一共有 8 对类似上面的函数,分别对应java的 8 个基本数据类型。因此,native程序需要:
  1. 接受来自java的JNI array,然后转换成本地array
  1. 进行需要的数据操作
  1. 将需要返回的数据转换成jni的array,然后返回

JNI基本类型的array函数

JNI基本类型的array(jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray,jcharArray 和 jbooleanArray)函数如下:

// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray,jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean
NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array,jboolean *isCopy);

void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array,NativeType *elems, jint mode);

void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start,jsize length, NativeType *buffer);

void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start,jsize length, const NativeType *buffer);

ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);

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

同样地,在get函数和release函数之间也不能always block。

访问Java对象变量和回调Java方法

访问Java对象实例的变量和静态变量

java代码

//接口声明
private native void modifyInstanceVariable();
private static int staticNumber = 21 ;
private int number = 88 ;
private String message = "Hello from Java";
//main中代码
final TestJNIPrimitive jniPrimitive = new TestJNIPrimitive();
jniPrimitive.modifyInstanceVariable();
System.out.println("modifyInstanceVariable:In Java, int is " +jniPrimitive.number);
System.out.println("modifyInstanceVariable:In Java, String is " +jniPrimitive.message);
System.out.println("modifyInstanceVariable:In Java, static number is " +staticNumber);

头文件

/*
* Class: testprimitive_TestJNIPrimitive
* Method: modifyInstanceVariable
* Signature: ()V
*/
JNIEXPORT void JNICALL
Java_testprimitive_TestJNIPrimitive_modifyInstanceVariable
(JNIEnv *, jobject);

C++实现

JNIEXPORT void JNICALL
Java_testprimitive_TestJNIPrimitive_modifyInstanceVariable(JNIEnv *env, jobject obj) {
    jclass p_jclass = (*env).GetObjectClass(obj);
    jfieldID p_number = (*env).GetFieldID(p_jclass, "number", "I");
    if (NULL == p_number) {
    	return;
	}
    jint number = (*env).GetIntField(p_jclass, p_number);
    cout << "In c++ ,the int is " << number << endl;
    number = 99 ;
    (*env).SetIntField(obj, p_number, number);
    jfieldID messageId = (*env).GetFieldID(p_jclass, "message","Ljava/lang/String;");
    if (messageId == NULL) {
    	return;
    }
    auto message = (jstring)((*env).GetObjectField(obj, messageId));
    const char *cStr = (*env).GetStringUTFChars(message, NULL);
    if (cStr == nullptr) {
    	return;
	}
}

执行效果:

In c++ ,the int is 0
In c++,the string is Hello from Java
In c++ , sNumber is 21
modifyInstanceVariable:In Java, int is 99
modifyInstanceVariable:In Java, String is hello from c++
modifyInstanceVariable:In Java, static number is 233

为了访问对象中的变量,我们需要:

  1. 调用GetObjectClass()获得目标对象的类引用
  1. 从上面获得的类引用中获得Field ID来访问变量,你需要提供这个变量的名字,变量的描述符(也称为签名)。对于java类而言,描述符是这样的形式:“Lfully-qualified-name;”(注意最后有一个英文半角分号),其中的包名点号换成斜杠(/),比如java的Stirng类的描述符就是“Ljava/lang/String;”。对于基本类型而言,I代表int,B代表byte,S代表short,J代表long,F代表float,D代表double,C代表char,Z代表boolean。对于array而言,使用左中括号”[“来表示,比如“[Ljava/lang/Object;”表示Object的array,“[I”表示int型的array。
  1. 基于上面获得的Field ID,使用GetObjectField() 或者 Get_primitive-type_Field()函数来从中解析出我们想要的数据
  1. 使用SetObjectField() 或者 Set_primitive-type_Field()函数来修改变量JNI中用来访问实例变量的函数有:
  1. 访问类中的static变量类似于上面访问普通的实例变量,只是我们这里使用的函数是GetStaticFieldID(), Get|SetStaticObjectField(),

    jclass GetObjectClass(JNIEnv *env, jobject obj);
    // Returns the class of an object.
    jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
    // Returns the field ID for an instance variable of a class.
    NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
    
    void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeTypevalue);
    // Get/Set the value of an instance variable of an object
    // <type> includes each of the eight primitive types plus Object.
    

回调实例的普通和static方法

java

// Declare a native method that calls back the Java methods below
private native void nativeMethod();
// To be called back by the native code
private void callback() {
	System.out.println("In Java");
}

private void callback(String message) {
	System.out.println("In Java with " + message);
}

private double callbackAverage(int n1, int n2) {
	return ((double) n1 + n2) / 2.0;
}

// Static method to be called back
private static String callbackStatic() {
	return "From static Java method";
}

//main
final TestJNIPrimitive jniPrimitive1 = new TestJNIPrimitive();
jniPrimitive1.nativeMethod();

头文件

/*
* Class: testprimitive_TestJNIPrimitive
* Method: nativeMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_testprimitive_TestJNIPrimitive_nativeMethod(JNIEnv *, jobject);
JNIEXPORT void JNICALL Java_testprimitive_TestJNIPrimitive_nativeMethod(JNIEnv *env, jobject obj) {
	jclass p_jclass = env->GetObjectClass(obj);
}

c++实现

jmethodID callMethodId = env->GetMethodID(p_jclass, "callback", "()V");
    if (callMethodId == NULL) {
    	return;
    }
    cout << "callback() execute in c++" << endl;
    env->CallVoidMethod(obj, callMethodId);

    jmethodID callBackStr = env->GetMethodID(p_jclass, "callback", "(Ljava/lang/String;)V");

    jstring message = env->NewStringUTF("hello from c++");
    cout << " callBack(str) execute in c++:" << endl;
    env->CallVoidMethod(obj, callBackStr, message);

    jmethodID midCallBackAverage = env->GetMethodID(p_jclass,"callbackAverage", "(II)D");

    if (NULL == midCallBackAverage) return;
    jdouble average = env->CallDoubleMethod(obj, midCallBackAverage, 2 , 3 );
    cout << "In C++, the average is " << average << endl;

    jmethodID midCallBackStatic = env->GetStaticMethodID(p_jclass,"callbackStatic", "()Ljava/lang/String;");

    if (NULL == midCallBackStatic) return;
    jstring resultJNIStr = (jstring)env->CallStaticObjectMethod(p_jclass,
    midCallBackStatic);
    const char *resultCStr = env->GetStringUTFChars(resultJNIStr, NULL);
    if (NULL == resultCStr) return;
    cout << "In C++, the returned string is " << resultCStr << endl;
    env->ReleaseStringUTFChars(resultJNIStr, resultCStr);
}

执行效果

callback() execute in c++
In Java
callBack(str) execute in c++:
In Java with hello from c++
In C++, the average is 2.
In C++, the returned string is From static Java method

为了能够回调实例中的方法,我们需要:

  1. 通过GetObjectClass()函数获得这个实例的类对象
  1. 从上面获得类对象中,调用GetMethodID()函数来获得Method ID,Method ID表示了实例中的某个方法的抽象。你需要提供这个方法的名字和签名信息,签名规则和变量类似。签名的格式是这样的:(parameters)return-type。如果我们实在觉得jni的签名不好记忆的话,我们可以是用JDK为我们提供的工具javap来获得某个class类中的所有方法的签名,使用-s选项表示打印签名,-p表示显示private成员:

    image-20220822175054119

从上面的输出我们可以清楚地看到类中每一个方法的签名。

  1. 基于上面我们获得的Method ID,我们可以调用_Primitive-type_Method() 或者 CallVoidMethod()或者 CallObjectMethod()来调用这个方法。如果某个方法需要参数的话,就在后面跟上参数即可。
  1. 如果想要调用一个static方法的话,使用GetMethodID(), CallStatic_Primitive-type_Method(),CallStaticVoidMethod() 或者 CallStaticObjectMethod()。

JNI中用来回调实例和static方法的所有函数(两类,普通的和static的):

jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char*sig);
// Returns the method ID for an instance method of a class or interface.
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID,va_list args);
// Invoke an instance method of the object.
// The <type> includes each of the eight primitive and Object.
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, constchar *sig);
// Returns the method ID for an instance method of a class or interface.
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID,...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
// Invoke an instance method of the object.
// The <type> includes each of the eight primitive and Object.

回调复写的父类实例方法

JNI提供了一系列的形如 CallNonvirtual_Type_Method()之类的函数来调用父类实例的方法:

  1. 首先获得Method ID,使用GetMethodID()
  1. 基于上获得的Method ID,通过调用 CallNonvirtual_Type_Method()函数来调用相应的方法,并且在参数中给出object,父类和参数列表。JNI中用来访问父类方法的函数:

创建Object

在native代码中构造jobject和jobjectarray,通过调用NewObject() 和 newObjectArray()函数,然后将

它们返回给java代码

java

private native Integer getIntegerObject(int number);
//main
final TestJNIPrimitive jniPrimitive2 = new TestJNIPrimitive();
System.out.println("In Java, the number is :" +
jniPrimitive2.getIntegerObject( 9999 ));

头文件

/*
* Class: testprimitive_TestJNIPrimitive
* Method: getIntegerObject
* Signature: (I)Ljava/lang/Integer;
*/
JNIEXPORT jobject JNICALL Java_testprimitive_TestJNIPrimitive_getIntegerObject(JNIEnv *, jobject, jint);

c++实现

JNIEXPORT jobject JNICALL Java_testprimitive_TestJNIPrimitive_getIntegerObject(JNIEnv *env, jobject obj, jint number) {
	jclass cls = (*env).FindClass("java/lang/Integer");
    // Get the Method ID of the constructor which takes an int
    jmethodID midInit = (*env).GetMethodID(cls, "<init>", "(I)V");
    if (NULL == midInit) return NULL;
    // Call back constructor to allocate a new instance, with an int argument
    jobject newObj = (*env).NewObject(cls, midInit, number);
        // Try runnning the toString() on this newly create object
    jmethodID midToString = (*env).GetMethodID(cls, "toString", "()Ljava/lang/String;");
    if (NULL == midToString) return NULL;
    jstring resultStr = (jstring)(*env).CallObjectMethod(newObj, midToString);
    const char *resultCStr = (*env).GetStringUTFChars(resultStr, NULL);
    cout << "In C++: the number is " << resultCStr;
    return newObj;
}

执行效果

In Java, the number is :9999

JNI中用于创建对象(jobject)的函数有:

创建Object arrays

java

private native Integer getIntegerObject(int number);
//main
Integer[] numbers = { 11 , 22 , 32 };
Double[] results = new TestJNIPrimitive().sumAndAverageToo(numbers);
System.out.println("In Java, the sum is " + results[ 0 ]);
System.out.println("In Java, the average is " + results[ 1 ]);

头文件

/*
* Class: testprimitive_TestJNIPrimitive
* Method: getIntegerObject
* Signature: (I)Ljava/lang/Integer;
*/
JNIEXPORT jobject JNICALL Java_testprimitive_TestJNIPrimitive_getIntegerObject
(JNIEnv *, jobject, jint);

c++实现

JNIEXPORT jobject JNICALL Java_testprimitive_TestJNIPrimitive_getIntegerObject(JNIEnv *env, jobject obj, jint number) {
    jclass cls = (*env).FindClass("java/lang/Integer");
    // Get the Method ID of the constructor which takes an int
    
    jmethodID midInit = (*env).GetMethodID(cls, "<init>", "(I)V");
    if (NULL == midInit) return NULL;
    // Call back constructor to allocate a new instance, with an int argument
    
    jobject newObj = (*env).NewObject(cls, midInit, number);
    // Try runnning the toString() on this newly create object
    jmethodID midToString = (*env).GetMethodID(cls, "toString", "()Ljava/lang/String;");
    if (NULL == midToString) return NULL;
    
    jstring resultStr = (jstring)(*env).CallObjectMethod(newObj, midToString);
    const char *resultCStr = (*env).GetStringUTFChars(resultStr, NULL);
    cout << "In C++: the number is " << resultCStr;
    return newObj;
}

执行结果

In C++: the number is 9999In C++, the sum is 65
In C++, the average is 21.
In Java, the sum is 65.
In Java, the average is 21.

不像基本数据类型的array那样,你需要使用Get|SetObjectArrayElement()函数来处理每一个元素。

JNI提供了创建对象array(jobjectArray)的函数如下:

jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass,
jobject initialElement);
// Constructs a new array holding objects in class elementClass.
// All elements are initially set to initialElement.
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
// Returns an element of an Object array.
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject
value);
// Sets an element of an Object array.

JNA

Java Native Access ( JNA ) 是一个由社区开发的库,它使Java程序无需使用Java Native Interface即可轻松访问本地共享库。JNA的设计旨在以最少的努力以本地的方式提供本地访问,且不需要样板代码或胶水代码。从上面的文档可以看出JNI使用起来还是相当繁琐的。因此JNA就在此条件下孕育而出,但是JNA并不是银弹。它只能实现java调用c++反过来却不行。可以说它是JNI的替代者,是因为JNA大大简化了调用本地方法的过程,使用很方便,基本上不需要脱离Java环境就可以完成。JNA调用C/C++的过程大致如下:

image-20220822175756741

可以看到步骤减少了很多,最重要的是我们不需要重写我们的动态链接库文件,而是有直接调用的API,大大简化了我们的工作量。

2 、原理

JNA使用一个小型的JNI库插桩程序来动态调用本地代码。开发者使用java接口描述目标本地库的功能和结构,这使得它很容易利用本机平台的功能,而不会产生多平台配置和生成JNI代码的高开销。这样的性能、准确性和易用性显然受到很大的重视。此外JNA包括一个已与许多本地函数映射的平台库,以及一组简化本地访问的共用接口。

类型对照

image-20220822175845678

实战

java部分

public interface MYTestJNA extends Library {
    MYTestJNA INSTANCE =(MYTestJNA) Native.load("libtest-jna",
    MYTestJNA.class);
    void sayHello();
    int testReturnInt();
    void returnArray(double[] d, int[] a, String b, int length);
    void sendString(String val);
    void getString(PointerByReference val);
    void cleanup(Pointer p);
}

Java Test代码

@Test
void testJNA3() {
    MYTestJNA.INSTANCE.sayHello();
    System.out.println("return from c++: " +
    MYTestJNA.INSTANCE.testReturnInt());
    int[] a = { 1 , 2 , 3 , 4 };
    double[] d = new double[ 2 ];
    MYTestJNA.INSTANCE.returnArray(d, a, "lucky", 4 );
    for (double lucky : d) {
    	System.out.println(lucky);
    }
}

C++

void sayHello() {
cout << "hello jna" << endl;
}
int testReturnInt() {
return 1 ;
}
char *returnArray(double d[], int a[], const char *str, int length) {
    for (int i = 0 ; i < length; i++) {
    cout << "from c++ value is d" << a[i] << endl;
    }
    cout << "from C++ str value: " << str << endl;
    while (*str) {
        cout << *str++ << endl;
    }
    d[ 0 ] = 1.0;
    d[ 1 ] = 4.0;
    char *return_str = "hello from c++";
    return return_str;
}

执行效果

return from c++: 1
1.0
4.0
hello jna
from c++ value is d1
from c++ value is d2
from c++ value is d3
from c++ value is d4
from C++ str value: lucky
l
u
c
k
y

是不是感觉意外的简单,我当时也是这样的。但是随着深入也发现不少问题,比如当你想返回double[]的他就给报错。仔细信息jni也不是直接返回doule[],而且也涉及到资源释放问题。因此JNA虽然大大简化了native编程,但他也不是万能药。最终还是要二者结合使用。但是JNA本身封装好了大量C++本身的库,比如kernel32等等,如果有需求就可以直接拿来使用。详细的JNA使用方法,请看此链接

总结

一般需求使用JNA,特殊需求JNI。

\