原文链接:http://blog.sina.com.cn/s/blog_53988c0c0100osom.html
第九章 利用存在的本地库(Leveraging Existing Native Libraries)
一个JNI的应用程序是写利用在存在本地库中代码的本地方法。在这章中,一个典型的方法(approach)是生成一个包装一些列本地函数的类库。
这章首先讨论最易懂的(straightforward)写封装类(wrapper classer)的方法--一对一的映射。然后我们介绍一个技术,共享存根(shared stubs),简化了些封装类的任务。
一对一的映射和共享存根都是封装本地函数的技术。这章的最后,我们将讨论怎样使用"peer classes"来封装本地数据结构。
这章中描述的方法直接公开了一个使用本地方法的本地库,因此使一个应用调用依赖于这样本地库的这样的本地方法是有缺点。如此一个应用可能只能运行在提供本地库的操作系统上。一个更好的方法是宣布一个独立于操作系统的本地方法。仅仅实现这些本地方法的本地函数直接地使用本地库,限制了对这些本地函数移植的需要。包含本地方法声明的应用程序不需要被移植。
9.1 一对一映射(One-to-One Mapping)
让我们开始一个简单的例子。假设我们想写个封装类来导出在标准"C"库的"atol"函数:
long atol (const char *str) ;
这个"atol"函数解析一个字符串和返回被字符串代表的十进制的值。实际上(in practice)可能没有原因要定义如此一个本地方法,因为"Integer.parseInt"方法,"Java API"的一部分,提供同样的功能(equivalent functionality)。例如,估算"atol("100")",结果是整数100(result in the integer value 100)。我们定义一个封装类如下:
public class C{
}
为说明在"C++"中"JNI"编程的好处,我们将在这章中使用"C++"来实现本地方法。"C.atol"本地方法的"C++"实现如下:
JNIEXPORT jint JNICALL
Java_C_atol(JNIEnc *env, jclass, jstring str)
{
}
这个实现是很简单的。我们使用"GetStringUTFChars"来转换"Unicode string",因为十进制数是"ASCII"码的字符(ASCII characters)。
现在让我们测试一个复杂的例子,它调用一个传递结构体指针的C函数。假设我们想写个公开来自"Win32 API"的"CreateFile"函数的封装类:
typedef void *HANDLE ;
typedef long DWORD ;
typedef struct {...}SECURITY_ATTRIBUTES ;
HANDLE CreateFile(
) ;
"CreateFile"函数支持大量的"Win32"规定特征,在平台无关的"Java File API"中不可用。例如,"CreateFile"函数可以被用来指定特殊的访问模式和文件属于,来打开Win32命名管道(Win32 named pips)和来处理串口通讯(serial port communications)。
我们在这本书中将不讨论更多的"CreateFile"函数的细节。关注于"CreateFile"可以怎样被映射到一个本地函数,函数被定义在一个叫"Win32"的封装类中:
public class Win32{
}
从"char"指针类型到"String"的映射是明显的。我们映射本地"Win32"类型"long(DWORD)"到在"Java"编程中的"int"。"Win32"类型"HANDLE",一个不透明的"32-bit"指针类型,也被映射为"int"。
因为在内存中怎样安排成员域的潜在的不同,我们不能映射"C"结构到在"Java"编程语言中的类。作为替代,我们使用一个数组来存储"C"结构的"SECURITY_ATTIBUTES"的内容。调用者也可以传递NULL作为"seAttrs"来指定默认的"Win32"安全属性。我们将不讨论"SECURITY_ATTRIBUTES"结构的内容或者怎样在一个"int"数组中编码。
一个"C++"上面本地方法的实现如下:
JNIEXPORT jint JNICALL Java_Win32_CreateFile(
{
}
首先,我们转化存储在"int"数组中的安全属性到一个"jint"数组。如果"setAttrs"参数是一个"NULL"引用,我们传递一个"NULL"作为安全属性到"Win32 CreateFile"函数。下一步,我们调用工具函数"JNU_GetStringNativeChars"(8.2节)来获得文件名,使用本地描述的"C"字符串。一旦我们已经转换了安全属性和文件名,我们传递转换的结果和剩余参数给"Win32 CreateFile"函数。
我们关心异常的检查和虚拟器资源的释放(例如 cSetAttrs)。
"C.atol"和"Win32.CreateFile"的例子示范了一个封装类合格本地方法的一般方法.每个本地函数(例如,"CreateFile")映射一个单独的本地存根(stub)函数(例如,"Java_Win32_CreateFile"),它再依次地映射到一个单独的本地的方法定义(例如,Win32.CreateFile)。在一对一映射,存根的(stub)函数有两个目的(serve two purpose):
1.存根函数使本地函数的参数传递的协定适合"Java"虚拟器的期望。虚拟器期望本地方法实现为一个被给地命名协定和接受俩个额外的参数("JNIEnv"指针和"this"指针)。
2.存根函数在"Java"编程语言类型和本地类型之间转换。例如,"Java_Win32_CreateFile"函数转换"jstring"文件名为一个本地描述的"C"字符串。
9.2 共享存根(Share stubs)
一对一的映射方法(approach)需要你来写一个存根函数为你想要打包的没个本地函数。当你面对(be faced with)为大量本地函数写分装的类的任务时,这变的冗长乏味。在这章,我们介绍共享存根的内容和示范共享存根可以怎样被用来简化(simplify)写封装类的任务。
一个共享存根是一个本地函数,这函数分配给其他本地函数。共享存根负责转换来自调用者提供的参数类型到本地函数能够接受的参数类型。
我们将马上介绍一个共享存根类"CFunction",但首先让我们显示它能怎样简化"C.atol"方法的实现:
public class C{
}
"C.atol"不在是一个本地方法(因而不在需要一个存根函数)。相反,使用"CFunction"类来定义"C.atol"函数。"CFunction"类内部实现一个共享存根。静态变量"C.c_atol"存储一个"CFunction"对象,对象对应在"msvcrt.dll"库中的"C"函数"atol"(在"Win32"上的多线程"C"库)。"CFunction"构造器调用也指定"atol"追寻"C"调用的协定(11.4部分)。一旦"c_atol"域被初始化,"C.atol"方法的调用只需要通过"c_atol.callInt",共享存根,来重新发送。
"CFunction"类属于一个类继承,我们将建立和简单地使用:
"CFunction"类的实例表示一个"C"函数的一个指针。"CFunction"是一个CPointer的子类,"CPointer"表示任意的"C"指针:
public class CFunction extends CPointer{
}
"callInt"方法是用一个"java.lang.Object"数组作为它的参数。它检查(inspect)在数组中的元素类型,转换它们(例如,从"jstring"到"char *"),同时作为参数传递它们给底层的"C"函数。然后,"callInt"方法返回底层"C"函数的结果作为一个"int"。"CFunction"类可能定义了方法例如"callFloat or callDouble"来处理带有其他返回类型的"C"函数。
"CPointer"类定义如下:
public abstract class CPointer{
}
"CPointer"是一个抽象的类支持"C pointers"的任意的访问。例如,"copyIn"的方法,复制大量元素从一个"int"数组到被"C pointer"指向的位置。这个方法应该小心使用(be used with care),因为它能很容易被用来破坏(corrupt)在地址空间中的任意内存位置。本地方法例如"CPointer.copyIn"和在"C"中直接的指针处理(direct pointer manipulation)一样不安全。
"CMalloc"是"CPointer"的一个子类,"CMalloc"指向一个用"malloc"在"C"堆上分配(allocated in the C heap using malloc)的一个内存块:
pubic class CMalloc extends CPointer{
}
"CMalloc"构造器在"C"堆上分配给定大小的内存块。"CMalloc.free"方法释放这个内存块。
装备(Equipped with)了"CFunction and CMalloc classes",我们能在实现"Win32.CreateFile"如下:
public class Win32{
}
在一个静态变量中我们缓冲了"CFunction"对象。"Win32 API CreateFile"被作为"CreateFileA"从"kernel32.dll"库中导出。另一个导出入口"CreateFileW",使用一个"Unicode"编码的字符串作为文件名参数。这个函数遵守了"JNI"调用协定,这是标准的"Win32"调用的协定(stdcall)。
"Win32.CreateFile"实现首先在"C"堆上分配一个内存块,这块足够大能暂时保存安全属性值。然后,封包所有参数到一个数组中和通过共享分发器来调用底层的"C"函数"CreateFileA"。最后"Win32.CreateFile"方法释放被用来保存安全属性的"C"内存块。我们在最后一行中调用"cSecAttrs.free"来保证临时使用的"C"内存被释放,即使"c_CreateFile.callInt"调用产生一个异常。
9.3 一对一映射与共享存根相对(One-to-One Mapping versus Shared Stubs)
一对一的映射和共享存根是两种为本地库建立封装类的方法。每个方法有它自己优点。
共享存根的主要优点是程序员不需要写在本地代码中写大量的存根函数。一旦一个共享存根实现例如"CFunction"是有用的,程序员可以不用写本地代码的一个单独行来建立封装类。
然而(however),共享存根必须小心使用。使用共享存根,程序员实质地是在用"Java"编程语言来写"C"代码。这破坏(defeat)了"Java"编程语言的类型安全。在使用共享存根中错误可能导致破坏内存(corrupted memory)和应用程序的奔溃。
一对一映射的优点是典型地在转换数据类型中更有效率,数据是在"Java"虚拟器和本地代码(native code)之间传递。另一方面,共享存根(Shared stubs),能处理最多(at most)预定的(predetermined)一组参数类型, 同时不能实现(achieve)最佳的性能(optimal performance)为这些参数类型。"CFunction.callInt"的调用者总是必须创建一个"Ineger"对象来为每个"int"参数。对于共享存根设计(scheme),这增加了空间和时间开销。
实际,你需要平衡性能,可移植性(portability)和短期生产率(short-term productivity)。共享存根可能适合利用固有地不可移植的本地代码,本地代码能忍受(tolerate)一点稍微的性能的下降(degradation),然而一对一映射应该在最佳性能是必须或可移植性问题的地方被使用。
9.4 共享存根的实现(Implementation of Shared Stubs)
到目前为止(so far),我们已经对待"CFunction, CPointer, and CMalloc"类为黑盒子。这章描述怎样使用基础的"JNI"特性来实现它们。
9.4.1 CPointer类(The CPointer Class)
我们首先看看"CPointer"类,因为它是"CFunction and CMalloc"的父类(superclass)。"CPointer"的抽象类包含一个"64-bit"成员域,"peer",存储底层的"C"指针:
public abstract class CPointer{
}
本地方法例如"copyIn"的"C++"实现是简单的:
JNIEXPORT void JNICALL
Java_CPointer_copyIn_I_3III(JNIEnv *env, jobject self,
{
}
"FID_CPointer_peer"是预算的成员域ID(precomputed field ID)为"CPointer.peer"。这个本地方法的实现使用长名字编码设计来解决和重载的"copyIn"本地方法的实现的冲突,为在"CPointer"类中其他数组类型。
9.4.2 CMalloc类(The CMalloc Class)
"CMalloc"类增加两个本地方法,方法被用来分配和释放"C"的内存块:
public class CMalloc extends CPointer{
}
"CMalloc"构造器调用一个本地方法(native methhod)"CMalloc.malloc",同时如果"CMalloc.malloc"对返回一个最新在"C"堆上分配内存块失败,抛出一个"OutOfMemoryError"的错误。我们能够实现"CMalloc.malloc"和"CMalloc.free"方法如下:
JNIEXPORT jlong JNICALL
Java_CMalloc_malloc(JNIEnv *env, jclass cls, jintsize)
{
}
JNIEXPORT void JNICALL
Java_CMalloc_free(JNIEnv *env, jclass self)
{
}
9.4.3 CFunction类(The CFunction Class)
"CFunction"类的实现需要使用在操作系统中的动态链接支持和CPU特定的汇编代码。下面呈现的实现是特别面向"Win32/Intel x86"环境。一旦你理解在实现"CFunction"类后面的规则,你能按一样的步骤在其他平台上实现它。
"CFunction"类被定义如下:
pubic class CFunction extends CPointer{
}
"CFunction"类声明一个私有成员域"conv",被用来存储"C"函数调用的协定。"CFunction.find"本地方法实现(be implemeneted)如下(as follows):
JNIEXPORT jlong JNICALL
Java_CFunction_find(JNIEnv *env, jobject self, jstring lib, jstring fun)
{
}
"CFunciton.find"转换库名哈函数名到局部指定的"C"字符串,然后调用"Win32 API"函数"LoadLibrary"和"GetProcAddress"来定位"C"函数在命名的本地库中。
"callInt"方法,实现如下,进行了重分配到底层"C"函数的主要任务:
JNIEXPORT jint JNICALL
Java_CFunction_callInt(JNIEnv *env, jobject self,
{
#define MAX_NARGS 32
cleanup:
}
我们假设我们已经建立大量的全局变量为获得恰当的类引用和成员域"IDs"。例如,全局变量"FID_CPointer_peer"为了"CPointer.peer"而获得成员域"ID",和全局变量"Class_String"是一个"java.lang.String"类对象的一个全局引用。"word_t"类型表示一个机器字(a machine word),定义如下:
typedef union{
}word_t ;
"Java_CFunction_callInt"函数遍历(iterate through)参数数组,同时检查了每个参数类型:
.如果元素是一个"NULL"引用,他是作为一个"NULL"指针传递给"C"函数。
.如果元素是"java.lang.Integer"类的实例,"integer"值被得到和传递给"C"函数。
.如果元素是"java.lang.Float"类的实例,"floating-point"值被得到和传递给"C"函数。
.如果参数是"java.lang.String"类的实例,转换它为一个本地指定的(locale-specific)"C"字符串和传递它给"C"函数。
.另外(otherwise),一个"IllegalArgumentException
我们在参数转换期间细心地检查可能的错误,同时在从"Java_CFunction_callInt"函数返回前,释放全部的为"C"字符串分配的临时存储。
从临时缓冲器"args"到"C"函数的传递参数的代码需要直接地处理C的堆栈。用内部的汇编来写:
int asm_dispatch( void *func,
{
}
这个汇编函数复制参数到"C"的堆栈,然后,重分配到"C"函数"func".在"func"返回后,"asm_dispatch"函数检查"func"的调用协定(convention)。如果"func"允许"C"调用协定,"asm_dispatch"弹出传递给"func"的参数。如果"func"允许"JNI"调用协定,"asm_dispatch"不弹出参数;"func"在"asm_dispatch"返回前弹出参数。
9.5 Peer类(Peer Classes)
一对一的映射和共享存根两都解决了封装本地函数的问题。在构建共享存根实现的期间,我们也遇到(encounter)封装数据结构体的问题。重看"CPointer"类的定义:
public abstract class CPointer{
}
它包含一个"64-bit"的"peer"成员域,它参考本地数据结构(在这个情况中,在"C"地址空间中的一块内存)。"CPointer"的子类赋予这个"peer"成员域指定的意思(specific meanings)。"CMalloc"类,例如,使用"peer"成员域来指向在"C"堆上的一块(chunck)内存:
直接对应到本地数据结构的类,例如"CPointer and CMalloc",被称为"peer classes"。你能构建"peer classes"为各种不同的本地数据结构,包括(including),例如(for example):
.文件描述符(file descriptors)
.埠描述符(socket descriptors)
.窗口或其他图形用户接口控件(windows or other graphics user interface components)
9.5.1 在Java平台中的Peer classes(Peer Classes in the Java Platform)
当前"JDK"和"Java 2 SDK release“内部使用"peer classes"来实现了"java.io,java.net and java.awt"包。例如,"java.io.FileDescroptor"类的实例,包含一个私有的成员域"fd",它代表一个本地文件描述符:
// Implemention of the java.io.FileDescriptor class
public final class FileDescriptor{
}
假设你想执行一个"Java"平台"API"不支持的文件操作。你可以试探(tempt)使用"JNI"来找出一个"java.io.FileDescriptor"实例的底层本地文件描述符。一旦你知道它的名字和类型,"JNI"允许你访问一个私有成员域。然后,你可以认为你能够直接地在那个文件操作符上执行本地文件操作。然而,这个方法有两个问题:
.首先,你依赖于一个存储了本地文件描述符在一个私有成员域"fd"中的"java.io.FileDescriptor"实现。然而,不保证,来自"Sun"的将来的实现或第三方的"java.io.FileDescriptor"类的实现将仍然使用同样的使用成员域名"fd"为本地文件描述符。假设"peer"成员域的名字的本地代码可能在一个不同实现的"Java"平台上不能工作。
.第二,你在本地文件描述上的直接地执行的操作可能破坏(disrupt)"peer class"的内部一致性(internal consistency)。例如,"java.io.FileDescriptor"实例维持一个内部状态指示,是否底层本地文件描述已经被关闭。如果你使用本地代码来迂回跳过(bypass)"peer class",同时关闭了底层的文件描述符,在"java.io.FileDescriptor"实例中维持的状体将不再(no longer)和本地文件描述符的真实状态一致了(be consistent with)。"Peer class"实现假设他们有独家访问底层的本地数据结构。
客服这些问题的唯一方法是定义你自己的"peer classed"来封装本地数据结构。在上面例子中,你能定义你自己文件描述符"peer class"来支持操作请求集(the required set of operations)。这个方法不是让你用你自己的"peer classes"来实现"Java API classes".例如,你不能传递你自己文件描述符实例到期望一个"java.io.FileDescriptor"实例的方法。然而,你能容易定义你自己"peer class"来实现在"Java API"上的一个标准接口.这有一个强壮的参数为设计"APIs"基于接口,而不是类(classes)。
9.5.2 释放本地数据结构
"Peer classes"用Java编程语言定义;因此"peer classes"实例自动被垃圾收集(garbage collected)。然而,你需要保证,底层的本地数据结构也将被释放。
回想"CMalloc"类包含一个"free"方法来明确地释放分配(malloc'ed)的"C"内存:
public class CMalloc extends CPointer{
}
你必须记住调用"free"在"CMalloc"类的实例上;否则一个"CMalloc"实例可以被垃圾收集,但它对应的"malloc'ed"的"C"内存将不被收回(reclaim)。
一些程序员喜欢在"peer classes"中放置一个终止函数(finalizer)例如"CMalloc":
public class CMalloc extends CPointer{
}
在虚拟器垃圾收集一个"CMalloc"实例前,虚拟器调用"finalize"方法。即使你忘记调用"free"函数,"finalize"方法为你释放"malloc'ed"的"C"内存。
你需要做个小改变在"CMalloc.free"的本地方法的实现来计数(account for)它被多次(multiple times)调用的可能(possibility)。你也需要使"CMalloc.free"为一个同步的方法来避免线程竞争情况(thread race conditions):
JNIEXPORT void JNICALL
Java_CMalloc_free(JNIEnv *env, jobject self)
{
}
我们用两句来设置"peer"成员域:
peer = 0 ;
env->SetLongField(self, FID_CPointer_peer, peer) ;
替代一句:
env->SetLongField(self, FID_CPointer_peer, 0) ;
因为"C++"编译器将把文字"0"认为一个"32-bit"的整数(integer),而不是(as opposed to)一个"64-bit"整数。一些"C++"编译器允许你来指定"64-bit"整数文字,但使用"64-bit"文字将不能移植。
定义一个"finalize"方法是一个适合的安全装置,但你不应该依赖"finalizers"作为释放本地数据结构的中心方法(sole means)。原因是本地数据结构可能消耗太多的资源比它们的"peer"实例。"Java"虚拟器可能不垃圾收集和完成对"peer classes"的实例的最快的释放他们本地副本。
定义一个终结器也有性能的影响(performance consequences)。典型地带有"finalizers"的类型的实例的创建和收回都要慢于不带"finalizers"的类型的实例的创建和收回。
如果你也能确保你人工地(manually)释放本地数据结果为"peer classes",你不必定义一个终结器(finalizer)。然而,你应该确保在所有的执行路径中(in all paths of execution)都释放本地数据结构;否则你可能已经创建了一个资源泄漏。在使用一个"peer"实例的处理期间,特别注意可能的异常抛出。在"finally"语句中,总是释放本地数据结构:
CMalloc cptr = new CMalloc(10) ;
try{
}finally{
}
即使一个异常发生在"try"块中,"finally"语句确保"cptr"被释放。
9.5.3 Peer实例的返回点(Backpointers to Peer Instances)
我们已经显示典型的"peer classes"包含一个参考底层本地数据结构的私有成员域。在一些例子中,最好也包括一个参考来自本地数据结构的"peer class"的实例(from the native data structure to instances of the peer class)。例如,这发生在,当本地代码需要初始化在"peer class"中的实例方法的回调。
假设我们建立一个叫做"KeyInput"的假设(hypothetical)用户接口控件。当用户按了一个键时,"KeyInput"的本地"C++"控件,"key_input",接受一个事件,做来自操作系统的一个Key_pressed"的"C++"函数调用。"key_input"的"C++"控件通过调用在"KeyInput"实例上的"keyPressed"方法来报告操作系统事件到"KeyInput"实例。在下图中,箭头指示怎样通过一个用户按键产生一个按键事件和怎样从"key_input"的"C++"控件到"KeyInput peer"实例的传播(propagate):
| KeyInput(){ |<------|
| ...
| }
KeyInput instance
"KeyInput peer class"被定义如下:
class KeyInput{
}
"create"本地方法实现分配一个"C++"机构体"key_input"的实例。"C++"机构体和"C++"类相似,唯一的不同是所有的成员默认都是公有的而不是(as opposed to)私有的。在这个例子中,我们使用一个"C++"机构体替代一个"C++"类主要地为避免和在"Java"编程语言中类产生混淆(avoid confusion with)。
// C++ structure, native counterpart of KeyInput
struct key_input{
};
JNIEXPORT jlong JNICALL
Java_KeyInput_create(JNIEnv *env, jobject self)
{
}
JNIEXPORT void JNICALL
Java_KeyInput_destroy(JNIEnv *env, jobject self, jlong peer)
{
}
"create"本地方法分配"C++"结构体和初始化它的"back_ptr"成员域为"KeyInput peer"实例的全局引用。"destroy"本地方法删除了"peer"实例的全局引用和被"peer"实例引用的"C++"的结构体。"KeyInput"构建函数调用了"create"本地方法来建立在一个"peer"实例和她的本地副本之间的连接:
|
| peer | JNI global reference
|
|
|
KeyInput instance
当用户按一个键,操作系统调用"C++"成员函数"key_input::key_pressed"。这个成员函数通过调用在"KeyInput peer"实例上的"keyPressed"方法的调用来响应事件。
// return 0 on success, -1 on failure
int key_input::key_pressed(int key)
{
}
在回调后,"key_press"成员函数清除所有异常,同时使用-1放回代码来返回错误情况到操作系统。引用了在6.2.3和8.4.1部分中分别地(respectively)"JNU_CallMethodByName and JNU_GetEnv"的工具函数的定义。
在结束(concluding)这部分前,让我们讨论最后一个问题。假设(Suppose)你添加一个"finalize"方法在"KeyInput"类中为避免潜在的内存泄漏(potential memory leaks):
class KeyInput{
}
"destroy"方法检查是否"peer"成员域是0,同时在调用重载的"destory"本地方法后,设置"peer"成员域为0。"destroy"被定义为一个同步方法(synchronized method)来避免竞争情况(race conditions)。
然而,以上代码将不能像你期望的工作。虚拟器将不能垃圾收集任何KeyInput实例,除非你明确地调用"destroy"。"KeyInput"构造函数创建一个"JNI"全局的"KeyInput"实例的引用。全局引用阻止了"KeyInput"实例被垃圾收集。你能通过使用一个弱全局应用替代一个全局应用来克服这个问题。
JNIEXPORT jlong JNICALL
Java_KeyInput_create(JNIEnv *env, jobject self)
{
}
JNIEXPORT void JNICALL
Java_KeyInput_destroy(JNIEnv *env, jobject self, jlong peer)
{
}