多语言编程之 C and Java:JNI进阶--字段访问与方法回调

在上一章(《多语言编程之 C and Java:JNI 30 分钟入门》)中讲解了 JNI 的基本使用。这一章将讨论一些更加实用的用法:如何在 JNI 中访问 Java 对象的成员变量或静态变量,以及回调 Java 中的成员方法或静态方法。

1 JNI 访问 Java 成员变量

1.1 实例

Java 实现

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

//成员变量(字段)
private int age = 88;
private String name = "Jims";

//静态成员变量
private static int years = 20;

private native void modifyInstanceVariable();
}

这个 Java 类中包含 3 个私有成员变量,名为 age 的 int 型变量, 名为 name 的 String 型变量和一个名为 years 的 int 型静态变量。我们将在使用 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
26
27
28
29
30
JNIEXPORT void JNICALL Java_transvar_TransVar_modifyInstanceVariable
(JNIEnv *env, jobject obj)
{
//获取 this object 的类
jclass thisClass = (*env)->GetObjectClass(env,obj);
if(thisClass == NULL)
{
printf("thisClass null\n");
}

//获取字段ID
jfieldID fidAge = (*env)->GetFieldID(env, thisClass, "age", "I");
jfieldID fidName = (*env)->GetFieldID(env, thisClass, "name", "Ljava/lang/String;");
jfieldID fidYear = (*env)->GetStaticFieldID(env, thisClass, "years", "I");

//获取字段
jint age = (*env)->GetIntField(env, obj, fidAge);
jint years = (*env)->GetStaticIntField(env, obj, fidYear);
jstring name = (*env)->GetObjectField(env, obj, fidName);
const char* pszName = (*env)->GetStringUTFChars(env, name, NULL);

printf("[%d Years Later for %s] \n", years, pszName);

(*env)->ReleaseStringUTFChars(env, name, pszName);

age += years;

//修改成员变量
(*env)->SetIntField(env, obj, fidAge, age);
}

1.2 访问 Java 变量

在 JNI 中我们获取了这 3 个变量,并对 age 变量进行了修改。
由这段代码可以看出,访问 Java 成员变量的一般步骤为:

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

  2. 通过 GetFieldID()/GetStaticFieldID() 从 Java 类中获取 FieldID 。

  3. 基于 FieldID, 通过 GetField()/GetStaticField() 取回变量。

  4. 基于 FieldID, 通过 SetField()/GetStaticField() 修改变量。

GetFieldID()/GetStaticFieldID() 原型如下:

1
2
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

此函数可以通过字段名和字段标识(signature)获取类或对象的字段 ID.

Table 1: Java 字段类型标识
Java 类型 signature 例子
Boolean Z
Byte B
Char C
Int I
Short S
Long J
Float F
Double D
Void V
Object[注] L/; 如 String 为 Ljava/lang/String;
Array[注] [ 如 int[] 为 [I

:数组和引用类型的对象(Object),都是以分号(;)结尾。对于嵌套类,使用 $ 符号来表示。

GetField()/SetField() 有一组函数,用于从对象中获取字段值或给变量设置值。

1
<type> Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);

为 Java 原始类型名称。
还有一组函数用于获取或设置静态字段 : GetStaticField() / SetStaticField() 。

2 JNI 访问 Java 成员方法

我们可以在 native code 中访问 Java 的成员方法和静态成员方法。这种访问也可以称之为回调。
和访问 Java 成员变量类似,访问Java 成员方法的一般步骤为:

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

  2. 通过 GetMethodID() 获取 MethodID.

  3. 基于 MethodID, 调用 CallMethod() / CallVoidMethod() / CallObjectMethod(), 这些方法会返回 类型的值。 相应地,有 CallStaticMethod() / CallStaticVoidMethod() / CallStaticObjectMethod()用于回调静态方法。

GetMethodID() 原型如下:

1
jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);

最后一个参数 sig 为方法的标识(signature)。对于这个标识,我们并不陌生,在上一章,我们使用 javah 命令生成的头文件中,对方法有一个名为 Signature 的注释,就是这个标识。 方法标识以括号 “(” 开始,一对括号内部为方法参数的标识,括号后面跟返回值的标识。如方法

1
void  MethedName(int a, int b);

的标识为: (II)V 。
除了参考 .h 文件的注释,还可以使用 Java 工具 javap 来查看方法标识。

1
> javap -s -p <JavaClassName>

在得到 MethodID 后,即可以通过如下函数来调用 Java 成员方法或表态方法:

1
2
3
4
5
6
7
<type> Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
<type> Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
<type> Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

<type> CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
<type> CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
<type> CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);

3 JNI 创建 Java 对象

通过JNI提供的 NewObject() / NewObjectArray() 函数,可以在 JNI 中创建 Java 对象(jobject),并传递回 Java 程序。

3.1 在 JNI 中调用 Java 对象构造器(Constructor)

这个过程和上一节调用 Java 方法没有什么不同,只不过,GetMethodID() 中,方法名和标识不一样,这里分别为 “” 和 “V” .获取 MethodID 后,就可以通过 NewObject() 来创建 Java 对象。
创建 Java 对象相关的函数还有:

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.

此外,还有如下函数可以创建 Java 数组对象:

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.

3.2 示例

定义 Person 类,有构造函数 Person(String, int):

1
2
3
4
5
6
7
8
9
public class Person{
public Person(){};
public Person(String name , int age){
_name = name;
_age = age;
}
private int _age;
private String _name;
}

并有 native 方法,将 name 和 age 传入 JNI 中,用于实例化一个 Person 对象并传回 Java:

1
private native Person createPerson(String name, int age);

则在 native 实现中,有如下过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JNIEXPORT jobject JNICALL Java_package_class_createPerson(JNIEnv *env, jobject obj, jstring name, jint age)
{
jclass personClass = (*env)->FindClass(env, "packagename/Person");
if(personClass == NULL)
{
printf("can not find class : packagename.Person");
return NULL;
}

jmethodID midpersonInit = (*env)->GetMethodID(env, personClass, "<init>", "(Ljava/lang/String;I)V");
if(midpersonInit == NULL)
{
printf("can not get methodID : <init>(Ljava/lang/String;I)V");
return NULL;
}
jobject newPerson = (*env)->NewObject(env, personClass, midpersonInit, name, age);

return newPerson;
}

4 附录

4.1 本章源码

TransVar.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
32
33
34
35
package transvar;
public class TransVar{
static{
System.loadLibrary("hello");
}

//成员变量(字段)
private int age = 8;
private String name = "Jims";

//静态成员变量
private static int years = 20;

public int progress(int pro){
System.out.println("[... " + pro + " YEARS LATER ...]");
return 1;
}

private native void modifyInstanceVariable();

private native Person createPerson(String name, int age);

//For test
public static void main(String args[]){
TransVar tv = new TransVar();
System.out.println(tv.name + " is in the age of " + tv.age);
tv.modifyInstanceVariable();
System.out.println(tv.name + " is in the age of " + tv.age);

Person pjimSon = tv.createPerson("Jimson",1);
if(pjimSon != null){
System.out.println("The person: " + pjimSon.getName() + " create from JNI in the age of " + pjimSon.getAge());
}
}
}

Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package transvar;
public class Person{
public Person(){};
public Person(String name , int age){
_name = name;
_age = age;
}
private int _age;
private String _name;

public int getAge(){return _age;}
public String getName(){return _name;};
}

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

JNIEXPORT void JNICALL Java_transvar_TransVar_modifyInstanceVariable
(JNIEnv *env, jobject obj)
{
//获取 this object 的类
jclass thisClass = (*env)->GetObjectClass(env,obj);
if(thisClass == NULL)
{
printf("thisClass null\n");
}

//获取字段ID
jfieldID fidAge = (*env)->GetFieldID(env, thisClass, "age", "I");
jfieldID fidName = (*env)->GetFieldID(env, thisClass, "name", "Ljava/lang/String;");
jfieldID fidYear = (*env)->GetStaticFieldID(env, thisClass, "years", "I");

//获取字段
jint age = (*env)->GetIntField(env, obj, fidAge);
jint years = (*env)->GetStaticIntField(env, obj, fidYear);
jstring name = (*env)->GetObjectField(env, obj, fidName);
const char* pszName = (*env)->GetStringUTFChars(env, name, NULL);

printf("[... Time flies for %s ...] \n", pszName);

(*env)->ReleaseStringUTFChars(env, name, pszName);

jmethodID mid = (*env)->GetMethodID(env, thisClass, "progress", "(I)I");

jint y = 0;
for(; y< years; y+=5)
{
jint i = (*env)->CallIntMethod(env,obj,mid,y);
if(i != 1) break;
}

age += y;

//修改成员变量
(*env)->SetIntField(env, obj, fidAge, age);
}

JNIEXPORT jobject JNICALL Java_transvar_TransVar_createPerson(JNIEnv *env, jobject obj, jstring name, jint age)
{
jclass personClass = (*env)->FindClass(env, "transvar/Person");
if(personClass == NULL)
{
printf("can not find class : transvar.Person");
return NULL;
}

jmethodID midpersonInit = (*env)->GetMethodID(env, personClass, "<init>", "(Ljava/lang/String;I)V");
if(midpersonInit == NULL)
{
printf("can not get methodID : <init>(Ljava/lang/String;I)V");
return NULL;
}
jobject newPerson = (*env)->NewObject(env, personClass, midpersonInit, name, age);

return newPerson;
}

4.2 参考