1 - 简介 本章介绍 Java 本地接口(Java Native Interface,JNI)。JNI 是本地编程接口。它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程语言(如 C、C++ 和汇编语言)编写的应用程序和库进行互操作。
JNI 最重要的好处是它没有对底层 Java 虚拟机的实现施加任何限制。因此,Java 虚拟机厂商可以在不影响虚拟机其它部分的情况下添加对 JNI 的支持。程序员只需编写一种版本的本地应用程序或库,就能够与所有支持 JNI 的 Java 虚拟机协同工作。
本章论及以下主题:
Java 本地接口概述 背景 目标 Java 本地接口方法 利用 JNI 编程 JDK 1.1.2 中的变化
Java 本地接口概述 尽管可以完全用 Java 编写应用程序,但是有时单独用 Java 不能满足应用程序的需要。程序员使用 JNI 来编写 Java 本地方法,可以处理那些不能完全用 Java 编写应用程序的情况。
以下示例说明了何时需要使用 Java 本地方法:
标准 Java 类库不支持与平台相关的应用程序所需的功能。 已经拥有了一个用另一种语言编写的库,而又希望通过 JNI 使 Java 代码能够访问该库。 想用低级语言(如汇编语言)实现一小段时限代码。
通过用 JNI 编程,可以将本地方法用于:
创建、检查及更新 Java 对象(包括数组和字符串)。 调用 Java 方法。 捕捉和抛出异常。 加载类和获得类信息。 执行运行时类型检查。
也可以与调用 API 一起使用 JNI,以允许任意本地应用程序嵌入到 Java 虚拟机中。这样使得程序员能够轻易地让已有应用程序支持 Java,而不必与虚拟机源代码相链接。
背景 目前,不同厂商的虚拟机提供了不同的本地方法接口。这些不同的接口使程序员不得不在给定平台上编写、维护和分发多种版本的本地方法库。
下面简要分析一下部分已有本地方法接口,例如:
JDK 1.0 本地方法接口 Netscape 的 Java 运行时接口 Microsoft 的原始本地接口和 Java/COM 接口
JDK 1.0 本地方法接口 JDK 1.0 附带有本地方法接口。遗憾的是,有两点原因使得该接口不适合于其它 Java 虚拟机。
第一,平台相关代码将 Java 对象中的域作为 C 结构的成员来进行访问。但是,Java 语言规范没有规定在内存中对象是如何布局的。如果 Java 虚拟机在内存中布局对象的方式有所不同,程序员就不得不重新编译本地方法库。
第二,JDK 1.0 的本地方法接口依赖于保守的垃圾收集器。例如,无限制地使用 unhand 宏使得有必要以保守方式扫描本地堆栈。
Java 运行时接口 Netscape 建议使用 Java 运行时接口 (JRI),它是 Java 虚拟机所提供服务的通用接口。JRI 的设计融入了可移植性---它几乎没有对底层 Java 虚拟机的实现细节作任何假设。JRI 提出了各种各样的问题,包括本地方法、调试、反射、嵌入(调用)等等。
原始本地接口和 Java/COM 接口 Microsoft Java 虚拟机支持两种本地方法接口。在低一级,它提供了高效的原始本地接口 (RNI)。RNI 提供了与 JDK 本地方法接口有高度源代码级的向后兼容性,尽管它们之间还有一个主要区别,即平台相关代码必须用 RNI 函数来与垃圾收集器进行显式的交互,而不是依赖于保守的垃圾收集。
在高一级,Microsoft 的 Java/COM 接口为 Java 虚拟机提供了与语言无关的标准二进制接口。Java 代码可以象使用 Java 对象一样来使用 COM 对象。Java 类也可以作为 COM 类显示给系统的其余部分。
目标 我们认为统一的,经过细致考虑的标准接口能够向每个用户提供以下好处:
每个虚拟机厂商都可以支持更多的平台相关代码。 工具构造器不必维护不同的本地方法接口。 应用程序设计人员可以只编写一种版本的平台相关代码就能够在不同的虚拟机上运行。
获得标准本地方法接口的最佳途径是联合所有对 Java 虚拟机有兴趣的当事方。因此,我们在 Java 获得许可方之间组织了一系列研讨会,对设计统一的本地方法接口进行了讨论。从研讨会可以明确地看出标准本地方法接口必须满足以下要求:
二进制兼容性 - 主要的目标是在给定平台上的所有 Java 虚拟机实现之间实现本地方法库的二进制兼容性。对于给定平台,程序员只需要维护一种版本的本地方法库。 效率 - 若要支持时限代码,本地方法接口必须增加一点系统开销。所有已知的用于确保虚拟机无关性(因而具有二进制兼容性)的技术都会占用一定的系统开销。我们必须在效率与虚拟机无关性之间进行某种折衷。 功能 - 接口必须显示足够的 Java 虚拟机内部情况以使本地方法能够完成有用的任务。
Java 本地接口方法 我们希望采用一种已有的方法作为标准接口,因为这样程序员(程序员不得不学习在不同虚拟机中的多种接口)的工作负担最轻。遗憾的是,已有解决方案中没有任何方案能够完全地满足我们的目标。
Netscape 的 JRI 最接近于我们所设想的可移植本地方法接口,因而我们采用它作为设计起点。熟悉 JRI 的读者将会注意到在 API 命名规则、方法和域 ID 的使用、局部和全局引用的使用,等等中的相似点。虽然我们进行了最大的努力,但是 JNI 并不具有对 JRI 的二进制兼容性,不过虚拟机既可以支持 JRI,又可以支持 JNI。
Microsoft 的 RNI 是对 JDK 1.0 的改进,因为它可以解决使用非保守的垃圾收集器的本地方法的问题。然而,RNI 不适合用作与虚拟机无关的本地方法接口。与 JDK 类似,RNI 本地方法将 Java 对象作为 C 结构来访问。这将导致两个问题:
RNI 将内部 Java 对象的布局暴露给了平台相关代码。 将 Java 对象作为 C 结构直接进行访问使得不可能有效地加入“写屏障”,写屏障是高级的垃圾收集算法所必需的。
作为二进制标准,COM 确保了不同虚拟机之间的完全二进制兼容性。调用 COM 方法只要求间接调用,而这几乎不会占用系统开销。另外,COM 对象对动态链接库解决版本问题的方式也有很大的改进。
然而,有几个因素阻碍了将 COM 用作标准 Java 本地方法接口:
第一,Java/COM 接口缺少某些必需功能,例如访问私有域和抛出普通异常。 第二,Java/COM 接口自动为 Java 对象提供标准的 IUnknown 和 IDispatch COM 接口,因而平台相关代码能够访问公有方法和域。遗憾的是,IDispatch 接口不能处理重载的 Java 方法,而且在匹配方法名称时不区别大小写。另外,通过 IDispatch 接口暴露的所有 Java 方法被打包在一起来执行动态类型检查和强制转换。这是因为 IDispatch 接口的设计只考虑到了弱类型的语言(例如 Basic)。 第三,COM 允许软件组件(包括完全成熟的应用程序)一起工作,而不是处理单个低层函数。我们认为将所有 Java 类或低层本地方法都当作软件组件是不恰当的。 第四,在 UNIX 平台上由于缺少对 COM 的支持,所以阻碍了直接采用 COM。
虽然我们没有将 Java 对象作为 COM 对象暴露给平台相关代码,但是 JNI 接口自身与 COM 具有二进制兼容性。我们采用与 COM 一样的跳转表和调用约定。这意味着,一旦具有对 COM 的跨平台支持,JNI 就能成为 Java 虚拟机的 COM 接口。
我们认为 JNI 不应该是给定 Java 虚拟机所支持的唯一的本地方法接口。标准接口的好处在于程序员可以将自己的平台相关代码库加载到不同的 Java 虚拟机上。在某些情况下,程序员可能不得不使用低层且与虚拟机有关的接口来获得较高的效率。但在其它情况下,程序员可能使用高层接口来建立软件组件。实际上,我们希望随着 Java 环境和组件软件技术发展得越来越成熟,本地方法将变得越来越不重要。
利用 JNI 编程 本地方法程序设计人员应开始利用 JNI 进行编程。利用 JNI 编程隔离了一些未知条件,例如终端用户可能正在运行的厂商的虚拟机。遵守 JNI 标准是本地库能在给定 Java 虚拟机上运行的最好保证。例如,虽然 JDK 1.1 将继续支持 JDK 1.0 中所实现的旧式的本地方法接口,但是可以肯定的是 JDK 的未来版本将停止支持旧式的本地方法接口。依赖于旧式接口的本地方法将不得不重新编写。
如果您正在实现 Java 虚拟机,则应该实现 JNI。我们(Javasoft 和获得许可方)尽力确保 JNI 不会占用虚拟机实现的系统开销或施加任何限制,包括对象表示,垃圾收集机制等。如果您遇到了我们可能忽视了的问题,请告知我们。
JDK 1.1.2 中的变化 为了更好地支持 Java 运行时环境 (JRE),在 JDK 1.1.2 中对调用 API 在几个方面作了扩展。这些变化没有破坏任何已有代码,JNI 本地方法接口也没有改变。
JDK1_1InitArgs 结构中的 reserved0 域已被重新命名为 version。JDK1_1InitArgs 结构保存 JNI_CreateJavaVM 的初始化参数。JNI_GetDefaultJavaVMInitArgs 和 JNI_CreateJavaVM 的调用者必须将版本域设置为 0x00010001。JNI_GetDefaultJavaVMInitArgs 被更改为返回 jint,用于表示是否支持所请求的版本。 JDK1_1InitArgs 结构中的 reserved1 域已被重新命名为 properties。这是一个 NULL-终结的字符串数组。每个字符串具有以下格式: name=value
表示系统属性(该功能对应于 Java 命令行中的 -D 选项)。
在 JDK 1.1.1 中,调用 DestroyJavaVM 的线程必须是虚拟机中的唯一用户线程。JDK 1.1.2 放松了这一限制。如果调用 DestroyJavaVM 时有多个用户线程,则虚拟机将等待直到当前线程成为唯一的用户线程,然后销毁自己。