JNI编程指南-第十章 使用JNI时容易出错的地方

来源:互联网 发布:sql update批量更新 编辑:程序博客网 时间:2024/05/14 23:15

第十章使用JNI时容易出错的地方

 

本章总结了JNI实际应用中容易出错的一些情况供JNI程序员参考。

 

10.1 错误检查

 

编写本地方法时最常见的错误就是忘记检查是否发生了异常。我承认,JNI里面的异常检查确实比较麻烦,但是,这很重要。

 

10.2 向JNI函数传递非法参数

 

JNI不会检查参数是否正确,如果你自己不保证参数的正确有效,那么出现什么样的错误是未知的。通常,不检查参数的有效性在C/C++库中是比较常见的。

 

10.3 把jclass和jobject弄混

 

一开始使用JNI时,很容易把对象引用(jobject类型的值)和类引用(jclass类型的值)弄混。对象引用对应的是数组或者java.lang.Object及其子类的对象实例,而类引用对应的是java.lang.Class的实例。

像GetFieldID这样需要传入jclass作为参数的方法做的是一个类操作,因为它是从一个类中获取字段的描述。而GetIntField这样需要传入jobject作为参数的方法做的是一个对象操作,因为它从一个对象实例中获取字段的值。

 

10.4jboolean会面临数据截取的问题

 

Jboolean是一个8-bitunsigned的C类型,可以存储0~255的值。其中,0对应常量JNI_FALSE,而1~255对应常量JNI_TRUE。但是,32或者16位的值,如果最低的8位是0的话,就会引起问题。

假设你定义了一个函数print,需要传入一个jboolean类型的condition作为参数:

void print(jboolean condition)

 {

        /* C compilers generate code that truncatescondition

       to its lower 8 bits. */

     if (condition) {

         printf("true\n");

     } else {

         printf("false\n");

     }

 }

对上面这段代码来说,下面这样用就会出现问题:

int n = 256; /* the value 0x100,whose lower 8 bits are all 0 */

 print(n);

我们传入了一个非0的值256(0X100),因为这个值的低8位(即,0)被截出来使用,上面的代码会打印“false”。

根据经验,这里有一个常用的解决方案:

n = 256;

 print (n ?JNI_TRUE : JNI_FALSE);

 

10.5 编程的时候,什么用JAVA,什么时候用C?

 

这里有一些经验性的注意事项:

1、 尽量让JAVA和C之间的接口简单化,C和JAVA间的调用过于复杂的话,会使得BUG调试、代码维护和JVM对代码进行优化都会变得很难。比如虚拟机很容易对一些JAVA方法进行内联,但对本地方法却无能为力。

2、 尽量少写本地代码。因为本地代码即不安全又是不可移植的,而且本地代码中的错误检查很麻烦。

3、 让本地代码尽量独立。也就是说,实际使用的时候,尽量让所有的本地方法都在同一个包甚至同一个类中。

JNI把JVM的许多功能开发给了本地代码:类加载、对象创建、字段访问、方法调用、线程同步等。虽然用JAVA来做这些事情的时候很容易,但有时候,用本地代码来做很诱人。下面的代码会告诉你,为什么用本地代码进行JAVA编程是愚蠢的。假设我们需要创建一个线程并启动它,JAVA代码这样写:

newJobThread().start();

而用本地代码却需要这样:

/* Assume thesevariables are precomputed and cached:

  *     Class_JobThread:  the class "JobThread"

  *     MID_Thread_init:  method ID of constructor

  *     MID_Thread_start: method ID ofThread.start()

  */

 aThreadObject =

     (*env)->NewObject(env, Class_JobThread,MID_Thread_init);

 if (aThreadObject== NULL) {

     ... /* out of memory */

 }

 (*env)->CallVoidMethod(env,aThreadObject, MID_Thread_start);

 if((*env)->ExceptionOccurred(env)) {

     ... /* thread did not start */

 }

比较起来,本地代码写会使用编程变得复杂,代码量大,错误处理多。通常,如果不得不用本地代码来做这些事的话,在JAVA中提供一个辅助函数,并在本地代码中对这个辅助函数进行回调。

 

10.6 混淆ID和引用

 

本地代码中使用引用来访问JAVA对象,使用ID来访问方法和字段。

引用指向的是可以由本地代码来管理的JVM中的资源。比如DeleteLocalRef这个本地函数,允许本地代码删除一个局部引用。而字段和方法的ID由JVM来管理,只有它所属的类被unload时,才会失效。本地代码不能显式在删掉一个字段或者方法的ID。

本地代码可以创建多个引用并让它们指向相同的对象。比如,一个全局引用和一个局部引用可能指向相同的对象。而字段ID和方法ID是唯一的。比如类A定义了一个方法f,而类B从类A中继承了方法f,那么下面的调用结果是相同的:

jmethodID MID_A_f =(*env)->GetMethodID(env, A, "f", "()V");

jmethodID MID_B_f =(*env)->GetMethodID(env, B, "f", "()V");

 

10.7 缓存字段ID和方法ID

 

这里有一个缓存ID的例子:

class C {

     private int i;

     native void f();

 }

下面是本地方法的实现,没有使用缓存ID。

// No field IDs cached.

 JNIEXPORT voidJNICALL

 Java_C_f(JNIEnv*env, jobject this) {

     jclass cls = (*env)->GetObjectClass(env,this);

     ... /* error checking */

     jfieldID fid = (*env)->GetFieldID(env,cls, "i", "I");

     ... /* error checking */

     ival = (*env)->GetIntField(env, this,fid);

     ... /* ival now has the value of this.i */

 }

上面的这些代码一般可以运行正确,但是下面的情况下,就出错了:

// Trouble in the absence of IDcaching

 class D extends C{

     private int i;

     D() {

         f(); // inherited from C

     }

 }

类D继承了类C,并且也有一个私有的字段i。

当在D的构造方法中调用f时,本地方法接收到的参数中,cls指向提类D的对象,fid指向的是D.i这个字段。在这个本地方法的末尾,ival里面是D.i的值,而不是C.i的值。这与你想象的是不一样的。

上面这种问题的解决方案是:

// Version that caches IDs in staticinitializers

 class C {

     private int i;

     native void f();

     private static native void initIDs();

     static {

         initIDs(); // Call an initializingnative method

     }

 }

本地方法这样实现:

static jfieldID FID_C_i;

 

 JNIEXPORT voidJNICALL

 Java_C_initIDs(JNIEnv*env, jclass cls) {

     /* Get IDs to all fields/methods of C that

        native methods will need. */

     FID_C_i = (*env)->GetFieldID(env, cls,"i", "I");

 }

 

 JNIEXPORT voidJNICALL

 Java_C_f(JNIEnv*env, jobject this) {

     ival = (*env)->GetIntField(env, this,FID_C_i);

     ... /* ival is always C.i, not D.i */

 }

字段ID在类C的静态初始时被计算并缓存下来,这样就可以确保缓存的是C.i的ID,因此,不管本地方法中接收到的jobject是哪个类的实例,访问的永远是C.i的值。

另外,同样的情况也可能会出现在方法ID上面。

 

10.8 Unicode字符串结尾

 

从GetStringChars和GetStringCritical两个方法获得的Unicode字符串不是以NULL结尾的,需要调用GetStringLength来获取字符串的长度。一些操作系统,如Windows NT中,Unicode字符串必须以两个’\0’结尾,这样的话,就不能直接把GetStringChars得到的字符串传递给Windows NT系统的API,而必须复制一份并在字符串的结尾加入两个“\0”

 

10.9 访问权限失效

 

在本地代码中,访问方法和变量时不受JAVA语言规定的限制。比如,可以修改private和final修饰的字段。并且,JNI中可以访问和修改heap中任意位置的内存。这些都会造成意想不到的结果。比如,本地代码中不应该修改java.lang.String和java.lang.Integer这样的不可变对象的内容。否则,会破坏JAVA规范。

 

10.10 忽视国际化

 

JVM中的字符串是Unicode字符序列,而本地字符串采用的是本地化的编码。实际编码的时候,我们经常需要使用像JNU_NewStringNative和JNU_GetStringNativeChars这样的工具函数来把Unicode编码的jstring转化成本地字符串,要对消息和文件名尤其关注,它们经常是需要国际化的,可能包含各种字符。

如果一个本地方法得到了一个文件名,必须把它转化成本地字符串之后才能传递给C库函数使用:

JNIEXPORT jint JNICALL

 Java_MyFile_open(JNIEnv*env, jobject self, jstring name,

                  jint mode)

 {

     jint result;

     char *cname = JNU_GetStringNativeChars(env,name);

     if (cname == NULL) {

         return 0;

     }

     result = open(cname, mode);

     free(cname);

    return result;

 }

上例中,我们使用JNU_GetStringNativeChars把Unicode字符串转化成本地字符串。

 

10.11 确保释放VM资源

 

JNI编程时常见的错误之一就是忘记释放VM资源,尤其是在执行路径分支时,比如,有异常发生的时候:

JNIEXPORT void JNICALL

 Java_pkg_Cls_f(JNIEnv*env, jclass cls, jstring jstr)

 {

    const jchar *cstr =

         (*env)->GetStringChars(env, jstr,NULL);

    if (cstr == NULL) {

         return;

     }

    ...

    if (...) { /* exception occurred */

         /* misses a ReleaseStringChars call */

         return;

    }

     ...

    /* normal return */

    (*env)->ReleaseStringChars(env, jstr, cstr);

 }

忘记调用ReleaseStringChars可能导致jstring永远被VM给pin着不被回收。一个GetStringChars必然要对应着一个ReleaseStringChars,下面的代码就没有正确地释放VM资源:

/* The isCopy argument is misusedhere! */

 JNIEXPORT voidJNICALL

 Java_pkg_Cls_f(JNIEnv*env, jclass cls, jstring jstr)

 {

     jboolean isCopy;

     const jchar *cstr =(*env)->GetStringChars(env, jstr,

                                                &isCopy);

     if (cstr == NULL) {

         return;

     }

     ... /* use cstr */

     /* This is wrong. Always need to callReleaseStringChars. */

     if (isCopy) {

         (*env)->ReleaseStringChars(env,jstr, cstr);

     }

 }

即使在isCopy的值是JNI_FALSE时,也应该调用ReleaseStringChars在unpin掉jstring。

 

10.12 过多的创建局部引用

 

大量的局部引用创建会浪费不必要的内存。一个局部引用会导致它本身和它所指向的对象都得不到回收。尤其要注意那些长时间运行的方法、创建局部引用的循环和工具函数,充分得利用Pus/PopLocalFrame来高效地管理局部引用。

 

10.13 使用已经失效的局部引用

 

局部引用只在一个本地方法的调用期间有效,方法执行完成后会被自动释放。本地代码不应该把存储局部引用存储到全局变量中在其它地方使用。

 

10.14 跨进程使用JNIEnv

 

JNIEnv这个指针只能在当前线程中使用,不要在其它线程中使用。

 

10.15 错误的线程模型(Thread Models)
0 0