JNI一文详解

本文主要介绍一下JNI。

1. 准备工作

  • Ubuntu更换阿里云软件源

  • 在Ubuntu上安装gcc和g++

  • 在Ubuntu上安装OpenJDK

    1
    sudo apt install default—jdk

    设置JAVA_HOME环境变量:找到OpenJDK的安装路径(e.g. /usr/lib/jvm/java—11—openjdk—amd64)

    1
    sudo vim /etc/profile

    在文件末尾添加

    1
    2
    export JAVA_HOME=/usr/lib/jvm/java—11—openjdk—amd64
    export PATH=$PATH:$JAVA_HOME/bin

    检查是否配置成功

    1
    2
    echo $JAVA_HOME
    echo $PATH

2. JNI简介

全称Java Native Interface,主要用于在Java代码中调用非Java代码,以此绕过Java的内存管理和解决Java的性能问题。

3. Getting Started

3.1 JNI with C

先看一下Java调用C的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HelloJNI {  // Save as HelloJNI.java
static {
System.loadLibrary("hello"); // Load native library hello.dll (Windows) or libhello.so (Unixes)
// at runtime
// This library contains a native method called sayHello()
}

// Declare an instance native method sayHello() which receives no parameter and returns void
private native void sayHello();

// Test Driver
public static void main(String[] args) {
new HelloJNI().sayHello(); // Create an instance and invoke the native method
}
}

关键在于System.loadLibrary(“hello”);这行代码,Java通过这行代码调用动态链接库(Windows中的后缀名是.dll,Unix系OS中是.so),在运行时Java文件时可以通过设置虚拟机参数—Djava.library.path=/path/to/lib将本地库引入。

通过下面的代码来编译Java代码并生成对应的头文件:

1
javac —h . HelloJNI.java

生成的头文件代码如下:

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—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

头文件中定义了一个函数:

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

依照惯例函数的命名规则是Java_{package_and_classname}_{function_name}(JNI_arguments)。

函数的两个参数的含义分别是:

  • JNIEnv*: reference to JNI environment, which lets you access all the JNI functions.
  • jobject: reference to “this“ Java object.

extern “C”只被C++编译器识别,用于提示编译器这些函数应当使用C的函数命名协议来编译,C和C++的函数命名协议不同,C++支持重载。

下面的代码是要被调用的C文件:

1
2
3
4
5
6
7
8
9
10
// Save as "HelloJNI.c"
#include <jni.h> // JNI header provided by JDK
#include <stdio.h> // C Standard IO Header
#include "HelloJNI.h" // Generated

// Implementation of the native method sayHello()
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
return;
}

jni.h是包含在JDK中的,不同OS平台的存放位置不同。

下面来编译和运行:

1
$ gcc —fPIC —I"$JAVA_HOME/include" —I"$JAVA_HOME/include/linux" —shared —o libhello.so HelloJNI.c
1
$ java —Djava.library.path=. HelloJNI

3.2 JNI with C++

将上一个模块中c文件替换成cpp文件:

1
2
3
4
5
6
7
8
9
10
11
// Save as "HelloJNI.cpp"
#include <jni.h> // JNI header provided by JDK
#include <iostream> // C++ standard IO header
#include "HelloJNI.h" // Generated
using namespace std;

// Implementation of the native method sayHello()
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
cout << "Hello World from C++!" << endl;
return;
}

编译:

1
$ g++ —fPIC —I"$JAVA_HOME/include" —I"$JAVA_HOME/include/linux" —shared —o libhello.so HelloJNI.cpp

运行:

1
java —Djava.library.path=. HelloJNI

4. JNI 基础

JNI中定义了一下两类数据类型和Java中的类型来对应:

  • 基础类型:

    jint—int
    jbyte—byte
    jshort—short
    jlong—long
    jfloat—float
    jdouble—double
    jchar—char
    jboolean—boolean

  • 引用类型:

    jobject—java.lang.Object

    jclass—java.lang.Class

    jstringjava.lang.String

    jthrowablejava.lang.Throwable

    jarray—Java array jarray包括八种基础类型(jintArray,jbyteArray,jshortArray,jlongArray,jfloatArray,jdoubleArray,jcharArrayandjbooleanArray)和一种引用类型(jobjectArray)

native程序要做的事是:

  1. 通过上面的JNI类型获取函数参数,这些参数都是从Java层传递过来的。
  2. 同样,JNI类型和native类型也有对应关系,例如:jstring对应c的string类型,jintArray对应c的int[]类型,jni中的基础类型像jint这种不需要转换,可以直接操作。
  3. 执行操作。
  4. 创建一个jni类型的返回对象,然后把返回值复制到这个对象中。
  5. 返回。

最困难的一步就是jni类型和native类型的转换,jni为此提供了很多的转换接口。

jni本质上是一个c的接口,它并不会真正传递对象。

5. 在Java和native之间传递参数和返回结果

5.1 传递基础类型

jni和java基础类型不需要转换,八种jni基础类型和java基础类型一一对应。

jni和c基础类型需要转换,在”jni.h“ 和”win32\jni_mh.h“中可以查看类型定义的声明:

1
2
3
4
5
6
7
8
9
10
11
12
// In "win\jni_mh.h" - machine header which is machine dependent
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;

需要注意的是:jint对应着c中的long(至少32位),而不是int(可以为16位)。

TestJNIPrimitive.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestJNIPrimitive {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}

// Declare a native method average() that receives two ints and return a double containing the average
private native double average(int n1, int n2);

// Test Driver
public static void main(String args[]) {
System.out.println("In Java, the average is " + new TestJNIPrimitive().average(3, 2));
}
}
TestJNIPrimitive.c:
1
2
3
4
5
6
7
8
9
10
11
12
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitive.h"

JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
(JNIEnv *env, jobject thisObj, jint n1, jint n2) {
jdouble result;
printf("In C, the numbers are %d and %d\n", n1, n2);
result = ((jdouble)n1 + n2) / 2.0;
// jint is mapped to int, jdouble is mapped to double
return result;
}
TestJNIPrimitive.cpp:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <jni.h>
#include <iostream>
#include "TestJNIPrimitive.h"
using namespace std;

JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
(JNIEnv *env, jobject obj, jint n1, jint n2) {
jdouble result;
cout << "In C++, the numbers are " << n1 << " and " << n2 << endl;
result = ((jdouble)n1 + n2) / 2.0;
// jint is mapped to int, jdouble is mapped to double
return result;
}

5.2 传递String类型

头文件中的函数定义:

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

在jni和c之间传递string类型比基本类型更加复杂,需要在jni类型jstring和c类型char*之间做转换。

针对这种转换,jni提供了两个函数:

  • jstring—>char*

    1
    const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)
  • char*—>jstring

    1
    jstring NewStringUTF(JNIEnv*, char*)

示例代码:

TestJNIString.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <jni.h>
#include <stdio.h>
#include "TestJNIString.h"

JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
// Step 1: Convert the JNI String (jstring) into C-String (char*)
const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL);
if (NULL == inCStr) return NULL;

// Step 2: Perform its intended operations
printf("In C, the received string is: %s\n", inCStr);
(*env)->ReleaseStringUTFChars(env, inJNIStr, inCStr); // release resources

// Prompt user for a C-string
char outCStr[128];
printf("Enter a String: ");
scanf("%s", outCStr); // not more than 127 characters

// Step 3: Convert the C-string (char*) into JNI String (jstring) and return
return (*env)->NewStringUTF(env, outCStr);
}

jni支持Unicode (16-bit characters) 和 UTF-8 (encoded in 1-3 bytes) 两种类型的string,UTF-8类似于c中的char array(从本质而言,C语言把字符串字面量作为字符数组来处理。当C语言编译器在程序中遇到长度为n的字符串字面量时,它会为字符串字面量分配长度为n+1的内存空间。这块内存空间将用来存储字符串字面量中的字符,以及一个用来标志字符串末尾的额外字符(空字符)。空字符是一个所有位都为0的字节,因此用转义序列\0来表示,空字符的码值为0),用于C/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
// 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

函数GetStringUTFChars()用于从一个java的jstring类型生成c的char*类型:

1
2
// Returns a pointer to an array of bytes representing the string in modified UTF-8                encoding.
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);

其中的第三个参数(类型为jboolean*), isCopy的值为JNI_TRUE的时候说明函数的返回值是原始的String实例的复制,为JNI_FALSE的时候则是指向原始实例的指针(这种情况下native层不可以修改字符串的内容)。一般来说jni优先返回一个指针,其次是返回一个复制实例。但是,我们很少修改基础字符串,所以第三个参数经常传递NULL指针。

ReleaseStringUTFChars()和GetStringUTFChars()必须成对出现,用来释放内存和引用,使对象可以被GC回收。

函数NewStringUTF()通过c的string创建一个新的jstring。

JDK1.2中引入了GetStringUTFRegion()可以用来替代GetStringUTFChars(),isCopy不再需要,因为这个函数直接将jstring复制到了c的char array中。

JDK 1.2中还引入了函数Get/ReleaseStringCritical(),这两个函数成对出现,且中间不能被阻塞。它的功能和GetStringUTFChars()类似,一般返回指针,如果失败的话,会返回一个复制实例。

示例代码:

TestJNIString.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <jni.h>
#include <iostream>
#include <string>
#include "TestJNIString.h"
using namespace std;

JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
// Step 1: Convert the JNI String (jstring) into C-String (char*)
const char *inCStr = env->GetStringUTFChars(inJNIStr, NULL);
if (NULL == inCStr) return NULL;

// Step 2: Perform its intended operations
cout << "In C++, the received string is: " << inCStr << endl;
env->ReleaseStringUTFChars(inJNIStr, inCStr); // release resources

// Prompt user for a C++ string
string outCppStr;
cout << "Enter a String: ";
cin >> outCppStr;

// Step 3: Convert the C++ string to C-string, then to JNI String (jstring) and return
return env->NewStringUTF(outCppStr.c_str());
}

注意c++和c的string函数语法不通,在c++中,我们可以用 “env->“代替”(env*)->“,此外,在c++函数中我们可以省略JNIEnv* 参数。c++支持string类,也支持c中的char array。

5.3 传递基础类型数组

TestJNIPrimitiveArray.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestJNIPrimitiveArray {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}

// Declare a native method sumAndAverage() that receives an int[] and
// return a double[2] array with [0] as sum and [1] as average
private native double[] sumAndAverage(int[] numbers);

// Test Driver
public static void main(String args[]) {
int[] numbers = {22, 33, 33};
double[] results = new TestJNIPrimitiveArray().sumAndAverage(numbers);
System.out.println("In Java, the sum is " + results[0]);
System.out.println("In Java, the average is " + results[1]);
}
}

生成的头文件中的函数定义如下:

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

在java中,数组是引用类型。有九种数组类型,包括8个基础类型和一个引用类型。jni分别对应地定义了8种基础类型数组,jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray,对应java中的8种基础类型:int, byte, short, long, float, double, char, boolean。jobjectArray对应引用类型数组。

和前面介绍的基础类型类似,需要在java类型和jni类型之间进行转换:

jni的jintArray—>c的jint[]

1
jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)

c的jint[]—>jni的jintArray

Step1 : 分配内存

1
jintArray NewIntArray(JNIEnv *env, jsize len)

Step2 : 从 jint[] 复制内容到 jintArray

1
void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)

上述的函数有八种,对应8个基础类型。

native代码需要做的事:

  1. 把jni数组转换成c的数组
  2. 执行操作
  3. 把c的数组转换成jni的数组,然后返回
TestJNIPrimitiveArray.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
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitiveArray.h"

JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage
(JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
// Step 1: Convert the incoming JNI jintarray to C's jint[]
jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL);
if (NULL == inCArray) return NULL;
jsize length = (*env)->GetArrayLength(env, inJNIArray);

// Step 2: Perform its intended operations
jint sum = 0;
int i;
for (i = 0; i < length; i++) {
sum += inCArray[i];
}
jdouble average = (jdouble)sum / length;
(*env)->ReleaseIntArrayElements(env, inJNIArray, inCArray, 0); // release resources

jdouble outCArray[] = {sum, average};

// Step 3: Convert the C's Native jdouble[] to JNI jdoublearray, and return
jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2); // allocate
if (NULL == outJNIArray) return NULL;
(*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2, outCArray); // copy
return outJNIArray;
}
jni基础类型数组函数
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);

GET|Release<*PrimitiveType*>ArrayElements() 用于从java的jxxxArray创建c的array jxxx[]

GET|Set<*PrimitiveType*>ArrayRegion() 用于将一个jxxxArray复制到预分配的c的 jxxx[],或者从c的 jxxx[]复制到jxxxArray中。

The New<PrimitiveType>Array() 用于给 jxxxArray 分配一个给定的大小,然后可以用Set<*PrimitiveType*>ArrayRegion() 从一个jxxx[]中获取内容填充 jxxxArray

Get|ReleasePrimitiveArrayCritical()成对出现,中间不允许被阻塞。

6. 获取java类的成员变量/方法和类静态变量/方法

6.1 获取类的成员变量

  1. 通过 GetObjectClass()获取对象的类的引用。

  2. 获取成员ID GetFieldID()。需要提供变量名和成员描述。格式如下:”L<fully-qualified-name>;“。用/代替.。例如:String - “Ljava/lang/String;“。

    对于基础类型:

    "I" for int,

    "B" for byte,

    "S" for short,

    "J" for long,

    "F" for float,

    "D" for double,

    "C" for char,

    "Z" for boolean

    对于数组:

    要加一个前缀"[",例如 Object array-“[Ljava/lang/Object;“,int array-"[I"

  3. 基于 Field ID, 通过 GetObjectField() or Get<primitive-type>Field() 获取成员变量。

  4. 使用 SetObjectField() or Set<primitive-type>Field() 修改成员变量。

示例代码:

TestJNIInstanceVariable.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestJNIInstanceVariable {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}

// Instance variables
private int number = 88;
private String message = "Hello from Java";

// Declare a native method that modifies the instance variables
private native void modifyInstanceVariable();

// Test Driver
public static void main(String args[]) {
TestJNIInstanceVariable test = new TestJNIInstanceVariable();
test.modifyInstanceVariable();
System.out.println("In Java, int is " + test.number);
System.out.println("In Java, String is " + test.message);
}
}
TestJNIInstanceVariable.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
#include <jni.h>
#include <stdio.h>
#include "TestJNIInstanceVariable.h"

JNIEXPORT void JNICALL Java_TestJNIInstanceVariable_modifyInstanceVariable
(JNIEnv *env, jobject thisObj) {
// Get a reference to this object's class
jclass thisClass = (*env)->GetObjectClass(env, thisObj);

// int
// Get the Field ID of the instance variables "number"
jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I");
if (NULL == fidNumber) return;

// Get the int given the Field ID
jint number = (*env)->GetIntField(env, thisObj, fidNumber);
printf("In C, the int is %d\n", number);

// Change the variable
number = 99;
(*env)->SetIntField(env, thisObj, fidNumber, number);

// Get the Field ID of the instance variables "message"
jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;");
if (NULL == fidMessage) return;

// String
// Get the object given the Field ID
jstring message = (*env)->GetObjectField(env, thisObj, fidMessage);

// Create a C-string with the JNI String
const char *cStr = (*env)->GetStringUTFChars(env, message, NULL);
if (NULL == cStr) return;

printf("In C, the string is %s\n", cStr);
(*env)->ReleaseStringUTFChars(env, message, cStr);

// Create a new C-string and assign to the JNI string
message = (*env)->NewStringUTF(env, "Hello from C");
if (NULL == message) return;

// modify the instance variables
(*env)->SetObjectField(env, thisObj, fidMessage, message);
}

获取成员变量的jni方法:

1
2
3
4
5
6
7
8
9
10
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, NativeType value);
// Get/Set the value of an instance variable of an object
// <type> includes each of the eight primitive types plus Object.

6.2 获取类静态变量

获取静态变量和实例变量类似,只是在调用的函数上有所差别,GetStaticFieldID(), Get|SetStaticObjectField(), Get|SetStatic<*Primitive-type*>Field()。

TestJNIStaticVariable.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestJNIStaticVariable {
static {
System.loadLibrary("myjni"); // nyjni.dll (Windows) or libmyjni.so (Unixes)
}

// Static variables
private static double number = 55.66;

// Declare a native method that modifies the static variable
private native void modifyStaticVariable();

// Test Driver
public static void main(String args[]) {
TestJNIStaticVariable test = new TestJNIStaticVariable();
test.modifyStaticVariable();
System.out.println("In Java, the double is " + number);
}
}
TestJNIStaticVariable.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <jni.h>
#include <stdio.h>
#include "TestJNIStaticVariable.h"

JNIEXPORT void JNICALL Java_TestJNIStaticVariable_modifyStaticVariable
(JNIEnv *env, jobject thisObj) {
// Get a reference to this object's class
jclass cls = (*env)->GetObjectClass(env, thisObj);

// Read the int static variable and modify its value
jfieldID fidNumber = (*env)->GetStaticFieldID(env, cls, "number", "D");
if (NULL == fidNumber) return;
jdouble number = (*env)->GetStaticDoubleField(env, cls, fidNumber);
printf("In C, the double is %f\n", number);
number = 77.88;
(*env)->SetStaticDoubleField(env, cls, fidNumber, number);
}

获取静态变量的jni函数:

1
2
3
4
5
6
7
jfieldID GetStaticFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
// Returns the field ID for a static variable of a class.

NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);
// Get/Set the value of a static variable of a class.
// <type> includes each of the eight primitive types plus Object.

6.3. 调用Java类的成员方法和类静态方法

调用成员方法的步骤:

step1:通过 GetObjectClass()获取对象的类的引用。

step2:GetMethodID()获取Method ID,提供方法名和签名,签名的格式是 “(parameters)return-type“。可通过javap utility (Class File Disassembler) with -s (print signature) and -p (show private members)这个命令来列出java类的方法签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> javap --help
> javap -s -p TestJNICallBackMethod
.......
private void callback();
Signature: ()V

private void callback(java.lang.String);
Signature: (Ljava/lang/String;)V

private double callbackAverage(int, int);
Signature: (II)D

private static java.lang.String callbackStatic();
Signature: ()Ljava/lang/String;
.......

step3:基于Method ID,调用CallMethod()orCallVoidMethod()orCallObjectMethod(),返回值类型分别是<*Primitive-type*>, void and Object,对于有参数的方法要提供参数。

示例代码:

TestJNICallBackMethod.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
25
26
27
28
29
30
31
public class TestJNICallBackMethod {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}

// 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";
}

// Test Driver
public static void main(String args[]) {
new TestJNICallBackMethod().nativeMethod();
}
}
TestJNICallBackMethod.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
#include <jni.h>
#include <stdio.h>
#include "TestJNICallBackMethod.h"

JNIEXPORT void JNICALL Java_TestJNICallBackMethod_nativeMethod
(JNIEnv *env, jobject thisObj) {

// Get a class reference for this object
jclass thisClass = (*env)->GetObjectClass(env, thisObj);

// Get the Method ID for method "callback", which takes no arg and return void
jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "callback", "()V");
if (NULL == midCallBack) return;
printf("In C, call back Java's callback()\n");
// Call back the method (which returns void), baed on the Method ID
(*env)->CallVoidMethod(env, thisObj, midCallBack);

jmethodID midCallBackStr = (*env)->GetMethodID(env, thisClass,
"callback", "(Ljava/lang/String;)V");
if (NULL == midCallBackStr) return;
printf("In C, call back Java's called(String)\n");
jstring message = (*env)->NewStringUTF(env, "Hello from C");
(*env)->CallVoidMethod(env, thisObj, midCallBackStr, message);

jmethodID midCallBackAverage = (*env)->GetMethodID(env, thisClass,
"callbackAverage", "(II)D");
if (NULL == midCallBackAverage) return;
jdouble average = (*env)->CallDoubleMethod(env, thisObj, midCallBackAverage, 2, 3);
printf("In C, the average is %f\n", average);

jmethodID midCallBackStatic = (*env)->GetStaticMethodID(env, thisClass,
"callbackStatic", "()Ljava/lang/String;");
if (NULL == midCallBackStatic) return;
jstring resultJNIStr = (*env)->CallStaticObjectMethod(env, thisClass, midCallBackStatic);
const char *resultCStr = (*env)->GetStringUTFChars(env, resultJNIStr, NULL);
if (NULL == resultCStr) return;
printf("In C, the returned string is %s\n", resultCStr);
(*env)->ReleaseStringUTFChars(env, resultJNIStr, resultCStr);
}

如果要调用静态方法,使用GetStaticMethodID(), CallStaticMethod(),CallStaticVoidMethod() or CallStaticObjectMethod()。

调用成员和静态方法的jni函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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, const char *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.

6.4. 调用父类的实例方法

CallNonvirtualMethod(),类似于java中的super.methodName()。

具体步骤:

step1:GetMethodID()

step2:CallNonvirtualMethod()

jni函数如下:

1
2
3
NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);

7. 创建java对象和对象数组

在native中调用NewObject()andnewObjectArray(),构建jobjectandjobjectArray,然后返回到java中。

7.1 在native中调用java的构造方法创建java对象

调用构造方法和调用实例方法类似:

step1: 获取Method ID method name:”<init>“ return-type:”V

step2: 调用NewObject()创建java对象

示例代码:

TestJavaConstructor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestJNIConstructor {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}

// Native method that calls back the constructor and return the constructed object.
// Return an Integer object with the given int.
private native Integer getIntegerObject(int number);

public static void main(String args[]) {
TestJNIConstructor obj = new TestJNIConstructor();
System.out.println("In Java, the number is :" + obj.getIntegerObject(9999));
}
}
TestJavaConstructor.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
#include <jni.h>
#include <stdio.h>
#include "TestJNIConstructor.h"

JNIEXPORT jobject JNICALL Java_TestJNIConstructor_getIntegerObject
(JNIEnv *env, jobject thisObj, jint number) {
// Get a class reference for java.lang.Integer
jclass cls = (*env)->FindClass(env, "java/lang/Integer");

// Get the Method ID of the constructor which takes an int
jmethodID midInit = (*env)->GetMethodID(env, 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(env, cls, midInit, number);

// Try running the toString() on this newly create object
jmethodID midToString = (*env)->GetMethodID(env, cls, "toString", "()Ljava/lang/String;");
if (NULL == midToString) return NULL;
jstring resultStr = (*env)->CallObjectMethod(env, newObj, midToString);
const char *resultCStr = (*env)->GetStringUTFChars(env, resultStr, NULL);
printf("In C: the number is %s\n", resultCStr);

//May need to call releaseStringUTFChars() before return
return newObj;
}

用于创建对象的jni函数:

1
2
3
4
5
6
7
8
9
jclass FindClass(JNIEnv *env, const char *name);

jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
// Constructs a new Java object. The method ID indicates which constructor method to invoke

jobject AllocObject(JNIEnv *env, jclass cls);
// Allocates a new Java object without invoking any of the constructors for the object.

7.2 对象数组

不同于基础类型的数组可以批量处理所有元素,对象数组需要使用Get|SetObjectArrayElement()处理每个元素。

示例代码:创建一个Integer数组,计算总和和平均值,返回Double数组。

TestJNIObjectArray.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.ArrayList;

public class TestJNIObjectArray {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Native method that receives an Integer[] and
// returns a Double[2] with [0] as sum and [1] as average
private native Double[] sumAndAverage(Integer[] numbers);

public static void main(String args[]) {
Integer[] numbers = {11, 22, 32}; // auto-box
Double[] results = new TestJNIObjectArray().sumAndAverage(numbers);
System.out.println("In Java, the sum is " + results[0]); // auto-unbox
System.out.println("In Java, the average is " + results[1]);
}
}
TestJNIObjectArray.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
#include <jni.h>
#include <stdio.h>
#include "TestJNIObjectArray.h"

JNIEXPORT jobjectArray JNICALL Java_TestJNIObjectArray_sumAndAverage
(JNIEnv *env, jobject thisObj, jobjectArray inJNIArray) {
// Get a class reference for java.lang.Integer
jclass classInteger = (*env)->FindClass(env, "java/lang/Integer");
// Use Integer.intValue() to retrieve the int
jmethodID midIntValue = (*env)->GetMethodID(env, classInteger, "intValue", "()I");
if (NULL == midIntValue) return NULL;

// Get the value of each Integer object in the array
jsize length = (*env)->GetArrayLength(env, inJNIArray);
jint sum = 0;
int i;
for (i = 0; i < length; i++) {
jobject objInteger = (*env)->GetObjectArrayElement(env, inJNIArray, i);
if (NULL == objInteger) return NULL;
jint value = (*env)->CallIntMethod(env, objInteger, midIntValue);
sum += value;
}
double average = (double)sum / length;
printf("In C, the sum is %d\n", sum);
printf("In C, the average is %f\n", average);

// Get a class reference for java.lang.Double
jclass classDouble = (*env)->FindClass(env, "java/lang/Double");

// Allocate a jobjectArray of 2 java.lang.Double
jobjectArray outJNIArray = (*env)->NewObjectArray(env, 2, classDouble, NULL);

// Construct 2 Double objects by calling the constructor
jmethodID midDoubleInit = (*env)->GetMethodID(env, classDouble, "<init>", "(D)V");
if (NULL == midDoubleInit) return NULL;
jobject objSum = (*env)->NewObject(env, classDouble, midDoubleInit, (double)sum);
jobject objAve = (*env)->NewObject(env, classDouble, midDoubleInit, average);
// Set to the jobjectArray
(*env)->SetObjectArrayElement(env, outJNIArray, 0, objSum);
(*env)->SetObjectArrayElement(env, outJNIArray, 1, objAve);

return outJNIArray;
}

jni中用于创建和操作对象数组的函数:

1
2
3
4
5
6
7
8
9
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.

8. jni中的局部引用和全局引用

对于追求效率的程序而言,管理引用非常关键,有助于节省开销,提高可重用性。

jni把对象引用(对于jobject的引用)分为两种:局部和全局引用。

  1. 局部引用在native方法内创建,一旦方法结束就被释放。它的生命周期和方法相同。你也可以调用DeleteLocalRef()主动释放,使对象可以被回收。对象都是作为局部引用被传递给native方法的。所有的jni函数返回的对象都是局部引用,通过CallStaticObjectMethod()获取的jobject也是局部引用
  2. 全局引用会一直存在知道通过DeleteGlobalRef()被主动释放,可以用一个局部引用通过NewGlobalRef()来创建全局引用。

示例代码:在java中定义两个native方法,都用于创建Integer对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestJNIReference {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}

// A native method that returns a java.lang.Integer with the given int.
private native Integer getIntegerObject(int number);

// Another native method that also returns a java.lang.Integer with the given int.
private native Integer anotherGetIntegerObject(int number);

public static void main(String args[]) {
TestJNIReference test = new TestJNIReference();
System.out.println(test.getIntegerObject(1));
System.out.println(test.getIntegerObject(2));
System.out.println(test.anotherGetIntegerObject(11));
System.out.println(test.anotherGetIntegerObject(12));
System.out.println(test.getIntegerObject(3));
System.out.println(test.anotherGetIntegerObject(13));
}
}

在c代码中我们要通过FindClass()获取class java.lang.Integer的引用,然后获取构造方法的method ID。但是我们想复用class ref 和method ID。

下面看一个错误的使用情况:

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
#include <jni.h>
#include <stdio.h>
#include "TestJNIReference.h"

// Global Reference to the Java class "java.lang.Integer"
static jclass classInteger;
static jmethodID midIntegerInit;

jobject getInteger(JNIEnv *env, jobject thisObj, jint number) {

// Get a class reference for java.lang.Integer if missing
if (NULL == classInteger) {
printf("Find java.lang.Integer\n");
classInteger = (*env)->FindClass(env, "java/lang/Integer");
}
if (NULL == classInteger) return NULL;

// Get the Method ID of the Integer's constructor if missing
if (NULL == midIntegerInit) {
printf("Get Method ID for java.lang.Integer's constructor\n");
midIntegerInit = (*env)->GetMethodID(env, classInteger, "<init>", "(I)V");
}
if (NULL == midIntegerInit) return NULL;

// Call back constructor to allocate a new instance, with an int argument
jobject newObj = (*env)->NewObject(env, classInteger, midIntegerInit, number);
printf("In C, constructed java.lang.Integer with number %d\n", number);
return newObj;
}

JNIEXPORT jobject JNICALL Java_TestJNIReference_getIntegerObject
(JNIEnv *env, jobject thisObj, jint number) {
return getInteger(env, thisObj, number);
}

JNIEXPORT jobject JNICALL Java_TestJNIReference_anotherGetIntegerObject
(JNIEnv *env, jobject thisObj, jint number) {
return getInteger(env, thisObj, number);
}

在第二次调用时,java.lang.Integer的引用失效了(并且不是NULL),这是因为FindClass()返回的是一个局部引用,方法一旦结束就失效了。

为了解决这个问题,我们需要创建一个全局引用:

1
2
3
4
5
6
7
8
9
10
// Get a class reference for java.lang.Integer if missing
if (NULL == classInteger) {
printf("Find java.lang.Integer\n");
// FindClass returns a local reference
jclass classIntegerLocal = (*env)->FindClass(env, "java/lang/Integer");
// Create a global reference from the local reference
classInteger = (*env)->NewGlobalRef(env, classIntegerLocal);
// No longer need the local reference, free it!
(*env)->DeleteLocalRef(env, classIntegerLocal);
}

需要注意的是jmethodID和jfieldID不是jobject,无法创建全局引用。

9. jni的常见错误

错误信息:SEVERE: java.lang.UnsatisfiedLinkError: no xxx in java.library.path

原因:使用了第三方的本地库

结局方法:将本地库的路径添加到”java.library.path”,使用如下命令(以JOGL为例):

1
> java -Djava.library.path=d:\bin\jogl2.0\lib myjoglapp

源码地址:关注下面的公众号回复“JNI一文详解”可获得源码地址。

微信公众号 长夜西风

个人网站 http://www.cmder.info/

书痴者文必工,艺痴者技必良。