JNI (二)

JNI提供了一些实例和数组类型(jobject、jclass、jstring、jarray等)作为不透明的引用供本地代码使用。本地代码永远不会直接操作引用指向的VM内部的数据内容。要进行这些操作,必须通过使用JNI操作一个不引用来间接操作数据内容。因为只操作引用,你不必担心特定JVM中对象的存储方式等信息。这样的话,你有必要了解一下JNI中的几种不同的引用:

1、 JNI支持三种引用:局部引用、全局引用、弱全局引用(下文简称“弱引用”)。

2、 局部引用和全局引用有不同的生命周期。当本地方法返回时,局部引用会被自动释放。而全局引用和弱引用必须手动释放。

3、 局部引用或者全局引用会阻止GC回收它们所引用的对象,而弱引用则不会。

4、 不是所有的引用可以被用在所有的场合。例如,一个本地方法创建一个局部引用并返回后,再对这个局部引用进行访问是非法的。

本章中,我们会详细地讨论这些问题。合理地管理JNI引用是写出高质量的代码的基础。

5.1 局部引用和全局引用

什么是全局引用和局部引用?它们有什么不同?我们下面使用一些例子来说明。

5.1.1 局部引用

大多数JNI函数会创建局部引用。例如,NewObject创建一个新的对象实例并返回一个对这个对象的局部引用。

局部引用只有在创建它的本地方法返回前有效。本地方法返回后,局部引用会被自动释放。

你不能在本地方法中把局部引用存储在静态变量中缓存起来供下一次调用时使用。下面的例子是MyNewString函数的一个修改版本,这里面使用局部引用的方法是错误的:

/* This code is illegal */

jstring

MyNewString(JNIEnv *env, jchar *chars, jint len)

{

static jclass stringClass = NULL;

jmethodID cid;

jcharArray elemArr;

jstring result;

if (stringClass == NULL) {

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

"java/lang/String");

if (stringClass == NULL) {

return NULL; /* exception thrown */

}

}

/* It is wrong to use the cached stringClass here,

because it may be invalid. */

cid = (*env)->GetMethodID(env, stringClass,

"<init>", "([C)V");

...

elemArr = (*env)->NewCharArray(env, len);

...

result = (*env)->NewObject(env, stringClass, cid, elemArr);

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

return result;

}

上面代码中,我们省略了和我们的讨论无关的代码。因为FindClass返回一个对java.lang.String对象的局部引用,上面的代码中缓存stringClassr做法是错误的。假设一个本地方法C.f调用了MyNewString:

JNIEXPORT jstring JNICALL

Java_C_f(JNIEnv *env, jobject this)

{

char *c_str = ...;

...

return MyNewString(c_str);

}

C.f方法返回后,VM释放了在这个方法执行期间创建的所有局部引用,也包含对String类的引用stringClass。当再次调用MyNewString时,会试图访问一个无效的局部引用,从而导致非法的内存访问甚至系统崩溃。

释放一个局部引用有两种方式,一个是本地方法执行完毕后VM自动释放,另外一个是程序员通过DeleteLocalRef手动释放。

既然VM会自动释放局部引用,为什么还需要手动释放呢?因为局部引用会阻止它所引用的对象被GC回收。

局部引用只在创建它们的线程中有效,跨线程使用是被禁止的。不要在一个线程中创建局部引用并存储到全局引用中,然后到另外一个线程去使用。

5.1.2 全局引用

全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,全局引用也会阻止它所引用的对象被GC回收。

与局部引用可以被大多数JNI函数创建不同,全局引用只能使用一个JNI函数创建:NewGlobalRef。下面这个版本的MyNewString演示了怎么样使用一个全局引用:

/* This code is OK */

jstring

MyNewString(JNIEnv *env, jchar *chars, jint len)

{

static jclass stringClass = NULL;

...

if (stringClass == NULL) {

jclass localRefCls =

(*env)->FindClass(env, "java/lang/String");

if (localRefCls == NULL) {

return NULL; /* exception thrown */

}

/* Create a global reference */

stringClass = (*env)->NewGlobalRef(env, localRefCls);

/* The local reference is no longer useful */

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

/* Is the global reference created successfully? */

if (stringClass == NULL) {

return NULL; /* out of memory exception thrown */

}

}

...

}

上面这段代码中,一个由FindClass返回的局部引用被传入NewGlobalRef,用来创建一个对String类的全局引用。删除localRefCls后,我们检查NewGlobalRef是否成功创建stringClass。

5.1.3 弱引用

弱引用使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放。与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象。

在MyNewString中,我们也可以使用弱引用来存储stringClass这个类引用,因为java.lang.String这个类是系统类,永远不会被GC回收。

当本地代码中缓存的引用不一定要阻止GC回收它所指向的对象时,弱引用就是一个最好的选择。假设,一个本地方法mypkg.MyCls.f需要缓存一个指向类mypkg.MyCls2的引用,如果在弱引用中缓存的话,仍然允许mypkg.MyCls2这个类被unload:

JNIEXPORT void JNICALL

Java_mypkg_MyCls_f(JNIEnv *env, jobject self)

{

static jclass myCls2 = NULL;

if (myCls2 == NULL) {

jclass myCls2Local =

(*env)->FindClass(env, "mypkg/MyCls2");

if (myCls2Local == NULL) {

return; /* can't find class */

}

myCls2 = NewWeakGlobalRef(env, myCls2Local);

if (myCls2 == NULL) {

return; /* out of memory */

}

}

... /* use myCls2 */

}

我们假设MyCls和MyCls2有相同的生命周期(例如,他们可能被相同的类加载器加载),因为弱引用的存在,我们不必担心MyCls和它所在的本地代码在被使用时,MyCls2这个类出现先被unload,后来又会preload的情况。

当然,真的发生这种情况时(MyCls和MyCls2的生命周期不同),我们必须检查缓存过的弱引用是指向活动的类对象,还是指向一个已经被GC给unload的类对象。下一节将告诉你怎么样检查弱引用是否活动。

5.1.4 引用比较

给定两个引用(不管是全局、局部还是弱引用),你可以使用IsSameObject来判断它们两个是否指向相同的对象。例如:

(*env)->IsSameObject(env, obj1, obj2)

如果obj1和obj2指向相同的对象,上面的调用返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)。

JNI中的一个引用NULL指向JVM中的null对象。如果obj是一个局部或者全局引用,你可以使用(*env)->IsSameObject(env, obj, NULL)或者obj == NULL来判断obj是否指向一个null对象。

在这一点儿上,弱引用有些有同,一个NULL弱引用同样指向一个JVM中的null对象,但不同的是,在一个弱引用上面使用IsSameObject时,返回值的意义是不同的:

(*env)->IsSameObject(env, wobj, NULL)

上面的调用中,如果wobj已经被回收,会返回JNI_TRUE,如果wobj仍然指向一个活动对象,会返回JNI_FALSE。

5.2 释放引用

每一个JNI引用被建立时,除了它所指向的JVM中的对象以外,引用本身也会消耗掉一个数量的内存。作为一个JNI程序员,你应该对程序在一个给定时间段内使用的引用数量十分小心。短时间内创建大量不会被立即回收的引用会导致内存溢出。

5.2.1 释放局部引用

大部分情况下,你在实现一个本地方法时不必担心局部引用的释放问题,因为本地方法被调用完成后,JVM会自动回收这些局部引用。尽管如此,以下几种情况下,为了避免内存溢出,JNI程序员应该手动释放局部引用:

1、 在实现一个本地方法调用时,你需要创建大量的局部引用。这种情况可能会导致JNI局部引用表的溢出,所以,最好是在局部引用不需要时立即手动删除。比如,在下面的代码中,本地代码遍历一个大的字符串数组,每遍历一个元素,都会创建一个局部引用,当对这个元素的遍历完成时,这个局部引用就不再需要了,你应该手动释放它:

for (i = 0; i < len; i++) {

jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);

... /* process jstr */

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

}

2、 你想写一个工具函数,这个函数被谁调用你是不知道的。4.3节中的MyNewString演示了怎么样在工具函数中使用引用后,使用DeleteLocalRef删除。不这样做的话,每次MyNewString被调用完成后,就会有两个引用仍然占用空间。

3、 你的本地方法不会返回任何东西。例如,一个本地方法可能会在一个事件接收循环里面被调用,这种情况下,为了不让局部引用累积造成内存溢出,手动释放也是必须的。

4、 你的本地方法访问一个大对象,因此创建了一个对这个大对象的引用。然后本地方法在返回前会有一个做大量的计算过程,而在这个过程中是不需要前面创建的对大对象的引用的。但是,计算过程,对大对象的引用会阻止GC回收大对象。

在下面的程序中,因为预先有一个明显的DeleteLocalRef操作,在函数lengthyComputation的执行过程中,GC可能会释放由引用lref指向的对象。

5.2.2 管理局部引用

JDK提供了一系列的函数来管理局部引用的生命周期。这些函数包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame。

JNI规范中指出,VM会确保每个本地方法可以创建至少16个局部引用。经验表明,这个数量已经满足大多数不需要和JVM中的内部对象有太多交互的本地方法。如果真的需要创建更多的引用,本地方法可以通过调用EnsureLocalCapacity来支持更多的局部引用。在下面的代码中,对前面的例子做了些修改,不考虑内存因素的情况下,它可以为创建大量的局部引用提供足够的空间。

· /* The number of local references to be created is equal to

· the length of the array. */

· if ((*env)->EnsureLocalCapacity(env, len)) < 0) {

· ... /* out of memory */

· }

· for (i = 0; i < len; i++) {

· jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);

· ... /* process jstr */

· /* DeleteLocalRef is no longer necessary */

· }

当然,上面这个版本中没有立即删除不使用的局部引用,因此会比前面的版本消耗更多的内存。

另外,Push/PopLocalFrame函数对允许程序员创建作用范围层层嵌套的局部引用。例如,我们可以把上面的代码重写:

· #define N_REFS ... /* the maximum number of local references

· used in each iteration */

· for (i = 0; i < len; i++) {

· if ((*env)->PushLocalFrame(env, N_REFS) < 0) {

· ... /* out of memory */

· }

· jstr = (*env)->GetObjectArrayElement(env, arr, i);

· ... /* process jstr */

· (*env)->PopLocalFrame(env, NULL);

· }

PushLocalFrame为一定数量的局部引用创建了一个使用堆栈,而PopLocalFrame负责销毁堆栈顶端的引用。

Push/PopLocalFrame函数对提供了对局部引用的生命周期更方便的管理。上面的例子中,如果处理jstr的过程中创建了局部引用,则PopLocalFrame执行时,这些局部引用全部会被销毁。

当你写一个会返回局部引用的工具函数时,NewLocalRef非常有用,我们会在5.3节中演示NewLocalRef的使用。

本地代码可能会创建大量的局部引用,其数量可能会超过16个或PushLocaFrame和EnsureLocalCapacity调用设置的个数。VM可能会尝试分配足够的内存,但不能够保证分配成功。如果失败,VM会退出。

5.2.3 释放全局引用

当你的本地代码不再需要一个全局引用时,你应该调用DeleteGlobalRef来释放它。如果你没有调用这个函数,即使这个对象已经没用了,JVM也不会回收这个全局引用所指向的对象。

当你的本地代码不再需要一个弱引用时,应该调用DeleteWeakGlobalRef来释放它,如果你没有调用这个函数,JVM仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存永远也不会被回收。

5.3 管理引用的规则

前面已经做了一个全面的介绍,现在我们可以总结一下JNI引用的管理规则了,目标就是减少内存使用和对象被引用保持而不能释放。

通常情况下,有两种本地代码:直接实现本地方法的本地代码和可以被使用在任何环境下的工具函数。

当编写实现本地方法的本地代码时,当心不要造成全局引用和弱引用的累加,因为本地方法执行完毕后,这两种引用不会被自动释放。

当编写一个工具函数的本地代码时,当心不要在函数的调用轨迹上面遗漏任何的局部引用,因为工具函数被调用的场合是不确定的,一旦被大量调用,很有可能造成内存溢出。

编写工具函数时,请遵守下面的规则:

1、 一个返回值为基本类型的工具函数被调用时,它决不能造成局部、全局、弱引用不被回收的累加。

2、 当一个返回值为引用类型的工具函数被调用时,它除了返回的引用以外,它决不能造成其它局部、全局、弱引用的累加。

对工具函数来说,为了使用缓存技术而创建一些全局引用或者弱引用是正常的。

如果一个工具函数返回一个引用,你应该详细说明返回的引用的类型,以便于调用者更好地管理它们。下面的代码中,频繁地调用工具函数GetInfoString,我们需要知道GetInfoString返回的引用的类型,以便于在每次使用完成后可以释放掉它:

· while (JNI_TRUE) {

· jstring infoString = GetInfoString(info);

· ... /* process infoString */

·

· ??? /* we need to call DeleteLocalRef, DeleteGlobalRef,

· or DeleteWeakGlobalRef depending on the type of

· reference returned by GetInfoString. */

· }

函数NewLocalRef有时被用来确保一个工具函数返回一个局部引用。为了演示这个用法,我们对MyNewString函数做了一些修改。下面的版本把一个被频繁调用的字符串“CommonString” 缓存在了全局引用里:

· jstring

· MyNewString(JNIEnv *env, jchar *chars, jint len)

· {

· static jstring result;

·

· /* wstrncmp compares two Unicode strings */

· if (wstrncmp("CommonString", chars, len) == 0) {

· /* refers to the global ref caching "CommonString" */

· static jstring cachedString = NULL;

· if (cachedString == NULL) {

· /* create cachedString for the first time */

· jstring cachedStringLocal = ... ;

· /* cache the result in a global reference */

· cachedString =

· (*env)->NewGlobalRef(env, cachedStringLocal);

· }

· return (*env)->NewLocalRef(env, cachedString);

· }

·

· ... /* create the string as a local reference and store in

· result as a local reference */

· return result;

· }

在管理局部引用的生命周期中,Push/PopLocalFrame是非常方便的。你可以在本地函数的入口处调用PushLocalFrame,然后在出口处调用PopLocalFrame,这样的话,在函数对中间任何位置创建的局部引用都会被释放。而且,这两个函数是非常高效的,强烈建议使用它们。

如果你在函数的入口处调用了PushLocalFrame,记住在所有的出口(有return出现的地方)调用PopLocalFrame。在下面的代码中,对PushLocalFrame的调用只有一次,但对PopLocalFrame的调用却需要多次。

· jobject f(JNIEnv *env, ...)

· {

· jobject result;

· if ((*env)->PushLocalFrame(env, 10) < 0) {

· /* frame not pushed, no PopLocalFrame needed */

· return NULL;

· }

· ...

· result = ...;

· if (...) {

· /* remember to pop local frame before return */

· result = (*env)->PopLocalFrame(env, result);

· return result;

· }

· ...

· result = (*env)->PopLocalFrame(env, result);

· /* normal return */

· return result;

· }

 

很多情况下,本地代码做JNI调用后都要检查是否有错误发生,本章讲的就是怎么样检查错误和处理错误。

我重点放在JNI函数调用引发的错误上面。如果一个本地方法中调用了一个JNI函数,它必须遵守下面几个步骤来检查和处理这个JNI函数调用时可能引发的错误。至于其它可能的错误,比如本地代码中调用了一个可能引发错误的系统方法,那只需要按照该系统方法的标准文档中规定的来处理就可以了。

6.1 概述

我们通过一些例子来介绍一些JNI异常处理函数

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方法调用本地方法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 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

回调方法抛出一个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 code that 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; /* error occurred. */

24. }

25. FID_Window_length =

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

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

28. return; /* error occurred. */

29. }

30. FID_Window_width =

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

32. /* no checks necessary; we are 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 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 */

}

当一个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 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的参数当中有一个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函数后,我们不需要检查异常,因为本地方法后面会立即返回。

本章讲述如何把一个JVM嵌入到你的本地程序当中去。一个JVM可以看作就是一个本地库。本地程序可以链接这个库,然后通过“调用接口”(invocation interface)来加载JVM。实际上,JDK中标准的启动器也就是一段简单的链接了JVM的C代码。启动器解析命令、加载JVM、并通过“调用接口”(invocation interface)运行JAVA程序。

7.1 创建JVM

我们用下面这段C代码来加载一个JVM并调用Prog.main方法来演示如何使用调用接口。

public class Prog {

public static void main(String[] args) {

System.out.println("Hello World " + args[0]);

}

}

下面是启动器:

#include <jni.h>

#define PATH_SEPARATOR ';' /* define it to be ':' on Solaris */

#define USER_CLASSPATH "." /* where Prog.class is */

main() {

JNIEnv *env;

JavaVM *jvm;

jint res;

jclass cls;

jmethodID mid;

jstring jstr;

jclass stringClass;

jobjectArray args;

#ifdef JNI_VERSION_1_2

JavaVMInitArgs vm_args;

JavaVMOption options[1];

options[0].optionString =

"-Djava.class.path=" USER_CLASSPATH;

vm_args.version = 0x00010002;

vm_args.options = options;

vm_args.nOptions = 1;

vm_args.ignoreUnrecognized = JNI_TRUE;

/* Create the Java VM */

res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);

#else

JDK1_1InitArgs vm_args;

char classpath[1024];

vm_args.version = 0x00010001;

JNI_GetDefaultJavaVMInitArgs(&vm_args);

/* Append USER_CLASSPATH to the default system class path */

sprintf(classpath, "%s%c%s",

vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);

vm_args.classpath = classpath;

/* Create the Java VM */

res = JNI_CreateJavaVM(&jvm, &env, &vm_args);

#endif /* JNI_VERSION_1_2 */

if (res < 0) {

fprintf(stderr, "Can't create Java VM\n");

exit(1);

}

cls = (*env)->FindClass(env, "Prog");

if (cls == NULL) {

goto destroy;

}

mid = (*env)->GetStaticMethodID(env, cls, "main",

"([Ljava/lang/String;)V");

if (mid == NULL) {

goto destroy;

}

jstr = (*env)->NewStringUTF(env, " from C!");

if (jstr == NULL) {

goto destroy;

}

stringClass = (*env)->FindClass(env, "java/lang/String");

args = (*env)->NewObjectArray(env, 1, stringClass, jstr);

if (args == NULL) {

goto destroy;

}

(*env)->CallStaticVoidMethod(env, cls, mid, args);

destroy:

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

(*env)->ExceptionDescribe(env);

}

(*jvm)->DestroyJavaVM(jvm);

}

上面的代码有条件地编译一个初始化JDK1_1InitArgs这个structure。这个structure是JDK1.1下特有的,尽管JDK1.2也会支持,但JDK1.2引入了一个更通用的叫作JavaVMInitArgs的VM初始化structure。

常量JNI_VERSION_1_2在JDK1.2下定义,JDK1.1下是没有的。

当目标平台是1.1时,C代码首先调用JNI_GetDefaultJavaVMInitArgs来获得默认的VM设置。这个调用会返回heap size、stack size、默认类路径等信息,并把这些信息存放在参数vm_args中。然后我们把Prog.class所在的目录附加到vm_args.classpath中。

当平台目标是1.2时,C代码创建了一个JavaVMInitArgs的structure。VM的初始化参数被存放在一个JavaVMOption数组中。

设置完VM初始化structure后,C程序调用JNI_CreateJavaVM来加载和初始化JVM,传入的前两个参数:

1、 接口指针jvm,指向新创建的JVM。

2、 当前线程的JNIEnv接口指针env。本地代码通过env指针访问JNI函数。

当函数JNI_CreateJavaVM函数成功返回时,当前本地线程已经把自己的控制权交给JVM。这时,它会就像一个本地方法一样运行。以后就可以通过JNI函数来启动Prog.main方法。

接着,程序调用DestroyJavaVM函数来unloadJVM。不幸的是,在JDK1.1和JDK1.2中你不能unloadJVM,它会一直返回一个错误码。

运行上面的程序,产生如下输出:

Hello World from C!

7.2 把本地程序和JVM链接在一起

通过调用接口,你可把invoke.c这样的程序和一个JVM链接到一起。怎么样链接JVM取决于本地程序是要和一个特定的VM一起工作,还是要和多个具体实现方式未知的不同VM一起工作。

7.2.1 和一个己知的JVM链接到一起

这种情况下,你可以把你的本地程序和实现了JVM的本地库链接在一起。编译链接成功后,你就可以运行得到的可执行文件。运行时,你可能会得到一个错误信息,比如“无法找到共享库或者动态链接库”,在Windows下,错误信息可能会指出无法发现动态链接库javai.dll(JDK1.1)或者jvm.dll(JDK1.2),这时,你需要把DLL文件加载到你的PATH环境变量中去。

7.2.2 和未知的多个JVM链接到一起

这种情况下,你就不能把本地程序直接和一个特定的库链接在一起了。比如,JDK1.1的库是javai.dll,而JDK1.2的库是jvm.dll。

解决方案是根据本地程序的需要,用运行时动态链接来加载不同的VM库。例如,下面的win32代码,根据给定的VM库的路径找到JNI_CreateJavaVM函数的入口。

LoadLibrary和GetProcAddress是Win32平台上用来动态链接的API。虽然LoadLibrary可以实现了JVM的本地库的名字(如“jvm”)或者路径(如“C:\\jdk1.2\\jre\\bin\\classic\\jvm.dll”)。最好把本地库的绝对路径传递给JNU_FindCreateJavaVM,让LoadLibrary去搜索jvm.dll,这样程序就不怕环境变量被改变了。

7.3 附加本地线程

假设,你有一个用C写的服务器这样的多线程程序。当HTTP请求进来的时候,服务器创建许多本地线程来并行的处理HTTP请求。为了让多个线程可以同时操作JVM,我们可能需要把一个JVM植入这个服务器。

图7.1 把JVM嵌入WEB服务器

服务器上的本地方法的生命周期一般会比JVM要短,因此我们需要一个方法把本地线程附加到一个已经在运行的JVM上面,然后在这个本地方法中进行JNI调用,最后在不打扰其它连接到JVM上的线程的情况下把这个本地线程和JVM分离。

下面这个例子中,attach.c演示了怎么样使用调用接口(invocation interface)把本地线程附加到VM上去,这段程序使用的是Win32线程API。

/* Note: This program only works on Win32 */

#include <windows.h>

#include <jni.h>

JavaVM *jvm; /* The virtual machine instance */

#define PATH_SEPARATOR ';'

#define USER_CLASSPATH "." /* where Prog.class is */

void thread_fun(void *arg)

{

jint res;

jclass cls;

jmethodID mid;

jstring jstr;

jclass stringClass;

jobjectArray args;

JNIEnv *env;

char buf[100];

int threadNum = (int)arg;

/* Pass NULL as the third argument */

#ifdef JNI_VERSION_1_2

res = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);

#else

res = (*jvm)->AttachCurrentThread(jvm, &env, NULL);

#endif

if (res < 0) {

fprintf(stderr, "Attach failed\n");

return;

}

cls = (*env)->FindClass(env, "Prog");

if (cls == NULL) {

goto detach;

}

mid = (*env)->GetStaticMethodID(env, cls, "main",

"([Ljava/lang/String;)V");

if (mid == NULL) {

goto detach;

}

sprintf(buf, " from Thread %d", threadNum);

jstr = (*env)->NewStringUTF(env, buf);

if (jstr == NULL) {

goto detach;

}

stringClass = (*env)->FindClass(env, "java/lang/String");

args = (*env)->NewObjectArray(env, 1, stringClass, jstr);

if (args == NULL) {

goto detach;

}

(*env)->CallStaticVoidMethod(env, cls, mid, args);

detach:

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

(*env)->ExceptionDescribe(env);

}

(*jvm)->DetachCurrentThread(jvm);

}

main() {

JNIEnv *env;

int i;

jint res;

#ifdef JNI_VERSION_1_2

JavaVMInitArgs vm_args;

JavaVMOption options[1];

options[0].optionString =

"-Djava.class.path=" USER_CLASSPATH;

vm_args.version = 0x00010002;

vm_args.options = options;

vm_args.nOptions = 1;

vm_args.ignoreUnrecognized = TRUE;

/* Create the Java VM */

res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);

#else

JDK1_1InitArgs vm_args;

char classpath[1024];

vm_args.version = 0x00010001;

JNI_GetDefaultJavaVMInitArgs(&vm_args);

/* Append USER_CLASSPATH to the default system class path */

sprintf(classpath, "%s%c%s",

vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);

vm_args.classpath = classpath;

/* Create the Java VM */

res = JNI_CreateJavaVM(&jvm, &env, &vm_args);

#endif /* JNI_VERSION_1_2 */

if (res < 0) {

fprintf(stderr, "Can't create Java VM\n");

exit(1);

}

for (i = 0; i < 5; i++)

/* We pass the thread number to every thread */

_beginthread(thread_fun, 0, (void *)i);

Sleep(1000); /* wait for threads to start */

(*jvm)->DestroyJavaVM(jvm);

}

上面这段attach.c代码是invoke.c的一个变形。与在主线程中调用Prog.main不同,本地代码开启了五个线程。开启线程完成以后,它就会等待1秒钟让线程可以运行完毕,然后调用DestroyJavaVM来销毁JVM。而每一个线程都会把自己附加到JVM上面,然后调用Prog.main方法,最后断开与JVM的连接。

JNI_AttachCurrentThread的第三个参数需要传入NULL。JDK1.2引入了JNI_ThreadAttachArgs这个structure。它允许你向你要附加的线程传递特定的信息,如线程组等。JNI_ThreadAttachArgs这个structure的详细描述在13.2节里面,作为JNI_AttachCurrentThread的规范的一部分被提到。

当程序运行函数DetachCurrentThread时,它释放属于当前线程的所有局部引用。

运行程序,输出如下:

Hello World from thread 1

Hello World from thread 0

Hello World from thread 4

Hello World from thread 2

Hello World from thread 3

上面这些输出根据不同的线程调试策略,可能会出现不同的顺序。

 

我们已经讨论了JNI在写本地代码和向本地应用程序中集成JVM时的特征。本章接下来的部分分介绍其它的JNI特征。

8.1 JNI和线程

JVM可以做到在相同的地址空间内执行多个线程。由于多个线程可能会在同时共享资源,所以,增加了程序的复杂性。

要完全理解本章的东西,你需要对多线程编程比较熟悉,知道怎么样在JAVA中用多线程访问共享资源。

8.1.1 约束限制

如果你的本地代码要运行在多个线程中,有一些约束条件需要注意,这样的话,才能使得你的本地代码无论被多少个线程同时运行,都不会出现问题。

1、 JNIEnv指针只在它所在的线程中有效,不能跨线程传递和使用。不同线程调用一个本地方法时,传入的JNIEnv指针是不同的。

2、 局部引用只在创建它们的线程中有效,同样不能跨线程传递。但可以把局部引用转化成全局引用来供多线程使用。

8.1.2 监视器的入口和出口

监视器是JAVA平台的基本同步机制。每一个对象都可以和一个监视器绑定:

synchronized (obj) {

... // synchronized block

}

本地代码中可以通过调用JNI函数来达到与上述JAVA代码中等效的同步目的。这要用到两个JNI函数:MonitorEnter负责进入同步块,MonitorExit用来函数同步块。

if ((*env)->MonitorEnter(env, obj) != JNI_OK) {

... /* error handling */

}

... /* synchronized block */

if ((*env)->MonitorExit(env, obj) != JNI_OK) {

... /* error handling */

};

运行上面这段代码时,线程必须先进入obj的监视器,再执行同步块中的代码。MonitorEnter需要传入jobject作为参数。同时,如果另一个线程已经进入了这个与jobject监视器的话,当前线程会阻塞。如果当前线程在不拥有监视器的情况下调用MonitorExit的话,会产生一个错误,并抛出一个IllegalMonitorStateException异常。上面的代码中包含了MonitorEnterMonitorExit这对函数的调用,在这对函数的使用时,我们一定要注意错误检查,因为这对函数有可能执行失败(比如,建立监视器的资源分配不成功等原因)。这对函数可以工作在jclass、jstring、jarray等类型上面,这些类型的共同特征是,都是jobject引用的特殊类型

有一个MonitorEnter方法,一定也要有一个与之对应的MonitorExit方法。尤其是在有错误或者异常需要处理的地方,要尤其小心。

if ((*env)->MonitorEnter(env, obj) != JNI_OK) ...;

...

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

... /* exception handling */

/* remember to call MonitorExit here */

if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;

}

... /* Normal execution path.

if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;

调用MonitorEnter而不调用MonitorExit的话,很可能会引起死锁。通过上面这段代码和本节开始时的JAVA代码的比较,你一定能发现用JAVA来进行同步要方便的多,所以,尽量用JAVA来做同步吧,把与同步相关的代码都挪到JAVA中去吧。

8.1.3 监视器等待和唤醒

JAVA还提供了其它一些和线程监视器有关的API:Object.wait、Object.notify、Object.notifyAll。因为监视器等待和唤醒操作没有进入和退出操作对时效性要求那么高,所以,没有提供与这些方法相对应的JNI函数。我们可以通过JNI调用JAVA的机制来调用这些方法。

/* precomputed method IDs */

static jmethodID MID_Object_wait;

static jmethodID MID_Object_notify;

static jmethodID MID_Object_notifyAll;

void

JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout)

{

(*env)->CallVoidMethod(env, object, MID_Object_wait,

timeout);

}

void

JNU_MonitorNotify(JNIEnv *env, jobject object)

{

(*env)->CallVoidMethod(env, object, MID_Object_notify);

}

void

JNU_MonitorNotifyAll(JNIEnv *env, jobject object)

{

(*env)->CallVoidMethod(env, object, MID_Object_notifyAll);

}

上例中,我们假设Object.wait、Object.notify和Object.notifyAll已经在其它地方计算好并缓存在全局引用里面了。

8.1.4 在任意地方获取JNIEnv指针

前面我们提到了,JNIEnv指针只在当前线程中有效。那么有没有办法可以从本地代码的任意地方获取到JNIEnv指针呢?比如,一个操作系统的回调函数中,本地代码是无法通过传参的方式获取到JNIEnv指针的。

可以通过调用接口(invocation interface)中的AttachCurrentThread方法来获取到当前线程中的JNIEnv指针:

JavaVM *jvm; /* already set */

f()

{

JNIEnv *env;

(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);

... /* use env */

}

一旦当前线程被附加到JVM上,AttachCurrentThread函数就会返回一个属于当前线程的JNIEnv指针。

有许多方式可以获取JavaVM指针。可以在VM创建的时候记录下来,也可以通过JNI_GetCreatedJavaVMs查询被创建的虚拟机,还可以通过调用JNI函数GetJavaVM或者定义JNI_OnLoad句柄接口。与JNIEnv不同的是,JavaVM只要被缓存在全局引用中,是可以被跨线程使用的。

JDK1.2以后提供了一个新调用接口(invocation interface)函数GetEnv,这样,你就可以检查当前线程是否被附加到JVM上,然后返回属于当前线程的JNIEnv指针。如果当前线程已经被附加到VM上的话,GetEnvAttachCurrentThread在功能上是等价的。

 

 

JNI的一个使用方式就是编写一些本地方法来使用己有的本地库。本章介绍了一种生成一个包含一系列本地函数的类库的经典的方式。

本章首先用一对一映射这种(one-to-one mapping)最直接的方式来写封装类.接下来,我们会介绍一种叫做共享stubs(shared stubs)的技术来简化编写封装类的任务。然后,在本章的最后,我们会讨论怎么样使用peer classes来封装本地数据结构。

本章介绍的方式都是通过本地方法直接使用一个本地库,这样的话,应用程序调用本地方法时会依赖于本地库。这样应用程序只能运行在支持这个本地库的操作系统上面。一个更好的办法是声明一些与操作系统无关的本地方法,让这些方法来调用本地库。这样,当我们移植程序时,只需要修改这些实现中间层本地方法的本地函数就可以了,而不必动应用程序和这些中间层本地方法。

9.1 一对一映射(one-to-one mapping)

我们从一个简单的例子开始。假设我们想写一个封装类,它向标准C库提供atol函数:long atol(const char *str);

这个函数解析一个字符串并返回十进制数字。首先,我们像下面这样写:

public class C {

public static native int atol(String str);

...

}

为了演示如何使用C++进行JNI编程,我们用C++来实现本地方法:

JNIEXPORT jint JNICALL

Java_C_atol(JNIEnv *env, jclass cls, jstring str)

{

const char *cstr = env->GetStringUTFChars(str, 0);

if (cstr == NULL) {

return 0; /* out of memory */

}

int result = atol(cstr);

env->ReleaseStringUTFChars(str, cstr);

return result;

}

9.2 Shred Stubs

一对一映射要求你为每一个你想封装的本地函数写一个stub函数,那么,当你需要为大量本地函数写封装类时,你的工作会很烦琐。本节中,我们介绍shared stubs的思想来简化工作量。

Shared stubs负责把调用者的请求分发到相应的本地函数,并负责把调用者提供的参数类型转化成本地函数需要的类型。

我们先看一下shared stubs怎么样简化C.atol方法的实现,然后还会介绍一个使用了shared stub思想的类CFunction。

public class C {

private static CFunction c_atol =

new CFunction("msvcrt.dll", // native library name

"atol", // C function name

"C"); // calling convention

public static int atol(String str) {

return c_atol.callInt(new Object[] {str});

}

...

}

C.atol不再是一个本地方法,而是使用CFunction类来定义。这个类内部实现了一个shared stub。静态变量C.c_atol存储了一个CFunction对象,这个对象对应了msvcrt.dll库中的C函数atol。一旦c_atol这个字段初始化,对C.atol的调用只需要调用c_atol.callInt这个shared stub。

一个CFunction类代表一个指向C函数的指针。

CFunction的类层次结构图如下:

public class CFunction extends CPointer {

public CFunction(String lib, // native library name

String fname, // C function name

String conv) { // calling convention

...

}

public native int callInt(Object[] args);

...

}

callInt方法接收一个java.lang.Object对象的数组作为参数,它检查数组中每一个元素的具体类型,并把它们转化成相应的C类型(比如,把String转化成char*)。然后把它们传递给相应的C函数,最后返回一个int型的结果。CFunction类还可以定义许多类似的方法,如callFloat、callDouble等来处理其它返回类型的C函数。

CPointor的定义如下:

public abstract class CPointer {

public native void copyIn(

int bOff, // offset from a C pointer

int[] buf, // source data

int off, // offset into source

int len); // number of elements to be copied

public native void copyOut(...);

...

}

CPointer是一个抽象类,它支持对任意C指针的访问。例如copyIn这个方法,它会把一个int数组里面的元素复制到C指针指向的位置中去。但是这种操作方式可以访问地址空间里面任意的内存位置,一定要小心地使用。像CPointer.copyIn这样的本地方法可对直接对C指针进行操作,是不安全的。CMalloc是CPointer的一个子类,它指向内存中由malloc在heap上分配的一块儿内存。

public class CMalloc extends CPointer {

public CMalloc(int size) throws OutOfMemoryError { ... }

public native void free();

...

}

CMalloc的构造函数根据给定的大小,在C的heap上创建一块儿内存。CMalloc.free方法用来释放这个内存块儿。我们可以用CFunction和CMalloc重新实现Win32.CreateFile:

public class Win32 {

private static CFunction c_CreateFile =

new CFunction ("kernel32.dll", // native library name

"CreateFileA", // native function

"JNI"); // calling convention

public static int CreateFile(

String fileName, // file name

int desiredAccess, // access (read-write) mode

int shareMode, // share mode

int[] secAttrs, // security attributes

int creationDistribution, // how to create

int flagsAndAttributes, // file attributes

int templateFile) // file with attr. to copy

{

CMalloc cSecAttrs = null;

if (secAttrs != null) {

cSecAttrs = new CMalloc(secAttrs.length * 4);

cSecAttrs.copyIn(0, secAttrs, 0, secAttrs.length);

}

try {

return c_CreateFile.callInt(new Object[] {

fileName,

new Integer(desiredAccess),

new Integer(shareMode),

cSecAttrs,

new Integer(creationDistribution),

new Integer(flagsAndAttributes),

new Integer(templateFile)});

} finally {

if (secAttrs != null) {

cSecAttrs.free();

}

}

}

...

}

我们在一个静态变量当中缓存CFunction对象,Win32的CreateFile 这个API从kernel32.dll中通过调用方法CreateFileA来访问,另外一个方法CreateFileW需要传入一个Unicode字符串参数作为文件名。CFunction负责做标准的Win32调用转换(stdcall)。

上面的代码中,首先在C的heap上面分配一个足够大的内存块儿来存储安全属性,然后把所有的参数打包成一个数组并通过CFunction这个函数调用处理器来调用底层的C函数CreateFileA。最后释放掉存储安全属性的C内存块儿。

9.3 一对一映射(one-to-one mapping)和Shared Stubs的对比

这是两种把本地库封装成包装类的方式,各有自己的优点。

Shared Stubs的主要优点是程序员不必在本地代码中写一大堆的stub函数。一旦像CFunction这样的shared stub被创建以后,程序员可能就不用写代码了。

但是,使用shared stubs时一定要非常小心,因为这相当于程序员在JAVA语言中写C代码,已经违反了JAVA中的类型安全机制。一旦使用的过程中出现错误,就有可能引起内存破坏甚至程序崩溃。

一对一映射的优点是高效,因为它不需要太多附加的数据类型转换。而这一点正是shared stubs的缺点,例如CFunction.callInt必须为每一个int创建一个Integer对象。

9.4 如何实现Shared Stubs

到现在为止,我们一直是把CFunction、CPointer、CMalloc这三个类当作黑匣子的。本节中,我们就来详细描述一下它们是如何使用JNI实现的。

9.4.1 CPointer的实现

抽象类CPointer包含了一个64位的字段peer,它里面存放的是一个C指针:

public abstract class CPointer {

protected long peer;

public native void copyIn(int bOff, int[] buf,

int off,int len);

public native void copyOut(...);

...

}

对于像copyIn这样的本地方法的C++实现是比较简明的:

JNIEXPORT void JNICALL

Java_CPointer_copyIn__I_3III(JNIEnv *env, jobject self,

jint boff, jintArray arr, jint off, jint len)

{

long peer = env->GetLongField(self, FID_CPointer_peer);

env->GetIntArrayRegion(arr, off, len, (jint *)peer + boff);

}

在这里,我们假设FID_CPointer_peer是CPointer.peer的字段ID,是被提前计算出来。

9.4.2 CMalloc

CMalloc这个类中添加了两个本地方法用来分配和释放C内存块儿:

public class CMalloc extends CPointer {

private static native long malloc(int size);

public CMalloc(int size) throws OutOfMemoryError {

peer = malloc(size);

if (peer == 0) {

throw new OutOfMemoryError();

}

}

public native void free();

...

}

这个类的构造方法调用了本地方法CMalloc.malloc,如果CMalloc.malloc分配失败的话,会抛出一个OutOfMemoryError。我们可以像下面这样实现CMalloc.malloc和CMalloc.free两个方法:

JNIEXPORT jlong JNICALL

Java_CMalloc_malloc(JNIEnv *env, jclass cls, jint size)

{

return (jlong)malloc(size);

}

JNIEXPORT void JNICALL

Java_CMalloc_free(JNIEnv *env, jobject self)

{

long peer = env->GetLongField(self, FID_CPointer_peer);

free((void *)peer);

}

9.4.3 CFunction

这个类的实现要求操作系统支持动态链接,下面的代码是针对Win32/Intel X86平台的。一旦你理解了CFunction这个类背后的设计思想,你可以把它扩展到其它平台。

public class CFunction extends CPointer {

private static final int CONV_C = 0;

private static final int CONV_JNI = 1;

private int conv;

private native long find(String lib, String fname);

public CFunction(String lib, // native library name

String fname, // C function name

String conv) { // calling convention

if (conv.equals("C")) {

conv = CONV_C;

} else if (conv.equals("JNI")) {

conv = CONV_JNI;

} else {

throw new IllegalArgumentException(

"bad calling convention");

}

peer = find(lib, fname);

}

public native int callInt(Object[] args);

...

}

类中使用了一个conv字段来保存C函数的调用转换类型。

JNIEXPORT jlong JNICALL

Java_CFunction_find(JNIEnv *env, jobject self, jstring lib,

jstring fun)

{

void *handle;

void *func;

char *libname;

char *funname;

if ((libname = JNU_GetStringNativeChars(env, lib))) {

if ((funname = JNU_GetStringNativeChars(env, fun))) {

if ((handle = LoadLibrary(libname))) {

if (!(func = GetProcAddress(handle, funname))) {

JNU_ThrowByName(env,

"java/lang/UnsatisfiedLinkError",

funname);

}

} else {

JNU_ThrowByName(env,

"java/lang/UnsatisfiedLinkError",

libname);

}

free(funname);

}

free(libname);

}

return (jlong)func;

}

CFunction.find把库名和函数名转化成本地C字符串,然后调用Win32下的APILoadLibrary和GetProcAddress来定义本地库中的函数。

方法callInt的实现如下:

JNIEXPORT jint JNICALL

Java_CFunction_callInt(JNIEnv *env, jobject self,

jobjectArray arr)

{

#define MAX_NARGS 32

jint ires;

int nargs, nwords;

jboolean is_string[MAX_NARGS];

word_t args[MAX_NARGS];

nargs = env->GetArrayLength(arr);

if (nargs > MAX_NARGS) {

JNU_ThrowByName(env,

"java/lang/IllegalArgumentException",

"too many arguments");

return 0;

}

// convert arguments

for (nwords = 0; nwords < nargs; nwords++) {

is_string[nwords] = JNI_FALSE;

jobject arg = env->GetObjectArrayElement(arr, nwords);

if (arg == NULL) {

args[nwords].p = NULL;

} else if (env->IsInstanceOf(arg, Class_Integer)) {

args[nwords].i =

env->GetIntField(arg, FID_Integer_value);

} else if (env->IsInstanceOf(arg, Class_Float)) {

args[nwords].f =

env->GetFloatField(arg, FID_Float_value);

} else if (env->IsInstanceOf(arg, Class_CPointer)) {

args[nwords].p = (void *)

env->GetLongField(arg, FID_CPointer_peer);

} else if (env->IsInstanceOf(arg, Class_String)) {

char * cstr =

JNU_GetStringNativeChars(env, (jstring)arg);

if ((args[nwords].p = cstr) == NULL) {

goto cleanup; // error thrown

}

is_string[nwords] = JNI_TRUE;

} else {

JNU_ThrowByName(env,

"java/lang/IllegalArgumentException",

"unrecognized argument type");

goto cleanup;

}

env->DeleteLocalRef(arg);

}

void *func =

(void *)env->GetLongField(self, FID_CPointer_peer);

int conv = env->GetIntField(self, FID_CFunction_conv);

// now transfer control to func.

ires = asm_dispatch(func, nwords, args, conv);

cleanup:

// free all the native strings we have created

for (int i = 0; i < nwords; i++) {

if (is_string[i]) {

free(args[i].p);

}

}

return ires;

}

上面的代码中我们假设已经有了一些全局变量来缓存一些类引用和字段ID。例如,全局变量FID_CPointer_peer缓存了CPointer.peer的字段ID,而全局变量Class_String是对java.lang.String类对象的全局引用。类型word_t定义如下:

typedef union {

jint i;

jfloat f;

void *p;

} word_t;

函数Java_CFunction_callInt遍历参数数组并检查每一个元素的类型。

1、 如果元素是null,向C函数传递一个NULL指针。

2、 如果参数是java.lang.Integer类的实例,取出其中的int值并传递给C函数。

3、 如果元素是java.lang.Float类的实例,取出其中的float值传递给C函数。

4、 如果元素是一个CPointer类的实例,取出其中的peer指针并传递给C函数。

5、 如果参数是一个java.lang.String的实例,则把字符串转换成本地C字符串,然后传递给C函数。

6、 否则的话,抛出IllegalArgumentException

Java_CFunction_callInt函数之前,我们会在参数转换时检查可能会发生的错误,然后释放掉为C字符串临时分配的内存。

下面的代码需要把参数从临时缓冲区args中传递到C函数中,这个过程需要直接操作C的栈(stack),因此需要用到汇编,代码和对代码的解释不再翻译,懂得不多,翻译出来也是莫名其妙,不能保证正确性。

9.5 Peer

无论哪种封装方式,都会遇到一个问题,就是数据结构的传递。我们先看一下CPointer这个类的定义。

public abstract class CPointer {

protected long peer;

public native void copyIn(int bOff, int[] buf,

int off, int len);

public native void copyOut(...);

...

}

这个类中包含了一个指向本地数据结构的64位的peer字段。CPointer的子类用这个指针来操作C里面的数据结构:

CPointerCMalloc这些类被称作peer classes。你可以用这些类封装各种各样的本地数据结构,如:

1、 文件描述符(file descriptors)。

2、 Socket描述符(socket descriptors)。

3、 窗口或者其它UI元素。

9.5.1 JAVA平台下的Peer Classes

JDK中,java.iojava.netjava.awt等包的内部实现就是利用了peer classes。例如,一个java.io.FileDescriptor类的实例,其实就包含了一个私有的字段fd,而fd这个字段就是指向一个本地文件描述符。

// Implementation of the java.io.FileDescriptor class

public final class FileDescriptor {

private int fd;

...

}

假如现在你想做一个JAVA平台的文件API不支持的操作,你可能就会通过本地方法中的JNI来找到一个java.io.FileDescriptor中的fd字段,然后试图去操作这个字段所代表的文件。这样会存在一些问题:

1、 首先,这种方式严重依赖于java.io.FileDescriptor的实现,如果有一天这个类的内部发生了变动,本地方法就要修改。

2、 你直接操作fd字段可能会破坏java.io.FileDescriptor内部的完整性。比如内部实现中,fd字段可能会和其它某个数据相关联。

解决这些问题最根本的方案就是定义你自己的peer classes来封装本地数据结构。在上面的情况中,你可以定义自己的peer class来包含file descriptor,并在这个peer class上面定义一些自己的操作。并且,你也可以很容易地定义一个自己的peer class来实现一个标准的JAVA API中的接口。

9.5.2 释放本地数据结构

Peer classes被定义在JAVA中,因此它们的实例对象会被自动回收,因此,你要保证在这些对象被回收的时候,它们所指向的C语言数据结构的内存块也要被释放。

前面提到过,CMalloc类包含一个用来手动释放被malloc分配的C内存的free方法:

public class CMalloc extends CPointer {

public native void free();

...

}

所以,有些人爱这么干:

public class CMalloc extends CPointer {

public native synchronized void free();

protected void finalize() {

free();

}

...

}

JVM在回收CMalloc的对象实例之前,会调用对象的finalize方法。这样的话,即使你忘记调用freefinalize方法也会帮你释放掉malloc分配的内存。

可是,为了防止本地方法被重复调用,你不仅要在free方法前面加上synchronized关键字,还需要对CMalloc.free这个本地方法的实现做一些修改:

JNIEXPORT void JNICALL

Java_CMalloc_free(JNIEnv *env, jobject self)

{

long peer = env->GetLongField(self, FID_CPointer_peer);

if (peer == 0) {

return; /* not an error, freed previously */

}

free((void *)peer);

peer = 0;

env->SetLongField(self, FID_CPointer_peer, peer);

}

请注意,要设置peer的值的话,需要用两句来完成:

peer = 0;

env->SetLongField(self, FID_CPointer_peer, peer);

而不是一句:

env->SetLongField(self, FID_CPointer_peer, 0);

因为C++编译器会把0当作32int值来处理。

另外,定义finalize方法是一个很好的保障措施,但决不能把它作为释放本地C语言数据结构的主要方式:

1、 是本地数据结构可能会消耗比它们的peer对象实例更多的资源,但JVM看在眼里的是,这个对象只有一个long型的字段,这样JVM可能就会以为它占用很少的资源而不会及时回收掉。

2、 定义了finalize方法的类,在对象的创建和回收时可能会比没有定义finalize方法的类在效率上要差些。

其实,你完全不必用finalize方法就可以手动保证一个本地C语言数据结构被释放。但这样的话,你就必须确保在所有的执行路径上面都要执行释放代码,否则可能会造成内存泄漏。比如下面这种情况就是需要提起注意的:

CMalloc cptr = new CMalloc(10);

try {

... // use cptr

} finally {

cptr.free();

}

9.5.3 peer对象背后的东西

前面我们介绍了一个peer class通常会包含一个指向本地数据结构的私有字段。其实,有些情况下,在本地数据结构中包含一个指向peer class的引用也是很有用的。比如,当本地代码需要回调peer class中的实例方法的时候。

假设KeyInput是一个UI控件:

class KeyInput {

private long peer;

private native long create();

private native void destroy(long peer);

public KeyInput() {

peer = create();

}

public destroy() {

destroy(peer);

}

private void keyPressed(int key) {

... /* process the key event */

}

}

还有一个本地数据结构key_input

// C++ structure, native counterpart of KeyInput

struct key_input {

jobject back_ptr; // back pointer to peer instance

int key_pressed(int key); // called by the operating system

};

它们的关系如下:

整个流程是这样的,JAVA当中生成一个KeyInput对象用来处理按键。KeyInput对象生成的时候,会在本地内存中创建一个key_input结构,这个结构中包含一个方法key_pressed供操作系统在发生事件时调用。

当用户按某个键时,操作系统产生一个事件,并调用key_pressed(int key);,在这个方法里面,本地代码会调用KeyInputkeyPressed方法,并把键值传入。

KeyInput的两个本地方法实现如下:

JNIEXPORT jlong JNICALL

Java_KeyInput_create(JNIEnv *env, jobject self)

{

key_input *cpp_obj = new key_input();

cpp_obj->back_ptr = env->NewGlobalRef(self);

return (jlong)cpp_obj;

}

JNIEXPORT void JNICALL

Java_KeyInput_destroy(JNIEnv *env, jobject self, jlong peer)

{

key_input *cpp_obj = (key_input*)peer;

env->DeleteGlobalRef(cpp_obj->back_ptr);

delete cpp_obj;

return;

}

本地方法create生成一个C++结构key_input,并初始化back_ptr字段。其中back_ptr是一个全局引用,指向KeyInput这个peer class对象的实例。本地方法destroy删除指向KeyInput对象的引用和KeyInput指向的本地数据结构。KeyInput构造方法调用本地方法create来建立KeyInput这个对象实例和它的副本key_input这个本地数据结构之间的链接。

Key_input::key_pressed(int key)方法的实现如下:

// returns 0 on success, -1 on failure

int key_input::key_pressed(int key)

{

jboolean has_exception;

JNIEnv *env = JNU_GetEnv();

JNU_CallMethodByName(env,

&has_exception,

java_peer,

"keyPressed",

"()V",

key);

if (has_exception) {

env->ExceptionClear();

return -1;

} else {

return 0;

}

}

本节结束之间,我们还有最后一个话题需要讨论。假设,你为KeyInput类添加了一个finalize方法来避免内存泄漏。

class KeyInput {

...

public synchronized destroy() {

if (peer != 0) {

destroy(peer);

peer = 0;

}

}

protect void finalize() {

destroy();

}

}

考虑到多线程的情况,destroy方法被加上了synchronized关键字。但是,上面的代码不会像你期望的那样执行的,因为JVM永远不会回收KeyInput这个对象,除非你手动调用destory方法。因为,KeyInput的构造方法创建了一个到KeyInput对象的JNI全局引用,这个全局引用会阻止GC回收KeyInput的。解决办法就是使用弱引用来替代全局引用:

JNIEXPORT jlong JNICALL

Java_KeyInput_create(JNIEnv *env, jobject self)

{

key_input *cpp_obj = new key_input();

cpp_obj->back_ptr = env->NewWeakGlobalRef(self);

return (jlong)cpp_obj;

}

JNIEXPORT void JNICALL

Java_KeyInput_destroy(JNIEnv *env, jobject self, jlong peer)

{

key_input *cpp_obj = (key_input*)peer;

env->DeleteWeakGlobalRef(cpp_obj->back_ptr);

delete cpp_obj;

return;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值