《JNIAPI完全介绍材料.doc》由会员分享,可在线阅读,更多相关《JNIAPI完全介绍材料.doc(76页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、-/1 - 简介 本章介绍 Java 本地接口(Java Native Interface,JNI)。JNI 是本地编程接口。它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程语言(如 C、C+ 和汇编语言)编写的应用程序和库进行互操作。JNI 最重要的好处是它没有对底层 Java 虚拟机的实现施加任何限制。因此,Java 虚拟机厂商可以在不影响虚拟机其它部分的情况下添加对 JNI 的支持。程序员只需编写一种版本的本地应用程序或库,就能够与所有支持 JNI 的 Java 虚拟机协同工作。本章论及以下主题: Java 本地接口概述 背景 目标 Java 本地接口方法
2、 利用 JNI 编程 JDK 1.1.2 中的变化 Java 本地接口概述 尽管可以完全用 Java 编写应用程序,但是有时单独用 Java 不能满足应用程序的需要。程序员使用 JNI 来编写 Java 本地方法,可以处理那些不能完全用 Java 编写应用程序的情况。以下示例说明了何时需要使用 Java 本地方法: 标准 Java 类库不支持与平台相关的应用程序所需的功能。 已经拥有了一个用另一种语言编写的库,而又希望通过 JNI 使 Java 代码能够访问该库。 想用低级语言(如汇编语言)实现一小段时限代码。通过用 JNI 编程,可以将本地方法用于: 创建、检查及更新 Java 对象(包括数
3、组和字符串)。 调用 Java 方法。 捕捉和抛出异常。 加载类和获得类信息。 执行运行时类型检查。也可以与调用 API 一起使用 JNI,以允许任意本地应用程序嵌入到 Java 虚拟机中。这样使得程序员能够轻易地让已有应用程序支持 Java,而不必与虚拟机源代码相链接。背景 目前,不同厂商的虚拟机提供了不同的本地方法接口。这些不同的接口使程序员不得不在给定平台上编写、维护和分发多种版本的本地方法库。下面简要分析一下部分已有本地方法接口,例如: JDK 1.0 本地方法接口 Netscape 的 Java 运行时接口 Microsoft 的原始本地接口和 Java/COM 接口 JDK 1.0
4、 本地方法接口 JDK 1.0 附带有本地方法接口。遗憾的是,有两点原因使得该接口不适合于其它 Java 虚拟机。第一,平台相关代码将 Java 对象中的域作为 C 结构的成员来进行访问。但是,Java 语言规范没有规定在内存中对象是如何布局的。如果 Java 虚拟机在内存中布局对象的方式有所不同,程序员就不得不重新编译本地方法库。第二,JDK 1.0 的本地方法接口依赖于保守的垃圾收集器。例如,无限制地使用 unhand 宏使得有必要以保守方式扫描本地堆栈。Java 运行时接口 Netscape 建议使用 Java 运行时接口 (JRI),它是 Java 虚拟机所提供服务的通用接口。JRI
5、的设计融入了可移植性-它几乎没有对底层 Java 虚拟机的实现细节作任何假设。JRI 提出了各种各样的问题,包括本地方法、调试、反射、嵌入(调用)等等。原始本地接口和 Java/COM 接口 Microsoft Java 虚拟机支持两种本地方法接口。在低一级,它提供了高效的原始本地接口 (RNI)。RNI 提供了与 JDK 本地方法接口有高度源代码级的向后兼容性,尽管它们之间还有一个主要区别,即平台相关代码必须用 RNI 函数来与垃圾收集器进行显式的交互,而不是依赖于保守的垃圾收集。在高一级,Microsoft 的 Java/COM 接口为 Java 虚拟机提供了与语言无关的标准二进制接口。J
6、ava 代码可以象使用 Java 对象一样来使用 COM 对象。Java 类也可以作为 COM 类显示给系统的其余部分。目标 我们认为统一的,经过细致考虑的标准接口能够向每个用户提供以下好处: 每个虚拟机厂商都可以支持更多的平台相关代码。 工具构造器不必维护不同的本地方法接口。 应用程序设计人员可以只编写一种版本的平台相关代码就能够在不同的虚拟机上运行。获得标准本地方法接口的最佳途径是联合所有对 Java 虚拟机有兴趣的当事方。因此,我们在 Java 获得许可方之间组织了一系列研讨会,对设计统一的本地方法接口进行了讨论。从研讨会可以明确地看出标准本地方法接口必须满足以下要求: 二进制兼容性 -
7、 主要的目标是在给定平台上的所有 Java 虚拟机实现之间实现本地方法库的二进制兼容性。对于给定平台,程序员只需要维护一种版本的本地方法库。 效率 - 若要支持时限代码,本地方法接口必须增加一点系统开销。所有已知的用于确保虚拟机无关性(因而具有二进制兼容性)的技术都会占用一定的系统开销。我们必须在效率与虚拟机无关性之间进行某种折衷。 功能 - 接口必须显示足够的 Java 虚拟机内部情况以使本地方法能够完成有用的任务。Java 本地接口方法 我们希望采用一种已有的方法作为标准接口,因为这样程序员(程序员不得不学习在不同虚拟机中的多种接口)的工作负担最轻。遗憾的是,已有解决方案中没有任何方案能够
8、完全地满足我们的目标。Netscape 的 JRI 最接近于我们所设想的可移植本地方法接口,因而我们采用它作为设计起点。熟悉 JRI 的读者将会注意到在 API 命名规则、方法和域 ID 的使用、局部和全局引用的使用,等等中的相似点。虽然我们进行了最大的努力,但是 JNI 并不具有对 JRI 的二进制兼容性,不过虚拟机既可以支持 JRI,又可以支持 JNI。Microsoft 的 RNI 是对 JDK 1.0 的改进,因为它可以解决使用非保守的垃圾收集器的本地方法的问题。然而,RNI 不适合用作与虚拟机无关的本地方法接口。与 JDK 类似,RNI 本地方法将 Java 对象作为 C 结构来访问
9、。这将导致两个问题: RNI 将内部 Java 对象的布局暴露给了平台相关代码。 将 Java 对象作为 C 结构直接进行访问使得不可能有效地加入“写屏障”,写屏障是高级的垃圾收集算法所必需的。作为二进制标准,COM 确保了不同虚拟机之间的完全二进制兼容性。调用 COM 方法只要求间接调用,而这几乎不会占用系统开销。另外,COM 对象对动态链接库解决版本问题的方式也有很大的改进。然而,有几个因素阻碍了将 COM 用作标准 Java 本地方法接口: 第一,Java/COM 接口缺少某些必需功能,例如访问私有域和抛出普通异常。 第二,Java/COM 接口自动为 Java 对象提供标准的 IUnk
10、nown 和 IDispatch COM 接口,因而平台相关代码能够访问公有方法和域。遗憾的是,IDispatch 接口不能处理重载的 Java 方法,而且在匹配方法名称时不区别大小写。另外,通过 IDispatch 接口暴露的所有 Java 方法被打包在一起来执行动态类型检查和强制转换。这是因为 IDispatch 接口的设计只考虑到了弱类型的语言(例如 Basic)。 第三,COM 允许软件组件(包括完全成熟的应用程序)一起工作,而不是处理单个低层函数。我们认为将所有 Java 类或低层本地方法都当作软件组件是不恰当的。 第四,在 UNIX 平台上由于缺少对 COM 的支持,所以阻碍了直接
11、采用 COM。虽然我们没有将 Java 对象作为 COM 对象暴露给平台相关代码,但是 JNI 接口自身与 COM 具有二进制兼容性。我们采用与 COM 一样的跳转表和调用约定。这意味着,一旦具有对 COM 的跨平台支持,JNI 就能成为 Java 虚拟机的 COM 接口。我们认为 JNI 不应该是给定 Java 虚拟机所支持的唯一的本地方法接口。标准接口的好处在于程序员可以将自己的平台相关代码库加载到不同的 Java 虚拟机上。在某些情况下,程序员可能不得不使用低层且与虚拟机有关的接口来获得较高的效率。但在其它情况下,程序员可能使用高层接口来建立软件组件。实际上,我们希望随着 Java 环境
12、和组件软件技术发展得越来越成熟,本地方法将变得越来越不重要。利用 JNI 编程 本地方法程序设计人员应开始利用 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 的调用者必须将
14、版本域设置为 0x00010001。JNI_GetDefaultJavaVMInitArgs 被更改为返回 jint,用于表示是否支持所请求的版本。 JDK1_1InitArgs 结构中的 reserved1 域已被重新命名为 properties。这是一个 NULL-终结的字符串数组。每个字符串具有以下格式: name=value 表示系统属性(该功能对应于 Java 命令行中的 -D 选项)。 在 JDK 1.1.1 中,调用 DestroyJavaVM 的线程必须是虚拟机中的唯一用户线程。JDK 1.1.2 放松了这一限制。如果调用 DestroyJavaVM 时有多个用户线程,则虚拟机
15、将等待直到当前线程成为唯一的用户线程,然后销毁自己。2 - 设计概述 本章着重讨论 JNI 中的主要设计问题,其中的大部分问题都与本地方法有关。调用 API 的设计将在 第 5 章 “调用 API” 中讨论。JNI 接口函数和指针 平台相关代码是通过调用 JNI 函数来访问 Java 虚拟机功能的。JNI 函数可通过接口指针来获得。接口指针是指针的指针,它指向一个指针数组,而指针数组中的每个元素又指向一个接口函数。每个接口函数都处在数组的某个预定偏移量中。图 2-1 说明了接口指针的组织结构。图 2-1 接口指针 JNI 接口的组织类似于 C+ 虚拟函数表或 COM 接口。使用接口表而不使用硬
16、性编入的函数表的好处是使 JNI 名字空间与平台相关代码分开。虚拟机可以很容易地提供多个版本的 JNI 函数表。例如,虚拟机可支持以下两个 JNI 函数表: 一个表对非法参数进行全面检查,适用于调试程序; 另一个表只进行 JNI 规范所要求的最小程度的检查,因此效率较高。JNI 接口指针只在当前线程中有效。因此,本地方法不能将接口指针从一个线程传递到另一个线程中。实现 JNI 的虚拟机可将本地线程的数据分配和储存在 JNI 接口指针所指向的区域中。本地方法将 JNI 接口指针当作参数来接受。虚拟机在从相同的 Java 线程中对本地方法进行多次调用时,保证传递给该本地方法的接口指针是相同的。但是
17、,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNI 接口指针。加载和链接本地方法 对本地方法的加载通过 System.loadLibrary 方法实现。下例中,类初始化方法加载了一个与平台有关的本地库,在该本地库中给出了本地方法 f 的定义: package pkg; class Cls native double f(int i, String s); static System.loadLibrary(pkg_Cls); System.loadLibrary 的参数是程序员任意选取的库名。系统按照标准的但与平台有关的处理方法将该库名转换为本地库名。例如,Solari
18、s 系统将名称 pkg_Cls 转换为 libpkg_Cls.so,而 Win32 系统将相同的名称 pkg_Cls 转换为 pkg_Cls.dll。程序员可用单个库来存放任意数量的类所需的所有本地方法,只要这些类将被相同的类加载器所加载。虚拟机在其内部为每个类加载器保护其所加载的本地库清单。提供者应该尽量选择能够避免名称冲突的本地库名。如果底层操作系统不支持动态链接,则必须事先将所有的本地方法链接到虚拟机上。这种情况下,虚拟机实际上不需要加载库即可完成 System.loadLibrary 调用。程序员还可调用 JNI 函数 RegisterNatives() 来注册与类关联的本地方法。在与
19、静态链接的函数一起使用时,RegisterNatives() 函数将特别有用。解析本地方法名 动态链接程序是根据项的名称来解析各项的。本地方法名由以下几部分串接而成: 前缀 Java_ mangled 全限定的类名 下划线(“_”)分隔符 mangled 方法名 对于重载的本地方法,加上两个下划线(“_”),后跟 mangled 参数签名 虚拟机将为本地库中的方法查找匹配的方法名。它首先查找短名(没有参数签名的名称),然后再查找带参数签名的长名称。只有当某个本地方法被另一个本 地方法重载时程序员才有必要使用长名。但如果本地方法的名称与非本地方法的名称相同,则不会有问题。因为非本地方法(Java
20、 方法)并不放在本地库中。下例中,不必用长名来链接本地方法 g,因为另一个方法 g 不是本地方法,因而它并不在本地库中。 class Cls1 int g(int i); native int g(double d); 我们采取简单的名字搅乱方案,以保证所有的 Unicode 字符都能被转换为有效的 C 函数名。我们用下划线(“_”) 字符来代替全限定的类名中的斜杠(“/”)。由于名称或类型描述符从来不会以数字打头,我们用 _0、.、_9 来代替转义字符序列,如表 2-1 所示: 表 2-1 Unicode 字符转换 转义字符序列 表示 _0XXXX Unicode 字符 XXXX。_1 字符
21、“_” _2 签名中的字符“;” _3 签名中的字符“” 本地方法和接口 API 都要遵守给定平台上的库调用标准约定。例如,UNIX 系统使用 C 调用约定,而 Win32 系统使用 _stdcall。本地方法的参数 JNI 接口指针是本地方法的第一个参数。其类型是 JNIEnv。第二个参数随本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对对象的引用,而静态本地方法的第二个参数是对其 Java 类的引用。其余的参数对应于通常 Java 方法的参数。本地方法调用利用返回值将结果传回调用程序中。第 3 章 “JNI 的类型和数据结构” 将描述 Java 类型和 C 类型之间的映射
22、。代码示例 2-1 说明了如何用 C 函数来实现本地方法 f。对本地方法 f 的声明如下: package pkg; class Cls native double f(int i, String 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, /*
23、 第一个参数 */ jstring s) /* 第二个参数 */ /* 取得 Java 字符串的 C 版本 */ const char *str = (*env)-GetStringUTFChars(env, s, 0); /* 处理该字符串 */ . /* 至此完成对 str 的处理 */ (*env)-ReleaseStringUTFChars(env, s, str); return . 注意,我们总是用接口指针 env 来操作 Java 对象。可用 C+ 将此代码写得稍微简洁一些,如代码示例 2-2 所示: 代码示例 2-2: 用 C+ 实现本地方法 extern C /* 指定 C
24、调用约定 */ jdouble Java_pkg_Cls_f_ILjava_lang_String_2 ( JNIEnv *env, /* 接口指针 */ jobject obj, /* “this”指针 */ jint i, /* 第一个参数 */ jstring s) /* 第二个参数 */ const char *str = env-GetStringUTFChars(s, 0); . env-ReleaseStringUTFChars(s, str); return . 使用 C+ 后,源代码变得更为直接,且接口指针参数消失。但是,C+ 的内在机制与 C 的完全一样。在 C+ 中,JN
25、I 函数被定义为内联成员函数,它们将扩展为相应的 C 对应函数。引用 Java 对象 基本类型(如整型、字符型等)在 Java 和平台相关代码之间直接进行复制。而 Java 对象由引用来传递。虚拟机必须跟踪传到平台相关代码中的对象,以使这些对象不会被垃圾收集器释放。反之,平台相关代码必须能用某种方式通知虚拟机它不再需要那些对象,同时,垃圾收集器必须能够移走被平台相关代码引用过的对象。全局和局部引用 JNI 将平台相关代码使用的对象引用分成两类:局部引用和全局引用。局部引用在本地方法调用期间有效,并在本地方法返回后被自动释放掉。全局引用将一直有效,直到被显式释放。对象是被作为局部引用传递给本地方
26、法的,由 JNI 函数返回的所有 Java 对象也都是局部引用。JNI 允许程序员从局部引用创建全局引用。要求 Java 对象的 JNI 函数既可接受全局引用也可接受局部引用。本地方法将局部引用或全局引用作为结果返回。大多数情况下,程序员应该依靠虚拟机在本地方法返回后释放所有局部引用。但是,有时程序员必须显式释放某个局部引用。例如,考虑以下的情形: 本地方法要访问一个大型 Java 对象,于是创建了对该 Java 对象的局部引用。然后,本地方法要在返回调用程序之前执行其它计算。对这个大型 Java 对象的局部引用将防止该对象被当作垃圾收集,即使在剩余的运算中并不再需要该对象。 本 地方法创建了
27、大量的局部引用,但这些局部引用并不是要同时使用。由于虚拟机需要一定的空间来跟踪每个局部引用,创建太多的局部引用将可能使系统耗尽内存。 例如,本地方法要在一个大型对象数组中循环,把取回的元素作为局部引用,并在每次迭代时对一个元素进行操作。每次迭代后,程序员不再需要对该数组元素的局 部引用。JNI 允许程序员在本地方法内的任何地方对局部引用进行手工删除。为确保程序员可以手工释放局部引用,JNI 函数将不能创建额外的局部引用,除非是这些 JNI 函数要作为结果返回的引用。局部引用仅在创建它们的线程中有效。本地方法不能将局部引用从一个线程传递到另一个线程中。实现局部引用 为了实现局部引用,Java 虚
28、拟机为每个从 Java 到本地方法的控制转换都创建了注册服务程序。注册服务程序将不可移动的局部引用映射为 Java 对象,并防止这些对象被当作垃圾收集。所有传给本地方法的 Java 对象(包括那些作为 JNI 函数调用结果返回的对象)将被自动添加到注册服务程序中。本地方法返回后,注册服务程序将被删除,其中的所有项都可以被当作垃圾来收集。可用各种不同的方法来实现注册服务程序,例如,使用表、链接列表或 hash 表来实现。虽然引用计数可用来避免注册服务程序中有重复的项,但 JNI 实现不是必须检测和消除重复的项。注意,以保守方式扫描本地堆栈并不能如实地实现局部引用。平台相关代码可将局部引用储存在全
29、局或堆数据结构中。访问 Java 对象 JNI 提供了一大批用来访问全局引用和局部引用的函数。这意味着无论虚拟机在内部如何表示 Java 对象,相同的本地方法实现都能工作。这就是为什么 JNI 可被各种各样的虚拟机实现所支持的关键原因。通过不透明的引用来使用访问函数的开销比直接访问 C 数据结构的开销来得高。我们相信,大多数情况下,Java 程序员使用本地方法是为了完成一些重要任务,此时这种接口的开销不是首要问题。访问基本类型数组 对于含有大量基本数据类型(如整数数组和字符串)的 Java 对象来说,这种开销将高得不可接受 (考虑一下用于执行矢量和矩阵运算的本地方法的情形便知)。对 Java
30、数组进行迭代并且要通过函数调用取回数组的每个元素,其效率是非常低的。一个解决办法是引入“钉住”概念,以使本地方法能够要求虚拟机钉住数组内容。而后,该本地方法将接受指向数值元素的直接指针。但是,这种方法包含以下两个前提: 垃圾收集器必须支持钉住。 虚拟机必须在内存中连续存放基本类型数组。虽然大多数基本类型数组都是连续存放的,但布尔数组可以压缩或不压缩存储。因此,依赖于布尔数组确切存储方式的本地方法将是不可移植的。我们将采取折衷方法来克服上述两个问题。首先,我们提供了一套函数,用于在 Java 数组的一部分和本地内存缓冲之间复制基本类型数组元素。这些函数只有在本地方法只需访问大型数组中的一小部分元
31、素时才使用。其次,程序员可用另一套函数来取回数组元素的受约束版本。记住,这些函数可能要求 Java 虚拟机分配存储空间和进行复制。虚拟机实现将决定这些函数是否真正复制该数组,如下所示: 如果垃圾收集器支持钉住,且数组的布局符合本地方法的要求,则不需要进行复制。 否则,该数组将被复制到不可移动的内存块中(例如,复制到 C 堆中),并进行必要的格式转换,然后返回指向该副本的指针。最后,接口提供了一些函数,用以通知虚拟机本地方法已不再需要访问这些数组元素。当调用这些函数时,系统或者释放数组,或者在原始数组与其不可移动副本之间进行协调并将副本释放。这种处理方法具有灵活性。垃圾收集器的算法可对每个给定的
32、数组分别作出复制或钉住的决定。例如,垃圾收集器可能复制小型对象而钉住大型对象。JNI 实现必须确保多个线程中运行的本地方法可同时访问同一数组。例如,JNI 可以为每个被钉住的数组保留一个内部计数器,以便某个线程不会解开同时被另一个线程钉住的数组。注意,JNI 不必将基本类型数组锁住以专供某个本地方法访问。同时从不同的线程对 Java 数组进行更新将导致不确定的结果。访问域和方法 JNI 允许本地方法访问 Java 对象的域或调用其方法。JNI 用符号名称和类型签名来识别方法和域。从名称和签名来定位域或对象的过程可分为两步。例如,为调用类 cls 中的 f 方法,平台相关代码首先要获得方法 ID
33、,如下所示: jmethodID mid = env-GetMethodID(cls, f, (ILjava/lang/String;)D);然后,平台相关代码可重复使用该方法 ID 而无须再查找该方法,如下所示: jdouble result = env-CallDoubleMethod(obj, mid, 10, str);域 ID 或方法 ID 并不能防止虚拟机卸载生成该 ID 的类。该类被卸载之后,该方法 ID 或域 ID 亦变成无效。因此,如果平台相关代码要长时间使用某个方法 ID 或域 ID,则它必须确保: 保留对所涉及类的活引用,或 重新计算该方法 ID 或域 ID。JNI 对域
34、 ID 和方法 ID 的内部实现并不施加任何限制。报告编程错误 JNI 不检查诸如传递 NULL 指针或非法参数类型之类的编程错误。非法的参数类型包括诸如要用 Java 类对象时却用了普通 Java 对象这样的错误。JNI 不检查这些编程错误的理由如下: 强迫 JNI 函数去检查所有可能的错误情况将降低正常(正确)的本地方法的性能。 在许多情况下,没有足够的运行时的类型信息可供这种检查使用。大多数 C 库函数对编程错误不进行防范。例如,printf() 函数在接到一个无效地址时通常是引起运行错而不是返回错误代码。强迫 C 库函数检查所有可能的错误情况将有可能引起这种检查被重复进行-先是在用户代
35、码中进行,然后又在库函数中再次进行。程序员不得将非法指针或错误类型的参数传递给 JNI 函数。否则,可能产生意想不到的后果,包括可能使系统状态受损或使虚拟机崩溃。Java 异常 JNI 允许本地方法抛出任何 Java 异常。本地方法也可以处理突出的 Java 异常。未被处理的 Java 异常将被传回虚拟机中。异常和错误代码 一些 JNI 函数使用 Java 异常机制来报告错误情况。大多数情况下,JNI 函数通过返回错误代码并抛出 Java 异常来报告错误情况。错误代码通常是特殊的返回值(如 NULL),这种特殊的返回值在正常返回值范围之外。因此,程序员可以: 快速检查上一个 JNI 调用所返回
36、的值以确定是否出错,并 通过调用函数 ExceptionOccurred() 来获得异常对象,它含有对错误情况的更详细说明。在以下两种情况中,程序员需要先查出异常,然后才能检查错误代码: 调用 Java 方法的 JNI 函数返回该 Java 方法的结果。程序员必须调用 ExceptionOccurred() 以检查在执行 Java 方法期间可能发生的异常。 某些用于访问 JNI 数组的函数并不返回错误代码,但可能会抛出 ArrayIndexOutOfBoundsException 或 ArrayStoreException。在所有其它情况下,返回值如果不是错误代码值就可确保没有抛出异常。异步异
37、常 在多个线程的情况下,当前线程以外的其它线程可能会抛出异步异常。异步异常并不立即影响当前线程中平台相关代码的执行,直到出现下列情况: 该平台相关代码调用某个有可能抛出同步异常的 JNI 函数,或者 该平台相关代码用 ExceptionOccurred() 显式检查同步异常或异步异常。注意,只有那些有可能抛出同步异常的 JNI 函数才检查异步异常。本地方法应在必要的地方(例如,在一个没有其它异常检查的紧密循环中)插入 ExceptionOccurred() 检查以确保当前线程可在适当时间内对异步异常作出响应。异常的处理 可用两种方法来处理平台相关代码中的异常: 本地方法可选择立即返回,使异常在
38、启动该本地方法调用的 Java 代码中抛出。 平台相关代码可通过调用 ExceptionClear() 来清除异常,然后执行自己的异常处理代码。抛出了某个异常之后,平台相关代码必须先清除异常,然后才能进行其它的 JNI 调用。当有待定异常时,只有以下这些 JNI 函数可被安全地调用:ExceptionOccurred()、ExceptionDescribe() 和 ExceptionClear()。ExceptionDescribe() 函数将打印有关待定异常的调试消息。3 - JNI 的类型和数据结构 本章讨论 JNI 如何将 Java 类型映射到本地 C 类型。基本类型 表 3-1 描述
39、Java 基本类型及其与计算机相关的本地等效类型。表 3-1 基本类型和本地等效类型 Java 类型 本地类型 说明 boolean jboolean 无符号,8 位 byte jbyte 无符号,8 位 char jchar 无符号,16 位 short jshort 有符号,16 位 int jint 有符号,32 位 long jlong 有符号,64 位 float jfloat 32 位 double jdouble 64 位 void void N/A 为了使用方便,特提供以下定义。 #define JNI_FALSE 0 #define JNI_TRUE 1jsize 整数类型用
40、于描述主要指数和大小: typedef jint jsize;引用类型 JNI 包含了很多对应于不同 Java 对象的引用类型。JNI 引用类型的组织层次如图 3-1 所示。 图 3-1 引用类型层次 在 C 中,所有其它 JNI 引用类型都被定义为与 jobject 一样。例如: typedef jobject jclass;在 C+ 中,JNI 引入了虚构类以加强子类关系。例如: class _jobject ; class _jclass : public _jobject ; . typedef _jobject *jobject; typedef _jclass *jclass;域
41、ID 和方法 ID 方法 ID 和域 ID 是常规的 C 指针类型: struct _jfieldID; /*不透明结构 */ typedef struct _jfieldID *jfieldID; /* 域 ID */ struct _jmethodID; /* 不透明结构 */ typedef struct _jmethodID *jmethodID; /* 方法 ID */值类型 jvalue 联合类型在参数数组中用作单元类型。其声明方式如下: typedef union jvalue jboolean z; jbyte b; jchar c; jshort s; jint i; jlo
42、ng j; jfloat f; jdouble d; jobject l; jvalue;类型签名 JNI 使用 Java 虚拟机的类型签名表述。表 3-2 列出了这些类型签名。表 3-2 Java 虚拟机类型签名 类型签名 Java 类型 Z boolean B byte C char S short I int J long F float D double L fully-qualified-class ; 全限定的类 type type ( arg-types ) ret-type 方法类型 例如,Java 方法: long f (int n, String s, int arr);具
43、有以下类型签名: (ILjava/lang/String;I)JUTF-8 字符串 JNI 用 UTF-8 字符串来表示各种字符串类型。UTF-8 字符串和 Java 虚拟机所使用的一样。UTF-8 字符串的编码方式使得仅包含非空 ASCII 字符的字符序列能够按每字符一个字节表示,但是最多只能表示 16 位的字符。所有在 u0001 到 u007F 范围内的字符都用单字节表示,如下所示: 字节中的七位数据确定了所表示字符的值。空字符 (u000) 和 u0080 到 u07FF 范围内的字符用一对字节表示, 即 x 和 y,如下所示: 值为 (x&0x1f)6)+(y&0x3f) 的字符需用
44、两个字节表示。u0800 到 uFFFF 范围内的字符用三个字节表示,即 x,y,和 z: 值为 (x&0xf)12)+(y&0x3f)6)+(z&0x3f) 的字符需用三个字节表示。此格式与“标准” UTF-8 格式之间有两个区别。第一,空字节 (byte)0 使用双字节格式进行编码,而不是单字节格式。这意味着 Java 虚拟机的 UTF-8 字符串不可能有嵌入的空值。第二,只使用单字节、双字节和三字节格式。Java 虚拟机不能识别更长的 UTF-8 格式。4 - JNI 函数 本章为 JNI 函数提供参考信息。其中列出了全部 JNI 函数,同时也给出了 JNI 函数表的准确布局。 注意:“必须”一词用于约束 JNI 编程人员。例如,当说明某个 JNI 函数必须接收非空对象时,就应确保不要向该 JNI 函数传递 NULL。这时,JNI 实现将无需在该 JNI 函数中执行 NULL 指针检查。 本章的部分资料改编自 Netscape 的 JRI 文档。该参考资料按用法对函数进行组织。参考部分按下列函数区域进行组织: 版本信息 类操作 异常 全局及局部引用 对象操作 访问对象的域 调用实例方法 访问静态域 调用静态方法 字符串操作 数组操作 注册本地方法 监视程序操作 Java