我们遇到了很多这样的情况,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;
/*skip over argument types to find out the
returntype */
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);
}
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方法之后直接返回。