JNI API完全手册.docx
《JNI API完全手册.docx》由会员分享,可在线阅读,更多相关《JNI API完全手册.docx(68页珍藏版)》请在冰豆网上搜索。
![JNI API完全手册.docx](https://file1.bdocx.com/fileroot1/2023-1/22/d90ec6e1-3b25-42f7-bd83-bae3bd4a1743/d90ec6e1-3b25-42f7-bd83-bae3bd4a17431.gif)
JNIAPI完全手册
1-简介
本章介绍Java本地接口(JavaNativeInterface,JNI)。
JNI是本地编程接口。
它使得在Java虚拟机(VM)内部运行的Java代码能够与用其它编程语言(如C、C++和汇编语言)编写的应用程序和库进行互操作。
JNI最重要的好处是它没有对底层Java虚拟机的实现施加任何限制。
因此,Java虚拟机厂商可以在不影响虚拟机其它部分的情况下添加对JNI的支持。
程序员只需编写一种版本的本地应用程序或库,就能够与所有支持JNI的Java虚拟机协同工作。
本章论及以下主题:
∙
∙
∙
∙
∙
∙
Java本地接口概述
尽管可以完全用Java编写应用程序,但是有时单独用Java不能满足应用程序的需要。
程序员使用JNI来编写Java本地方法,可以处理那些不能完全用Java编写应用程序的情况。
以下示例说明了何时需要使用Java本地方法:
∙标准Java类库不支持与平台相关的应用程序所需的功能。
∙已经拥有了一个用另一种语言编写的库,而又希望通过JNI使Java代码能够访问该库。
∙想用低级语言(如汇编语言)实现一小段时限代码。
通过用JNI编程,可以将本地方法用于:
∙创建、检查及更新Java对象(包括数组和字符串)。
∙调用Java方法。
∙捕捉和抛出异常。
∙加载类和获得类信息。
∙执行运行时类型检查。
也可以与调用API一起使用JNI,以允许任意本地应用程序嵌入到Java虚拟机中。
这样使得程序员能够轻易地让已有应用程序支持Java,而不必与虚拟机源代码相链接。
背景
目前,不同厂商的虚拟机提供了不同的本地方法接口。
这些不同的接口使程序员不得不在给定平台上编写、维护和分发多种版本的本地方法库。
下面简要分析一下部分已有本地方法接口,例如:
∙JDK1.0本地方法接口
∙Netscape的Java运行时接口
∙Microsoft的原始本地接口和Java/COM接口
JDK1.0本地方法接口
JDK1.0附带有本地方法接口。
遗憾的是,有两点原因使得该接口不适合于其它Java虚拟机。
第一,平台相关代码将Java对象中的域作为C结构的成员来进行访问。
但是,Java语言规范没有规定在内存中对象是如何布局的。
如果Java虚拟机在内存中布局对象的方式有所不同,程序员就不得不重新编译本地方法库。
第二,JDK1.0的本地方法接口依赖于保守的垃圾收集器。
例如,无限制地使用unhand宏使得有必要以保守方式扫描本地堆栈。
Java运行时接口
Netscape建议使用Java运行时接口(JRI),它是Java虚拟机所提供服务的通用接口。
JRI的设计融入了可移植性---它几乎没有对底层Java虚拟机的实现细节作任何假设。
JRI提出了各种各样的问题,包括本地方法、调试、反射、嵌入(调用)等等。
原始本地接口和Java/COM接口
MicrosoftJava虚拟机支持两种本地方法接口。
在低一级,它提供了高效的原始本地接口(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是对JDK1.0的改进,因为它可以解决使用非保守的垃圾收集器的本地方法的问题。
然而,RNI不适合用作与虚拟机无关的本地方法接口。
与JDK类似,RNI本地方法将Java对象作为C结构来访问。
这将导致两个问题:
∙RNI将内部Java对象的布局暴露给了平台相关代码。
∙将Java对象作为C结构直接进行访问使得不可能有效地加入“写屏障”,写屏障是高级的垃圾收集算法所必需的。
作为二进制标准,COM确保了不同虚拟机之间的完全二进制兼容性。
调用COM方法只要求间接调用,而这几乎不会占用系统开销。
另外,COM对象对动态链接库解决版本问题的方式也有很大的改进。
然而,有几个因素阻碍了将COM用作标准Java本地方法接口:
∙第一,Java/COM接口缺少某些必需功能,例如访问私有域和抛出普通异常。
∙第二,Java/COM接口自动为Java对象提供标准的IUnknown和IDispatchCOM接口,因而平台相关代码能够访问公有方法和域。
遗憾的是,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虚拟机上运行的最好保证。
例如,虽然JDK1.1将继续支持JDK1.0中所实现的旧式的本地方法接口,但是可以肯定的是JDK的未来版本将停止支持旧式的本地方法接口。
依赖于旧式接口的本地方法将不得不重新编写。
如果您正在实现Java虚拟机,则应该实现JNI。
我们(Javasoft和获得许可方)尽力确保JNI不会占用虚拟机实现的系统开销或施加任何限制,包括对象表示,垃圾收集机制等。
如果您遇到了我们可能忽视了的问题,请告知我们。
JDK1.1.2中的变化
为了更好地支持Java运行时环境(JRE),在JDK1.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选项)。
∙在JDK1.1.1中,调用DestroyJavaVM的线程必须是虚拟机中的唯一用户线程。
JDK1.1.2放松了这一限制。
如果调用DestroyJavaVM时有多个用户线程,则虚拟机将等待直到当前线程成为唯一的用户线程,然后销毁自己。
2-设计概述
本章着重讨论JNI中的主要设计问题,其中的大部分问题都与本地方法有关。
调用API的设计将在中讨论。
JNI接口函数和指针
平台相关代码是通过调用JNI函数来访问Java虚拟机功能的。
JNI函数可通过接口指针来获得。
接口指针是指针的指针,它指向一个指针数组,而指针数组中的每个元素又指向一个接口函数。
每个接口函数都处在数组的某个预定偏移量中。
说明了接口指针的组织结构。
图2-1接口指针
JNI接口的组织类似于C++虚拟函数表或COM接口。
使用接口表而不使用硬性编入的函数表的好处是使JNI名字空间与平台相关代码分开。
虚拟机可以很容易地提供多个版本的JNI函数表。
例如,虚拟机可支持以下两个JNI函数表:
∙一个表对非法参数进行全面检查,适用于调试程序;
∙另一个表只进行JNI规范所要求的最小程度的检查,因此效率较高。
JNI接口指针只在当前线程中有效。
因此,本地方法不能将接口指针从一个线程传递到另一个线程中。
实现JNI的虚拟机可将本地线程的数据分配和储存在JNI接口指针所指向的区域中。
本地方法将JNI接口指针当作参数来接受。
虚拟机在从相同的Java线程中对本地方法进行多次调用时,保证传递给该本地方法的接口指针是相同的。
但是,一个本地方法可被不同的Java线程所调用,因此可以接受不同的JNI接口指针。
加载和链接本地方法
对本地方法的加载通过System.loadLibrary方法实现。
下例中,类初始化方法加载了一个与平台有关的本地库,在该本地库中给出了本地方法f的定义:
packagepkg;
classCls{
nativedoublef(inti,Strings);
static{
System.loadLibrary("pkg_Cls");
}
}
System.loadLibrary的参数是程序员任意选取的库名。
系统按照标准的但与平台有关的处理方法将该库名转换为本地库名。
例如,Solaris系统将名称pkg_Cls转换为libpkg_Cls.so,而Win32系统将相同的名称pkg_Cls转换为pkg_Cls.dll。
程序员可用单个库来存放任意数量的类所需的所有本地方法,只要这些类将被相同的类加载器所加载。
虚拟机在其内部为每个类加载器保护其所加载的本地库清单。
提供者应该尽量选择能够避免名称冲突的本地库名。
如果底层操作系统不支持动态链接,则必须事先将所有的本地方法链接到虚拟机上。
这种情况下,虚拟机实际上不需要加载库即可完成System.loadLibrary调用。
程序员还可调用JNI函数RegisterNatives()来注册与类关联的本地方法。
在与静态链接的函数一起使用时,RegisterNatives()函数将特别有用。
解析本地方法名
动态链接程序是根据项的名称来解析各项的。
本地方法名由以下几部分串接而成:
∙前缀Java_
∙mangled全限定的类名
∙下划线(“_”)分隔符
∙mangled方法名
∙对于重载的本地方法,加上两个下划线(“__”),后跟mangled参数签名
虚拟机将为本地库中的方法查找匹配的方法名。
它首先查找短名(没有参数签名的名称),然后再查找带参数签名的长名称。
只有当某个本地方法被另一个本地方法重载时程序员才有必要使用长名。
但如果本地方法的名称与非本地方法的名称相同,则不会有问题。
因为非本地方法(Java方法)并不放在本地库中。
下例中,不必用长名来链接本地方法g,因为另一个方法g不是本地方法,因而它并不在本地库中。
classCls1{
intg(inti);
nativeintg(doubled);
}
我们采取简单的名字搅乱方案,以保证所有的Unicode字符都能被转换为有效的C函数名。
我们用下划线(“_”)字符来代替全限定的类名中的斜杠(“/”)。
由于名称或类型描述符从来不会以数字打头,我们用_0、...、_9来代替转义字符序列,如所示:
表2-1Unicode字符转换
转义字符序列
表示
_0XXXX
Unicode字符XXXX。
_1
字符“_”
_2
签名中的字符“;”
_3
签名中的字符“[”
本地方法和接口API都要遵守给定平台上的库调用标准约定。
例如,UNIX系统使用C调用约定,而Win32系统使用__stdcall。
本地方法的参数
JNI接口指针是本地方法的第一个参数。
其类型是JNIEnv。
第二个参数随本地方法是静态还是非静态而有所不同。
非静态本地方法的第二个参数是对对象的引用,而静态本地方法的第二个参数是对其Java类的引用。
其余的参数对应于通常Java方法的参数。
本地方法调用利用返回值将结果传回调用程序中。
将描述Java类型和C类型之间的映射。
说明了如何用C函数来实现本地方法f。
对本地方法f的声明如下:
packagepkg;
classCls{
nativedoublef(inti,Strings);
...
}
具有长mangled名称Java_pkg_Cls_f_ILjava_lang_String_2的C函数实现本地方法f:
代码示例2-1:
用C实现本地方法
jdoubleJava_pkg_Cls_f__ILjava_lang_String_2(
JNIEnv*env,/*接口指针*/
jobjectobj,/*“this”指针*/
jinti,/*第一个参数*/
jstrings)/*第二个参数*/
{
/*取得Java字符串的C版本*/
constchar*str=(*env)->GetStringUTFChars(env,s,0);
/*处理该字符串*/
...
/*至此完成对str的处理*/
(*env)->ReleaseStringUTFChars(env,s,str);
return...
}
注意,我们总是用接口指针env来操作Java对象。
可用C++将此代码写得稍微简洁一些,如所示:
代码示例2-2:
用C++实现本地方法
extern"C"/*指定C调用约定*/
jdoubleJava_pkg_Cls_f__ILjava_lang_String_2(
JNIEnv*env,/*接口指针*/
jobjectobj,/*“this”指针*/
jinti,/*第一个参数*/
jstrings)/*第二个参数*/
{
constchar*str=env->GetStringUTFChars(s,0);
...
env->ReleaseStringUTFChars(s,str);
return...
}
使用C++后,源代码变得更为直接,且接口指针参数消失。
但是,C++的内在机制与C的完全一样。
在C++中,JNI函数被定义为内联成员函数,它们将扩展为相应的C对应函数。
引用Java对象
基本类型(如整型、字符型等)在Java和平台相关代码之间直接进行复制。
而Java对象由引用来传递。
虚拟机必须跟踪传到平台相关代码中的对象,以使这些对象不会被垃圾收集器释放。
反之,平台相关代码必须能用某种方式通知虚拟机它不再需要那些对象,同时,垃圾收集器必须能够移走被平台相关代码引用过的对象。
全局和局部引用
JNI将平台相关代码使用的对象引用分成两类:
局部引用和全局引用。
局部引用在本地方法调用期间有效,并在本地方法返回后被自动释放掉。
全局引用将一直有效,直到被显式释放。
对象是被作为局部引用传递给本地方法的,由JNI函数返回的所有Java对象也都是局部引用。
JNI允许程序员从局部引用创建全局引用。
要求Java对象的JNI函数既可接受全局引用也可接受局部引用。
本地方法将局部引用或全局引用作为结果返回。
大多数情况下,程序员应该依靠虚拟机在本地方法返回后释放所有局部引用。
但是,有时程序员必须显式释放某个局部引用。
例如,考虑以下的情形:
∙本地方法要访问一个大型Java对象,于是创建了对该Java对象的局部引用。
然后,本地方法要在返回调用程序之前执行其它计算。
对这个大型Java对象的局部引用将防止该对象被当作垃圾收集,即使在剩余的运算中并不再需要该对象。
∙本地方法创建了大量的局部引用,但这些局部引用并不是要同时使用。
由于虚拟机需要一定的空间来跟踪每个局部引用,创建太多的局部引用将可能使系统耗尽内存。
例如,本地方法要在一个大型对象数组中循环,把取回的元素作为局部引用,并在每次迭代时对一个元素进行操作。
每次迭代后,程序员不再需要对该数组元素的局部引用。
JNI允许程序员在本地方法内的任何地方对局部引用进行手工删除。
为确保程序员可以手工释放局部引用,JNI函数将不能创建额外的局部引用,除非是这些JNI函数要作为结果返回的引用。
局部引用仅在创建它们的线程中有效。
本地方法不能将局部引用从一个线程传递到另一个线程中。
实现局部引用
为了实现局部引用,Java虚拟机为每个从Java到本地方法的控制转换都创建了注册服务程序。
注册服务程序将不可移动的局部引用映射为Java对象,并防止这些对象被当作垃圾收集。
所有传给本地方法的Java对象(包括那些作为JNI函数调用结果返回的对象)将被自动添加到注册服务程序中。
本地方法返回后,注册服务程序将被删除,其中的所有项都可以被当作垃圾来收集。
可用各种不同的方法来实现注册服务程序,例如,使用表、链接列表或hash表来实现。
虽然引用计数可用来避免注册服务程序中有重复的项,但JNI实现不是必须检测和消除重复的项。
注意,以保守方式扫描本地堆栈并不能如实地实现局部引用。
平台相关代码可将局部引用储存在全局或堆数据结构中。
访问Java对象
JNI提供了一大批用来访问全局引用和局部引用的函数。
这意味着无论虚拟机在内部如何表示Java对象,相同的本地方法实现都能工作。
这就是为什么JNI可被各种各样的虚拟机实现所支持的关键原因。
通过不透明的引用来使用访问函数的开销比直接访问C数据结构的开销来得高。
我们相信,大多数情况下,Java程序员使用本地方法是为了完成一些重要任务,此时这种接口的开销不是首要问题。
访问基本类型数组
对于含有大量基本数据类型(如整数数组和字符串)的Java对象来说,这种开销将高得不可接受(考虑一下用于执行矢量和矩阵运算的本地方法的情形便知)。
对Java数组进行迭代并且要通过函数调用取回数组的每个元素,其效率是非常低的。
一个解决办法是引入“钉住”概念,以使本地方法能够要求虚拟机钉住数组内容。
而后,该本地方法将接受指向数值元素的直接指针。
但是,这种方法包含以下两个前提:
∙垃圾收集器必须支持钉住。
∙虚拟机必须在内存中连续存放基本类型数组。
虽然大多数基本类型数组都是连续存放的,但布尔数组可以压缩或不压缩存储。
因此,依赖于布尔数组确切存储方式的本地方法将是不可移植的。
我们将采取折衷方法来克服上述两个问题。
首先,我们提供了一套函数,用于在Java数组的一部分和本地内存缓冲之间复制基本类型数组元素。
这些函数只有在本地方法只需访问大型数组中的一小部分元素时才使用。
其次,程序员可用另一套函数来取回数组元素的受约束版本。
记住,这些函数可能要求Java虚拟机分配存储空间和进行复制。
虚拟机实现将决定这些函数是否真正复制该数组,如下所示:
∙如果垃圾收集器支持钉住,且数组的布局符合本地方法的要求,则不需要进行复制。
∙否则,该数组将被复制到不可移动的内存块中(例如,复制到C堆中),并进行必要的格式转换,然后返回指向该副本的指针。
最后,接口提供了一些函数,用以通知虚拟机本地方法已不再需要访问这些数组元素。
当调用这些函数时,系统或者释放数组,或者在原始数组与其不可移动副本之间进行协调并将副本释放。
这种处理方法具有灵活性。
垃圾收集器的算法可对每个给定的数组分别作出复制或钉住的决定。
例如,垃圾收集器可能复制小型对象而钉住大型对象。
JNI实现必须确保多个线程中运行的本地方法可同时访问同一数组。
例如,JNI可以为每个被钉住的数组保留一个内部计数器,以便某个线程不会解开同时被另一个线程钉住的数组。
注意,JNI不必将基本类型数组锁住以专供某个本地方法访问。
同时从不同的线程对Java数组进行更新将导致不确定的结果。
访问域和方法
JNI允许本地方法访问Java对象的域或调用其方法。
JNI用符号名称和类型签名来识别