JNI編程指南-第六章 異常

第六章 異常

 

很多情況下,本地代碼做JNI調用後都要檢查是否有錯誤發生,本章講的就是怎麼樣檢查錯誤和處理錯誤。

我重點放在JNI函數調用引發的錯誤上面。如果一個本地方法中調用了一個JNI函數,它必須遵守下面幾個步驟來檢查和處理這個JNI函數調用時可能引發的錯誤。至於其它可能的錯誤,比如本地代碼中調用了一個可能引發錯誤的系統方法,那只需要按照該系統方法的標準文檔中規定的來處理就可以了。

 

6.1 概述

 

我們通過一些例子來介紹一些JNI異常處理函數

 

6.1.1 本地代碼中如何緩存和拋出異常

 

下面的代碼中演示瞭如何聲明一個會拋出異常的本地方法。CatchThrow這個類聲明瞭一個會拋出IllegalArgumentException異常的名叫doit的本地方法。

class CatchThrow {

     private native void doit()

         throws IllegalArgumentException;

     private void callback() throwsNullPointerException {

         throw newNullPointerException("CatchThrow.callback");

     }

 

     public static void main(String args[]) {

         CatchThrow c = new CatchThrow();

         try {

             c.doit();

         } catch (Exception e) {

             System.out.println("InJava:\n\t" + e);

         }

     }

     static {

         System.loadLibrary("CatchThrow");

     }

 }

Main方法調用本地方法doit,doit方法的實現如下:

JNIEXPORT void JNICALL

 Java_CatchThrow_doit(JNIEnv*env, jobject obj)

 {

     jthrowable exc;

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

     jmethodID mid =

         (*env)->GetMethodID(env, cls,"callback", "()V");

     if (mid == NULL) {

         return;

     }

     (*env)->CallVoidMethod(env, obj, mid);

     exc = (*env)->ExceptionOccurred(env);

     if (exc) {

         /* We don't do much with the exception,except that

            we print a debug message for it,clear it, and

            throw a new exception. */

         jclass newExcCls;

         (*env)->ExceptionDescribe(env);

         (*env)->ExceptionClear(env);

         newExcCls = (*env)->FindClass(env,

                      "java/lang/IllegalArgumentException");

         if (newExcCls == NULL) {

             /* Unable to find the exceptionclass, give up. */

             return;

         }

         (*env)->ThrowNew(env, newExcCls,"thrown from C code");

     }

 }

運行程序,輸出是:

java.lang.NullPointerException:

         at CatchThrow.callback(CatchThrow.java)

         at CatchThrow.doit(Native Method)

         at CatchThrow.main(CatchThrow.java)

 In Java:

         java.lang.IllegalArgumentException:thrown from C code

回調方法拋出一個NullPointerException異常。當CallVoidMethod把控制權交給本地方法時,本地代碼會通過ExceptionOccurred來檢查這個異常。在我們的例子中,當一個異常被檢測到時,本地代碼通過調用ExceptionDescribe來輸出一個關於這個異常的描述信息,然後通過調用ExceptionClear清除異常信息,最後,拋出一個IllegalArgumentException。

和JAVA中的異常機制不一樣,JNI拋出的異常(例如,通過ThrowNew方法)不被處理的話,不會立即終止本地方法的執行。異常發生後,JNI程序員必須手動處理。

 

6.1.2 製作一個拋出異常的工具函數

 

拋出異常通常需要兩步:通過FindClass找到異常類、調用ThrowNew函數生成異常。爲了簡化這個過程,我們寫了一個工具函數專門用來生成一個指定名字的異常。

void

 JNU_ThrowByName(JNIEnv *env, const char *name,const char *msg)

 {

    jclass cls = (*env)->FindClass(env, name);

    /* if cls is NULL, an exception has already been thrown */

    if (cls != NULL) {

         (*env)->ThrowNew(env, cls, msg);

    }

     /* free the local ref */

     (*env)->DeleteLocalRef(env, cls);

 }

本書中,如果一個函數有JNU前綴的話,意味它是一個工具函數。JNU_ThrowByName這個工具函數首先使用FindClass函數來找到異常類,如果FindClass執行失敗(返回NULL),VM會拋出一個異常(比如NowClassDefFoundError),這種情況下JNI_ThrowByName不會再拋出另外一個異常。如果FindClass執行成功的話,我們就通過ThrowNew來拋出一個指定名字的異常。當函數JNU_ThrowByName返回時,它會保證有一個異常需要處理,但這個異常不一定是name參數指定的異常。當函數返回時,記得要刪除指向異常類的局部引用。向DeleteLocalRef傳遞NULL不會產生作用。

 

6.2 妥善地處理異常

 

JNI程序員必須能夠預測到可能會發生異常的地方,並編寫代碼進行檢查。妥善地異常處理有時很繁鎖,但是一個高質量的程序不可或缺的。

 

6.2.1 異常檢查

 

檢查一個異常是否發生有兩種方式。

第一種方式是:大部分JNI函數會通過特定的返回值(比如NULL)來表示已經發生了一個錯誤,並且當前線程中有一個異常需要處理。在C語言中,用返回值來標識錯誤信息是一個很常見的方式。下面的例子中演示瞭如何通過GetFieldID的返回值來檢查錯誤。這個例子包含兩部分,定義了一些實例字段(handle、length、width)的類Window和一個緩存這些字段的字段ID的本地方法。雖然這些字段位於Window類中,調用GetFieldID時,我們仍然需要檢查是否有錯誤發生,因爲VM可能沒有足夠的內存分配給字段ID。

1.  /* a class in the Java programming language */

2.   public class Window {

3.       long handle;

4.       int length;

5.       int width;

6.       static native void initIDs();

7.       static {

8.           initIDs();

9.       }

10. }

11.

12. /* C codethat implements Window.initIDs */

13. jfieldID FID_Window_handle;

14. jfieldID FID_Window_length;

15. jfieldID FID_Window_width;

16.

17. JNIEXPORT void JNICALL

18. Java_Window_initIDs(JNIEnv *env, jclass classWindow)

19. {

20.     FID_Window_handle =

21.         (*env)->GetFieldID(env, classWindow,"handle", "J");

22.     if (FID_Window_handle == NULL) {  /* important check. */

23.         return; /* erroroccurred. */

24.     }

25.     FID_Window_length =

26.         (*env)->GetFieldID(env, classWindow,"length", "I");

27.     if (FID_Window_length == NULL) {  /* important check. */

28.         return; /* erroroccurred. */

29.     }

30.     FID_Window_width =

31.         (*env)->GetFieldID(env, classWindow,"width", "I");

32.     /* no checks necessary; weare about to return anyway */

33. }

第二種方式:

public class Fraction {

     // details such as constructors omitted

     int over, under;

     public int floor() {

         return Math.floor((double)over/under);

     }

 }

/* Native code that callsFraction.floor. Assume method ID

    MID_Fraction_floor has been initializedelsewhere. */

 void f(JNIEnv*env, jobject fraction)

 {

    jint floor = (*env)->CallIntMethod(env, fraction,

                                        MID_Fraction_floor);

     /* important: check if an exception wasraised */

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

         return;

     }

     ... /* use floor */

 }

當一個JNI函數返回一個明確的錯誤碼時,你仍然可以用ExceptionCheck來檢查是否有異常發生。但是,用返回的錯誤碼來判斷比較高效。一旦JNI函數的返回值是一個錯誤碼,那麼接下來調用ExceptionCheck肯定會返回JNI_TRUE。

 

6.2.2 異常處理

 

本地代碼通常有兩種方式來處理一個異常:

1、一旦發生異常,立即返回,讓調用者處理這個異常。

2、通過ExceptionClear清除異常,然後執行自己的異常處理代碼。

當一個異常發生後,必須先檢查、處理、清除異常後再做其它JNI函數調用,否則的話,結果未知。當前線程中有異常的時候,你可以調用的JNI函數非常少,11.8.2節列出了這些JNI函數的詳細列表。通常來說,當有一個未處理的異常時,你只可以調用兩種JNI函數:異常處理函數和清除VM資源的函數。

當異常發生時,釋放資源是一件很重要的事,下面的例子中,調用GetStringChars函數後,如果後面的代碼發生異常,不要忘了調用ReleaseStringChars釋放資源。

JNIEXPORT void JNICALL

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

 {

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

     if (c_str == NULL) {

         return;

     }

     ...

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

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

         return;

     }

     ...

     /* normal return */

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

 }

 

6.2.3 工具函數中的異常

 

程序員編寫工具函數時,一定要把工具函數內部分發生的異常傳播到調用它的方法中去。這裏有兩個需要注意的地方:

1、對調用者來說,工具函數提供一個錯誤返回碼比簡單地把異常傳播過去更方便一些。

2、工具函數在發生異常時尤其需要注意管理局部引用的方式。

爲了說明這兩點,我們寫了一個工具函數,這個工具函數根據對象實例方法的名字和描述符做一些方法回調。

·        jvalue

·         JNU_CallMethodByName(JNIEnv*env,

·                              jboolean *hasException,

·                              jobject obj,

·                              const char *name,

·                              const char *descriptor,...)

·         {

·             va_list args;

·             jclass clazz;

·             jmethodID mid;

·             jvalue result;

·             if ((*env)->EnsureLocalCapacity(env, 2)== JNI_OK) {

·                 clazz = (*env)->GetObjectClass(env,obj);

·                 mid = (*env)->GetMethodID(env,clazz, name,

·                                           descriptor);

·                 if (mid) {

·                     const char *p = descriptor;

·                     /* skip over argument types to findout the

·                        return type */

·                     while (*p != ')') p++;

·                     /* skip ')' */

·                     p++;

·                     va_start(args, descriptor);

·                     switch (*p) {

·                     case 'V':

·                         (*env)->CallVoidMethodV(env,obj, mid, args);

·                         break;

·                     case '[':

·                     case 'L':

·                         result.l =(*env)->CallObjectMethodV(

·                                                env,obj, mid, args);

·                         break;

·                     case 'Z':

·                         result.z =(*env)->CallBooleanMethodV(

·                                                env,obj, mid, args);

·                         break;

·                     case 'B':

·                         result.b =(*env)->CallByteMethodV(

·                                                env, obj, mid, args);

·                         break;

·                     case 'C':

·                         result.c =(*env)->CallCharMethodV(

·                                                env,obj, mid, args);

·                         break;

·                     case 'S':

·                         result.s =(*env)->CallShortMethodV(

·                                                env,obj, mid, args);

·                         break;

·                     case 'I':

·                         result.i =(*env)->CallIntMethodV(

·                                                env,obj, mid, args);

·                         break;

·                     case 'J':

·                         result.j =(*env)->CallLongMethodV(

·                                                env,obj, mid, args);

·                         break;

·                     case 'F':

·                         result.f =(*env)->CallFloatMethodV(

·                                                env,obj, mid, args);

·                         break;

·                     case 'D':

·                         result.d =(*env)->CallDoubleMethodV(

·                                                env,obj, mid, args);

·                         break;

·                     default:

·                         (*env)->FatalError(env,"illegal descriptor");

·                     }

·                     va_end(args);

·                 }

·                 (*env)->DeleteLocalRef(env, clazz);

·             }

·             if (hasException) {

·                 *hasException =(*env)->ExceptionCheck(env);

·             }

·             return result;

·         }

JNU_CallMethodByName的參數當中有一個jboolean指針,如果函數執行成功的話,指針指向的值會被設置爲JNI_TRUE,如果有異常發生的話,會被設置成JNI_FALSE。這就可以讓調用者方便地檢查異常。

JNU_CallMethodByName首先通過EnsureLocalCapacity來確保可以創建兩個局部引用,一個類引用,一個返回值。接下來,它從對象中獲取類引用並查找方法ID。根據返回類型,switch語句調用相應的JNI方法調用函數。回調過程完成後,如果hasException不是NULL,我們調用ExceptionCheck檢查異常。

函數ExceptionCheck和ExceptionOccurred非常相似,不同的地方是,當有異常發生時,ExceptionCheck不會返回一個指向異常對象的引用,而是返回JNI_TRUE,沒有異常時,返回JNI_FALSE。而ExceptionCheck這個函數不會返回一個指向異常對象的引用,它只簡單地告訴本地代碼是否有異常發生。上面的代碼如果使用ExceptionOccurred的話,應該這麼寫:

·        if (hasException) {

·                 jthrowable exc =(*env)->ExceptionOccurred(env);

·                 *hasException = exc != NULL;

·                 (*env)->DeleteLocalRef(env, exc);

   }

爲了刪除指向異常對象的局部引用,DeleteLocalRef方法必須被調用。

使用JNU_CallMethodByName這個工具函數,我們可以重寫Instance-MethodCall.nativeMethod方法的實現:

·        JNIEXPORT void JNICALL

·         Java_InstanceMethodCall_nativeMethod(JNIEnv*env, jobject obj)

·         {

·             printf("In C\n");

·             JNU_CallMethodByName(env, NULL, obj,"callback", "()V");

·         }

調用JNU_CallMethodByName函數後,我們不需要檢查異常,因爲本地方法後面會立即返回。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章