多语言编程之 C and Java:JNI 30 分钟入门

1 引言

1.1 简介

在实际项目中,很多时候是以多种语言混合开发的。常用的模式是使用 C/C++ 做底层支撑,然后上层辅之以高级语言。 JNI可以使 native 代码与 Java 对象交互,而不会像 Java 代码中的功能那样受到诸多限制, 往往充作 Java 与 C/C++/Assembly 语言的粘合剂。

JNI 明确分开了 Java 代码与 native 代码的执行,为两者的通信定义了一套清晰的API.避免 native 代码对 JVM 的直接内存引用。

2 JNI 入门

2.1 JNI 调用 C

2.1.1 java 中的 native 方法声明

HelloJNI.java

1
2
3
4
5
6
7
8
9
10
11
public class HelloJNI{
static{
System.loadLibrary("hello"); //运行时加载本地库 hello.dll(Windows)或libhello.so(Unixes) 或 libhello.dylib(MacOX)
}

private native void sayHello(); //声明 native 方法

public static void main(String[] args ){
new HelloJNI().sayHello(); //调用本地方法
}
}

当HelloJNI类加载的时候,静态初始化调用 System.loadLibrary() 来加载本地库 hello.dll(或libhello.so)。如果不能加载 hello 库,会抛出 UnsatisfiedLinkError 异常。
然后我们使用 native 关键字声明了 native 方法 sayHello() ,这表明该方法是使用其它语言实现的。
最后 main() 方法实例化了 HelloJNI 并调用了 sayHello().
然后我们编译这个类:

1
> javac HelloJNI.java

2.1.2 生成 C/C++ 头文件

使用 javah 创建头文件:

1
> javah HelloJNI

会得到如下 .h 文件 HelloJNI.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 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
#endif

这份自动生成的头文件里声明一个 C 函数 Java_HelloJNI_sayHello:

1
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

这个方法名是由javah转换来的,转换规则为:

1
Java_{package_and_classname}_{function_name}(JNI arguments)

包名中的 点 会被转换为 下划线
函数中带有两个参数(虽然我们在本例中并没有使用这两个参数)

  • JNIEnv*: 指向 JNI environment ,提供对 JNI 方法的访问

  • jobject: 指向 this Java 对象

2.1.3 C 代码的实现

HelloJNI.c

1
2
3
4
5
6
7
8
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv * env, jobject obj)
{
printf("Hello World!");
}

这个实现将在控制台打印万能的 “Hello World!”
文件中包含了 jni.h 头文件。这个头文件由 java 提供,位于 %JAVA_HOME%/include 路径下(Windows下还需要引用 %JAVA_HOME%/include/win32).

2.1.4 编译 C 库

接下来,编译这个 c 文件:

1
2
3
4
5
6
#Windows gcc
> gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c

#Windows VS cl.exe link.exe
> cl /c /I%JAVA_HOME%/include /I%JAVA_HOME%/include/win32 HelloJNI.c
> link HelloJNI.obj /DLL /OUT:hello.dll

2.1.5 检查成果

1
2
3
> java HelloJNI
#or
> java -Djava.library.path=. HelloJNI

3 在 Java 和 Native 间传递参数

3.1 传递原始类型参数

Java 原始参数可以在 Java 和 Native 单直接传递,只不过这些类型在JNI中的名称不一样。JNI 定义了 jxxx 类型来对应相应的原始类型。如 jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean 分别对应 Java 中的 int, byte, short, long, float, double, char, boolean. 下例中,将演示如何传递 Java 原始参数。这一次,我们编辑的顺序将从 native 到 Java。

3.1.1 编辑 C 头文件

打开 HelloJNI.h,在文件中添加函数的声明:

1
JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv*, jobject, jint, jint);

这里用到了 jint 。我们可以在 jni.h 和 jni_md.h 中找到各原始类型的定义:

1
2
3
4
5
6
7
8
9
10
11
12
// In "jni_md.h"
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;

// In "jni.h"
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;

3.1.2 实现函数

在 HelloJNI.c 中实现上一步声明的函数:

1
2
3
4
JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv* env, jobject obj, jint a , jint b)
{
return (a + b);
}

3.1.3 编辑 Java 文件

在Java中声明 native 方法并调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HelloJNI{
static{
System.loadLibrary("hello");
}

private native void sayHello();

private native int add(int a, int b);

public static void main(String[] args ){
HelloJNI hj = new HelloJNI();
hj.sayHello();
System.out.println("the result of 3 add 4 is: " + hj.add(3,4));
}
}

3.1.4 编译并执行

像上一章那样,编译 HelloJNI.c 和 HelloJNI.java 并执行 Java 程序,结果如下:

1
2
Hello World!
the result of 3 add 4 is: 7

3.2 传递 String 参数

3.2.1 示例

Java 代码

1
private native String sayHi(String name);

C 函数声明

1
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv *, jobject, jstring);

JNI 使用 jstring 来表示 Java String. 最后一个参数 jstring 是 Java 传递到 native 的 String 参数

C 函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv * env, jobject obj , jstring str)
{
// jstring 转换为 C-string
const char* pcsz = (*env)->GetStringUTFChars(env, str, NULL);
if (NULL == pcsz)
return NULL;

char szMsg[256];
sprintf(szMsg,"say hi to %s from C native",pcsz);

// 释放内存
(*env)->ReleaseStringUTFChars(env, str, pcsz);

// C-string 转换为 jstring
return (*env)->NewStringUTF(env, szMsg);
}

一般使用 GetStringUTFChars() 将接收到的 jstring 转换为 C-string ; 使用 NewStringUTF 将 C-string 转换为 jstring 返回给 Java

Java String 和 C-string 不同,是一个复杂的引用类型,在传递过程中需要将字符串在这两种类型间转换。JNIEnv*(JNI Enviroment) 提供了转换的方法:

1
2
3
4
5
// jstring to C-string
const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)

// C-string to jstring
jstring NewStringUTF(JNIEnv*, char*)

GetStringUTFChars() 函数将从第二个参数 jstring 参数中返回 C-string(char*)。如果有内存分配错误,则返回NULL. 函数的第三个参数 isCopy 可以设置为 JNI_TRUE 或 JNI_FALSE . 如果是 JNI_TRUE, 则返回Java String 的副本的 C-string. 如果是 JNI_FALSE,则直接返回 Java String 参数的 C-string(char*) 指针,此时不要改变返回值的内容。
ReleaseStringUTFChars() 函数将用来释放 GetStringUTFChars() 的返回值。只要 GetStringUTFChars() 的返回值不再被使用时,它应该被立即释放以便垃圾回收器介入处理。
NewStringUTF() 用来从 C-string 创建 jstring.

3.2.2 JNI String 操作函数

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
// 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 modified UTF-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 in modified UTF-8 encoding.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
// Returns the length in bytes of the modified UTF-8 representation of a string.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
// Translates len number of Unicode characters beginning at offset start into modified 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 Unicode characters.
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 the given buffer buf

更多的 JNI 函数可参考这里

3.3 传递原始类型数组

3.3.1 示例

Java 代码

1
private native double[] sumAndAvg(int[] nums);

JNI C 函数声明

1
JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *, jobject, jintArray);

JNI C 函数实现

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
JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *env, jobject obj, jintArray nums)
{
//获取 C 数组
jint* pcArray = (*env)->GetIntArrayElements(env,nums,NULL);
if(pcArray == NULL)
return NULL;

//数组长度
jsize len = (*env)->GetArrayLength(env,nums);

jint sum = 0;
for(int i = 0; i < len; i++)
{
sum += pcArray[i];
}

jdouble avg = (jdouble)sum / len;

//释放内存
(*env)->ReleaseIntArrayElements(env,nums, pcArray, 0);

jdouble outArray[] = {sum,avg};

//C 数组转为 Java 数组
jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2);
if(outArray == NULL)
return NULL;
(*env)->SetDoubleArrayRegion(env, outJNIArray, 0, 2, outArray);

return outJNIArray;
}

3.3.2 JNI 原始类型数组操作函数

JNI 原始数据类型的数组(jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray and jbooleanArray)操作函数如下:

1
2
3
4
5
6
7
8
9
10
// 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);
  • GetArrayElements() 从 Java 数组创建 C 数组

  • Get|SetArrayRegion()将从 Java 数组和 C 数组中来回拷贝数据

  • NewArray() 分配新的 Java 数组内存

  • GetPrimitiveArrayCritical() 得到原始数据类型内容的指针,可能使得垃圾回收器不回收此内存,直到 ReleasePrimitiveArrayCritical() 调用

4 附录

4.1 本文源码

  • HelloJNI.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HelloJNI{
static{
System.loadLibrary("hello");
}

private native void sayHello();

private native int add(int a, int b);

private native String sayHi(String name);

private native double[] sumAndAvg(int[] nums);

public static void main(String[] args ){
HelloJNI hj = new HelloJNI();
hj.sayHello();
System.out.println("the result of 3 add 4 is: " + hj.add(3,4));
System.out.println("say hi to wandoer: " + hj.sayHi("wandoer.com"));

int[] nums = new int[]{3,4};
double[] dArr = hj.sumAndAvg(nums);
System.out.println("the sum and avg of 3 and 4 is :" + dArr[0] + " and " + dArr[1]);
}
}
  • HelloJNI.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* 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);

/*
* Class: HelloJNI
* Method: add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv *, jobject, jint, jint);

/*
* Class: HelloJNI
* Method: sayHi
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv *, jobject, jstring);

/*
* Class: HelloJNI
* Method: sunAndAvg
* Signature: ([I)[D
*/
JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *, jobject, jintArray);

#ifdef __cplusplus
}
#endif
#endif
  • HelloJNI.c
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv * env, jobject obj)
{
printf("Hello World!\n");
}

JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv* env, jobject obj, jint a , jint b)
{
return (a + b);
}

JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv * env, jobject obj , jstring str)
{
// jstring 转换为 C-string
const char* pcsz = (*env)->GetStringUTFChars(env, str, NULL);
if (NULL == pcsz)
return NULL;

char szMsg[256];
sprintf(szMsg,"say hi to %s from C native",pcsz);

// 释放内存
(*env)->ReleaseStringUTFChars(env, str, pcsz);

// C-string 转换为 jstring
return (*env)->NewStringUTF(env, szMsg);
}

JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *env, jobject obj, jintArray nums)
{
//获取 C 数组
jint* pcArray = (*env)->GetIntArrayElements(env,nums,NULL);
if(pcArray == NULL)
return NULL;

//数组长度
jsize len = (*env)->GetArrayLength(env,nums);

jint sum = 0;
for(int i = 0; i < len; i++)
{
sum += pcArray[i];
}

jdouble avg = (jdouble)sum / len;

//释放内存
(*env)->ReleaseIntArrayElements(env,nums, pcArray, 0);

jdouble outArray[] = {sum,avg};

//C 数组转为 Java 数组
jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2);
if(outArray == NULL)
return NULL;
(*env)->SetDoubleArrayRegion(env, outJNIArray, 0, 2, outArray);

return outJNIArray;
}

4.2 参考