Jimmy Chen

A Programmer

(译文) JNI编程指南与规范 第九章 利用现有的本地库

第九章 利用现有的本地库

JNI的一个应用就是利用已存在的本地库中现有的代码来编写本地方法。本章介绍的一个典型方法是生成一个封装了一系列本地方法的类库。

  本章首先介绍编写包装类最直接的方法–一对一映射。然后我们介绍一项技术,共享桩,来简化编写封装类的任务。

  一对一映射和共享桩都是封装本地方法的技术。在本章的最后,我们还将讨论如何使用peer类来封装本地数据结构。

  本章中描述的方法直接使用本地方法公开本地库,因此具有使得调用这种本地方法的应用程序依赖于本地库的缺点。这样一个应用程序只能够运行在提供了该本地库的操作系统上。一个比较好的方法是声明操作系统无关的本地方法。只有实现这些本地方法的本地函数直接使用本地库,限制了移植到本地函数的需要。包括原生方法声明在内的应用程序不需要移植。(这最后一句不是很明白什么意思,原文:A preferred approach is to declare operating system-independent native methods. Only the native functions implementing those native methods use the native libraries directly, limiting the need for porting to those native functions. The application, including the native method declarations, does not need to be ported.)

9.1 一对一映射

让我们从一个简单的例子开始。假设我们想编写一个封装类来暴露标准C库中的atol方法:

  atol方法解析一个字符串并返回该字符串代表的十进制值。可能没有什么理由需要在实践在定义这样一个函数,因为Java API已经提供了类型的方法,Integer.parseInt。例如,atol(“100”)的结果是一个100的整型值。我们定义的封装类如下:

  为了演示使用C++来进行JNI编程,我们将会在这一章中使用C++来实现本地方法。使用C++实现的C.atol本地方法如下:

  方法的实现是非常直接的。我们使用GetStringUTFChars转换Unicode字符串,因为十进制数值是ASCII字符。

  让我们来测试第一个更复杂的例子,在例子中我们将传递一个结构指针给一个C函数。假设我们想编写这样一个封装类,这个类暴露出Win32平台的CreateFile API函数:

  CreateFile函数支持一些Win32特有的而Java独立平台上的文件API不可用的特性。例如,CreateFile方法可能会被用来指定特殊的访问模式和文件属性来打开Win32命名管道,和处理串口通信。

  这这本书中,我们不会讨论CreateFile函数的太多细节。我们的关注点将会在如何将CreateFile函数映射到一个定义在名为Win32的封装类的本地方法中:

  从char型指针到String类型是明显的。我们将本地Win32类型long(DWORD)映射为Java编程语言的int型。Win32类型HANDLE,一个不透明的32位指针类型,也映射为int型。

  因为字段是如何在内存在排列的潜在不同,我们不能将C结构体映射到Java编程语言的类中。作为替代,我们使用一个数组来存储C结构体SECURITY_ATTRIBUTES的内容。调用者也可能传递null作为secAttrs的参数来指定Win32安全属性的默认值。我们不会讨论SECURITY_ATTRIBUTES结构体的内容和如何在一个int数组中编码它们。

  上面的本地方法的一个C++实现如下所示:

  首先我们将存储在int数组中的安全属性转换成一个jint数组。如果secAttrs参数是一个NULL引用,我们将NULL作为安全属性传递给Win32 CreateFile方法。接下来,我们调用一个辅助函数JNU_GetStringNativeChars(8.2.2节)来获取表示为特定语言环境的C字符串文件名。一旦我们完成安全属性和文件名的转换,我们将转换后的结果以及剩余的参数传递到Win32 CreateFile函数。

  我们主要检查抛出的异常以及释放虚拟机资源(例如CSecAttrs)。

  C.atol和Win32.CreateFile例子演示了一个通常的编写封装类和本地方法的方式。每一个本地函数(例如,CreateFile)映射到一个单独的本地桩函数(例如,Java_Win32_CreateFile),然后映射到一个定义的本地方法(例如,Win32.CreateFile)。在一对一映射中,桩函数有两个目的:

  1. 这个桩将本地方法传递约定调整为Java虚拟机所期望的。虚拟机期望本地方法实现遵循给定的命名约定,并接受两个额外的参数(JNIEnv指针和“this”指针)。
  2. 这个桩在Java编程语言类型和本地类型间转换。例如,Java_Win32_CreateFile函数将jstring文件名转换成一个特地语言环境的C字符串。

9.2 共享桩

一对一映射方法需要你为每一个你想封装的本地方法编写一个桩函数。当你面对为大量本地方法编写封装类的任务时,这将会变得很乏味。在这一节,我们将介绍共享桩的概念以及演示共享桩是如何用来简化编写封装类的任务的。

  一个共享桩是一个派生到其他本地方法的本地方法。共享桩负责将从调用者提供的参数类型转换成本地函数接收的类型。

  我们将要介绍的一个共享桩类CFunction,但是首先让我们演示它是如何简化C.atol方法的实现的。

  C.atol不再是一个本地方法(从而不再需要一个桩函数)。作为替代的,C.atol被定义为使用CFunction类。CFunction类内部实现了一个共享桩。静态变量C.c_atol存储了一个对应于在msvcrt.dll库(Win32上的多线程C库)中的C函数atol的CFunction实例对象。CFunction构造函数调用还指定atol遵循C调用约定。一旦c_atol字段被初始化,调用C.atol方法只需要通过c_atol.callInt(共享桩)来重新调度。

  CFunction类属于我们将要建立和使用的类层次结构:

《(译文) JNI编程指南与规范 第九章 利用现有的本地库》

  CFunction类的实例表示一个指向C函数的指针。 CFunction是CPointer的一个子类,它表示任意的C指针.

  callInt方法将一个java.lang.Object数组作为它的参数。它检查元素中的数组类型、转换它们(例如,从jstring到char *)并将它们作为参数传递给底层C函数。然后将callInt方法返回底层C函数的结果作为int型值。CFunction类能够定义诸如callFloat或者callDouble的方法来处理具有其他返回类型的C函数。

  CPointer类定义如下:

  CPointer是一个支持任意访问C指针的抽象类。例如,copyIn方法,从一个int数组中复制一些元素到C指针指向的位置。这个方法必须小心使用,因为它很容易被用来破坏地址空间中的任意内存位置。诸如CPointer.copyIn之类的本地方法与C中的直接指针操作一样不安全。

  CMalloc是CPointer的子类,它指向在C堆中使用malloc分配的一大块内存空间。

  CMalloc构造函数在C堆中分配给定大小的一片内存区域,而CMalloc.free方法释放这片内存区域。

  使用CFunction和CMalloc类,我们可以重新实现Win32.CreateFile,如下所示:

  我们在一个静态变量中缓存CFunction对象。Win32 API CreateFile作为CreateFileA方法从kernel32.dll导出。另一个导出的入口,CreateFileW,将一个Unicode字符串作为文件名参数。这个函数遵循JNI调用约定,这是标准的Win32.CreateFile调用约定(stdcall)。

  Win32.CreateFile实现首先在C堆中分配一块足够容纳安全属性参数的内存区域。然后将所有参数打包到数组中,并通过共享调度程序调用底层C函数CreateFileA。最后Win32.CreateFile方法释放用来暂时存放安全属性参数的内存块。我们在finally子句中调用cSecAttrs.free来确保临时C内存被释放,即使c_CreateFile.callInt调用引发异常。

9.3 一对一映射和共享桩对比

一对一映射和共享桩是为本地库建立封装类的两种方式。每一种都有各自的优势。

  共享桩方式的优势是程序员不需要在本地代码中编写大量的桩函数。一旦一个共享桩实现例如CFunction,可用,程序员可能就不需要在多写一行本地代码来建立封装类。

  但是必须小心的使用共享桩。通过共享桩,程序员在本质上可以使用Java编程语言编写C代码。这破坏了Java编程语言的类型安全性。错误的使用共享桩可能会导致破坏内存排布错误和应用程序奔溃。

  一对一映射的优势在于,它在转换Java虚拟机和本地代码之间的数据类型传输时通常更加高效。另一方面,共享桩最多处理一组预定义的参数类型,即使对于这些参数类型也不能实现最佳的性能。CFunction.callInt的调用者经常需要为每一个int参数创建Integer对象。这增加了共享桩方案的内存和时间开销。

  在实践中,你需要平衡性能、可移植性和短期生产力。共享桩可能适合利用本质上不可移植的代码,这些代码可以容忍轻微的性能下降,而在需要最高性能和可移植性的情况下,应该使用一对一映射。

9.4 实现共享桩

到目前为止,我们已经将CFunction、CPointer和CMalloc类视为黑盒子。本节介绍如何使用基本的JNI功能来实现他们。

9.4.1 CPointer类

我们想看看CPointer类,因为它是CFunction和CMalloc的超类。虚类CPointer包含一个64位的域,peer,存储着底层C指针:

本地方法例如copyIn的实现是相当直接的:

  FID_CPointer_peer是一个为CPointer.peer预先计算的字段ID。本地代码实现使用长名称命名方案(11.3节)来解决在CPointer类中与其他数组类型的重载copyIn本地方法实现的冲突。

9.4.2 CMalloc类

CMalloc类添加两个本地方法,用来分配是释放C内存块:

  CMalloc构造函数调用本地方法CMalloc.malloc,并且如果CMalloc.malloc在C堆栈空间中分配内存块失败的话,那么它会抛出一个异常。我们可以将CMallo.malloc和CMalloc.free方法实现如下:

9.4.3 CFunction类

CFunction类的实现要求在操作系统中使用动态链接支持以及CPU特定的汇编代码。下面介绍的实现是针对Win32/Intel X86环境。一旦你理解了实现CFunction类的原理,你可以按照相同的步骤在其他平台上实现它。

  CFunction类实现如下:

  CFunction类声明了一个私有字段conv,用来存储C函数的调用规则。CFunction.find本地方法实现如下:

  CFunction.find将库名和函数名转换为特定语言环境的C字符串,然后调用Win32 API函数LoadLibrary和GetProcAddress来定位在命名的本地库中C函数的位置。

  callInt方法,实现如下,执行重新调度到底层C语言的任务:

  我们假设我们已经设置了一系列的全局变量来缓存适当的类引用和字段ID。例如,全局引用FID_CPointer_peer缓存CPointer.peer的字段ID和全局引用Class_String是引用java.lang.String的全局引用。word_t类型代表一个机器字,定义如下:

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

  • 如果元素是null引用,它将NULL指针传递给C函数
  • 如果元素是java.lang.Integer类的实例,将提取其整数值并传递给C函数
  • 如果元素时java.lang.Float类的实例,将提取其浮点值并传递给C函数
  • 如果元素是CPointer类的实例,将提取其peer指针并传递给C函数
  • 如果参数是java.lang.String的实例,它将被转换为本地语言环境的C字符串并传递给C方法
  • 否则,IllegalArgumentException将会被抛出

在从Java_CFunction_callInt方法返回前,我们需要认真的检查在转换参数和释放C字符串分配的临时存储可能产生的错误。

  将参数从临时缓冲区参数传递给C函数的代码需要直接操作C堆栈。它是用内联程序集编写的:

  汇编例程将参数复制到C堆栈,然后重新分派到C函数func。func返回后,asm_dispatch例程检查func的调用约定。如果func遵循C调用约定,asm_dispatch将弹出传递给func的参数。 如果func遵循JNI调用约定,则asm_dispatch不会弹出参数; func在返回之前弹出参数。

9.5 Peer类

一对一映射和共享桩都解决了封装本地函数的问题。 在构建共享存根实现过程中,也遇到了包装本地数据结构的问题。 回想一下CPointer类的定义:

  它包含一个引用本地数据结构(在本例中是C地址空间中的一块内存)的64位peer字段。CPointer的子类为peer字段赋予特定的含义。例如,CMalloc类使用peer字段来指向C堆中的一块内存:

《(译文) JNI编程指南与规范 第九章 利用现有的本地库》

  与本地数据结构(如CPointer和CMalloc)直接对应的类称为peer类。您可以为各种本地数据结构构建peer类,例如:

  • 文件描述符
  • 套接字描述符
  • 窗口或其他图形用户界面组件

9.5.1 Java平台中的Peer类

当前Java 2 SDK 1.2版本在内部使用peer类来实现java.io、java.net和java.awt包。例如,java.io.FileDescriptor类,包含一个代表本地文件描述符的私有字段fd。

  假设你想执行一个Java平台不支持的文件操作。你可能会尝试使用JNI来查找java.io.FileDescriptor实例的底层文件描述符。只要你知道字段的名字和类型,JNI就允许你访问一个私有字段。你可能想你可以直接在本地文件描述符上直接操作。但是,这种方法存在一些问题:

  • 首先,你依赖在一个名为fd的私有字段中存储本地文件描述符的java.io.FileDescriptor实现。但是,不能保证将来Sun公司或java.io.FileDescriptor类的第三方实现的实现仍将使用相同的私有字段名称fd作为本机文件描述符。假定peer字段名称的本地代码可能无法与Java平台的不同实现一起使用。
  • 其次,直接在本机文件描述符上执行的操作可能会破坏peer类的内部一致性。例如,java.io.FileDescriptor实例维护一个内部状态,指示底层本机文件描述符是否已关闭。如果你使用本地代码绕过peer类并直接关闭底层文件描述符,则在java.io.FileDescriptor实例中维护的状态将不再与本地文件描述符的真实状态保持一致。peer类实现通常假定它们具有对底层本地数据结构的独占访问权。

  解决这些问题的唯一办法是定义你自己的封装了本地数据结构的peer类。在上面例子中,你可以定义你自己的瞒住需求操作的文件描述符peer类。这种方法不是让你使用你自己的peer类来实现Java API类。例如,您不能将自己的文件描述符实例传递给期望java.io.FileDescriptor实例的方法。但是,你可以在Java API中实现标准接口中轻松定义自己的peer类。这是基于接口而不是类来设计API的有力论据。

9.5.2 释放本地数据结构

Peer类使用Java编程语言来定义,尽管peer类的实例会被垃圾收集器自动回收。但是你要确保底层本地数据结构也会被同时释放。

  回想一下,CMalloc类包含一个用于显式释放malloc的C内存的free方法:

  你必须记住调用CMalloc类的实例的free方法,否则,CMalloc类实例可能会被垃圾收集器回收,但是他对应的malloc分配的C内存去不会被回收。

  一些程序员喜欢在peer类中加入一个finalize方法,例如CMalloc:

  虚拟机调用finalize方法,在它垃圾回收CMalloc实例前。及时你忘记调用free方法,finalize方法会帮你释放malloc申请的C内存区。

  你需要对CMalloc.free原生方法实现进行一些小改动,以说明可能被多次调用的可能性。你还需要使CMalloc.free成为一个同步方法以避免线程竞争条件:

我们使用两条语句来设置peer字段:

而不是一条语句:

因为C++编译器会将文字0视为32位整数,而不是64位整数。一些C++编译器允许你指定64位整数文字,但使用64位文字不会那么便于使用。

  定义一个finalize方法是一个适当的保护措施,但是你永远不应该依靠finalizer作为释放原生数据结构的唯一手段。原因是本地数据结构可能比对等实例消耗更多的资源。Java虚拟机在垃圾收集和finalize实例时,可能无法以足够快的速度释放本地资源。

  定义一个finalize方法可能会带来性能方面的问题。通常创建和回收带finalize方法的实例要比创建和回收不带finalize方法的实例要慢。

  如果你能确保你能够为peer类手动回收本地数据结构,你就不需要定义一个finalize方法。当时你必须确保在所有执行路径上释放了本地数据结构;否则你可能导致了资源泄漏。请特别注意在使用peer实例的过程中可能引发的异常。始终在finally子句中释放本地数据结构:

  finally子句确保cptr会被释放,即使在try块中发生了异常。

9.5.3 Peer实例的返回点

我们已经表明peer类通常包含一个指向底层本地数据结构的私有字段。在某些情况下,还希望将来自本地数据结构的引用包括在peer类的实例中。例如,当本地代码需要启动peer类中实例方法的回调时,就会出现这种情况。

  假设我们正在构建一个名为KeyInput的假想用户界面组件。KeyInput的本地C++组件,key_input,在用户按下一个键时从操作系统中接收一个key_pressed C++函数调用事件。key_input C++组件通过调用KeyInput实例上的keyPressed方法将操作系统事件报告给KeyInput实例。下图中的箭头指示按键事件是如何由用户按键发起的,并从key_input C ++组件传播到KeyInput对等实例:

《(译文) JNI编程指南与规范 第九章 利用现有的本地库》

KeyInput peer类定义如下:

  create本地方法实现分配一个C++结构体key_input的实例。C++结构体类似与C++类,仅有的不同是所有的成员在结构体中是公有的,而在类中是私有的。在这个例子中,我们使用一个C++结构体而不是一个C++类主要是毕淼和Java编程语言中的类混淆。

  create本地方法分配一个C++结构体并初始化它的back_ptr字段为一个指向KeyInput peer实例的全局引用。destroy本地方法删除指向peer实例的全局引用和由peer实例应用的C++结构体。keyInput构造函数调用create方法设置peer实例和本地实例之间的连接:

《(译文) JNI编程指南与规范 第九章 利用现有的本地库》

  当用户点击一个按键,操作系统调用C++的成员函数key_input::key_pressed。此成员函数通过对KeyInput peer实例上的keyPressed方法执行回调来响应事件。

  key_press成员函数在回调之后清除任何异常,并使用-1返回码将错误条件返回给操作系统。有关JNU_CallMethodByName和JNU_GetEnv实用程序函数的定义,分别参见6.2.3节和8.4.1节。

  让我们在结束本节之前讨论最后一个问题。 假设您在KeyInput类中添加了finalize方法,以避免潜在的内存泄漏:

  destroy方法检查peer字段是否为零,并在调用重载的destroy本地方法后将peer字段设置为零。它被定义为避免竞争条件的同步方法。

  但是,上面的代码不会像你所期望的那样工作。虚拟机将永远不会垃圾收集任何KeyInput实例,除非你显式调用destroy。KeyInput构造函数创建KeyInput实例的JNI全局引用。全局引用可防止垃圾收集KeyInput实例。你可以通过使用弱全局引用而不是全局引用来克服此问题:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注