JNI使用范围
1、 在Java程序中复用以前写过的C/C++代码。
2、 自己实现一个java虚拟机
3、 学习不同语言如何进行协作,尤其是如何实现垃圾回收和多线程。
4、 把一个虚拟机实现整合到用C/C++写的程序中。
hello world
1、 创建一个类(HelloWorld.java)声明本地方法。
2、 使用javac编译源文件HollowWorld.java,产生HelloWorld.class。使用javah –jni来生成C头文件(HelloWorld.h),这个头文件里面包含了本地方法的函数原型。
3、 用C代码写函数原型的实现。
4、 把C函数实现编译成一个本地库,创建Hello-World.dll或者libHello-World.so。
5、 使用java命令运行HelloWorld程序,类文件HelloWorld.class和本地库(HelloWorld.dll或者libHelloWorld.so)在运行时被加载。
1 | package com.jni.demo; |
1 | com_jni_demo.MyJNI.h |
1 | MyJNI.cpp |
基本类型、字符串、数组
示例
java端源代码
1 | class Prompt { |
- 本地方法的C函数原型
C函数原型
1 | Prompt.getLine方法可以用下面这个C函数来实现: |
其中,JNIEXPORT和JNICALL这两个宏(被定义在jni.h)确保这个函数在本地库外可见,并且C编译器会进行正确的调用转换。
本地方法参数
第一个参数JNIEnv接口指针,指向一个个函数表,函数表中的每一个入口指向一个JNI函数。本地方法经常通过这些函数来访问JVM中的数据结构。
第二个参数根据本地方法是一个静态方法还是实例方法而有所不同。本地方法是一个静态方法时,第二个参数代表本地方法所在的类;本地方法是一个实例方法时,第二个参数代表本地方法所在的对象。我们的例子当中,Java_Prompt_getLine是一个实例方法,因此jobject参数指向方法所在的对象。类型映射
AVA中有两种类型:基本数据类型(int,float,char等)和引用类型(类,对象,数组等)。
JNI对基本类型和引用类型的处理是不同的。基本类型的映射是一对一的。例如JAVA中的int类型直接对应C/C++中的jint(定义在jni.h中的一个有符号 32位整数)。
NI把JAVA中的对象当作一个C指针传递到本地方法中,这个指针指向JVM中的内部数据结构,而内部数据结构在内存中的存储方式是不可见的。本地代码必须通过在JNIEnv中选择适当的JNI函数来操作JVM中的对象。例如,对于java.lang.String对应的JNI类型是jstring,但本地代码只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容。
所有的JNI引用都是jobject类型,对了使用方便和类型安全,JNI定义了一个引用类型集合,集合当中的所有类型都是jobject的子类型。这些子类型和JAVA中常用的引用类型相对应。例如,jstring表示字符串,jobjectArray表示对象数组。
访问字符串
Java_Prompt_getLine接收一个jstring类型的参数prompt,jstring类型指向JVM内部的一个字符串,和常规的C字符串类型char*不同。你不能把jstring当作一个普通的C字符串。
- 转换为本地字符串
本地代码中,必须使用合适的JNI函数把jstring转化为C/C++字符串。JNI支持字符串在Unicode和UTF-8两种编码之间转换。Unicode字符串代表了16-bit的字符集合。UTF-8字符串使用一种向上兼容7-bit ASCII字符串的编码协议。UTF-8字符串很像NULL结尾的C字符串,在包含非ASCII字符的时候依然如此。所有的7-bitASCII字符的值都在1~127之间,这些值在UTF-8编码中保持原样。一个字节如果最高位被设置了,意味着这是一个多字节字符(16-bitUnicode值)。
函数Java_Prompt_getLine通过调用JNI函数GetStringUTFChars来读取字符串的内容。GetStringUTFChars可以把一个jstring指针(指向JVM内部的Unicode字符序列)转化成一个UTF-8格式的C字符串。如何你确信原始字符串数据只包含7-bit ASCII字符,你可以把转化后的字符串传递给常规的C库函数使用,如printf。
1 | JNIEXPORT jstring JNICALL |
不要忘记检查GetStringUTFChars。因为JVM需要为新诞生的UTF-8字符串分配内存,这个操作有可能因为内存太少而失败。失败时,GetStringUTFChars会返回NULL,并抛出一个OutOfMemoryError异常(对异常的处理在第6章)。这些JNI抛出的异常与JAVA中的异常是不同的。一个由JNI抛出的未决的异常不会改变程序执行流,因此,我们需要一个显示的return语句来跳过C函数中的剩余语句。Java_Prompt_getLine函数返回后,异常会在Prompt.main(Prompt.getLine这个发生异常的函数的调用者)中抛出,
释放本地字符串资源
从GetStringUTFChars中获取的UTF-8字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars告诉JVM这个UTF-8字符串不会被使用了,因为这个UTF-8字符串占用的内存会被回收。构造新的字符串
你可以通过JNI函数NewStringUTF在本地方法中创建一个新的java.lang.String字符串对象。这个新创建的字符串对象拥有一个与给定的UTF-8编码的C类型字符串内容相同的Unicode编码字符串。
如果一个VM不能为构造java.lang.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回一个NULL。在这个例子中,我们不必检查它的返回值,因为本地方法会立即返回。如果NewStringUTF失败,OutOfMemoryError这个异常会被在Prompt.main(本地方法的调用者)中抛出。如果NeweStringUTF成功,它会返回一个JNI引用,这个引用指向新创建的java.lang.String对象。这个对象被Prompt.getLine返回然后被赋值给Prompt.main中的本地input。其它JNI字符串处理函数
JNI支持许多操作字符串的函数,这里做个大致介绍。
GetStringChars和ReleaseStringChars获取以Unicode格式编码的字符串。当操作系统支持Unicode编码的字符串时,这些方法很有用。
UTF-8字符串以’\0’结尾,而Unicode字符串不是。如果jstring指向一个Unicode编码的字符串,为了得到这个字符串的长度,可以调用GetStringLength。如果一个jstring指向一个UTF-8编码的字符串,为了得到这个字符串的字节长度,可以调用标准C函数strlen。或者直接对jstring调用JNI函数GetStringUTFLength,而不用管jstring指向的字符串的编码格式。
GetStringChars和GetStringUTFChars函数中的第三个参数需要更进一步的解释:
const jchar
GetStringChars(JNIEnv env, jstring str, jboolean *isCopy);
当从JNI函数GetStringChars中返回得到字符串B时,如果B是原始字符串java.lang.String的拷贝,则isCopy被赋值为JNI_TRUE。如果B和原始字符串指向的是JVM中的同一份数据,则isCopy被赋值为JNI_FALSE。当isCopy值为JNI_FALSE时,本地代码决不能修改字符串的内容,否则JVM中的原始字符串也会被修改,这会打破JAVA语言中字符串不可变的规则。
通常,因为你不必关心JVM是否会返回原始字符串的拷贝,你只需要为isCopy传递NULL作为参数。
JVM是否会通过拷贝原始Unicode字符串来生成UTF-8字符串是不可以预测的,程序员最好假设它会进行拷贝,而这个操作是花费时间和内存的。一个典型的JVM会在heap上为对象分配内存。一旦一个JAVA字符串对象的指针被传递给本地代码,GC就不会再碰这个字符串。换言之,这种情况下,JVM必须pin这个对象。可是,大量地pin一个对象是会产生内存碎片的,因为,虚拟机会随意性地来选择是复制还是直接传递指针。
当你不再使用一个从GetStringChars得到的字符串时,不管JVM内部是采用复制还是直接传递指针的方式,都不要忘记调用ReleaseStringChars。根据方法GetStringChars是复制还是直接返回指针,ReleaseStringChars会释放复制对象时所占的内存,或者unpin这个对象。新JNI函数
为了提高JVM返回字符串直接指针的可能性,JDK1.2中引入了一对新函数,Get/ReleaseStringCritical。表面上,它们和Get/ReleaseStringChars函数差不多,但实际上这两个函数在使用有很大的限制。
使用这两个函数时,你必须两个函数中间的代码是运行在”critical region”(临界区)的,即,这两个函数中间的本地代码不能调用任何会让线程阻塞或等待JVM中的其它线程的本地函数或JNI函数。
有了这些限制, JVM就可以在本地方法持有一个从GetStringCritical得到的字符串的直接指针时禁止GC。当GC被禁止时,任何线程如果触发GC的话,都会被阻塞。而Get/ReleaseStringCritical这两个函数中间的任何本地代码都不可以执行会导致阻塞的调用或者为新对象在JVM中分配内存。否则,JVM有可能死锁,想象一下这样的场景中:
1、 只有当前线程触发的GC完成阻塞并释放GC时,由其它线程触发的GC才可能由阻塞中释放出来继续运行。
2、 在这个过程中,当前线程会一直阻塞。因为任何阻塞性调用都需要获取一个正被其它线程持有的锁,而其它线程正等待GC。
Get/ReleaseStringCritical的交迭调用是安全的,这种情况下,它们的使用必须有严格的顺序限制。而且,我们一定要记住检查是否因为内存溢出而导致它的返回值是NULL。因为JVM在执行GetStringCritical这个函数时,仍有发生数据复制的可能性,尤其是当JVM内部存储的数组不连续时,为了返回一个指向连续内存空间的指针,JVM必须复制所有数据。
总之,为了避免死锁,在Get/ReleaseStringCritical之间不要调用任何JNI函数。Get/ReleaseStringCritical和 Get/ReleasePrimitiveArrayCritical这两个函数是可以的。
下面代码演示了这对函数的正确用法:
1 |
|
JNI不支持Get/ReleaseStringUTFCritical,因为这样的函数在进行编码转换时很可能会促使JVM对数据进行复制,因为JVM内部表示字符串一般都是使用Unicode的。
JDK1.2还一对新增的函数:GetStringRegion和GetStringUTFRegion。这对函数把字符串复制到一个预先分配的缓冲区内。Prompt.getLine这个本地方法可以用GetStringUTFRegion重新实现如下:
1 | JNIEXPORT jstring JNICALL |
GetStringUTFRegion这个函数会做越界检查,如果必要的话,会抛出异常StringIndexOutOfBoundsException。这个方法与GetStringUTFChars比较相似,不同的是,GetStringUTFRegion不做任何内存分配,不会抛出内存溢出异常。
- JNI字符串操作函数总结
对于小字符串来说,Get/SetStringRegion和Get/SetString-UTFRegion这两对函数是最佳选择,因为缓冲区可以被编译器提前分配,而且永远不会产生内存溢出的异常。当你需要处理一个字符串的一部分时,使用这对函数也是不错的,因为它们提供了一个开始索引和子字符串的长度值。另外,复制少量字符串的消耗是非常小的。
在使用GetStringCritical时,必须非常小心。你必须确保在持有一个由GetStringCritical获取到的指针时,本地代码不会在JVM内部分配新对象,或者做任何其它可能导致系统死锁的阻塞性调用。
下面的例子演示了使用GetStringCritical时需要注意的一些地方:
1 |
|
上面代码的问题在于,GC被当前线程禁止的情况下,向一个文件写数据不一定安全。例如,另外一个线程T正在等待从文件fd中读取数据。假设操作系统的规则是fprintf会等待线程T完成所有对文件fd的数据读取操作,这种情况下就可能会产生死锁:线程T从文件fd中读取数据是需要缓冲区的,如果当前没有足够内存,线程T就会请求GC来回收一部分,GC一旦运行,就只能等到当前线程运行ReleaseStringCritical时才可以。而ReleaseStringCritical只有在fprintf调用返回时才会被调用。而fprintf这个调用,会一直等待线程T完成文件读取操作。
访问数组
JNI在处理基本类型数组和对象数组上面是不同的。对象数组里面是一些指向对象实例或者其它数组的引用。
本地代码中访问JVM中的数组和访问JVM中的字符串有些相似。看一个简单的例子。下面的程序调用了一个本地方法sumArray,这个方法对一个int数组里面的元素进行累加: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
29class IntArray {
private native int sumArray(int[] arr);
public static void main(String[] args) {
IntArray p = new IntArray();
int arr[] = new int[10];
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
int sum = p.sumArray(arr);
System.out.println("sum = " + sum);
}
static {
System.loadLibrary("IntArray");
}
}
- 在本地代码中访问数组
数组的引用类型是一般是jarray或者或者jarray的子类型jintArray。就像jstring不是一个C字符串类型一样,jarray也不是一个C数组类型。所以,不要直接访问jarray。你必须使用合适的JNI函数来访问基本数组元素:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint buf[10];
jint i, sum = 0;
(*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
for (i = 0; i < 10; i++) {
sum += buf[i];
}
return sum;
}
访问字段
JAVA支持两种field(字段),每一个对象的实例都有一个对象字段的复制;所有的对象共享一个类的静态字段。本地方法使用JNI提供的函数可以获取和修改这两种字段。先看一个从本地代码中访问对象字段的例子:
1 | class InstanceFieldAccess { |
InstanceFieldAccess这个类定义了一个对象字段s。main方法创建了一个对象并设置s的值,然后调用本地方法InstanceFieldAccess.accessField在本地代码中打印s的值,并把它修改为一个新值。本地方法返回后,JAVA中把这个值再打印一次,可以看出来,字段s的值已经被改变了。下面是本地方法的实现: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
64
65
66
67
68
69
70
71
72
73
74
75
76
77JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj)
{
jfieldID fid; /* store the field ID */
jstring jstr;
const char *str;
/* Get a reference to obj's class */
jclass cls = (*env)->GetObjectClass(env, obj);
printf("In C:\n");
/* Look for the instance field s in cls */
fid = (*env)->GetFieldID(env, cls, "s",
"Ljava/lang/String;");
if (fid == NULL) {
return; /* failed to find the field */
}
/* Read the instance field s */
jstr = (*env)->GetObjectField(env, obj, fid);
str = (*env)->GetStringUTFChars(env, jstr, NULL);
if (str == NULL) {
return; /* out of memory */
}
printf(" c.s = \"%s\"\n", str);
(*env)->ReleaseStringUTFChars(env, jstr, str);
/* Create a new string and overwrite the instance field */
jstr = (*env)->NewStringUTF(env, "123");
if (jstr == NULL) {
return; /* out of memory */
}
(*env)->SetObjectField(env, obj, fid, jstr);
}
运行程序,得到输出为:
In C:
c.s = "abc"
In Java:
c.s = "123"
- 访问一个对象字段的流程
为了访问一个对象的实例字段,本地方法需要做两步:
首先,通过在类引用上调用GetFieldID获取field ID(字段ID)、字段名字和字段描述符:
Fid=(env)->GetFieldID(env,cls,”s”,”Ljava/lang/String;”);
上例中的代码通过在对象引用obj上调用GetObjectClass获取到类引用。一旦获取到字段ID,你就可以把对象和字段ID作为参数来访问字段:
Jstr=(env)->GetObjectField(env,obj,fid);
因为字符串和数组是特殊的对象,所以我们使用GetObjectField来访问字符串类型的实例字段。除了Get/SetObjectField,JNI还支持其它如GetIntField、SetFloatField等用来访问基本类型字段的函数。
字段描述符
在上一节我们使用过一个特殊的C字符串“Ljava/lang/String”来代表一个JVM中的字段类型。这个字符串被称为JNI field descriptor(字段描述符)。
字符串的内容由字段被声明的类型决定。例如,使用“I”来表示一个int类型的字段,“F”来表示一个float类型的字段,“D”来表示一个double类型的字段,“Z”来表示一个boolean类型的字段等等。
像java.lang.String这样的引用类型的描述符都是以L开头,后面跟着一个JNI类描述符,以分号结尾。一个JAVA类的全名中的包名分隔符“.”被转化成“/”。因此,对于一个字段类型的字段来说,它的描述符是“Ljava/lang/String”。
数组的描述符中包含“]”字符,后面会跟着数组类型的描述符,如“[I”是int[]类型的字段的描述符。访问静态字段
访问静态字段和访问实例字段相似,看下面这个InstanceFieldAccess例子的变形:
1 | class StaticFielcdAccess { |
StaticFieldAccess这个类包含一个静态字段si,main方法创建了一个对象,初始化静态字段,然后调用本地方法StaticFieldAccess.accessField在本地代码中打印静态字段中的值,然后设置新的值,为了演示这个值确实被改变了,在本地方法返回后,JAVA中再次这个静态字段的值。
下面是本地方法StaticFieldAccess.accessField的实现: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
51JNIEXPORT void JNICALL
Java_StaticFieldAccess_accessField(JNIEnv *env, jobject obj)
{
jfieldID fid; /* store the field ID */
jint si;
/* Get a reference to obj's class */
jclass cls = (*env)->GetObjectClass(env, obj);
printf("In C:\n");
/* Look for the static field si in cls */
fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
if (fid == NULL) {
return; /* field not found */
}
/* Access the static field si */
si = (*env)->GetStaticIntField(env, cls, fid);
printf(" StaticFieldAccess.si = %d\n", si);
(*env)->SetStaticIntField(env, cls, fid, 200);
}
运行程序可得到输出结果:
In C:
StaticFieldAccess.si = 100
In Java:
StaticFieldAccess.si = 200
访问静态字段和对象实例字段的不同点
访问静态字段使用GetStaticFieldID,而访问对象的实例字段使用GetFieldID,但是,这两个方法都有相同的返回值类型:jfieldID。
调用方法
JAVA中有几种不同类型的方法,实例方法必须在一个类的某个对象实例上面调用。而静态方法可以在任何一个对象实例上调用。对于构建方法的调用我们推迟到下一节。
JNI支持一系列完整的函数让你可以在本地代码中回调JAVA方法,下面例子演示了如何从本地代码中调用一个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
25class InstanceMethodCall {
private native void nativeMethod();
private void callback() {
System.out.println("In Java");
}
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary("InstanceMethodCall");
}
}
下面的是本地方法的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj)
{
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid =
(*env)->GetMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return; /* method not found */
}
printf("In C\n");
(*env)->CallVoidMethod(env, obj, mid);
}
- 调用实例方法
1、 本地方法首先调用JNI函数GetMethodID。这个函数在指定的类中寻找相应的方法。这个寻找过程是基于方法描述符的。如果方法不存在,GetMethodID返回NULL。这时,立即从本地方法中返回,并引发一个NoSuchMethodError错误。
2、 本地方法通过调用CallVoidMethod来调用返回值为void的实例方法。
除了CallVoidMethod这个函数以外,JNI也支持对返回值为其它类型的方法的调用。如果你调用的方法返回值类型为int,你的本地方法会使用CallIntMethod。类似地,你可以调用CallObjectMethod来调用返回值为java.lang.String、数组等对象类型的方法。
你也可以使用CallMethod系列的函数来调用接口方法。你必须从接口类型中获取方法ID,下面的代码演示了如何在java.lang.Thread实例上面调用Runnable.run方法:
1 | jobject thd = ...; /* a java.lang.Thread instance */ |
生成方法描述符
JNI中描述字段使用字段描述符,描述方法同样有方法描述符。一个方法描述符包含参数类型和返回值类型。参数类型出现在前面,并由一对圆括号将它们括起来,参数类型按它们在方法声明中出现的顺序被列出来,并且多个参数类型之间没有分隔符。如果一个方法没有参数,被表示为一对空圆括号。方法的返回值类型紧跟参数类型的右括号后面。
例如,“(I)V”表示这个方法的一个参数类型为int,并且有一个void类回值。“()D”表示这个方法没有参数,返回值类型为double。
方法描述符中可能会包含类描述符,如方法native private String getLine(String);的描述符为:“(Ljava/lang/String;)Ljava/lang/String;”
数组类型的描述符以“[”开头,后面跟着数组元素类型的描述符。如,public static void main(String[] args);的描述符是:”([Ljava/lang/String;)V”调用静态方法
1、 通过GetStaticMethodID获取方法ID。对应于调用实例方法时的GetMethodID。
2、 传入类、方法ID、参数,并调用提供静态方法调用功能的JNI系列函数中的一个,如:CallStaticVoidMethod,CallStaticBooleanMethod等。
调用静态方法和调用实例方法的JNI函数有一个很大的不同,前者第二个参数是类引用,后者是对象实例的引用。
在JAVA访问一个静态方法可以通过类,也可以通过对象实例。而JNI的规定是,在本地代码中回调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
25class StaticMethodCall {
private native void nativeMethod();
private static void callback() {
System.out.println("In Java");
}
public static void main(String args[]) {
StaticMethodCall c = new StaticMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary("StaticMethodCall");
}
}
下面是本地方法的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
JNIEXPORT void JNICALL
Java_StaticMethodCall_nativeMethod(JNIEnv *env, jobject obj)
{
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid =
(*env)->GetStaticMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return; /* method not found */
}
printf("In C\n");
(*env)->CallStaticVoidMethod(env, cls, mid);
}
当调用CallStaticVoidMethod时,确保你传入的是类引用cls而不是对象引用obj。运行程序,输出为:
In C
In Java
调用父类的实例方法
如果一个方法被定义在父类中,在子类中被覆盖,你也可以调用这个实例方法。JNI提供了一系列完成这些功能的函数:CallNonvirtual
1、 使用GetMethodID从一个指向父类的引用当中获取方法ID。
2、 传入对象、父类、方法ID和参数,并调用CallNonvirtualVoidMethod、CallNonvirtualBooleanMethod等一系列函数中的一个。
这种调用父类实例方法的情况其实很少遇到,通常在JAVA中可以很简单地做到:super.f();
- 调用构造函数
JNI中,构造函数可以和实例方法一样被调用,调用方式也相似。传入“”作为方法名,“V”作为返回类型。你可以通过向JNI函数NewObject传入方法来调用构造函数。下面的代码实现了与JNI函数NewString相同的功能:把存储在C缓冲区内的Unicode编码的字符序列,创建成一个java.lang.String对象:
1 | jstring |
上面这个本地方法有些复杂,需要详细解释一下。首先,FindClass返回一个java.lang.String类的引用,接着,GetMethodID返回构造函数String(char[] chars)的方法ID。我们调用NewCharArray分配一个字符数组来保存字符串元素。JNI函数NewObject调用方法ID所标识的构造函数。NewObject函数需要的参数有:类的引用、构造方法的ID、构造方法需要的参数。
这个例子引出了一个问题,既然我们可以利用JNI函数来实现相同的功能,为什么JNI还需要NewString这样的内置函数?原因是,内置函数的效率远高于在本地代码里面调用构造函数的API。而字符串又是最常用到的对象类型,因此需要在JNI中给予特殊的支持。
你也可以做到通过CallNonvirtualVoidMethod函数来调用构造函数。这种情况下,本地代码必须首先通过调用AllocObject函数创建一个未初始化的对象。上面例子中的result = (*env)->NewObject(env, stringClass, cid, elemArr);可以被如下代码替换:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19result = (*env)->AllocObject(env, stringClass);
if (result) {
(*env)->CallNonvirtualVoidMethod(env, result, stringClass,
cid, elemArr);
/* we need to check for possible exceptions */
if ((*env)->ExceptionCheck(env)) {
(*env)->DeleteLocalRef(env, result);
result = NULL;
}
}
AllocObject创建了一个未初始化的对象,使用时一定要非常小心,确保一个对象上面,构造函数最多被调用一次。本地代码不应该在一个对象上面调用多次构造函数。有时,你可能会发现创建一个未初始化的对象然后一段时间以后再调用构造函数的方式是很有用的。尽管如此,大部分情况下,你应该使用NewObject,尽量避免使用容易出错的AllocObject/CallNonvirtualVoidMethod方法。
- 缓存字段ID和方法ID使用时缓存
字段ID和方法ID可以在字段的值被访问或者方法被回调的时候缓存起来。下面的代码中把字段ID存储在静态变量当中,这样当本地方法被重复调用时,不必重新搜索字段ID:
1 | JNIEXPORT void JNICALL |
由于多个线程可能同时访问这个本地方法,上面方法中的代码很可能会导致混乱,其实没事,多个线程计算的ID其实是相同的。
同样的思想,我们也可以缓存java.lang.String的构造方法的ID:
1 | jstring |
把一个JVM嵌入到本地程序中
一个JVM可以看作就是一个本地库。本地程序可以链接这个库,然后通过“调用接口”(invocation interface)来加载JVM。实际上,JDK中标准的启动器也就是一段简单的链接了JVM的C代码。启动器解析命令、加载JVM、并通过“调用接口”(invocation interface)运行JAVA程序。
创建JVM
我们用下面这段C代码来加载一个JVM并调用Prog.main方法来演示如何使用调用接口。
public class Prog {
public static void main(String[] args) {
System.out.println("Hello World " + args[0]);
}
}
下面是启动器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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
main() {
JNIEnv *env;
JavaVM *jvm;
jint res;
jclass cls;
jmethodID mid;
jstring jstr;
jclass stringClass;
jobjectArray args;
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString =
"-Djava.class.path=" USER_CLASSPATH;
vm_args.version = 0x00010002;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = JNI_TRUE;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
JDK1_1InitArgs vm_args;
char classpath[1024];
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* Append USER_CLASSPATH to the default system class path */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
cls = (*env)->FindClass(env, "Prog");
if (cls == NULL) {
goto destroy;
}
mid = (*env)->GetStaticMethodID(env, cls, "main",
"([Ljava/lang/String;)V");
if (mid == NULL) {
goto destroy;
}
jstr = (*env)->NewStringUTF(env, " from C!");
if (jstr == NULL) {
goto destroy;
}
stringClass = (*env)->FindClass(env, "java/lang/String");
args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
if (args == NULL) {
goto destroy;
}
(*env)->CallStaticVoidMethod(env, cls, mid, args);
destroy:
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionDescribe(env);
}
(*jvm)->DestroyJavaVM(jvm);
}
上面的代码有条件地编译一个初始化JDK1_1InitArgs这个structure。这个structure是JDK1.1下特有的,尽管JDK1.2也会支持,但JDK1.2引入了一个更通用的叫作JavaVMInitArgs的VM初始化structure。
常量JNI_VERSION_1_2在JDK1.2下定义,JDK1.1下是没有的。
当目标平台是1.1时,C代码首先调用JNI_GetDefaultJavaVMInitArgs来获得默认的VM设置。这个调用会返回heap size、stack size、默认类路径等信息,并把这些信息存放在参数vm_args中。然后我们把Prog.class所在的目录附加到vm_args.classpath中。
当平台目标是1.2时,C代码创建了一个JavaVMInitArgs的structure。VM的初始化参数被存放在一个JavaVMOption数组中。
设置完VM初始化structure后,C程序调用JNI_CreateJavaVM来加载和初始化JVM,传入的前两个参数:
1、 接口指针jvm,指向新创建的JVM。
2、 当前线程的JNIEnv接口指针env。本地代码通过env指针访问JNI函数。
当函数JNI_CreateJavaVM函数成功返回时,当前本地线程已经把自己的控制权交给JVM。这时,它会就像一个本地方法一样运行。以后就可以通过JNI函数来启动Prog.main方法。
接着,程序调用DestroyJavaVM函数来unloadJVM。不幸的是,在JDK1.1和JDK1.2中你不能unloadJVM,它会一直返回一个错误码。
运行上面的程序,产生如下输出:
Hello World from C!