我們遇到了很多這樣的情況,native代碼在進行JNI函數調用之後檢查可能的錯誤。這一章探讨native代碼怎樣檢測和修複這些錯誤。
我們會關注作為JNI函數調用傳回結果發生的錯誤(errors that occur as the result of issuing JNI function calls),而不是發生在native代碼中的任意的錯誤。如果native方法進行了系統調用,那麼可以很輕易地按照文檔提供的方法檢查系統調用中可能出現的失敗。另一方面,如果native方法執行一個Java方法的回調,那麼它必須遵循本章中介紹的步驟對可能發生的方法異常進行合适的檢查與修複。
6.1 概述(Overview)
我們通過一系列的例子來介紹JNI異常處理函數。
6.1.1 在native代碼中抓取并抛出異常(Caching and Throwing Exceptions in Native Code)
下面的程式展示了怎樣聲明一個抛出異常的native方法。CatchThrow類聲明native方法doit并且指明它抛出IllegalArgumentException異常:
classCatchThrow {
privatenative void doit()
throwsIllegalArgumentException;
privatevoid callback() throws NullPointerException {
thrownew NullPointerException("CatchThrow.callback");
}
publicstatic void main(String args[]) {
CatchThrowc = new CatchThrow();
try{
c.doit();
}catch (Exception e) {
System.out.println("InJava:\n\t" + e);
}
}
static{
System.loadLibrary("CatchThrow");
}
}
CatchThrow.main方法調用native方法doit,實作如下:
JNIEXPORTvoid JNICALL
Java_CatchThrow_doit(JNIEnv*env, jobject obj)
{
jthrowableexc;
jclasscls = (*env)->GetObjectClass(env, obj);
jmethodIDmid = (*env)->GetMethodID(env, cls, "callback", "()V");
if(mid== NULL){
return;
}
(*env)->CallVoidMethod(env,obj, mid);
exc= (*env)->ExceptionOccurred(env);
if(exc) {
//Wedon't do much with the exception, expect that we print a debug message for it,clear it, and thrown a new exception
jclassnewExcCls;
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
newExcCls= (*env)->FindClass(env, "java/lang/IllegalArgumentException");
if(newExcCls== NULL){
//Unableto find the exception class, give up
return;
}
(*env)->ThrowNew(env,newExcCls, "thrown from C code");
}
}
與本地庫一起運作這個程式,産生如下輸出:
java.lang.NullPointerException:
atCatchThrow.callback(CatchThrow.java)
atCatchThrow.doit(Native Method)
atCatchThrow.main(CatchThrow.java)
InJava:
java.lang.IllegalArgumentException:thrown from C code
回調的方法抛出NullPointerException。當CallVoidMethod将控制權傳回給native方法時,native代碼将通過調用ExceptionDescribe檢測這個異常,通過ExceptionClear清楚異常,并抛出IllegalArgumentException作為代替。
JNI産生的未知的(peding)異常(例如,調用ThrowNew)不會直接中斷native方法的異常。這與Java中異常表現(exceptionbehave)不同。當異常在Java中抛出,虛拟機自動将控制流轉到最近封裝的try/catch聲明來比對異常類型。然後虛拟機清除挂起的(peding)異常并執行異常處理函數。相反地,當異常發生後,JNI程式員必須顯式地實作控制流的改變。
6.1.2 一個公共函數(A Utility Function)
抛出一個異常,包含第一次發現的異常類,(Throwingan exception involves first finding the exception class)然後調用ThrowNew函數。為了簡化這個工作,我們寫一個公共函數來抛出命名的異常:
voidJNU_ThrowByName(JNIEnv *env, const char *name, const char *msg)
{
jclasscls = (*env)->FindClass(env, name);
//ifcls is NULL, an exception has already been thrown
if(cls!= NULL){
(*env)->ThrownNew(env,cls, msg);
}
//freethe local ref
(*env)->DeleteLocalRef(env,cls);
}
在本書中JNU字首代表JNIUtilities,JNU_ThrowByName使用FindClass函數首先找到異常類。如果FindClass執行失敗(傳回NULL),虛拟機必須抛出一個異常(比如NoClassDefFoundError)。在這種情況下,JNU_ThrowByName不會嘗試抛出另一個異常。如果FindCass執行成功,我們通過調用ThrowNew抛出命名的異常。當JNU_ThrowByName傳回時,它保證有一個未處理的異常(pending exception),雖然命名參數指明這個未處理的異常不是必須的。我們確定删掉了在這個函數中建立的指向異常類的局部引用。傳遞NULL給DeleteLocalRef是一個空操作,如果FindClass執行失敗并傳回NULL的話這是合适的行為。
6.2 恰當的異常處理
JNI程式員必須預見可能出現的異常情況,然後寫代碼來檢查和處理這些情況。恰當的異常處理有時候是令人厭煩的但是對于健壯的應用是必須的。
6.2.1異常檢查
有兩種方式來檢查異常是否發生:
1、大多數JNI函數使用明确的傳回值(如NULL)來表示錯誤的發生。錯誤傳回值也表明目前程序中有一個未處理的異常。(在傳回值中對錯誤情況進行編碼是對C語言來說很常見的)
下面的執行個體示範說明了使用GetFieldID傳回的NULL進行異常檢查。執行個體中包括兩部分:一個定義了大量執行個體成員(handle,length,width)的Window類和一個緩存了這些成員的成員ID的native方法。盡管這些成員在Windows類中,我們仍然需要對從GetFieldID傳回的可能的錯誤進行檢查,因為虛拟機可能不能配置設定表示一個成員ID所需的記憶體。
//aclass in java programming language
publicclass Window{
longhandle;
intlength;
intwindth;
staticnative void initIDs();
static(
initIDs();
}
}
//Ccode that implements Window.initIDs
jfieldIDFID_Window_handle;
jfieldIDFID_Window_length;
jfieldIDFID_Window_width;
JNIEXPORTvoid JNICALL
Java_Window_initIDs(JNIEnv*env, jclass classWindow)
{
FID_Window_handle= (*env)->GetFieldID(env, classWindow, "handle", "J");
if(FID_Window_handle== NULL){
return;//erroroccured
}
FID_Window_length= (*env)->GetFieldID(env, classWindow, "length", "I");
if(FID_Window_length== NULL){
return;//erroroccured
}
FID_Window_width= (*env)->GetFieldID(env, classWindow, "length", "I");
//nochecks necessary; we are about to return anyway;
}
2、當使用傳回值不是錯誤發生标志的JNI函數時,native代碼必須依賴于發生的異常進行錯誤檢查。在目前線程中執行未處理異常檢查的JNNI函數是ExceptionOccurred(ExceptionOccurred也是JDK1.2中新增的)。例如,JNI函數CallIntMethod不為錯誤情況在傳回值中編碼。典型情況傳回值的典型選擇,比如NULL和-1,失效了,因為它們可能是調用函數的合法傳回值。考慮一個Fraction類,它的floor方法傳回fraction值得整數部分,一些native代碼調用了這個方法。
publicclass Fraction {
//detailssuch as constructors ommitted
intover, under;
publicint floor() {
returnMath.floor((double)over/under);
}
}
//nativecode that calls Fraction.floor.Assume method ID MID_Fraction_floor has beeninitialized elsewhere
viodf(JNIEnv, *env, jobject fraction)
{
jintfloor = (*env)->CallIntMethod(env, fraction, MID_Fraction_floor);
//important:checkif an exception was raised
if((*env)->ExceptionCheck(env)){
return;
}
……//usefloor
}
當JNI函數傳回一個明确的錯誤代碼,native可能仍然顯式地調用ExceptionCheck進行異常檢查。然而,檢查明确的錯誤代碼傳回值更加高效。如果JNI函數傳回了它的錯誤值,接下來目前線中調用ExceptionCheck會傳回JNI_TRUE。
6.2.2 異常處理(Handing Exception)
native 代碼可能通過兩種方式處理挂起的異常(pending exception):
1、naitive代碼可以選擇直接傳回,在調用者中處理異常
2、native代碼可以通過調用ExceptionClear清除異常,然後執行它自己的異常處理代碼
在調用任何後續的JNI函數之前,檢查處理清除挂起的異常(pending exception)是極端重要的。調用大多數帶有未處理的異常(pending excep)的JNI函數——帶有你沒有明确清理的異常——可能會導緻意料之外的結果。當目前線程有未處理的異常時,你隻可以安全地調用少數JNI函數。一般來說,當有一個未處理的異常時,你可以調用通過JNI暴漏的進行異常處理的JNI函數和釋放各種虛拟機資源的JNI函數。
當異常發生時,釋放資源通常來說是必須的。在下面的例子中,native方法首先通過GetStringChars來獲得一個字元串的内容,如果後續操作失敗,它調用ReleaseStringChars:
JNIEXPORTvoid JNICALL
Java_pkg_Cls_f(JNIEnv *env,jclass cls, jstring jstr)
{
constjchar *cstr = (*env)->GetStringChars(env, jstr);
if(cstr== NULL){
return;
}
……
if(……){//exception occured
(*env)->ReleaseStringChars(env,jstr, cstr);
return;
}
……
//normalreturn
(*env)->ReleaseStringChars(env,jstr, cstr);
}
第一次調用ReleaseStringChars是解決有未處理異常的情況。native方法後來釋放字元串資源然後直接傳回,沒有首先清理異常。
6.2.3 工具函數中的異常(Exception in Utility Functions)
編寫工具函數的程式員應當格外注意確定異常傳遞給native方法的調用者。我們特别強調下面兩個問題:
1、更好的方式是,工具函數應當提供特别的傳回值來指出異常的發生,這簡化了調用者檢查挂起異常(pending exception)的任務。
2、此外,工具函數在異常處理代碼中應當遵循局部引用管理規則。
為了示範說明,我們引入這樣一個函數,它基于一個執行個體方法的名稱和描述符執行回調:
jvalue
JNU_CallMethodByName(JNIEnv*env,
jboolean*hasException,
jobjectobj,
constchar *name,
constchar *descriptor, ...)
{
va_listargs;
jclassclazz;
jmethodIDmid;
jvalueresult;
if((*env)->EnsureLocalCapacity(env, 2) == JNI_OK) {
clazz= (*env)->GetObjectClass(env, obj);
mid= (*env)->GetMethodID(env, clazz, name,
descriptor);
if(mid) {
constchar *p = descriptor;
while(*p != ')') p++;
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);
}
returnresult;
}
在其他參數中,JNU_CallMethodByName有一個指向jboolean的指針。如果一切都執行成功,jboolean被置為JNI_FALSE;如果在函數執行過程的任何一點出現異常,則置為JNI_TRUE。這就給了JNU_CallMethodByName的調用者一個明顯的(obvious)方式來檢查可能的異常。
JNU_CallMethodByName首先确認它可以建立兩個局部引用:一個作為類引用,另一個指向從方法調用傳回的結果。下一步,它從對象得到類引用,并查找方法ID。根據傳回類型,switch狀态分發給相應的JNI方法調用函數(JNI method call function)。回調傳回後,如果hasException傳回NULL,我們調用ExceptionCheck來檢查未處理的異常。
ExceptionCheck函數是JDK1.2新增的。它與ExceptionOccurred函數類似。他們的不同在于ExceptionCheck不傳回一個指向異常對象的引用,而是當有未處理異常時傳回JNI_TRUE,如果沒有傳回JNI_FALSE。當native代碼隻需要知道異常是否發生而不需要擷取指向異常對象的引用時,ExceptionCheck簡化了局部引用的管理。如果遵循JDK1.1,前面的代碼将不得不像下面這樣重寫:
if(hasException) {
jthrowableexc = (*env)->ExceptionOccurred(env);
*hasException= exc != NULL;
(*env)->DeleteLocalRef(env,exc);
}
為了删除指向異常對象的局部引用,添加的DeleteLocalRef調用是必須的。
使用JNU_CallMethodByName函數我們可以像下面這樣重寫4.2節中的InstanceMethodCall.nativeMethod:
JNIEXPORTvoid JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv*env, jobject obj)
{
printf("InC\n");
JNU_CallMethodByName(env,NULL, obj, "callback", "()V");
}
在JNU_CallMethodByName調用之後我們不需要進行異常檢查,因為native方法之後直接傳回。