1、JNI API完全手册1 - 简介 本章介绍 Java 本地接口(Java Native Interface,JNI)。JNI 是本地编程接口。它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程语言(如 C、C+ 和汇编语言)编写的应用程序和库进行互操作。JNI 最重要的好处是它没有对底层 Java 虚拟机的实现施加任何限制。因此,Java 虚拟机厂商可以在不影响虚拟机其它部分的情况下添加对 JNI 的支持。程序员只需编写一种版本的本地应用程序或库,就能够与所有支持 JNI 的 Java 虚拟机协同工作。本章论及以下主题: Java 本地接口概述 尽管可以完全用
2、Java 编写应用程序,但是有时单独用 Java 不能满足应用程序的需要。程序员使用 JNI 来编写 Java 本地方法,可以处理那些不能完全用 Java 编写应用程序的情况。以下示例说明了何时需要使用 Java 本地方法: 标准 Java 类库不支持与平台相关的应用程序所需的功能。 已经拥有了一个用另一种语言编写的库,而又希望通过 JNI 使 Java 代码能够访问该库。 想用低级语言(如汇编语言)实现一小段时限代码。通过用 JNI 编程,可以将本地方法用于: 创建、检查及更新 Java 对象(包括数组和字符串)。 调用 Java 方法。 捕捉和抛出异常。 加载类和获得类信息。 执行运行时类
3、型检查。也可以与调用 API 一起使用 JNI,以允许任意本地应用程序嵌入到 Java 虚拟机中。这样使得程序员能够轻易地让已有应用程序支持 Java,而不必与虚拟机源代码相链接。背景 目前,不同厂商的虚拟机提供了不同的本地方法接口。这些不同的接口使程序员不得不在给定平台上编写、维护和分发多种版本的本地方法库。下面简要分析一下部分已有本地方法接口,例如: JDK 1.0 本地方法接口 Netscape 的 Java 运行时接口 Microsoft 的原始本地接口和 Java/COM 接口 JDK 1.0 本地方法接口 JDK 1.0 附带有本地方法接口。遗憾的是,有两点原因使得该接口不适合于其
4、它 Java 虚拟机。第一,平台相关代码将 Java 对象中的域作为 C 结构的成员来进行访问。但是,Java 语言规范没有规定在内存中对象是如何布局的。如果 Java 虚拟机在内存中布局对象的方式有所不同,程序员就不得不重新编译本地方法库。第二,JDK 1.0 的本地方法接口依赖于保守的垃圾收集器。例如,无限制地使用 unhand 宏使得有必要以保守方式扫描本地堆栈。Java 运行时接口 Netscape 建议使用 Java 运行时接口 (JRI),它是 Java 虚拟机所提供服务的通用接口。JRI 的设计融入了可移植性-它几乎没有对底层 Java 虚拟机的实现细节作任何假设。JRI 提出了
5、各种各样的问题,包括本地方法、调试、反射、嵌入(调用)等等。原始本地接口和 Java/COM 接口 Microsoft Java 虚拟机支持两种本地方法接口。在低一级,它提供了高效的原始本地接口 (RNI)。RNI 提供了与 JDK 本地方法接口有高度源代码级的向后兼容性,尽管它们之间还有一个主要区别,即平台相关代码必须用 RNI 函数来与垃圾收集器进行显式的交互,而不是依赖于保守的垃圾收集。在高一级,Microsoft 的 Java/COM 接口为 Java 虚拟机提供了与语言无关的标准二进制接口。Java 代码可以象使用 Java 对象一样来使用 COM 对象。Java 类也可以作为 CO
6、M 类显示给系统的其余部分。目标 我们认为统一的,经过细致考虑的标准接口能够向每个用户提供以下好处: 每个虚拟机厂商都可以支持更多的平台相关代码。 工具构造器不必维护不同的本地方法接口。 应用程序设计人员可以只编写一种版本的平台相关代码就能够在不同的虚拟机上运行。获得标准本地方法接口的最佳途径是联合所有对 Java 虚拟机有兴趣的当事方。因此,我们在 Java 获得许可方之间组织了一系列研讨会,对设计统一的本地方法接口进行了讨论。从研讨会可以明确地看出标准本地方法接口必须满足以下要求: 二进制兼容性 - 主要的目标是在给定平台上的所有 Java 虚拟机实现之间实现本地方法库的二进制兼容性。对于
7、给定平台,程序员只需要维护一种版本的本地方法库。 效率 - 若要支持时限代码,本地方法接口必须增加一点系统开销。所有已知的用于确保虚拟机无关性(因而具有二进制兼容性)的技术都会占用一定的系统开销。我们必须在效率与虚拟机无关性之间进行某种折衷。 功能 - 接口必须显示足够的 Java 虚拟机内部情况以使本地方法能够完成有用的任务。Java 本地接口方法 我们希望采用一种已有的方法作为标准接口,因为这样程序员(程序员不得不学习在不同虚拟机中的多种接口)的工作负担最轻。遗憾的是,已有解决方案中没有任何方案能够完全地满足我们的目标。Netscape 的 JRI 最接近于我们所设想的可移植本地方法接口,
8、因而我们采用它作为设计起点。熟悉 JRI 的读者将会注意到在 API 命名规则、方法和域 ID 的使用、局部和全局引用的使用,等等中的相似点。虽然我们进行了最大的努力,但是 JNI 并不具有对 JRI 的二进制兼容性,不过虚拟机既可以支持 JRI,又可以支持 JNI。Microsoft 的 RNI 是对 JDK 1.0 的改进,因为它可以解决使用非保守的垃圾收集器的本地方法的问题。然而,RNI 不适合用作与虚拟机无关的本地方法接口。与 JDK 类似,RNI 本地方法将 Java 对象作为 C 结构来访问。这将导致两个问题: RNI 将内部 Java 对象的布局暴露给了平台相关代码。 将 Jav
9、a 对象作为 C 结构直接进行访问使得不可能有效地加入“写屏障”,写屏障是高级的垃圾收集算法所必需的。作为二进制标准,COM 确保了不同虚拟机之间的完全二进制兼容性。调用 COM 方法只要求间接调用,而这几乎不会占用系统开销。另外,COM 对象对动态链接库解决版本问题的方式也有很大的改进。然而,有几个因素阻碍了将 COM 用作标准 Java 本地方法接口: 第一,Java/COM 接口缺少某些必需功能,例如访问私有域和抛出普通异常。 第二,Java/COM 接口自动为 Java 对象提供标准的 IUnknown 和 IDispatch COM 接口,因而平台相关代码能够访问公有方法和域。遗憾的
10、是,IDispatch 接口不能处理重载的 Java 方法,而且在匹配方法名称时不区别大小写。另外,通过 IDispatch 接口暴露的所有 Java 方法被打包在一起来执行动态类型检查和强制转换。这是因为 IDispatch 接口的设计只考虑到了弱类型的语言(例如 Basic)。 第三,COM 允许软件组件(包括完全成熟的应用程序)一起工作,而不是处理单个低层函数。我们认为将所有 Java 类或低层本地方法都当作软件组件是不恰当的。 第四,在 UNIX 平台上由于缺少对 COM 的支持,所以阻碍了直接采用 COM。虽然我们没有将 Java 对象作为 COM 对象暴露给平台相关代码,但是 JN
11、I 接口自身与 COM 具有二进制兼容性。我们采用与 COM 一样的跳转表和调用约定。这意味着,一旦具有对 COM 的跨平台支持,JNI 就能成为 Java 虚拟机的 COM 接口。我们认为 JNI 不应该是给定 Java 虚拟机所支持的唯一的本地方法接口。标准接口的好处在于程序员可以将自己的平台相关代码库加载到不同的 Java 虚拟机上。在某些情况下,程序员可能不得不使用低层且与虚拟机有关的接口来获得较高的效率。但在其它情况下,程序员可能使用高层接口来建立软件组件。实际上,我们希望随着 Java 环境和组件软件技术发展得越来越成熟,本地方法将变得越来越不重要。利用 JNI 编程 本地方法程序
12、设计人员应开始利用 JNI 进行编程。利用 JNI 编程隔离了一些未知条件,例如终端用户可能正在运行的厂商的虚拟机。遵守 JNI 标准是本地库能在给定 Java 虚拟机上运行的最好保证。例如,虽然 JDK 1.1 将继续支持 JDK 1.0 中所实现的旧式的本地方法接口,但是可以肯定的是 JDK 的未来版本将停止支持旧式的本地方法接口。依赖于旧式接口的本地方法将不得不重新编写。如果您正在实现 Java 虚拟机,则应该实现 JNI。我们(Javasoft 和获得许可方)尽力确保 JNI 不会占用虚拟机实现的系统开销或施加任何限制,包括对象表示,垃圾收集机制等。如果您遇到了我们可能忽视了的问题,请
13、告知我们。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
14、 被更改为返回 jint,用于表示是否支持所请求的版本。 JDK1_1InitArgs 结构中的 reserved1 域已被重新命名为 properties。这是一个 NULL-终结的字符串数组。每个字符串具有以下格式: name=value 表示系统属性(该功能对应于 Java 命令行中的 -D 选项)。 在 JDK 1.1.1 中,调用 DestroyJavaVM 的线程必须是虚拟机中的唯一用户线程。JDK 1.1.2 放松了这一限制。如果调用 DestroyJavaVM 时有多个用户线程,则虚拟机将等待直到当前线程成为唯一的用户线程,然后销毁自己。2 - 设计概述 本章着重讨论 JNI
15、中的主要设计问题,其中的大部分问题都与本地方法有关。调用 API 的设计将在 中讨论。JNI 接口函数和指针 平台相关代码是通过调用 JNI 函数来访问 Java 虚拟机功能的。JNI 函数可通过接口指针来获得。接口指针是指针的指针,它指向一个指针数组,而指针数组中的每个元素又指向一个接口函数。每个接口函数都处在数组的某个预定偏移量中。 说明了接口指针的组织结构。图 2-1 接口指针 JNI 接口的组织类似于 C+ 虚拟函数表或 COM 接口。使用接口表而不使用硬性编入的函数表的好处是使 JNI 名字空间与平台相关代码分开。虚拟机可以很容易地提供多个版本的 JNI 函数表。例如,虚拟机可支持以
16、下两个 JNI 函数表: 一个表对非法参数进行全面检查,适用于调试程序; 另一个表只进行 JNI 规范所要求的最小程度的检查,因此效率较高。JNI 接口指针只在当前线程中有效。因此,本地方法不能将接口指针从一个线程传递到另一个线程中。实现 JNI 的虚拟机可将本地线程的数据分配和储存在 JNI 接口指针所指向的区域中。本地方法将 JNI 接口指针当作参数来接受。虚拟机在从相同的 Java 线程中对本地方法进行多次调用时,保证传递给该本地方法的接口指针是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNI 接口指针。加载和链接本地方法 对本地方法的加载通过 Sy
17、stem.loadLibrary 方法实现。下例中,类初始化方法加载了一个与平台有关的本地库,在该本地库中给出了本地方法 f 的定义: package pkg; class Cls native double f(int i, String s); static System.loadLibrary(pkg_Cls); System.loadLibrary 的参数是程序员任意选取的库名。系统按照标准的但与平台有关的处理方法将该库名转换为本地库名。例如,Solaris 系统将名称 pkg_Cls 转换为 libpkg_Cls.so,而 Win32 系统将相同的名称 pkg_Cls 转换为 pkg
18、_Cls.dll。程序员可用单个库来存放任意数量的类所需的所有本地方法,只要这些类将被相同的类加载器所加载。虚拟机在其内部为每个类加载器保护其所加载的本地库清单。提供者应该尽量选择能够避免名称冲突的本地库名。如果底层操作系统不支持动态链接,则必须事先将所有的本地方法链接到虚拟机上。这种情况下,虚拟机实际上不需要加载库即可完成 System.loadLibrary 调用。程序员还可调用 JNI 函数 RegisterNatives() 来注册与类关联的本地方法。在与静态链接的函数一起使用时,RegisterNatives() 函数将特别有用。解析本地方法名 动态链接程序是根据项的名称来解析各项的
19、。本地方法名由以下几部分串接而成: 前缀 Java_ mangled 全限定的类名 下划线(“_”)分隔符 mangled 方法名 对于重载的本地方法,加上两个下划线(“_”),后跟 mangled 参数签名 虚拟机将为本地库中的方法查找匹配的方法名。它首先查找短名(没有参数签名的名称),然后再查找带参数签名的长名称。只有当某个本地方法被另一个本 地方法重载时程序员才有必要使用长名。但如果本地方法的名称与非本地方法的名称相同,则不会有问题。因为非本地方法(Java 方法)并不放在本地库中。下例中,不必用长名来链接本地方法 g,因为另一个方法 g 不是本地方法,因而它并不在本地库中。 class
20、 Cls1 int g(int i); native int g(double d); 我们采取简单的名字搅乱方案,以保证所有的 Unicode 字符都能被转换为有效的 C 函数名。我们用下划线(“_”) 字符来代替全限定的类名中的斜杠(“/”)。由于名称或类型描述符从来不会以数字打头,我们用 _0、.、_9 来代替转义字符序列,如 所示: 表 2-1 Unicode 字符转换 转义字符序列 表示 _0XXXX Unicode 字符 XXXX。_1 字符“_” _2 签名中的字符“;” _3 签名中的字符“” 本地方法和接口 API 都要遵守给定平台上的库调用标准约定。例如,UNIX 系统使用
21、 C 调用约定,而 Win32 系统使用 _stdcall。本地方法的参数 JNI 接口指针是本地方法的第一个参数。其类型是 JNIEnv。第二个参数随本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对对象的引用,而静态本地方法的第二个参数是对其 Java 类的引用。其余的参数对应于通常 Java 方法的参数。本地方法调用利用返回值将结果传回调用程序中。 将描述 Java 类型和 C 类型之间的映射。 说明了如何用 C 函数来实现本地方法 f。对本地方法 f 的声明如下: package pkg; class Cls native double f(int i, String
22、s); . 具有长 mangled 名称 Java_pkg_Cls_f_ILjava_lang_String_2 的 C 函数实现本地方法f: 代码示例 2-1: 用 C 实现本地方法 jdouble Java_pkg_Cls_f_ILjava_lang_String_2 ( JNIEnv *env, /* 接口指针 */ jobject obj, /* “this”指针 */ jint i, /* 第一个参数 */ jstring s) /* 第二个参数 */ /* 取得 Java 字符串的 C 版本 */ const char *str = (*env)-GetStringUTFChars
23、(env, s, 0); /* 处理该字符串 */ . /* 至此完成对 str 的处理 */ (*env)-ReleaseStringUTFChars(env, s, str); return . 注意,我们总是用接口指针 env 来操作 Java 对象。可用 C+ 将此代码写得稍微简洁一些,如 所示: 代码示例 2-2: 用 C+ 实现本地方法 extern C /* 指定 C 调用约定 */ jdouble Java_pkg_Cls_f_ILjava_lang_String_2 ( JNIEnv *env, /* 接口指针 */ jobject obj, /* “this”指针 */ j
24、int i, /* 第一个参数 */ jstring s) /* 第二个参数 */ const char *str = env-GetStringUTFChars(s, 0); . env-ReleaseStringUTFChars(s, str); return . 使用 C+ 后,源代码变得更为直接,且接口指针参数消失。但是,C+ 的内在机制与 C 的完全一样。在 C+ 中,JNI 函数被定义为内联成员函数,它们将扩展为相应的 C 对应函数。引用 Java 对象 基本类型(如整型、字符型等)在 Java 和平台相关代码之间直接进行复制。而 Java 对象由引用来传递。虚拟机必须跟踪传到平台
25、相关代码中的对象,以使这些对象不会被垃圾收集器释放。反之,平台相关代码必须能用某种方式通知虚拟机它不再需要那些对象,同时,垃圾收集器必须能够移走被平台相关代码引用过的对象。全局和局部引用 JNI 将平台相关代码使用的对象引用分成两类:局部引用和全局引用。局部引用在本地方法调用期间有效,并在本地方法返回后被自动释放掉。全局引用将一直有效,直到被显式释放。对象是被作为局部引用传递给本地方法的,由 JNI 函数返回的所有 Java 对象也都是局部引用。JNI 允许程序员从局部引用创建全局引用。要求 Java 对象的 JNI 函数既可接受全局引用也可接受局部引用。本地方法将局部引用或全局引用作为结果返
26、回。大多数情况下,程序员应该依靠虚拟机在本地方法返回后释放所有局部引用。但是,有时程序员必须显式释放某个局部引用。例如,考虑以下的情形: 本地方法要访问一个大型 Java 对象,于是创建了对该 Java 对象的局部引用。然后,本地方法要在返回调用程序之前执行其它计算。对这个大型 Java 对象的局部引用将防止该对象被当作垃圾收集,即使在剩余的运算中并不再需要该对象。 本 地方法创建了大量的局部引用,但这些局部引用并不是要同时使用。由于虚拟机需要一定的空间来跟踪每个局部引用,创建太多的局部引用将可能使系统耗尽内存。 例如,本地方法要在一个大型对象数组中循环,把取回的元素作为局部引用,并在每次迭代
27、时对一个元素进行操作。每次迭代后,程序员不再需要对该数组元素的局 部引用。JNI 允许程序员在本地方法内的任何地方对局部引用进行手工删除。为确保程序员可以手工释放局部引用,JNI 函数将不能创建额外的局部引用,除非是这些 JNI 函数要作为结果返回的引用。局部引用仅在创建它们的线程中有效。本地方法不能将局部引用从一个线程传递到另一个线程中。实现局部引用 为了实现局部引用,Java 虚拟机为每个从 Java 到本地方法的控制转换都创建了注册服务程序。注册服务程序将不可移动的局部引用映射为 Java 对象,并防止这些对象被当作垃圾收集。所有传给本地方法的 Java 对象(包括那些作为 JNI 函数
28、调用结果返回的对象)将被自动添加到注册服务程序中。本地方法返回后,注册服务程序将被删除,其中的所有项都可以被当作垃圾来收集。可用各种不同的方法来实现注册服务程序,例如,使用表、链接列表或 hash 表来实现。虽然引用计数可用来避免注册服务程序中有重复的项,但 JNI 实现不是必须检测和消除重复的项。注意,以保守方式扫描本地堆栈并不能如实地实现局部引用。平台相关代码可将局部引用储存在全局或堆数据结构中。访问 Java 对象 JNI 提供了一大批用来访问全局引用和局部引用的函数。这意味着无论虚拟机在内部如何表示 Java 对象,相同的本地方法实现都能工作。这就是为什么 JNI 可被各种各样的虚拟机
29、实现所支持的关键原因。通过不透明的引用来使用访问函数的开销比直接访问 C 数据结构的开销来得高。我们相信,大多数情况下,Java 程序员使用本地方法是为了完成一些重要任务,此时这种接口的开销不是首要问题。访问基本类型数组 对于含有大量基本数据类型(如整数数组和字符串)的 Java 对象来说,这种开销将高得不可接受 (考虑一下用于执行矢量和矩阵运算的本地方法的情形便知)。对 Java 数组进行迭代并且要通过函数调用取回数组的每个元素,其效率是非常低的。一个解决办法是引入“钉住”概念,以使本地方法能够要求虚拟机钉住数组内容。而后,该本地方法将接受指向数值元素的直接指针。但是,这种方法包含以下两个前
30、提: 垃圾收集器必须支持钉住。 虚拟机必须在内存中连续存放基本类型数组。虽然大多数基本类型数组都是连续存放的,但布尔数组可以压缩或不压缩存储。因此,依赖于布尔数组确切存储方式的本地方法将是不可移植的。我们将采取折衷方法来克服上述两个问题。首先,我们提供了一套函数,用于在 Java 数组的一部分和本地内存缓冲之间复制基本类型数组元素。这些函数只有在本地方法只需访问大型数组中的一小部分元素时才使用。其次,程序员可用另一套函数来取回数组元素的受约束版本。记住,这些函数可能要求 Java 虚拟机分配存储空间和进行复制。虚拟机实现将决定这些函数是否真正复制该数组,如下所示: 如果垃圾收集器支持钉住,且数
31、组的布局符合本地方法的要求,则不需要进行复制。 否则,该数组将被复制到不可移动的内存块中(例如,复制到 C 堆中),并进行必要的格式转换,然后返回指向该副本的指针。最后,接口提供了一些函数,用以通知虚拟机本地方法已不再需要访问这些数组元素。当调用这些函数时,系统或者释放数组,或者在原始数组与其不可移动副本之间进行协调并将副本释放。这种处理方法具有灵活性。垃圾收集器的算法可对每个给定的数组分别作出复制或钉住的决定。例如,垃圾收集器可能复制小型对象而钉住大型对象。JNI 实现必须确保多个线程中运行的本地方法可同时访问同一数组。例如,JNI 可以为每个被钉住的数组保留一个内部计数器,以便某个线程不会解开同时被另一个线程钉住的数组。注意,JNI 不必将基本类型数组锁住以专供某个本地方法访问。同时从不同的线程对 Java 数组进行更新将导致不确定的结果。访问域和方法 JNI 允许本地方法访问 Java 对象的域或调用其方法。JNI 用符号名称和类型签名来识别
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1