異常處理

很多情況下,本地代碼做JNI調(diào)用后都要檢查是否有錯誤發(fā)生,本章講的就是怎么樣檢查錯誤和處理錯誤。
我重點放在JNI函數(shù)調(diào)用引發(fā)的錯誤上面。如果一個本地方法中調(diào)用了一個JNI函數(shù),它必須遵守下面幾個步驟來檢查和處理這個JNI函數(shù)調(diào)用時可能引發(fā)的錯誤。至于其它可能的錯誤,比如本地代碼中調(diào)用了一個可能引發(fā)錯誤的系統(tǒng)方法,那只需要按照該系統(tǒng)方法的標(biāo)準(zhǔn)文檔中規(guī)定的來處理就可以了。
6.1 概述
我們通過一些例子來介紹一些JNI異常處理函數(shù)
6.1.1 本地代碼中如何緩存和拋出異常
下面的代碼中演示了如何聲明一個會拋出異常的本地方法。CatchThrow這個類聲明了一個會拋出IllegalArgumentException異常的名叫doit的本地方法。
class CatchThrow {
private native void doit()
throws IllegalArgumentException;
private void callback() throws NullPointerException {
throw new NullPointerException("CatchThrow.callback");
}

 public static void main(String args[]) {
     CatchThrow c = new CatchThrow();
     try {
         c.doit();
     } catch (Exception e) {
         System.out.println("In Java:\n\t" + e);
     }
 }
 static {
     System.loadLibrary("CatchThrow");
 }

}
Main方法調(diào)用本地方法doit,doit方法的實現(xiàn)如下:
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 exception class, 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
回調(diào)方法拋出一個NullPointerException異常。當(dāng)CallVoidMethod把控制權(quán)交給本地方法時,本地代碼會通過ExceptionOccurred來檢查這個異常。在我們的例子中,當(dāng)一個異常被檢測到時,本地代碼通過調(diào)用ExceptionDescribe來輸出一個關(guān)于這個異常的描述信息,然后通過調(diào)用ExceptionClear清除異常信息,最后,拋出一個IllegalArgumentException。
和JAVA中的異常機(jī)制不一樣,JNI拋出的異常(例如,通過ThrowNew方法)不被處理的話,不會立即終止本地方法的執(zhí)行。異常發(fā)生后,JNI程序員必須手動處理。
6.1.2 制作一個拋出異常的工具函數(shù)
拋出異常通常需要兩步:通過FindClass找到異常類、調(diào)用ThrowNew函數(shù)生成異常。為了簡化這個過程,我們寫了一個工具函數(shù)專門用來生成一個指定名字的異常。
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);
}
本書中,如果一個函數(shù)有JNU前綴的話,意味它是一個工具函數(shù)。JNU_ThrowByName這個工具函數(shù)首先使用FindClass函數(shù)來找到異常類,如果FindClass執(zhí)行失敗(返回NULL),VM會拋出一個異常(比如NowClassDefFoundError),這種情況下JNI_ThrowByName不會再拋出另外一個異常。如果FindClass執(zhí)行成功的話,我們就通過ThrowNew來拋出一個指定名字的異常。當(dāng)函數(shù)JNU_ThrowByName返回時,它會保證有一個異常需要處理,但這個異常不一定是name參數(shù)指定的異常。當(dāng)函數(shù)返回時,記得要刪除指向異常類的局部引用。向DeleteLocalRef傳遞NULL不會產(chǎn)生作用。
6.2 妥善地處理異常
JNI程序員必須能夠預(yù)測到可能會發(fā)生異常的地方,并編寫代碼進(jìn)行檢查。妥善地異常處理有時很繁鎖,但是一個高質(zhì)量的程序不可或缺的。
6.2.1 異常檢查
檢查一個異常是否發(fā)生有兩種方式。
第一種方式是:大部分JNI函數(shù)會通過特定的返回值(比如NULL)來表示已經(jīng)發(fā)生了一個錯誤,并且當(dāng)前線程中有一個異常需要處理。在C語言中,用返回值來標(biāo)識錯誤信息是一個很常見的方式。下面的例子中演示了如何通過GetFieldID的返回值來檢查錯誤。這個例子包含兩部分,定義了一些實例字段(handle、length、width)的類Window和一個緩存這些字段的字段ID的本地方法。雖然這些字段位于Window類中,調(diào)用GetFieldID時,我們?nèi)匀恍枰獧z查是否有錯誤發(fā)生,因為VM可能沒有足夠的內(nèi)存分配給字段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. /* C code that implements Window.initIDs */
  12. jfieldID FID_Window_handle;
  13. jfieldID FID_Window_length;
  14. jfieldID FID_Window_width;
  15. JNIEXPORT void JNICALL
  16. Java_Window_initIDs(JNIEnv *env, jclass classWindow)
  17. {
  18.  FID_Window_handle =
    
  19.      (*env)->GetFieldID(env, classWindow, "handle", "J");
    
  20.  if (FID_Window_handle == NULL) {  /* important check. */
    
  21.      return; /* error occurred. */
    
  22.  }
    
  23.  FID_Window_length =
    
  24.      (*env)->GetFieldID(env, classWindow, "length", "I");
    
  25.  if (FID_Window_length == NULL) {  /* important check. */
    
  26.      return; /* error occurred. */
    
  27.  }
    
  28.  FID_Window_width =
    
  29.      (*env)->GetFieldID(env, classWindow, "width", "I");
    
  30.  /* no checks necessary; we are about to return anyway */
    
  31. }
    第二種方式:
    public class Fraction {
    // details such as constructors omitted
    int over, under;
    public int floor() {
    return Math.floor((double)over/under);
    }
    }
    /* Native code that calls Fraction.floor. Assume method ID
    MID_Fraction_floor has been initialized elsewhere. /
    void f(JNIEnv env, jobject fraction)
    {
    jint floor = (
    env)->CallIntMethod(env, fraction,
    MID_Fraction_floor);
    /
    important: check if an exception was raised /
    if ((
    env)->ExceptionCheck(env)) {
    return;
    }
    ... /* use floor /
    }
    當(dāng)一個JNI函數(shù)返回一個明確的錯誤碼時,你仍然可以用ExceptionCheck來檢查是否有異常發(fā)生。但是,用返回的錯誤碼來判斷比較高效。一旦JNI函數(shù)的返回值是一個錯誤碼,那么接下來調(diào)用ExceptionCheck肯定會返回JNI_TRUE。
    6.2.2 異常處理
    本地代碼通常有兩種方式來處理一個異常:
    1、 一旦發(fā)生異常,立即返回,讓調(diào)用者處理這個異常。
    2、 通過ExceptionClear清除異常,然后執(zhí)行自己的異常處理代碼。
    當(dāng)一個異常發(fā)生后,必須先檢查、處理、清除異常后再做其它JNI函數(shù)調(diào)用,否則的話,結(jié)果未知。當(dāng)前線程中有異常的時候,你可以調(diào)用的JNI函數(shù)非常少,11.8.2節(jié)列出了這些JNI函數(shù)的詳細(xì)列表。通常來說,當(dāng)有一個未處理的異常時,你只可以調(diào)用兩種JNI函數(shù):異常處理函數(shù)和清除VM資源的函數(shù)。
    當(dāng)異常發(fā)生時,釋放資源是一件很重要的事,下面的例子中,調(diào)用GetStringChars函數(shù)后,如果后面的代碼發(fā)生異常,不要忘了調(diào)用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 工具函數(shù)中的異常
    程序員編寫工具函數(shù)時,一定要把工具函數(shù)內(nèi)部分發(fā)生的異常傳播到調(diào)用它的方法中去。這里有兩個需要注意的地方:
    1、 對調(diào)用者來說,工具函數(shù)提供一個錯誤返回碼比簡單地把異常傳播過去更方便一些。
    2、 工具函數(shù)在發(fā)生異常時尤其需要注意管理局部引用的方式。
    為了說明這兩點,我們寫了一個工具函數(shù),這個工具函數(shù)根據(jù)對象實例方法的名字和描述符做一些方法回調(diào)。
    · 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 find out 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的參數(shù)當(dāng)中有一個jboolean指針,如果函數(shù)執(zhí)行成功的話,指針指向的值會被設(shè)置為JNI_TRUE,如果有異常發(fā)生的話,會被設(shè)置成JNI_FALSE。這就可以讓調(diào)用者方便地檢查異常。
    JNU_CallMethodByName首先通過EnsureLocalCapacity來確保可以創(chuàng)建兩個局部引用,一個類引用,一個返回值。接下來,它從對象中獲取類引用并查找方法ID。根據(jù)返回類型,switch語句調(diào)用相應(yīng)的JNI方法調(diào)用函數(shù)。回調(diào)過程完成后,如果hasException不是NULL,我們調(diào)用ExceptionCheck檢查異常。
    函數(shù)ExceptionCheck和ExceptionOccurred非常相似,不同的地方是,當(dāng)有異常發(fā)生時,ExceptionCheck不會返回一個指向異常對象的引用,而是返回JNI_TRUE,沒有異常時,返回JNI_FALSE。而ExceptionCheck這個函數(shù)不會返回一個指向異常對象的引用,它只簡單地告訴本地代碼是否有異常發(fā)生。上面的代碼如果使用ExceptionOccurred的話,應(yīng)該這么寫:
    · if (hasException) {
    · jthrowable exc = (*env)->ExceptionOccurred(env);
    · hasException = exc != NULL;
    · (
    env)->DeleteLocalRef(env, exc);
    }
    為了刪除指向異常對象的局部引用,DeleteLocalRef方法必須被調(diào)用。
    使用JNU_CallMethodByName這個工具函數(shù),我們可以重寫Instance-MethodCall.nativeMethod方法的實現(xiàn):
    · JNIEXPORT void JNICALL
    · Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj)
    · {
    · printf("In C\n");
    · JNU_CallMethodByName(env, NULL, obj, "callback", "()V");
    · }
    調(diào)用JNU_CallMethodByName函數(shù)后,我們不需要檢查異常,因為本地方法后面會立即返回。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容