柏寒的儿子,光荣帝国,复方金荞麦颗粒
google翻译
本章介绍java native interface(jni)。 jni是本机编程接口。 它允许在java虚拟机(vm)内运行的java代码与用其他编程语言(如c,c ++和汇编语言)编写的应用程序和库进行互操作。
jni最重要的好处是它对底层java vm的实现没有任何限制。 因此,java vm供应商可以添加对jni的支持,而不会影响vm的其他部分。 程序员可以编写一个本机应用程序或库的版本,并期望它可以与支持jni的所有java vm一起使用。
本章包含以下主题:
虽然您可以完全使用java编写应用程序,但有些情况下java本身并不能满足您的应用程序的需求。 当应用程序不能完全用java编写时,程序员使用jni编写java本机方法来处理这些情况。
以下示例说明何时需要使用java本机方法:
通过jni编程,您可以使用本机方法:
您还可以将jni与invocation api一起使用,以使任意本机应用程序能够嵌入java vm。 这使程序员可以轻松地使其现有应用程序支持java,而无需与vm源代码链接。
来自不同供应商的vm提供不同的本机方法接口。 这些不同的接口迫使程序员在给定平台上生成,维护和分发多个版本的本机方法库。
我们简要介绍一些本机方法接口,例如:
jdk 1.0附带了本机方法接口。 不幸的是,这个接口不适合其他java vm采用有两个主要原因。
首先,本机代码访问java对象中的字段作为c结构的成员。 但是,java语言规范没有定义如何在内存中布置对象。 如果java vm在内存中以不同方式布局对象,则程序员必须重新编译本机方法库。
其次,jdk 1.0的本机方法接口依赖于保守的垃圾收集器。 例如,不受限制地使用unhand宏使得必须保守地扫描本机堆栈。
netscape提出了java运行时接口(jri),它是java虚拟机中提供的服务的通用接口。 jri的设计考虑了可移植性 - 它对底层java vm中的实现细节做了很少的假设。 jri解决了广泛的问题,包括本机方法,调试,反射,嵌入(调用)等。
microsoft java vm支持两种本机方法接口。 在低级别,它提供了有效的原始本机接口(rni)。 rni提供了与jdk本机方法接口的高度源级向后兼容性,尽管它有一个主要区别。 本机代码必须使用rni函数与垃圾收集器明确交互,而不是依赖于保守的垃圾收集。
在更高级别,microsoft的java / com接口为java vm提供了与语言无关的标准二进制接口。 java代码可以像使用java对象一样使用com对象。 java类也可以作为com类公开给系统的其余部分。
我们相信,统一,经过深思熟虑的标准界面为每个人提供以下好处:
实现标准本机方法接口的最佳方法是让所有参与java vm的人都参与进来。因此,我们在java许可证持有者之间组织了一系列关于统一本机方法接口设计的讨论。从讨论中可以清楚地看出,标准本机方法接口必须满足以下要求:
我们希望采用现有方法之一作为标准接口,因为这会给必须在不同vm中学习多个接口的程序员带来最小的负担。不幸的是,没有现成的解决方案可以完全满足我们的目标。
netscape的jri最接近我们想象的可移植本机方法接口,并被用作我们设计的起点。熟悉jri的读者会注意到api命名约定,方法和字段id的使用,本地和全局引用的使用等方面的相似之处。尽管我们尽最大努力,但jni与jri不是二进制兼容的,尽管vm可以同时支持jri和jni。
微软的rni是对jdk 1.0的改进,因为它解决了使用非保守垃圾收集器的本机方法的问题。但是,rni不适合作为独立于vm的本机方法接口。与jdk一样,rni本机方法将java对象作为c结构访问,导致两个问题:
作为二进制标准,com确保跨不同vm的完全二进制兼容性。调用com方法只需要间接调用,这几乎不会产生任何开销。此外,com对象在解决版本问题方面比动态链接库有了很大的改进。
但是,使用com作为标准java本机方法接口受到以下几个因素的阻碍:
虽然java对象不作为com对象公开给本机代码,但jni接口本身与com二进制兼容。 jni使用与com相同的跳转表结构和调用约定。这意味着,只要跨平台支持com,jni就可以成为java vm的com接口。
jni不被认为是给定java vm支持的唯一本机方法接口。标准接口使程序员受益,他们希望将本机代码库加载到不同的java vm中。在某些情况下,程序员可能必须使用较低级别的vm特定接口来实现最高效率。在其他情况下,程序员可能使用更高级别的界面来构建软件组件。实际上,随着java环境和组件软件技术的日趋成熟,本机方法将逐渐失去意义。
本机方法程序员应该编程到jni。 对jni进行编程可以使您免于未知,例如最终用户可能正在运行的供应商的vm。 通过遵循jni标准,您将为本机库提供在给定java vm中运行的最佳机会。
如果要实现java vm,则应实现jni。 jni已经过时间测试,并确保不对您的vm实施施加任何开销或限制,包括对象表示,垃圾收集方案等。 如果您遇到我们可能忽略的任何问题,请将您的反馈发送给我们。
本章重点介绍jni中的主要设计问题。 本节中的大多数设计问题都与本机方法有关。 第5章:invocation api中介绍了invocation api的设计。
本章包含以下主题:
本机代码通过调用jni函数来访问java vm功能。 jni函数可通过接口指针获得。 接口指针是指向指针的指针。 该指针指向一个指针数组,每个指针指向一个接口函数。 每个接口函数都在数组内的预定义偏移处。 下图是接口指针,说明了接口指针的组织。
jni接口的组织方式类似于c ++虚函数表或com接口。使用接口表而不是硬连接函数条目的优点是jni名称空间与本机代码分离。 vm可以轻松提供多个版本的jni功能表。例如,vm可能支持两个jni功能表:
jni接口指针仅在当前线程中有效。因此,本机方法不能将接口指针从一个线程传递到另一个线程。实现jni的vm可以在jni接口指针指向的区域中分配和存储线程本地数据。
本机方法接收jni接口指针作为参数。当vm从同一java线程多次调用本机方法时,保证将vm传递给本机方法。但是,可以从不同的java线程调用本机方法,因此可以接收不同的jni接口指针。
由于java vm是多线程的,因此本机库也应该与多线程感知的本机编译器一起编译和链接。 例如,-mt标志应该用于使用sun studio编译器编译的c++代码。 对于符合gnu gcc编译器的代码,应使用标志-d_reentrant或-d_posix_c_source。 有关更多信息,请参阅本机编译器文档。
本机方法使用system.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_cls.dll。
程序员可以使用单个库来存储任意数量的类所需的所有本机方法,只要这些类要使用相同的类加载器加载即可。 vm在内部维护每个类加载器的加载本机库列表。 供应商应选择本地库名称,以尽量减少名称冲突的可能性。
库和版本管理的调用api部分详细介绍了对动态和静态链接库及其各自生命周期管理“加载”和“卸载”函数挂钩的支持。
动态链接器根据其名称解析条目。 本机方法名称由以下组件连接在一起:
vm检查驻留在本机库中的方法的方法名称匹配。 vm首先查找短名称; 也就是说,没有参数签名的名称。 然后它查找长名称,这是带有参数签名的名称。 只有当本机方法使用另一个本机方法重载时,程序员才需要使用长名称。 但是,如果本机方法与非本地方法具有相同的名称,则这不是问题。 非本地方法(java方法)不驻留在本机库中。
在以下示例中,不必使用长名称链接本机方法g,因为其他方法g不是本机方法,因此不在本机库中。
class cls1 { int g(int i); native int g(double d); }
我们采用了一种简单的别名方案,以确保所有unicode字符都转换为有效的c函数名称。 我们使用下划线(“_”)字符代替完全限定类名中的斜杠(“/”)。 由于名称或类型描述符从不以数字开头,因此我们可以使用_0,...,_9作为转义序列,如下表所示:
转义序列 | 表示 |
_0xxxx | unicode字符xxxx,表示ascii字母数字以外的字符([a-za-z0-9])。 请注意,使用小写,例如_0abcd而不是_0abcd。 |
_1 | 字符"_" |
_2 | 签名中的字符";" |
_3 | 签名中的字符"[" |
本机方法和接口api都遵循给定平台上的标准库调用约定。 例如,unix系统使用c调用约定,而win32系统使用__stdcall。
jni接口指针是本机方法的第一个参数。 jni接口指针的类型为jnienv。 第二个参数根据本机方法是静态方法还是非静态方法而有所不同。 非静态本机方法的第二个参数是对该对象的引用。 静态本机方法的第二个参数是对其java类的引用。
其余参数对应于常规java方法参数。 本机方法调用通过返回值将其结果传递回调用例程。 第3章:jni类型和数据结构描述了java和c类型之间的映射。
以下代码示例说明如何使用c函数实现本机方法f。 本机方法f声明如下:
package pkg; class cls { native double f(int i, string s); // ... }
具有别名java_pkg_cls_f_iljava_lang_string_2的c函数实现本机方法f:
jdouble java_pkg_cls_f__iljava_lang_string_2 ( jnienv *env, /* interface pointer */ jobject obj, /* "this" pointer */ jint i, /* argument #1 */ jstring s) /* argument #2 */ { /* obtain a c-copy of the java string */ const char *str = (*env)->getstringutfchars(env, s, 0); /* process the string */ ... /* now we are done with str */ (*env)->releasestringutfchars(env, s, str); return ... }
请注意,我们总是使用接口指针env操作java对象。 使用c ++,您可以编写稍微更简洁的代码版本,如以下代码示例所示:
extern "c" /* specify the c calling convention */ jdouble java_pkg_cls_f__iljava_lang_string_2 ( jnienv *env, /* interface pointer */ jobject obj, /* "this" pointer */ jint i, /* argument #1 */ jstring s) /* argument #2 */ { const char *str = env->getstringutfchars(s, 0); // ... env->releasestringutfchars(s, str); // return ... }
使用c ++,额外的间接级别和接口指针参数从源代码中消失。 但是,底层机制与c完全相同。在c++中,jni函数被定义为内联成员函数,它们扩展为c对应函数。
原始类型(如整数,字符等)在java和本机代码之间复制。 另一方面,任意java对象都通过引用传递。 vm必须跟踪已传递给本机代码的所有对象,以便垃圾收集器不会释放这些对象。 反过来,本机代码必须有一种方法来通知vm它不再需要这些对象。 此外,垃圾收集器必须能够移动本机代码引用的对象。
jni将本机代码使用的对象引用分为两类:本地引用和全局引用。本地引用在本机方法调用的持续时间内有效,并在本机方法返回后自动释放。全局引用在显式释放之前仍然有效。
对象作为本地引用传递给本机方法。 jni函数返回的所有java对象都是本地引用。 jni允许程序员从本地引用创建全局引用。期望java对象接受全局和本地引用的jni函数。本机方法可以返回对vm的本地或全局引用作为其结果。
在大多数情况下,程序员应该依赖vm在本机方法返回后释放所有本地引用。但是,有时程序员应该明确地释放本地引用。例如,考虑以下情况:
jni允许程序员在本机方法中的任何点手动删除本地引用。为了确保程序员可以手动释放本地引用,不允许jni函数创建额外的本地引用,除了它们作为结果返回的引用。
本地引用仅在创建它们的线程中有效。本机代码不能将本地引用从一个线程传递到另一个线程。
为了实现本地引用,java vm为从java到本机方法的每次控制转换创建了一个注册表。 注册表将不可移动的本地引用映射到java对象,并防止对象被垃圾回收。 传递给本机方法的所有java对象(包括那些作为jni函数调用结果返回的对象)都会自动添加到注册表中。 在本机方法返回后删除注册表,允许其所有条目被垃圾回收。
有不同的方法来实现注册表,例如使用表,链表或哈希表。 虽然引用计数可用于避免注册表中的重复条目,但jni实现没有义务检测和折叠重复条目。
请注意,通过保守扫描本机堆栈,无法忠实地实现本地引用。 本机代码可以将本地引用存储到全局或堆数据结构中。
jni在全局和本地引用上提供了一组丰富的访问器函数。 这意味着无论vm如何在内部表示java对象,相同的本机方法实现都可以工作。 这是jni可以被各种vm实现支持的关键原因。
通过不透明引用使用访问器函数的开销高于直接访问c数据结构的开销。 我们相信,在大多数情况下,java程序员使用本机方法来执行非常重要的任务,这些任务会掩盖此接口的开销。
对于包含许多基本数据类型的大型java对象(例如整数数组和字符串),此开销是不可接受的。 (考虑用于执行向量和矩阵计算的本机方法。)迭代java数组并使用函数调用检索每个元素是非常低效的。
一种解决方案引入了“pinning”的概念,以便本机方法可以要求vm确定数组的内容。然后,本机方法接收指向元素的直接指针。然而,这种方法有两个前提:
我们采取妥协方案,克服上述两个问题。
首先,我们提供了一组函数来复制java数组的一段和本机内存缓冲区之间的原始数组元素。如果本机方法只需要访问大型数组中的少量元素,请使用这些函数。
其次,程序员可以使用另一组函数来检索数组元素的固定版本。请记住,这些功能可能需要java vm执行存储分配和复制。这些函数实际上是否复制数组取决于vm实现,如下所示:
最后,该接口提供了通知vm本机代码不再需要访问数组元素的功能。当您调用这些函数时,系统会取消数组,或者将原始数组与其不可移动的副本进行协调并释放副本。
我们的方法提供灵活性垃圾收集器算法可以针对每个给定阵列单独决定复制或固定。例如,垃圾收集器可以复制小对象,但可以固定较大的对象。
jni实现必须确保在多个线程中运行的本机方法可以同时访问同一个数组。例如,jni可以为每个固定数组保留一个内部计数器,这样一个线程就不会取消固定另一个线程固定的数组。请注意,jni不需要锁定原始数组以供本机方法独占访问。同时从不同的线程更新java数组会导致不确定的结果。
jni允许本机代码访问字段并调用java对象的方法。 jni通过其符号名称和类型签名来标识方法和字段。 两步过程会从字段名称和签名中分析出定位字段或方法的成本。 例如,要在类cls中调用方法f,本机代码首先获取方法id,如下所示:
jmethodid mid = env->getmethodid(cls, "f", "(iljava/lang/string;)d");
然后,本机代码可以重复使用方法id,而无需查找方法,如下所示:
jdouble result = env->calldoublemethod(obj, mid, 10, str);
字段或方法id不会阻止vm卸载已从中派生id的类。 卸载类后,方法或字段id变为无效。 因此,如果它打算长时间使用方法或字段id,本机代码必须确保:
jni不对内部如何实现字段和方法id施加任何限制。
jni不检查编程错误,例如传入null指针或非法参数类型。非法参数类型包括使用普通java对象而不是java类对象。由于以下原因,jni不检查这些编程错误:
大多数c库函数不能防止编程错误。例如,printf()函数在收到无效地址时通常会导致运行时错误,而不是返回错误代码。强制c库函数检查所有可能的错误条件可能会导致重复此类检查 - 一次在用户代码中,然后再次在库中。
程序员不得将非法指针或错误类型的参数传递给jni函数。这样做可能会导致任意后果,包括系统状态损坏或vm崩溃。
jni允许本机方法引发任意java异常。 本机代码也可以处理未完成的java异常。 未处理的java异常会传播回vm。
某些jni函数使用java异常机制来报告错误情况。在大多数情况下,jni函数通过返回错误代码并抛出java异常来报告错误情况。错误代码通常是一个特殊的返回值(例如null),它超出了正常返回值的范围。因此,程序员可以:
有两种情况,程序员需要检查异常,而无法首先检查错误代码:
在所有其他情况下,非错误返回值可确保不会抛出任何异常。
一个线程可能通过调用thread.stop()方法在另一个线程中引发异步异常,该方法自java 2 sdk 1.2版以来已被弃用。强烈建议程序员不要使用thread.stop(),因为它通常会导致不确定的应用程序状态。
此外,jvm可能在当前线程中产生异常而不是jni api调用的直接结果,但是由于各种jvm内部错误,例如:virtualmachineerror,如stackoverflowerror或outofmemoryerror。这些也称为异步异常。
异步异常不会立即影响当前线程中本机代码的执行,直到:
请注意,只有那些可能引发同步异常的jni函数才会检查异步异常。
本机方法应在必要的位置插入exceptionoccurred()检查,例如在任何长时间运行的代码中,而不进行其他异常检查(这可能包括紧密循环)。这可确保当前线程在合理的时间内响应异步异常。但是,由于它们的异步性质,在调用之前进行异常检查并不能保证在检查和调用之间不会引发异步异常。
有两种方法可以处理本机代码中的异常:
本机方法可以选择立即返回,从而导致在启动本机方法调用的java代码中抛出异常。
本机代码可以通过调用exceptionclear()来清除异常,然后执行自己的异常处理代码。
引发异常后,本机代码必须首先清除异常,然后再进行其他jni调用。 当存在挂起的异常时,可以安全调用的jni函数是:
exceptionoccurred()
exceptiondescribe()
exceptionclear()
exceptioncheck()
releasestringchars()
releasestringutfchars()
releasestringcritical()
release<type>arrayelements()
releaseprimitivearraycritical()
deletelocalref()
deleteglobalref()
deleteweakglobalref()
monitorexit()
pushlocalframe()
poplocalframe()
detachcurrentthread()
本章讨论jni如何将java类型映射到本机c类型。
本章包含以下主题:
下表描述java基本类型及其机器相关的本地等价物。
java type | native type | description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | not applicable |
为方便起见,提供以下定义。
#define jni_true 1 #define jni_false 0
jsize整数类型用于描述基数索引和大小:
typedef jint jsize;
jni包含许多与不同类型的java对象相对应的引用类型。 jni引用类型按以下层次结构组织:
在c中,所有其他jni引用类型都定义为与jobject相同。 例如:
typedef jobject jclass;
在c++中,jni引入了一组虚拟类来强制执行子类型关系。 例如:
class _jobject {}; class _jclass : public _jobject {}; // ... typedef _jobject *jobject; typedef _jclass *jclass;
方法和字段id是常规c指针类型:
struct _jfieldid; /* opaque structure */ typedef struct _jfieldid *jfieldid; /* field ids */ struct _jmethodid; /* opaque structure */ typedef struct _jmethodid *jmethodid; /* method ids */
jvalue 联合体用作参数数组中的元素类型。 声明如下:
typedef union jvalue { jboolean z; jbyte b; jchar c; jshort s; jint i; jlong j; jfloat f; jdouble d; jobject l; } jvalue;
jni使用java vm的类型签名表示。 下表显示了这些类型签名。
类型签名 | java 类型 |
---|---|
z |
boolean |
b |
byte |
c |
char |
s |
short |
i |
int |
j |
long |
f |
float |
d |
double |
l fully-qualified-class ; |
fully-qualified-class |
[ type |
type[] |
( arg-types ) ret-type |
method type |
例如,java方法:
long f (int n, string s, int[] arr);
具有以下类型签名:
(iljava/lang/string;[i)j
jni使用修改的utf-8字符串来表示各种字符串类型。修改后的utf-8字符串与java vm使用的字符串相同。对已修改的utf-8字符串进行编码,以便只包含非空ascii字符的字符序列可以使用每个字符仅使用一个字节来表示,但可以表示所有unicode字符。
范围\ u0001到\ u007f中的所有字符都由单个字节表示,如下所示:
字节中的七位数据给出了所表示字符的值。
空字符('\ u0000')和'\ u0080'到'\ u07ff'范围内的字符由一对字节x和y表示:
字节表示具有值((x&0x1f)<< 6)+(y和0x3f)的字符。
'\ u0800'到'\ uffff'范围内的字符由3个字节x,y和z表示:
具有值((x&0xf)<< 12)+((y&0x3f)<< 6)+(z&0x3f)的字符由字节表示。
代码点高于u + ffff的字符(所谓的补充字符)通过分别编码其utf-16表示的两个代理代码单元来表示。每个代理代码单元由三个字节表示。这意味着,补充字符由六个字节u,v,w,x,y和z表示:
值为0x10000 +((v&0x0f)<< 16)+((w&0x3f)<< 10)+(y&0x0f)<< 6)+(z&0x3f)的字符由六个字节表示。
多字节字符的字节以big-endian(高字节优先)顺序存储在类文件中。
此格式与标准utf-8格式有两点不同。首先,使用双字节格式而不是单字节格式对空字符(char)0进行编码。这意味着修改后的utf-8字符串永远不会嵌入空值。其次,仅使用标准utf-8的单字节,双字节和三字节格式。 java vm无法识别标准utf-8的四字节格式;它使用自己的两倍三字节格式。
有关标准utf-8格式的更多信息,请参见unicode encoding forms of the unicode standard, version 4.0的第3.9章。
本章作为jni函数的参考部分。它提供了所有jni功能的完整列表。它还提供了jni函数表的确切布局。
请注意使用术语“必须”来描述对jni程序员的限制。例如,当您看到某个jni函数必须接收非null对象时,您有责任确保不将null传递给该jni函数。因此,jni实现不需要在该jni函数中执行null指针检查。如果不允许显式传递,则传递null可能会导致意外异常或致命崩溃。
定义可能都返回null并在出错时抛出异常的函数可能只选择返回null来指示错误,但不抛出任何异常。例如,jni实现可能会暂时考虑“内存不足”状态,并且可能不希望抛出outofmemoryerror,因为这看起来很致命(jdk api java.lang.error文档:“表示合理的应用程序不应该出现严重问题试着抓住“)。
本章的一部分改编自netscape的jri文档。
参考材料按其用法分组。参考部分由以下功能区域组成:
invocation api允许软件供应商将java vm加载到任意本机应用程序中。 供应商可以提供支持java的应用程序,而无需链接java vm源代码。
如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复
浅析我对 String、StringBuilder、StringBuffer 的理解
使用IDEA搭建SSM框架的详细教程(spring + springMVC +MyBatis)
Springboot整合freemarker 404问题解决方案
引入mybatis-plus报 Invalid bound statement错误问题的解决方法
网友评论