Java通过JNI调用C/C++动态链接库之参数传递及结果返回

接上一篇关于JNI的基本入门Java通过JNI调用C/C++动态链接库之HelloWorld,简述给JNI函数传递参数,以及通过JNI函数返回值的方式。

JNI类型映射

首先来回顾一下上一篇基础文章中通过javah生成的头文件的以下内容:

1
2
3
4
5
6
7
/*
* Class: hello
* Method: helloWorld
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_hello_helloWorld
(JNIEnv *, jobject);

发现注释中给出了类名hello,是我们JAVA程序的类名,后面跟着方法名以及方法的签名,注意方法的签名类型为()V,表示该方法不接收任何参数,且返回类型为void,即没有返回值。
JNI定义了一些基本类型用于在java与native代码之间的映射,这些基本类型都是j开头,后面跟java中的实际类型,具体的基本类型及方法签名类型如下表所示:

Native类型 Java类型 描述 签名类型
unsigned char jboolean unsigned 8 bits Z
signed char jbyte signed 8 bits B
unsigned short jchar unsigned 16 bits C
short jshort signed 16 bits S
long jint signed 32 bits I
long long __int64 jlong signed 64 bits J
float jfloat 32 bits F
double jdouble 64 bits D
void V

方法签名的形式即为(参数签名)返回值类型签名。例如一个形如double func(int a, int b)的方法签名为(II)D
另外还有几个比较特殊的签名,如object签名为L{用/分割的完整类名}Ljava/lang/String表示一个String对象。Array的签名为[{相应类型的签名},如[D,表示类型为D的数组,[Ljava/lang/String表示String对象的数组。
当然JNI也定义了很多引用类型的映射,其中jobject对应于java.lang.Object,以及其之类型:

  • jclassjava.lang.Class
  • jstringjava.lang.String
  • jthrowablejava.lang.Throwable
  • jarray与java中的Array对应,由于Array在java中支持8种基本数据类型,所以相应的有8种Array,分别为jintArrayjbyteArrayjshortArrayjlongArrayjfloatArrayjdoubleArrayjcharArrayjbooleanArray。以及一个对象类型的Array:jobjectArray

查看jni.h头文件会发现里面都是使用typedef进行的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 头文件"jni.h"中的声明
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;

struct _jobject;

typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;

//"linux/jni_mh.h"中的声明
typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif

typedef signed char jbyte;

现在可以清楚的知道JNI在java代码和native代码之间做了类型转换,以使两种语言之间的数据类型可以互通。

基本类型参数传递

举一个例子说明,程序的功能为给java程序传递两个int做为参数,将该参数传递给native函数,native函数累加两个参数之间的数字,然后取平均后将结果返回给java程序并输出。
java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//TestJint.java
public class TestJint
{
static {
System.loadLibrary("myjint"); //载入本地的动态链接库
}
private native double avg(int a, int b); //声明一个本地方法,接受2个int变量做为参数,返回其double类型的累加和的平均值
public static void main(String args[]) {
if (args.length < 2) {
System.out.println("Input Error!");
return;
}
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
System.out.println(a + " + ... + " + b + " / " + (b - a + 1) + " = " + new TestJint().avg(a, b));
}
}

头文件
通过javah生成头文件内容如下:
TestJint.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class TestJint */

#ifndef _Included_TestJint
#define _Included_TestJint
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: TestJint
* Method: avg
* Signature: (II)D
*/
JNIEXPORT jdouble JNICALL Java_TestJint_avg
(JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

可以看到其中的方法签名为(II)D,函数参数列表中多了2个jint类型的形参。

C++代码
注意其中传递进行的形参类型都是j开头的JNI类型,而在函数内部则可以使用任何你需要的C++数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
//TestJint.cpp
#include "TestJint.h"
#include <iostream>

JNIEXPORT jdouble JNICALL Java_TestJint_avg(JNIEnv *, jobject, jint a, jint b)
{
double res = 0.0;
for (int i = a; i <= b; ++i) {
res += i;
}
std::cout << "In C++, the sum " << a << " ... " << b << " = " << res << std::endl;
return res / double(b - a + 1);
}

编译并运行
编译动态链接库

1
g++ -fPIC -shared -o libmyjint.so -I /usr/lib/jvm/java-8-oracle/include/ -I /usr/lib/jvm/java-8-oracle/include/linux/ TestJint.cpp

编译java

1
javac TestJint.java

运行:

1
java -Djava.library.path=. TestJint 5 8

输出为:

1
2
 In C++, the sum 5 ... 8 = 26
5 + ... + 8 / 4 = 6.5

String类型的参数传递

String其实与基本类型的参数传递类似,只是需要通过JNIEnv参数调用调用jni中的函数进行转换成符合C/C++的字符串后再操作。因为java中的字符串是以unicode表示,而c/c++中是以utf方式表示。
举例说明,实现功能为通过向java程序传递字符串参数,然后该字符串传递给native函数,native函数将其反转后与在native函数中获取到的用户输入一起返回给java,并输出

java程序
如下,并没有太大区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// TestJstr.java
public class TestJstr
{
static {
System.loadLibrary("mystr"); //载入动态库
}
//native方法接受一个String对象,该String将由用户从java代码中以命令行参数方式输入
// 将返回一个C++库中用户输入的字符串加该字符串倒序字符串
private native String andReverse(String msg);
public static void main(String args[])
{
if (args.length < 1) {
System.out.println("Input Error!");
return;
}
String res = new TestJstr().andReverse(args[0]);
System.out.println("In Java, Result: " + res);
}
}

生成头文件
通过javah生成头文件,头文件中的函数声明如下:

1
2
3
4
5
6
7
/*
* Class: TestJstr
* Method: andReverse
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_TestJstr_andReverse
(JNIEnv *, jobject, jstring);

可以看到方法签名与我们之前的讨论一样,函数接收一个jstring参数

C++代码
注意里面的函数调用与oracle官方给的文档并不完全致,可以通过查看jni.h头文件来获取相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//TestJstr.cpp
#include "TestJstr.h"
#include <iostream>
#include <string>
#include <algorithm>

JNIEXPORT jstring JNICALL Java_TestJstr_andReverse(JNIEnv *env, jobject obj, jstring jStr)
{
//将Java的String转成C语言的char *
const char *cStr = env->GetStringUTFChars(jStr, NULL);
if (NULL == cStr) return NULL; //如果转换失败,则返回空

std::string tstr = cStr; //拷贝一份字符串

//释放cStr,如果不释放,该数组会一直存在,并且不会被自动回收
env->ReleaseStringUTFChars(jStr, cStr);

std::reverse(tstr.begin(), tstr.end()); //反转字符串

//在C++中获取用户输入的字符串
std::string ustr;
std::cout << "Enter a string in C++: ";
std::cin >> ustr;

//将c风格字符串转换成jstring并返回
return env->NewStringUTF(("Reverse: " + tstr + " C++ string: " + ustr).c_str());
}

注意调用方法为env->GetStringUTFChars(jStr, NULL);,官方文档中需要传递三个参数,是C风格的函数调用方式,C和C++的调用语法如下:

编译并运行
编译java代码:

1
javac TestJstr.java

编译C++代码:

1
g++ -fPIC -shared -o libmystr.so -I /usr/lib/jvm/java-8-oracle/include/ -I /usr/lib/jvm/java-8-oracle/include/linux/ TestJstr.cpp

运行:

1
java -Djava.library.path=. TestJstr HelloWorld

中间的输入部分,输入Maxwi,输出如下:

1
2
Enter a string in C++: Maxwi
In Java, Result: Reverse: dlroWolleH C++ string: Maxwi

完全正确运行

通过JNI传递数组

数组的传递同时是通过调用JNI函数进行转换后进行正常的操作即可,数组相关的JNI操作函数参见Oracle官网https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#array_operations
下面直接上代码,实现功能为从java中传递一个int数组到native中,native计算出其和以及平均值之后以数组形式返回。
java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//TestJarr.java
public class TestJarr
{
static {
System.loadLibrary("myjarr");
}

//该native函数接收一个int类型的数组,通过C++代码计算出其和以及平均值后以数组方式返回
private native double[] sumAndAvg(int[] num);

public static void main(String args[]) {
int[] num = {8, 19, 22, 45};
double[] result = new TestJarr().sumAndAvg(num);
System.out.println("In java, the sum is " + result[0]);
System.out.println("In java, the average is " + result[1]);
}
}

生成的头文件
只写函数:

1
2
3
4
5
6
7
/*
* Class: TestJarr
* Method: sumAndAvg
* Signature: ([I)[D
*/
JNIEXPORT jdoubleArray JNICALL Java_TestJarr_sumAndAvg
(JNIEnv *, jobject, jintArray);

C++代码
注意里面的转换,C++数组转成jintArray时需要先分配内存,再拷贝数据,而不是直接有函数进行转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//TestJarr.cpp
#include "TestJarr.h"

JNIEXPORT jdoubleArray JNICALL Java_TestJarr_sumAndAvg(JNIEnv *env, jobject obj, jintArray num)
{
//将输入的jintArray转换为C风格的数组
jint *carr = env->GetIntArrayElements(num, NULL);
if (NULL == carr) return NULL;
jsize length = env->GetArrayLength(num);

//对数组进行累加
jdouble sum = 0.0;
for (int i = 0; i < length; ++i)
sum += carr[i];

//求平均值
jdouble avg = sum / length;

//释放资源
env->ReleaseIntArrayElements(num, carr, 0);

//定义C++中的数组
jdouble resArr[] = {sum, avg};

//转换为JNI数组后返回
jdoubleArray outRes = env->NewDoubleArray(2); //分配数组所需要的空间
if(NULL == outRes) return NULL;
env->SetDoubleArrayRegion(outRes, 0, 2, resArr); //将数据拷贝到jdoubleArray所在空间
return outRes;
}

编译并运行

1
2
3
javac TestJarr.java
g++ -fPIC -shared -o libmyjarr.so -I /usr/lib/jvm/java-8-oracle/include/ -I /usr/lib/jvm/java-8-oracle/include/linux/ TestJarr.cpp
java -Djava.library.path=. TestJarr

运行结果:

1
2
In java, the sum is 94.0
In java, the average is 23.5

这些内容对于我当前已经够用,关于访问native访问java对象变量以及方法回调的内容请参见下面的参考文档。
以上这些例子都比较简单,实际上可以通过将复杂的计算放在C++函数中运行,最后通过native函数包裹该C++函数之后在JNI中调用,这样需要处理的内容会少很多,而且应该速度也会比较快,少去了那些过多的转换工作。

参考

1.Java Native Interface (JNI)
2.Wikipdeia-Java Native Interface
3.Oracle-JNI
4.Java programming with JNI