|
第十三章 native方法
这一章讨论Java与C的接口问题。目前在Java中使用C编写的函数有两种方法,本章将分别予以介绍。
13.1 引入native方法的原因
Java作为一种新兴的语言,在短时间内迅速崛起,给人们留下了深刻的印象。然而,它的功能毕竟还很不完善。而C语言不但拥有丰富的函数、众多的熟练程序员,而且有多年应用积累下来的宝贵经验和资源。另外某些工作无论从功能还是效率来考虑用C来做都更合适些。这就带来了Java与C的接口问题。
诚然,C的引入对Java的多平台适应性是有影响的。使用了native方法的Java程序只能在编译它的平台上运行——这或许就是“native方法”这个名字的由来。“native”的原义是“本地的”,而对native方法,我们可以称为“原生方法”,也即指用C实现的方法。
在Java这样一种纯面向对象的语言中引用C程序不是件简单的事情。由于C语言的灵活性,Java的设计者们不得不采用一些限制手段来防止产生安全问题。因此,使用native方法的过程可能比你设想的要复杂的多。但是,只要你按我们介绍,按部就班地去做,一样可以轻松地达到目的。
原生方法技术也在不断地发展。在1.0.2版本中使用的原生方法接口在性能上有所欠缺,不利于垃圾收集的进行。1.1.1版本已经引入了一种新的原生方法接口,叫做Java
Native Interface,简称JNI。但旧的接口仍被保留。二者的关系可以归纳为三点:
(1)可以用新版的JDK按旧的接口办法使用原生方法。
(2)用旧的JDK生成的程序和动态链接库可以在新版的虚拟机上运行(即可以用新版的解释器来解释执行)。
(3)新旧两种接口办法在编程细节和动态链接库生成步骤上略有差别。
本章将对新旧两种办法给予介绍。
最后需要附带提一句,由微软公司开发的Java软件开发包SDK在native方法的实现上有所改动,与SUN公司的JDK提供的实现有一些差异。本文将以SUN公司的JDK为准。如果读者使用的是微软公司的开发环境,请参阅有关文档。
13.2 使用native方法的步骤
这一节介绍旧的原生方法接口办法。在1.0x版本和1.1版本中此办法均适用。
在程序中使用native方法,大致需要5个步骤。为了说明问题,我们举一个很简单的例子。这个例子的功能是用C语言的printf()函数打印一个字符串常量。下面我们依次序进行。
13.2.1 创建Java源文件和.class文件
这一步与我们做普通Java程序的工作相似。native方法不是直接嵌入在Java代码中的,在Java源码中只列出原型。具体的实现(C文件)将被编译成动态链接库的一部分,由Java解释器装入并调用其中的native方法。因此,在程序中要做两件事:一是声明native方法,二是装入动态链接库(在Unix系统下应叫共享库)。下面我们来看看具体就怎样做(例13.1)。
例13.1 DispDemo.java。
1:class DispDemo{ 2: static{ 3:
Runtime.getRuntime().loadLibrary("dispdll"); 4: } 5:
public native void display(); 6: }
DispDemo是我们定义的一个类。在这个类中,有一个方法display()是用C语言实现的,也就是native方法。我们在第5行对它进行了声明。
第2至4行是一个语句,它的功能是装入动态链接库。它被static修饰符所修饰,因而它只能在DispDemo类被装入执行时执行一次。即使我们需要创建多个DispDemo类的对象,也只需装入动态链接库一次。装入动态链接库的工作由方法loadLibrary()完成。在语言类库java.lang中,System类和Runtime类中都定义了这个方法。这里用Runtime类的对象来调用它。我们也可以把第三行的语句改为
System.loadLibrary("dispdll");
一样可以达到目的。
为了调用native方法,我们又创建了一个类DispDemoMain(例13.2)。它包含Java解释器入口——main方法。它的功能是创建一个DispDemo对象并调用后者的native方法。
例 13.2 DispDemoMain.java。
1:class DispDemoMain{ 2: public static void main(String
args[]){ 3: ispDemo().display(); 4: } 5:}
然后我们把两个.java文件编译成.class文件。由于DispDemoMain中使用了DispDemo类,只需作:
javac DispDemoMain.java
就可以翻译好前述两个文件。编译器会自动寻找DispDemo.java并编译之。当然,手工一个个编译也可以。
13.2.2 生成头文件和存根文件
在第三章中,我们简单介绍了JDK中包含工具。其中之一是C头文件生成器javah(可参看节3.7)。它的主要功能是生成头文件和存根文件。头文件就是C程序员熟悉的.h文件,存根文件指由相应.class文件自动生成的.c文件。
在第一步得到了两个.class文件:DispDemoMain.class和DispDemo.class文件。前者与native方法实现没什么关联,这一步暂时不去管它。我们来为DispDemo.class生成头文件和存根文件(以Win95/NT平台为例,Uinx类似)。
用命令(“目录”指.class文件所在目录) C:\目录>javah
DispDemo 可以得到DispDemo.h文件,再用命令 C:\目录>javah -stubs
DispDemo 可以得到DispDemo.c文件。 如果打了上述命令后出现DispDemo:no such
class的提示信息,说明类路径设置不对。类路径必须包括.class文件所在目录。比如,当我们这个例子的文件都在C:\bookDemo\Ch13之下时,可用 C:\bookDemo\Ch13>set
classpath=\java\lib\classes.zip;.; (其中\java是JDK的主目录)设置类路径,再运用上述两个命令,即得到头文件和存根文件。
我们这个例子生成的头文件见例13.3。
例13.3 DispDemo.h文件。
1:/* DO NOT EDIT THIS FILE - it is machine generated
*/ 2:#include <native.h> 3:/*Header for class
DispDemo */ 4: 5:#ifndef _Included
_DispDemo 6:#define _Included _DispDemo 7: 8:#pragma
pach(4) 9: 10:typedef struct ClassDispDemo{ 11: char
PAD;/* ANSI C requires structures to have a least one member
*/ 12:}ClassDispDemo; 13:HandleTo(DispDemo); 14: 15:pragma
pack() 16: 17:#ifdef _cplusplus 18:extern
"C"{ 19:#endif 20:extern void DispDemo_display(struct
HDispDemo *); 21:#ifdef
_cplusplus 22:} 23:#endif 24:#endif
存根文件内容为(见例13.4):
例13.4 DispDemo.c文件。
1:/*DO NOT EDIT THIS FILE - it is machine generated
*/ 2:#include <StubPreamble.h> 3: 4:/*Stubs for
class DispDemo*/ 5:/* SYMBOL:
"DispDemo/display()V",Java_DispDemo_display_stub
*/ 6:__declspec(dllexport) stack_item
*Java_DispDemo_display_stub(stack_item * _P_,struct execenv
*_EE_){ 7: extern void DispDemo_display(void *); 8: (void)
DispDemo_display(_P_[0].p); 9: return _P_; 10:}
这两个文件是由机器生成的,不要修改它们。将这两个文件与DispDemo.java相对比,可以发现一些联系。在头文件DispDemo.h中,第20行是: 20:extern
void DispDemo_display(struct HDispDemo
*); 这显然是与我们在DispDemo.java中声明的native方法有关的。原文是: public native
void
display(); 两相对比可以发现,display()这一方法的名字在头文件中变成了“DispDemo_display”。由此可以得到一个结论:在实现相应方法时,应该用的名字是<类名>_<方法名>。另外,原本无参的方法多了一个参数struct
HDispDemo
*,这是一个指向该方法所在类的指针(这个指针的用法有些待殊,我们以后还将另外说明)。因此,实现该方法时,这个参数也要添上。
对这两个机器生成的文件,我们只需关心其中与实现native方法有关的细节。对我们这个例子,记住上一段所说的两点就足够了(名字和参数)。
13.2.3 创建C源文件
在上一节的基础上,我们来处理native方法的具体实现。
创建实现文件的第一个要点是包括应具备的文件。除了完成功能必需的C语言头文件之外,不要包括“StubPreamble.h”和刚才机器生成的类的头文件。在我们这个例子中,需要包括三个头文件,如例13.5所示。
例 13.5 DispImp.c
1:#include <stdio.h> 2:#include
<StubPreamble.h> 3:#include "DispDemo.h" 4:void
DispDemo_display(struct HDispDemo *this){ 5: printf("Native
method executing...\n"); 6:}
在例13.5的程序中,我们实现了native方法display(),或者更确切地说是:
void DispDemo_display(struct HDispDemo *this)
只要记住节13.2.2中所提到的两点——名字和增加的一个参数,不难写出这样一段程序。
13.2.4 生成动态链接库(共享库)
这是一个机械的过程,只需要用适当的工具去编译、链接即可。
在Unix下,用cc来编译:
%cc -G DispDemo.c DispImp.c -o libdispdll.so
假如这样不能产生预期结果,一般的原因是头文件的路径找不到。这样的情况可以加-I参数指明路径:
%cc -I<Java主目录>/include -I<Java主目录>/include/solaris
DispDemo.c DispImp.c -o libdispdll.so
其中,<Java主目录>指安装Java JDK的目录,dispdll.so是要生成的共享库的名称。
在Win95/NT平台下,需使用VC++2.0或更晚的版本来生成动态链接库。下面给出了VC++4.2版本生成动态链接库dispdll.dll的过程。
C:\bookDemo\ch13>cl DispDemo.c DispImp.c -Fedispdll.dll -MD
-LD javai.lib Microsoft (R) 32-bit C/C++ Optimizing Compiler
Version 10.20.6166 for 80x86 Copyright(C) Microsoft Corp
1984-1996. All rights reserved.
DispDemo.c DispImp.c Generating
Code... Microsoft (R) 32-Bit Incremental Linker Version
4.20.6164 Copyright (C) Microsoft Corp 1992-1996. All rights
reserved.
/out:dispdll.dll /dll /implib:dispdll.lib DispDemo.obj DispImp.obj javai.lib Creating
library dispdll.lib and object dispdll.exp
C:\bookDemo\ch13>java DispDemoMain Native method
executing...
C:\bookDemo\ch13>
命令中各参数含义可参看相应命令的帮助信息。如果出现问题,一般也是路径没设置好,需用以下命令设置:
set
INCLUDE=<VC原先的INCLUDE路径>;<Java主目录>\include;<Java主目录>\include\win32;
set LIB=<VC原先的LIB路径>;<Java主目录>\lib;
如:
set
INCLUDE=\msdev\include;\msdev\mfc\include;\jdk1.0.2\java\include;\jdk1.0.2\java\include\win32; set
LIB=\msdev\lib;\msdev\mfc\lib;\jdk\1.0.2\java\lib
使用它(节13.2.5中介绍)时,不要忘记将生成的.dll文件拷贝到.class文件所在目录。
13.2.5 装入动态链接库,调用native方法
这一节只要像往常一样运行Java程序即可。运行结果如下:
C:\bookDemo\ch13>java DispDemoMain Native method
executing...
C:\bookDemo\ch13> 如果出现 java.lang.NullPointerExeption或Java.lang.UnsatisfiedLinkError,可能是动态链接库路径有问题。最简单的办法是拷贝.dll(或.so)文件至.class文件所在目录。在Unix中,可以用
%setenv LD_LIBRARY_PATH<路径>
来指明.so文件的路径。
以上我们结合例子讲解了native方法的使用过程。读者不妨按这一节所述的步骤做一遍,这样可以尽快地掌握使用native方法的一般过程。
在下一节中,我们还将举两个例子,说明使用native方法的一些细节。本节中的例子不能涵盖所有要注意的问题。但读者可以学到遇到问题的基本对策。
13.3 要注意的问题
13.3.1 类型转换
在第二章中,我们讨论了Java的类型。可见它与C语言的类型是有差异的。这一差异体现在native方法的使用中,其现象就是类型发生了转换。下面我们来看一个例子。
这个例子的功能是这样的:从键盘读入5个整数,把它们存入一个链表,再遍历此链表,打印存储其中的值。指针是C的一大特点。我们这个例子中有意使用链表来演示native方法中指针的使用。
首先来看包含原生方法的Java类ListDemo的源程序(例13.6)。
例13.6 ListDemo.java
1: import java.io.*; 2: 3: public class
ListDemo{ //原生方法原型 4: public native void
createList(int[] ints); 5: public native void
showList(); //装入动态链接库 6: static{ 7:
System.loadLibrary("listdll"); 8: } 9: static int
intArray[]={0,0,0,0,0}; 10: public void run() throws
IOException{ //读五个整数,存入整型数组 11: DataInputStream
dataIn= 12: new
DataInputStream(System.in); 13: System.out.println("Enter 5
integers:"); 14: for(int i=0;i<5;i++) 15:
intArray[i]=Integer.valueOf(data.readLine()).intValue(); //调用原生方法 16: createList(intArray); 17:
showList(); 18: } 19: }
这个程序的第4、第5行声明了两个native方法。createList()用以创建一个链表,而showList()遍历链表并显示其值。run()方法中创建了一个数据输入流,用它读入了5个整数,存入整数数组intArray中(15行)。然后以intArray为参数调用createList(),最后调用showList()显示链表内容。
为了Java解释器能找到运行入口,我们又创建一个ListDemoMain类,源码见例13.7。
例13.7 ListDemoMain.java。
1:import java.io.*; 2: public class ListDemo{ 3:
public static void main(String args[]) throws IOException{ 4:
ListDemo ld = new ListDemo(); 5: ld.run(); 6:
} 7:}
将两个文件编译成.class文件,再用javah生成头文件和存根文件(例13.8、13.9)。
例13.8 ListDemo.h。
1: /*DO NOT EDIT THIS FILE - it is machine generated */ 2:
#include <natiove.h> 3: /* Header for class ListDemo
*/ 4: 5: #ifndef _Included _ListDemo 6: #define
_Included _ListDemo 7: 8: #pragma pack(4) 9: 10:
typdef struct ClassListDemo{ 11:/*Inaccessible
static:intArray*/ 12: char PAD;/* ANSI C requires structures to
have a least one member
*/ 13:}ClassListDemo; 14:HandleTo(ListDemo); 15: 16:#pragma
pack() 17: 18:#ifdef __cplusplus 19:extern
"C"{ 20:#endif 21:extern void ListDemo_createList(struct
HListDemo *,HArrayOfInt *); 22:extern void
ListDemo_showList(struct HListDemo *); 23:#ifdef
__cplusplus 24:} 25:#endif 26:#endif
例13.9 ListDemo.c。
1:/*DO NOT EDIT THIS FILE - it is machine generated
*/ 2:#include <StubPreamble.h> 3: 4:/*Stubs for
class
ListDemo*/ 5:/*SYMBOL:"ListDemo/createList([I)V",Java_ListDemo_createList_stub*/ 6:__declspec(dllexport)stack_item
*Java_ListDemo_createList_stub(stack_item *_P_,struct execenv
*_EE_){ 7: extern void ListDemo_createList(void *,void
*); 8:
(void)ListDemo_createList(_P_[0].p,((_P_[1].p))); 9:return
_P_; 10:} 11:/*SYMBOL:"ListDemo/showList()V",Java_ListDemo_showList_stub*/ 12:__declspec(dllexport)
stack_item *Java_ListDemo_showList_stub(stack_item *_P_,struct
execenv *_EE_){ 13: extern void ListDemo_showList(void
*); 14: (void)ListDemo_showList(_P_[0].p); 15: return
_P_; 16:}
我们只看一看ListDemo.h文件。第10行至第13行定义了一个结构,对应于前面定义的ListDemo类。从第11行可知,ListDemo类中定义的类变量intArray在native方法中是无法访问的,不过这并不妨碍它作为native方法的参数。
第21、22行对应于在例13.6中声明的两个native方法。像第二节中所提到的那样,两个方法都增加了一个参数struct
HListDemo *。另外,我们要传递的整数数组参数变成了“HArrayOfInt *”类型。那么,我们如何去取其中的值呢?
打开你的Java安装目录,在include子目录下找到文件oobj.h。打开它,找到这样一段(在不同的JDK版本中,这一段位于文件oobj.h中的行号未必相同)。
134:typedef sturct{ 135:long
body[1] 136:}ArrayOfInt 137:typedef ArrayOfInt
ClassArrayOfInt; 138:HandleTo(ArrayOfInt);
其中,HandleTo(T)是一个宏。它的定义可在47行找到: #define HandleTo(T) typedef
struct H##T Class##T *obj; struct methodtable
*methods; }H##T
从中可以看出,ArrayOfInt对应了整数数组,其值用body[]数组来表示。135行用body[1]是指认为数组中至少有一个元素,因为标准C需要结构中必须至少有一个元素。由此的启发是,我们可能要用
p->body[i]
来引用整数数组的元素,其中p是一个指向结构ArrayOfInt的指针。
另外要注意的是,body是一个long型数组。这是由Java中的int型转化来的。基本类型之间的转换可参见下表(表13.1)。
表13.1 类型转换
|
Java类型 |
对应C类型 |
| boolean |
long |
| byte |
char |
| short |
short |
| int |
long |
| long |
int 64_t |
| float |
float |
| double |
double |
| char |
unicode |
| object |
struct Hjava_lang_object* |
下面再来看看HArrayOfInt。从宏定义来看,我们可把HandleTo(ArrayOfInt)展开为:
struct HArrayOfInt{ ClassArrayOfInt *obj; struct
methodtable *methods; }HAarrayOfInt;
其中,ClassArrayOfInt不过是ArrayOfInt的一个别名(137行)。因此,使用HArrayOfInt*
指针的方法就很清楚了。设有这样一个指针p,则需做以下工作: (1)得到p所对应的ArrayOfint*
指针,即结构定义中的“obj”。这一步用 unhand(p); 可以做到。unhand的定义见<Java主目录>\include\interpretor.h中的第213行。 (2)用p->body[i]可以取对应整数数组中的值。 读者或许会问:为什么不直接交待一下类型转换的规则呢?原因在于,一是我们想向读者介绍一下native方法的内部机制,二是为读者提供一个思路:假如你遇到怪模怪样的指针,手头又无书本可查,怎么办(当然如果有现成资料可查就不必这么费劲了)?在第三个例子中读者可能检验一下自己的能力。
下面我们来看一下native方法的实现文件(例13.10)。有了前面的说明,读者理解也就不困难了。
例 13.10 ListImp.c
- #include <StubPreamble.h>
- #include "ListDemo.h"
- #include <stdio.h>
- struct Nodes{//定义链表结点的结构
- long value;
- struct Nodes *next;
- }
- static struct Nodes *List;
- void ListDemo_createList(struct HListDemo *this,HArrayOfInt
*Array){
- int i;
- ArrayOfInt *p=unhand(Array);
- struct Nodes *temp;
- temp=(struct Nodes *)malloc(sizeof(struct Nodes));
- temp->value=p->body[0];
- temp->next=NULL;
- List=temp;
- for(i=1;i<5;I++){
- temp->next=(struct Nodes*)malloc(sizeof(struct Nodes));
- temp-=temp->next;
- temp->value =p->body[i];
- temp->next=NULL;
- }
- }
- void ListDemo_showList(struct HListDemo *this){
- struct Nodes *tmp;
- tmp=List;
- printf("The content of list:");
- while(tmp!=NULL){
- printf("%4d",tmp->value);
- tmp = tmp->next;
- }
- }
程序第4~7行定义了链表结点的结构。对应于Java中的整数,链表的结点中存储的值为long型。
函数ListDemo_createList()用来创建链表。注意第11行和14、20行。这三行是引用参数数据的部分。
函数ListDemo_showList()打印链表内容。
这个例子给我们的最后一点启示是可以在native方法的实现程序中定义全局变量,如List。
最后看一下库的生成和程序结果。
生成动态链接库Listdll.dll的过程如下:
C:\bookDemo\ch13>cl ListDemo.c ListImp.c -Felispdll.dll -MD
-LD javai.lib Microsoft (R) 32-bit C/C++ Optimizing Compiler
Version 10.20.6166 for 80x86 Copyright (C) Microsoft Corp
1984-1996.Allrights reserved.
ListDemo.c ListImp.c Generating
Code... Microsoft (R) 32-Bit Incremental Linker Version
4.20.6164 Copyright (C) Microsoft Corp 1992-1996. All rights
reserved.
/out:lispdll.dll /dll /implib:lispdll.lib ListDemo.obj ListImp.obj javai.lib Creating
library lispdll.lib and object lispdll.exp
例13.10的运行结果如下: C:\bookDemo\ch13>java
ListDemoMain Enter 5
integers: 12 34 45 678 234 The content
of list:12 34 45 678 234 C:\bookDemo\ch13>
本节介绍了Java与C接口上的类型转换规则,以及使用对象作为参数的方法。附带说一句,当使用对象作参数时,如果不了解如何引用对象中变量的值,则需查找有关文件(如前面提到的oobj.h和interpretor.h)。一个快捷的途径是用VC集成环境中的查找功能(适用于Win95/NT平台)。对于已编译链接过的项目文件,还可用鼠标右键点击字串寻找这一字符串的定义/引用点。这属于VC的使用技巧,在此不多述。
13.3.2 复杂情况处理——一个测试
这一节中我们再用一个例子进一步说明一些细节问题。
这个例子的功能是:在Java主执行类上定义了一个字符串,用native方法打印该字符串的内容和长度。
下面我们给出这个例子的Java源文件(见例13.11)。读者可以试着编写一个实现文件ObjectImp.c,并用前面所学到的方法生成动态链接库。
例13.11 ObjectDemoMain.java。
- import java.lang.*;
- public class ObjectDemoMain{
- public String message="Can it work?";
- public static void main(String args[]){
- ObjectDemo objDemo = new ObjectDemo();
- objDemo.run();
- }
- }
- class ObjectDemo{
- private native void display(ObjectDemoMain obj);
- static{
- System.loadLibrary("objdll");
- }
- public void run(){
- ObjectDemoMain obj=new ObjectDemoMain();
- display(obj);
- }
- }
下面我们就本例中一些注意点提出解释。如果读者在某一方面遇到了问题,可以按下面的办法去修改。
问题一:生成什么类的头文件和存根文件?
这个例子中我们得到两个.class文件:objectdemoMain.class
和ObjectDemo.class。其中,第一个类中未声明native方法,因此可以不生成存根文件(生成也可以,见例13.12)。但两个类对应的头文件都必须生成,见例13.13。
例13.12 存根文件ObjectDemoMain.c和ObjectDemo.c。
//ObjectDemoMain.c 1:/* DO NOT EDIT THIS FILE - it is
machine generated */ 2:#include
<StubPreamble.h> 3: 4:/* Stubs for class
ObjectDemoMain*/
//ObjectDemo.c
- /* DO NOT EDIT THIS FILE - it is machine generated */
- #include <StubPreamble.h>
-
- /* Stubs for class ObjectDemo*/
- /*SYMBOL:"ObjectDemo/display(LObjectDemoMain;)V",Java_ObjectDemo_display_stub
*/
- __declspec(dllexport) stack_item
*Java_ObjectDemo_display_stub(stack_item *_P_,struct execenv
*__EE_){
- extern void ObjectDemo_display(void *,void *);
- (void)ObjectDemo_display(_P_[0].p,((_P_[1].p)));
- return _P_;
- }
例13.13 头文件ObjectDemoMain.h和objectDemo.h。
//ObjectDemoMain.h
- /* DO NOT EDIT THIS FILE - it is machine generated */
- #include <native.h>
- /* Header for class ObjectDemoMain*/
-
- #ifndef _Included_ObjectDemoMain
- #define _Included_ObjectDemoMain
- struct Hjava_lang_String;
-
- typedef sturct ClassObjectDemoMain{
- struct Hjava_lang_String *message;
- }ClassObjectDemoMain;
- HandleTo(ObjectDemoMain);
-
- #ifdef __cplusplus
- extern "C"{
- #endif
- #ifdef __cplusplus
- }
- #endif
- #endif
//ObjectDemo.h
- /* DO NOT EDIT THIS FILE - it is machine generated */
- #include <native.h>
- /* Header for class ObjectDemo*/
-
- #ifndef _Included_ObjectDemo
- #define _Included_ObjectDemo
-
- #pragma pach(4)
-
- typdef struct ClassObjectDemo{
- char PAD; /* ANSI C requires structures to have a least one
member */
- }ClassObjectDemo;
- HandleTo(ObjectDemo);
-
- #pragma pack()
-
- #ifdef __cplusplus
- extern "C"{
- #endif
- sturct HObjectDemoMain;
- extern void ObjectDemo_display(struct HObjectDemo*,struct
HObjectDemoMain*);
- #ifdef __cplusplus
- }
- #endif
- #endif
问题二:如何引用对象参数的值?
先请看一下native方法display()的实现文件(例13.14)。
例13.14 ObjectImp.c
- #include <stdio.h>
- #include "StubPreamble.h"
- #include "ObjectDemo.h"
- #include "ObjectDemoMain.h"
-
- void ObjectDemo_display(struct HObjectDemo *this,stuct
HObjectDemoMain* ptr){
- printf("The message is
%S\n",unhand(unhand(unhand(ptr)->message)->value)->body);
- printf("And the length is
%d",unhand(unhand(ptr)->message)->count);
- }
看一下例子中第7行。用了三个“unhand”。如果读者在取值上出现问题,那么说明读者没有查找到相应的结构定义。
下面让我们重复一下设法取message内容和长度的过程。
从ObjectDemoMain.h中找到(第9行~13行)HObjectDemoMain的定义。知道message的类型转换后变为指向结构Hjava_lang_String的指针。
从<Java安装目录>\include\java_lang_String.h文件中找到(第8~14行):
typedef struct Classjava_lang_String{ sturct
HArrayOfChar *value; long offset; long count; /*
Inaccessible
static:InternSet*/ }Classjava_lang_String; HandleTo(java_lang_String);
于是知道字符串值放在value指向的“HArrayOfChar”结构中,长度(count)为long型。
从oobj.h中找到ArrayOfChar的定义(第122~124):
typedef struct{ unicode body[1]; }ArrayOfChar;
于是可知用body访问ArrayOfchar中存放的字符数组元素。
由此,例13.14中第7行三个unhand的来历便解释清楚了:最内层的定位到结构ClassObjectDemoMain;第二个unhand定位到结构Classjava_laong_String,最外层一个定位到结构ArrayOfChar。可见,看似简单的一个字符串却要经过许多步骤才能访问到。
如果已经做了上面的“定位”工作后程序仍然运行不正常,请留意看一下ObjectImp.c的第7行打印语句中的格式标识:是“%S”而非“%s”。这是因为后者是用来打印单字节字符的,用它来打印是达不到预期效果的。
另外要提醒的是,例13.13中的两个头文件都必须在实现文件中包括。否则,有些结构定义就找不到了。
问题三:用哪些文件生成动态链接库?
答案是:ObjectDemoMain.c是可选的,加不加都可以。而ObjectDemo.c与ObjectImp.c是必要的。为了简单起见,不妨把他们都加进动态链接库。
下面是动态链接库的生成过程和运行结果。
生成动态链接库objdll.dll的过程如下:
C:\bookDemo\ch13>cl ObjectDemo.c ObjectImp.c -Feobjdll.dll
-MD -LD javai.lib Microsoft (R) 32-bit C/C++ Optimizing
Compiler Version 10.20.6166 for 80x86 Copyright (C) Microsoft
Corp 1984-1996. All rights reserved.
ObjectDemo.c ObjectImp.c Generating
Code... Microsoft (R) 32-Bit Incremental Linker Version
4.20.6164 Copyright (C) Microsoft Corp 1992-1966. All rights
reserved.
/out:objdll.dll /dll /implib:objdll.lib ObjectDemo.obj ObjectImp.obj javai.lib Creating
library.lib and object objdll.exp
ObjectDemoMain的运行结果如下: C:\bookDemo\ch13>java
ObjectDemoMain The message is Can work? And the length is
12 C:\bookDemo\ch13>
读者不妨按照上面介绍的几个问题对照一下自己的实践。
13.4 新的原生方法接口——JNI (以下尚未校对)
Java风行后,数家生产商如Netscapt,Microsoft都提出了自己的原生方法接口方案。SUN公司为了统一各种方案,提出了一套完整的原生方法接口规范,并在1.1版本的JDK中增加了相关功能。新的原生方法接口比原来的效率更高,功能也较强。这一节将介绍新的接口,并给出前文中三个例子的新版本。
13.4.1生成动态链接库的步骤
新方法中生成动态链接库的步骤与旧方法类似,但略徽简单一点。我们仍以节13.2中的例子DispDemo为例为说明。
(1)创建Java源文件和.class文件
这一步的工作与旧方法相同。我们得到的DispDemo.java和DispDemoMain.java也是一样的。用javac把它们编译成.class文件。
(2)生成头文件
这一步用javah生成一个JNI风格的头文件。新方法中不再需要存根文件。具体的做法是:注意命令行参数的变化。
由此可以得到如下头文件(例13.15)。
例13.15 JNI风格的DispDemo.h。
- /*DO NOT EDIT THIS FILE - it is machine generated */
- #include <jni.h>
- /* Header for class DispDemo */
-
- #ifndef _Included _DispDemo
- #define _Included _DispDemo
- #ifdef __cplusplus
- extern "C"{
- #endif
- /*
- * Class:DispDemo
- * Method:display
- * Signature:()V
- */
- JNIEXPORT void JNICALL Java _DispDemo _display
- (JNIEnv *,jobject);
-
- #ifdef __cplusplus
- }
- #endif
- #endif
注意这个文件的第15、16行,它说明了实现文件中应编写的方法的原型。
(3)创建C源文件
这一步用C语言编写方法的具体实现。我们这个例子的功能很简单,只要编写一个很短的函数。源码见例13.16。
例13.16 DispImp.c。
- #include <stdo.h>
- #include <jni.h>
- #include "DispDemo.h"
- JNIEXPORT void JNICALL
- Java _DispDemo _display(JNIEnv *env,jobject obj)
- {
- printf("Native method exceuting...\n");
- }
由示例可见,程序由引用的头文件与老方法不同,用“jni.h”代替了“StubPreamble.h”。总的说来,程序中需要引用的头文件有三类:jni.h,第三步中生成的头文件,以及实现功能所需的头文件。另外,函数原型也有变化。编程时,按头文件中的方法原型来编写即可。
(4)生成动态链接库(共享库)
对WIN95/NT平台,可以用下面的命令来生成例子所需的动态链接库:
cl -Ic:\java\include -Ic:\java\include\win32 -LD DispImp.c
-Fedispdll.dll或者,先设定路径,再编译。如先用一个批文件native.bat设定所有路径:
C:\bookDemo\ch13>native
C:\bookDemo\ch13>set path=c:\msdev\bin;c:\jdk1.1.1\bin
C:\bookDemo\ch13>set
include=c:\msdev\include;c:\msdev\mfc\include;
c:\jdk1.1.1\include;c:\jdk1.1.1\include\win32;
C:\bookDemo\ch13>set
lib=c:\msdev\lib;c:\msdev\mfc\lib;c:\jdk1.1.1\lib
C:\bookDemo\ch13>
再编译链接:
C:\bookDemo\ch13>cl -LD DispImp.c -Fedispdll.dll
Microsoft (R) 32 -bit C/C++ Optimizing Compiler Version
10.20.6166 for 80x86
Copyright (C) Microsoft Corp 1984-1996. All rights
reserved.
DispImp.c
Microsoft (R) 32-Bit Incremental Linker Version 4.20.6164
Copyright (C) Microsoft Corp 1992-1996. All rights
reserved.
/dll
/implib:dispdll.lib
/out:dispdll.dll
DispImp.obj
Creating library dispdll.lib and object dispdll.exp
就得到了我们需要的动态链接库。
类似地,在Solaris系统中,用下面的命令生成共享库:
cc -G -I/usr/local/java/include
-I/usr/local/java/include/solaris DispImp.c -o libDispdll.so
(5)装入动态链接库,调用native方法
用java解释器运行程序,得:
C:\bookDemo\ch13>java DispDemoMain
Native method executing...
C:\bookDemo\ch13>
整个过程就完成了。
13.4.2 传参与数组对象访问
本节给出例子ListDemo的新版本,并藉此说明原生方法中参数的传递和数组元素的访问。
1、类型转换
JNI规范规定了类型间的对应关系,表13.2说明了基本类型的转换关系。
表13.2 JNI中基本类型的转换
表13.2 JNI中基本类型的转换
| 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 |
|
对于类和数组,原生方法也有类似的转换规则,表13.3给出其对应关系。
表13.3 Java语言与原生方法中类与数组的类型转换
| 原生方法中类型 |
对应Java语言中的对象 |
| jobject |
所有Java对象 |
| jclass |
java.lang.Class对象 |
| jstring |
java.lang.String对象 |
| jarray |
数组 |
| jobjectArray |
对象数组 |
| jbooleanArray |
布尔数组 |
| jbyteArray |
字节数组 |
| jcharArray |
字符数组 |
| jshortArray |
短整数数组 |
| jintArray |
整数数组 |
| jlongArray |
长整数数组 |
| jfloatArray |
浮点数数组 |
| jdoubleArray |
双精度浮点数数组 |
| jthrowable |
java.lang.Throwable对象 |
下面结合例子ListDemo,讲解类型转换与数组元素的访问。
该例的Java源文件几乎可以沿用旧的,但由于在新API中,DataInputStream类的readLine()方法已经过时,故在ListDemo.java中改用了BufferedReader类。另一个程序ListDemoMain.java与前面给出的相同。这两个程序没有什么新内容,这里不再解释。
例13.17 ListDemo.java。
- import java.io.*;
-
- public class ListDemo{
- public native void createList(int[] ints);
- public native void showList();
- static{
- System.loadLibrary("listdll");
- }
- static int intArray[]={0,0,0,0,0};
- public void run() throws IOException{
- Bufferedreader bufReader=
- new BufferedReader(new InputStreamreader(system.in));
- System.out.println("Enter 5 integers:");
- for(int i=0;i<5;i++)
- intArray[i]=Integer.valueOf(bufReader.readLine()).intValue();
- createList(intArray);
- showList();
- }
- }
按照节13.4.1中的做法,生成头文件如下(例13.18)。
例13.18 ListDemo.h
- /* DO NOT EDIT THIS FILE - it is machine generated */
- #include <jni.h>
- /* Header for class ListDemo */
-
- #ifndef _Included _ListDemo
- #define _Included _ListDemo
- #ifdef __cplusplus
- extern "C"{
- #endif
- /*
- *Class:ListDemo
- *Method:createList
- *Signature:([I)V
- */
- JNIEXPORT void JNICALL Java_ListDemo_createList
- (JNIEnv *,jobject,jintArray);
-
- /*
- * Class:ListDemo
- * Method:showList
- * Signature:()V
- */
- JNIEXPORT void JNICALL Java_ListDemo_showList
- (JNIEnv *,jobject);
-
- #ifdef __cplusplus
- }
- #endif
- #endif
程序的15、16和23、24行给出了两个方法的原型,其参数符合类型转换的规则。
头文件中还有一处难理解的地方:13行和21行的注释中有一个新名词“Signature”,这是什么意思呢?原来,JNI中用了Java虚拟机中对类型的表示。所谓“Signature”的意思是“签名”,读者可以把它理解为对类型、类甚至方法的类型的表示。由于本例用不到它,我们把它搁置起来,到下一节再讲。
下面继续看ListDemo示例。例13.19是原生方法的实现程序。
例13.19 ListImp.c。
1: #include <jni.h>
2: #include "Listdemo.h"
3: #include <stdio.h>
4: struct Nodes{
5: jint value;
6: struct Nodes *next;
7: };
8: static struct Nodes *List;
9:
10: JNIEXPORT void JNICALL Java_ListDemo_createList
11: (JNIEnv *env,jobject obj,jintArray Array){
12: int i;
//得到数组长度
13: jsize len=(*env)->GetArrayLength(env,Array);
//得到数组指针
14: jint *p=(*env)->GetIntArrayElements(env,array,0);
15: struct Nodes *temp;
16: temp=(struct Nodes *)malloc(sizeof(struct Nodes));
//取数组头元素
17: temp->value=p[0];
18: temp->next=NULL;
19: List=temp;
//取数组的元素
20: for(i=1;i<len;i++){
21: temp->text=(struct Nodes*)malloc(sizeof(struct
Nodes));
22: temp=temp->next;
23: temp->value=p[i];
24: temp->next=NULL;
25: }
//释放数组指针
26: (*env)->ReleaseIntArrayElements(env,Array,p,0);
27: }
28: JNIEXPORT void JNICALL
29: Java_ListDemo_showList(JINEnv *env,jobject obj)
30: {
31: struct Nodes *tmp;
32: tmp=List;
33: printf("The content of list:");
34: while(tmp!=NULL){
35: printf("%4d",tmp->value);
36: tmp=tmp->next;
37: }
38: }
对比例13.10中的程序,我们可以发现以下几点:
(1)程序引用了jni.h,取代了原先的StubPreamble.h。
(2)函数的原型有变化(这两点在节13.4.1中已述及)。
(3)取数组元素的方法有变化。没有使用unhand操作,代之以JNI提供的函数。
让我们把注意力集中在第三点上。
例子对数组的操作使用了三个函数。它们是:
13:jsize len = (*env)->GetArrayLength(env,Array);
14: jint *p=(*env)->GetIntarrayElements(env,Array,0);
26:(*env)->ReleaseIntArrayelements(env,array,p,0);
它们的原型分别为:
☆jsize GetArrayLength(JINEnv *env,jarray array);
☆<指向数组元素的指针>Get<Java基本类型>ArrayElements(JNIEnv
*env,jarray array,jboolean *isCopy);
☆void Release <Java基本类型>ArrayElements(JNIEnv *env,jarray
array,<指向数组元素的指针>elems,jint mode);
下面分别讲解。
☆jsize GetArrayLength(JNIEnv *env,jarray array);
返回数组元素的个数。env是JNI接口指针,array是数组对象。返回值类型jsize实际是一种无符号整数,取值为地址空间的大小范围。
☆<指向数组元素的指针>Get<Java基本类型>ArrayElements(JNIEnv
*env,jarray array,jboolean *isCopy);
这实际上是一族函数,功能是取基本类型(表13.2中的类型,但不包括void)的数组元素,返回值是一个数组(或者说是指针)。从这个原型可以得到诸如GetBooleanArrayElements()、GetShortArrayElements()等函数。参数中的isCopy表明返回的数组指针是指向数据的真正存区(值为1时)还是指向拷贝了数组元素值的另一存区(值为0)。
☆void Release<Java基本类型>ArrayElements(JNIEnv *env,jarray
array,<指向数组元素的指针>elems,jint mode);
该函数与上一函数可以说是对应的。它完成的功能是释放资源和数据更新。由于Javar的垃圾收集具有可能改变内存中对象的位置,如不采取必要措施,被访问的数组指针就可能不再指向正确的存区。因此,对于数组,要么把它“钉”在固定的存区,要么把它拷贝至固定的存区,总之在访问它的期间要使数组元素总在原地。作完操作之后,再调用这个函数,解除对它的固定。另外,该函数还反对数组元素的所有屐按程序要求进行更新(在调用这个函数之前,所有更新都没有作用在数组本身上)。参数mode就是决定更新与否的。它取0时,更新数组并释放elems;取JNI——COMMIT时,更新但不释放elems;取JNI——ABORT时,释放elems,不作更新。
对数组的操作是多种多样的。如果数组元素是对象,用上面的函数就不行了,而要用
jobject GetObjectArrayElement(JNIEnv *env,jarray array,jsize
index);
得到一个给定元素(index是下标)。如果要改变一个对象数组元素的值,就要用
void SetObjectArrayelement(JNIEnv *env,jarray array,jsize
index,jobject value);
如果要创建对象数组,用
jarray NewObjectArray(JNIEnv *env,jsize length,jclass
elementClass,jobject initialElement);
其中elementClass是对象的类,initialElement是所有数组元素的初值。创建失败返回NULL。
创建基本类型的数组,用
jarray New<java基本类型>Array(JNIEnv *env,jsize length);
上面讲了访问整个数组的做法,如果数组很大,只需读一个相对较小的数组段,还可以用下面的函数:
void Get<Java基本类型>ArrayRegion(JNIEnv *env,jarray
array,jsize start,jsizelen,NativeType *buf);
其中start指明起始位置,len是长度,buf是得到的缓冲区指针,可用于操作数组段中的元素。对应地,有函数
void Set<Java基本类型>ArrayRegion(JNIEnv *env,jarray
array,jsize start,jsize len,NativeType *buf);
把缓冲区中的内容拷贝回原数组。
对数组的操作已经介绍完了。最后要注意的是上述函数往往会抛出越界异常,在编程时要加以注意。
13.4.3 域变量的访问
这一节给出例子ObjectDemo的新版本,并结合例子介绍JNI的类型签名,演示如何访问对象的域变量,以及如何使用字符串对象。
旧版本的Java程序是写在一个文件ObjectDemoMain.java中的,新版本程序与旧版本程序完全相同,可参看例13.11。下面给出ObjectDemo.h的源代码(例13.20)。
例13.20 ObjectDemo.h
/* DO NOT EDIT THIS FILE - It is machine generated */
#include <jni.h>
/* Header for class ObjectDemo */
#ifndef _Included_ObjectDemo
#define _Included_ObjectDemo
#ifdef cplusplus
extern "C"{
#endif
/*
* Class:ObjectDemo
* Method:display
* Signature:(LObjectDemoMain;)V
*/
JNIEXPORT void JNICALL Java_ObjectDemo_display
(JNIEnv *,jobject,jobject);
#ifdef __cplusplus
}
#endif
#endif
该文件的第15、16行给出了待实现的方法原型。第13行又有一个签名(“Signature”),这是上一节搁置的问题。“签名”是对基本类型、类甚至方法的类型的表示,是一种关于类型的符号约定。在访问域变量、调用类的方法时,需要知道关于域和方法相关信息,其中就包括对其类型的简明扼要的表示。
表13.4列出了类型与签名的对应关系。
表13.4 类型签名
|
类 型 |
类型签名 |
| boolean |
Z |
| byte |
B |
| char |
C |
| short |
S |
| int |
I |
| long |
J |
| float |
F |
| double |
D |
| void |
V |
| <类全名> |
L<类全名>; |
| <类型名>[] |
[<类型签名> |
| <方法的类型> |
(<参数类型签名>)返回值类型签名 |
可根据表13.4分析几个方法的类型。在ObjectDemoMain.java中,方法
private native void display(ObjectDemoMain obj);
的签名(见ObjectDemo.c)为:
13: *Signature:(LObjectDemoMain;)V
同表13.4可知,LObjectDemoMain是参数类型签名,V是返值类型签名。查表知“V”代表void,而“LObjectDemoMain;”说明参数是ObjectDemoMain类的对象。
再看ListDemo.java中声明的方法:
public native void createList(int[] ints);
它的签名是“([I)V”,易知[I是参数类型签名,[代表数组,I代表int型,故参数为整型数组。返值也是void型。
当要用到类型签名时,如何得到它呢?按照表13.4直接写当然是可以的,如果怕出错,可以用JDK中的工具java,用
javap -s -p <类名>
来取得类中域和方法的签名。例如:
C:\bookDemo\ch13>javap -s -p ObjectDemoMain
Compiled from ObjectDemoMain.java
public synchronized class ObjectDemoMain extends
java.lang.Object
/* ACC_SUPER bit set */
{
public java.lang.String message;
/* Ljava/lang/String; */
public static void main(java.lang.String[]);
/* ([Ljava/lang/String;)V */
public ObjectDemoMain();
/* ()V*/
}
C:\bookDemo\ch13>
得到了类ObjectDemoMain类的域和方法的类型签名。记住变量message的签名,我们很快就会用到它。
签名的问题解决了,现在来看实现(例13.21)。
例13.21 ObjectImp.c。
- #include <stdio.h>
- #include <jni.h>
- #include "ObjectDemo.h"
- JNIEXPORT void JNICALL Java_ObjectDemo_display
- (JNIEnv *env,jobject obj,jobject demo){
- jclass clazz=(*env)->GetObjectClass(env,demo);
- jfieldID
id=(*env)->GetFieldID(env,clazz,"message","Ljava/lang/String;");
- jstring js=(*env)->GetObjectField(env,demo,id);
- const char *s=(*env)->GetStringUTFChars(env,js,0);
- printf("The message is %s\n",s);
- (*env)->ReleaseStringUTFChars(env,js,s);
- printf("And the length is
%d\n",(*env)->getStringLength(env,js));
- printf("The length of c string is %d",strlen(s));
- }
这个程序重点说明两个问题:如何访问对象的域;如何操作字符串。 程序的第6~8行是取对象域的过程。访问一个对象的域,需经过这样一个过程:
(1)得到域的ID号。JNI为此提供了专门的函数,以便根据该域所在的类、域的名字和类型签名得到ID号。ID号在类被卸载(unload)前一直保持有效。 为了做到这一点,JNI提供了一些函数: ☆
jfieldID GetFieldID(JNIEnv *env,jclass clazz,const char *name,const
char
*sig); 得到static型域变量的ID。 为了得到clazz的值,往往还需要调用一个函数,用来从对象得到对应的类: ☆
jclass GetObjectClass(JNIEnv *env,jobject obj); 例中的第6行用到了它。
(2)访问该域。对于static域,需要把类作为参数之一,否则用对象作为参数之一。有这样一些函数: ☆
<原生方法中的类型>Get<类型*>Field(JNIEnv *env,jobject obj,jfieldID
fieldID); 这也是一族方法,其中的“<类型*>”可以为Object或表13.2中的八种基本类型(前八行)。返回值则是相应的原生方法中的类型(见表13.2)。如: jint
GetInfField(JNIEnv *env,jobject obj,jfieldID
fieldID); 读一个整形域。 ☆ void Set<类型*>Field(JNIEnv
*env,jobject obj,jfieldID fieldID,<原生方法中的类型>
value); 设置非static型的值。<类型*>、<原生方法中的类型>含义同上。 ☆<原生方法中的类型>
GetStatic<类型*>Field(JNIEnv *env,jclass clazz,jfieldID
fieldID); 读取static型域的值。clazz是域所在的类。 ☆ Void
Set<类型*>StaticField(JINEnv *env,jclass calzz,jfieldID
fieldID,<原生方法中的类型>value) 设置static型域的值。 读者可以根据上述过程看一下例子的6~8行,验证一下。
例子的9、11、12行是对字符串的操作。注意,在第8行得到的jstring变量不可直接输出,否则有可能引起虚拟机崩溃。因为Java虚拟机用的字符串是一种特殊的UTF-8字符串,它可以用一个、两个、三个字节来表示信息,而以字节的若干位来标识该信息由几个字节组成。它不同于C中的字符串char*。使用字符串时,必须用JNI提供的函数把它转换成C的字符串格式。
字符串操作的常用函数如下: ☆jstring NewString(JNIEnv *env,const jchar
*unicodeChars,jsize
len); 从一个Unicode数组创建一个java.lang.String对象。unicodeChars是指向Unicode数组的指针。 ☆jsize
GetStringLength(JNIEnv *env,jstring
string); 返回字符串长度。 ☆const jchar *GetStringChars(JNIEnv
*env,jstring string,jboolean
*isCopy); 返回一个指向Unicode字符数组的指针,该字符数组由组成字符串的字符组成。像访问一般数组一样,这个指针在执行对应的Release方法(这里是ReleaseStringChars())前保持有效。isCopy决定指针指向的字符数组是否经过拷贝得来。 ☆
void ReleaseStringchars(JNIEnv *env,jstring string,const jchar
*chars); 通知虚拟机不再需访问该字符串。chars是由GetStringChars()函数得到的。 ☆
jstring NewStringUTF(JNIEnv *env,const char *bytes,jsize
length); 由一个UTF-8字符组成的数组创建一个java.lang.String对象。bytes是指向UTF-8数组的打针。 ☆jsize
GetStringUTFLength(JNIEnv *env,jstring
string); 返回string的长度(按UTF-8格式计算的字节数)。 ☆jbyte
*GetStringUTFChars(JNIEnv *env,jstring string jboolean
*isCopy); 返回一个指向UTF-8字符数组的指针,该字符数组由组成字符串的字符组成。这个指针在执行对应的Release方法(这里是ReleaseStringUTFChars())前保持有效。isCopy决定指针指向的字符数组是否经过拷贝得来。当你确知串仅由7位的ASCII字符组成时,可以直接把它传递给C的函数,就像例子中做的那样(第9、10行)。 ☆
void ReleaseStringUTFChars(JNIEnv *env,jstring string,const jbyte
*utf); 通知虚拟机不再需要访问字符串string。utf由GetStringUTFChars()方法得来。 一般说来,在原生方法中访问字符串都必须经过获得指针、释放指针两步(见示例第9、11行),节不可直接输出未经转换的字符串,以免造成虚拟机崩溃。
13.4.4 方法调用与异常处理
本节讲述如何在原生方法中调用Java语言编写的方法,以及如何进行异常处理。
1.在原生方法中调用Java方法
在原生方法中调用Java方法,实际上就是访问Java编写的类(对象)的方法。这个工作与访问类、对象的域非常接近。
(1)得到方法的ID号。用JNI提供的函数 ☆ jmethodID GetMethodID(JNIEnv
*env,jclass clazz,const char *name,const char
*sig); 可以做到这一点。为了得到方法所在的类的描述(参数clazz),事先还要调用: ☆ jclass
GetObjectClass(JNIEnv *env,jobject obj); 例如: jclass
clazz=(*env)->GetObjectClass(env,obj); jmethodID
mid=(*env)->GetMethodID(env,clazz,"methodname","(J)V");得到对象Obj中方法 void
methodname(long
aLong); 的ID号。 上面说了对实例方法(非static方法)的处理。对static方法的处理略有不同,应采用函数 ☆
jmethodID GetStaticMethodID(JNIEnv *env,jclass clazz,const char
*name,const char
*sig);来得到ID号。 (2)根据方法返回值类型选用适当函数调用该方法。常用的一族方法是 ☆
<原生方法中的类型>Call<类型*>Method(JNIEnv *env,jobject
obj,jmethodID
methodID,...); <类型*>指“Object”或表13.2中列出的Java基本类型,由被调用方法的返回值决定。如,若被调用方法返回一个对象,则用 jobject
CallObjectMethod(JNIEnv *env,jobject obj,jmethodID
methodID,...);“...”是Java方法所需的参数值,只要按原顺序把它们罗列上去即可。如,对于(1)中提到的方法可以这样调用: (*env)->CallVoidMethod(env,obj,mid,aLong); 对于static方法,用的函数不同,应使用: ☆<原生方法中的类型>CallStatic<类型*>Method<JNIEnv
*env,jclass clazz,jmethodID methodID,...); 来调用。参数的含义同上。
2.异常处理 许多JNI方法的调用会引发异常。程序员应当检查每一个可能引起异常的JNI方法执行后的情况,发现有异常及时处理(在前面的例子中我们为了简单没有这样做,但在开发实用程序时这一点不可忽略)。否则,在有未处理异常的情况下调用JNI函数可能引起意想不到的后果。 下列函数是处理异常时可能用到的: ☆jobject
ExceptionOccurred(JNIEnv *env); 捕捉可能发生了的异常; ☆ void
ExecptionClear(JNIEnv *env); 清除异常; ☆ void
ExecptionDescribe(JNIEnv
*env); 输出异常信息。 例如,我们用这样一段程序: jthrowable
hasExc; hasExc=(*env)->ExceptionOccured(env); if(hasExc){ (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } 可以输出异常信息。 必要时,还可以把异常抛出去。这个工作由 ☆
jint ThrowNew(JNIEnv *env,jclass clazz,const char
*message); 来完成。其中clazz是java.lang.Throwable的子类,而message是字符信息。如: jclass
excClazz; excClazz=(*env)-FindClass(env,"java/lang/numberFormatException"); if(excClazz==0)//找不到,则放弃 return; (*env)->ThrowNew(env,excClazz,"thrown
from native
code"); 抛出一个数字格式异常。 对JNI的使用就介绍到这里。JNI的很多,应用到程序中更会有成千上万种变化。我们这里只能择要介绍一些,其它内容有待读者继续钻研。
本章小结
这一章介绍了在程序中应用native方法的一般步骤,并结合实例讲了具体实现的一些细节。 一般说来,语言间的接口问题都较为复杂的,Java与C的接口也不例外。要自如地运用native方法,不仅要掌握Java相关知识,对C的使用也是一个考验。因此,对于不很有经验的程序员,不要轻易使用它,以免造成严重的后果。
本章中还概要地解释了Java提供的几个头文件的部分内容。这些解释是有相当局限的,要对Java与C的接口有透彻的了解,还要解读许多内容。这只能让读者在实践中逐步摸索了。
另外,微软公司的SDK中考虑到垃圾收集和系统的效率,对native方法的实现做了修改。具体哪一种实现更好是个见仁见智的问题。对SDK中提供的接口,在SDK的文档中有较为详细的介绍,可以从网上方便地得到。有兴趣的读者可以自行查阅。
☆☆ 本章结束 ☆☆
|