第六章 異常
很多情況下,本地代碼做JNI調用後都要檢查是否有錯誤發生,本章講的就是怎麼樣檢查錯誤和處理錯誤。
我重點放在JNI函數調用引發的錯誤上面。如果一個本地方法中調用了一個JNI函數,它必須遵守下面幾個步驟來檢查和處理這個JNI函數調用時可能引發的錯誤。至於其它可能的錯誤,比如本地代碼中調用了一個可能引發錯誤的系統方法,那只需要按照該系統方法的標準文檔中規定的來處理就可以了。
6.1 概述
我們通過一些例子來介紹一些JNI異常處理函數
6.1.1 本地代碼中如何緩存和拋出異常
下面的代碼中演示瞭如何聲明一個會拋出異常的本地方法。CatchThrow這個類聲明瞭一個會拋出IllegalArgumentException異常的名叫doit的本地方法。
class CatchThrow {
throws IllegalArgumentException;
private void callback() throwsNullPointerException {
throw newNullPointerException("CatchThrow.callback");
public static void main(String args[]) {
CatchThrow c = new CatchThrow();
System.out.println("InJava:\n\t" + e);
System.loadLibrary("CatchThrow");
Main方法調用本地方法doit,doit方法的實現如下:
JNIEXPORT void JNICALL
Java_CatchThrow_doit(JNIEnv*env, jobject obj)
jclass cls = (*env)->GetObjectClass(env,obj);
(*env)->GetMethodID(env, cls,"callback", "()V");
(*env)->CallVoidMethod(env, obj, mid);
exc = (*env)->ExceptionOccurred(env);
/* We don't do much with the exception,except that
we print a debug message for it,clear it, and
(*env)->ExceptionDescribe(env);
newExcCls = (*env)->FindClass(env,
"java/lang/IllegalArgumentException");
/* Unable to find the exceptionclass, give up. */
(*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)
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);
}
(*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
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,
/* important: check if an exception wasraised */
if ((*env)->ExceptionCheck(env)) {
當一個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 (...) { /* exception occurred */
(*env)->ReleaseStringChars(env,jstr, cstr);
(*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函數後,我們不需要檢查異常,因爲本地方法後面會立即返回。