JNI(Java Native Interface
,
Java
本地接口
)
技术大家都不陌生,它可以帮助解决
Java
访问底层硬件的局限和执行效率的提高。关于
JNI
的开发,大多数资料讨论的都是如何用
C/C++
语言开发
JNI
,甚至于
JDK
也提供了一个
javah
工具来自动生成
C
语言程序框架。但是,对于广大的
Delphi
程序员来说,难道就不能用自己喜爱的
Delphi
与
Java
互通消息了吗?
通过对
javah
生成的
C
程序框架和
JDK
中的
jni.h
文件的分析,我们发现,
Java
利用
JNI
访问本地代码的关键在于
jni.h
中定义的
JNINativeInterface_
这个结构
(Struct)
,如果用
Delhpi
语言改写它的定义,应该也可以开发
JNI
的本地代码。幸运的是,在网上有现成的代码可以帮助你完成这个繁杂的工作,在
http://delphi-jedi.org
上提供了一个
jni.pas
文件,就是用
Delphi
语言重写的
jni.h
。我们只需在自己的
Delphi
工程中加入
jni.pas
就可以方便地开发出基于
Delphi
语言的
JNI
本地代码。
本文将利用
jni.pas
,讨论用
Delphi
语言开发
JNI
本地代码的基本方法。
先来看一个经典的
HelloWorld
例子。编写以下
Java
代码:
class HelloWorld
{
public native void displayHelloWorld();
static
{
System.loadLibrary("HelloWorldImpl");
}
}
|
这段代码声明了一个本地方法
displayHelloWorld
,它没有参数,也没有返回值,但是希望它能在屏幕上打印出“您好!中国。”字样。这个任务我们打算交给了本地的
Delphi
来实现。同时,在这个类的静态域中,用
System.loadLibrary()
方法装载
HelloWorldImpl.dll
。注意,这里只需要给出文件名而不需要给出扩展名
dll
。
这时候,如果在我们的
Java
程序中使用
HelloWorld
类的
displayHelloWorld
方法,系统将抛出一个
java.lang.UnsatisfiedLinkError
的错误,因为我们还没有为它实现本地代码。
下面再看一下在
Delphi
中的本地代码的实现。新建一个
DLL
工程,工程名为
HelloWorldImpl
,输入以下代码:
Uses
JNI;
procedure Java_HelloWorld_displayHelloWorld(PEnv: PJNIEnv; Obj: JObject);stdcall;
begin
Writeln('
您好!中国。
');
end;
exports
Java_HelloWorld_DisplayHelloWorld;
end.
|
这段代码首先导入
jni.pas
单元。然后实现了一个叫
Java_HelloWorld_displayHelloWorld
的过程,这个过程的命名很有讲究,它以
Java
开头,用下划线将
Java
类的包名、类名和方法名连起来。这个命名方法不能有误,否则,
Java
类将无法将
nativ
方法与它对应起来。同时,在
Win32
平台上,此过程的调用方式只能声明为
stdcall
。
虽然在
HelloWorld
类中声明的本地方法没有参数,但在
Delphi
中实现的具体过程则带有两个参数:
PEnv : PJNIEnv
和
Obj : JObject
。(这两种类型都是在
jni.pas
中定义的)。其中,
PEnv
参数代表了
Jvm
环境,而
Obj
参数则代表调用此过程的
Java
对象。当然,这两个参数,在我们这个简单的例子中是不会用到的。因为我们编译的是
dll
文件,所以在
exports
需要输出这个方法。
编译
Delphi
工程,生成
HelloWorldImp.dll
文件,放在运行时系统能够找到的目录,一般是当前目录下,
并编写调用
HelloWorld
类的
Java
类如下:
class MainTest
{
public static void main(String[] args)
{
new HelloWorld().displayHelloWorld();
}
}
|
运行它,如果控制台输出了“您好!中国。”,恭喜你,你已经成功地用
Delphi
开发出第一个
JNI
应用了。
接下来,我们稍稍提高一点,来研究一下参数的传递。还是
HelloWorld
,修改刚才写的
displayHelloWorld
方法,让显示的字符串由
Java
类动态确定。新的
displayHelloWorld
方法的
Java
代码如下:
public native void displayHelloWorld(String str);
|
修改
Delphi
的代码,这回用到了过程的第一个固有参数
PEnv
,如下:
procedure Java_HelloWorld_displayHelloWorld(PEnv: PJNIEnv; Obj: JObject; str: JString); stdcall;
var
JVM: TJNIEnv;
begin
JVM := TJNIEnv.Create(PEnv);
Writeln(JVM.UnicodeJStringToString(str));
JVM.Free;
end;
|
在该过程的参数表中我们增加了一个参数
str : JString
,这个
str
就负责接收来自
HelloWorld
传入的
str
实参。注意实现代码的不同,因为使用了参数,就涉及到参数的数据类型之间的转换。从
Java
程序传过来的
Java
的
String
对象现在成了特殊的
JString
类型,而
JString
在
Delphi
中是不可以直接使用的。需要借助
TJNIEnv
提供的
UnicodeJStringToString()
方法来转换成
Delphi
能识别的
string
类型。所以,需要构造出
TJNIEnv
的实例对象,使用它的方法(
TJNIEnv
提供了众多的方法,这里只使用了它最基本最常用的一个方法),最后,记得要释放它。对于基本数据类型的参数,从
Java
传到
Delphi
中并在
Delphi
中使用的步骤就是这么简单。
我们再提高一点点难度,构建一个自定义类
Book
,并把它的实例对象作为参数传入
Delphi
,研究一下在本地代码中如何访问对象参数的公共字段。
首先,定义一个简单的
Java
类
Book
,为了把问题弄得稍微复杂一点,我们在
Book
中增加了一个
java.util.Date
类型的字段,代码如下:
public class Book
{
public String title; //
标题
public double price; //
价格
public Date pdate; //
购买日期
}
|
同样,在
HelloWorld
类中增加一个本地方法
displayBookInfo
,代码如下:
public native void displayBookInfo(Book b);
|
Delphi
的代码相对于上面几个例子来说,显得复杂了一点,先看一下代码:
procedure Java_HelloWorld_displayBookInfo(PEnv: PJNIEnv; Obj: JObject; b:JObject); stdcall;
var
JVM: TJNIEnv;
c,c2: JClass;
fid:JFieldID;
mid:JMethodID;
title,datestr:string;
price:double;
pdate:JObject;
begin
JVM := TJNIEnv.Create(PEnv);
c:=JVM.GetObjectClass(b);
fid:=JVM.GetFieldID(c,'title','Ljava/lang/String;');
title:=JVM.UnicodeJStringToString(JVM.GetObjectField(b,fid));
fid:=JVM.GetFieldID(c,'price','D');
price:=JVM.GetDoubleField(b,fid);
fid:=JVM.GetFieldID(c,'pdate','Ljava/util/Date;');
pdate:=JVM.GetObjectField(b,fid);
c2:=JVM.GetObjectClass(pdate);
mid:=JVM.GetMethodID(c2,'toString','()Ljava/lang/String;');
datestr:=JVM.JStringToString(JVM.CallObjectMethodA(pdate,mid,nil));
WriteLn(Format('%s %f %s',[title,price,datestr]));
JVM.Free;
end;
|
参数
b:JObject
就是传入的
Book
对象。先调用
GetObjectClass
方法,根据
b
对象获得它所属的类
c
,然后调用
GetFieldID
方法从
ç
中获取一个叫做
title
的属性的字段
ID
,一定要传入正确的类型签名。然后通过
GetObjectField
方法就可以根据得到的字段
ID
从对象中得到字段的值。注意这里的次序:我们得到传入的对象参数
(Object)
,就要先得到它的类
(Class)
,这样既有了对象实例,又有了类,以后就从类中得到字段
ID
,根据字段
ID
从对象中得到字段值。对于类的静态字段,则可以直接从类中获取它的值而不需要通过对象。
如果要调用对象的方法,操作步骤也基本类似,也需要从类中获取方法
ID
,再执行对象的相应方法。在本例中,因为我们增加了一个
java.util.Date
类型的字段,要访问这样的字段,也只能先把它做为
JObject
读入,再以同样的方法进一步去访问它的成员(属性或方法)。本例中演示了如何访问
Date
对象的成员方法
toString
。
要正确地访问类对象的成员属性(字段)及成员方法,最重要的一点是一定要给出正确的签名,在
Java
中对于数据类型和方法的签名有如下的约定:
数据类型
/
方法
|
签名
|
byte
|
B
|
char
|
C
|
double
|
D
|
float
|
F
|
int
|
I
|
long
|
J (
注意:是
J
不是
L)
|
short
|
S
|
void
|
V
|
boolean
|
Z
(注意:是
Z
不是
B
)
|
类类型
|
L
跟完整类名,如
Ljava/lang/String;
(注意:以
L
开头,要包括包名,以斜杠分隔,最后有一个分号作为类型表达式的结束)
|
数组
type[]
|
[type
,例如
float[]
的签名就是
[float
,如果是二维数组,如
float[][]
,则签名为
[[float
,(注意:这里是两个
[
符号)。
|
方法
|
(
参数类型签名
)
返回值类型签名,例如方法:
float fun(int a,int b)
,它的签名为
(II)F
,
(
注意:两个
I
之间没有逗号!
)
,而对于方法
String toString()
,则是
()Ljava/lang/String;
。
|
通过上面的例子,我们了解了访问对象参数的成员属性或方法的基本步骤和多个
Get
方法的使用。
TJNIEnv
同时提供了多个
Set
方法,可以修改传入的对象参数的字段值,因为
Java
对象参数都是以传址的方式进行传递的,所以修改的结果可以在
Java
程序中得到反映。
TJNIEnv
提供的
Get/Set
方法,都需要两个基本参数:对象实例(
JObject
类型)和字段
ID
(
JField
类型),就可以根据提供的对象和字段
ID
来获取或设置这个对象的这个字段的值。
现在我们了解了在
Delphi
代码中使用以及修改
Java
对象的操作步骤。进一步,如果需要在
Delphi
中从无到有地创建一个新的
Java
对象,可以吗?再来看一个例子,在
Delphi
中创建
Java
类的实例,操作方法其实也非常简单。
先在
Java
代码中增加一个本地方法,如下:
public native Book findBook(String t);
|
然后,修改
Delphi
代码,增加一个函数(因为有返回值,所以不再是过程而是函数了):
function Java_HelloWorld_findBook(PEnv: PJNIEnv; Obj: JObject; t:JString):JObject; stdcall;
var
JVM: TJNIEnv;
c: JClass;
fid:JFieldID;
b:JObject;
mid:JMethodID;
begin
JVM := TJNIEnv.Create(PEnv);
c:=JVM.FindClass('Book');
mid:=JVM.GetMethodID(c,'<init>','()V');
b:=JVM.NewObjectV(c,mid,nil);
fid:=JVM.GetFieldID(c,'title','Ljava/lang/String;');
JVM.SetObjectField(b,fid,t);
fid:=JVM.GetFieldID(c,'price','D');
JVM.SetDoubleField(b,fid,99.8);
Result:=b;
JVM.Free;
end;
|
这里先用
FindClass
方法根据类名查找到类,然后获取构造函数的方法
ID
,构造函数名称固定为“
<init>
”,注意签名为“
()V
”说明使用了
Book
类的一个空的构造函数。然后就是使用方法
NewObjectV
根据类和构造函数的方法
ID
来创建类的实例。创建了类实例,再对它进行操作就与前面的例子没有什么两样了。对于非空的构造函数,则略为复杂一点。需要设置它的参数表。还是上面的例子,在
Book
类中增加一个非空构造函数:
public Book(Strint t,double p){
this.title=t;
this.price=p;
}
|
在
Delphi
代码中,
findBook
函数修改获取方法
ID
的代码如下:
mid:=JVM.GetMethodID(c,'<init>','(Ljava/lang/String;D)V');
|
构造函数名称仍是“
<init>
”,方法签名表示它有两个参数,分别是
String
和
double
。然后就是参数的传入了,在
Delphi
调用
Java
对象的方法如果需要传入参数,都需要构造出一个参数数组。在变量声明中加上:
args : array[0..1] of JValue;
|
注意!参数都是
JValue
类型,不管它是基本数据类型还是对象,都作为
JValue
的数组来处理。在代码实现中为参数设置值,并将数组的地址作为参数传给
NewObjectA
方法:
args[0].l:=t; // t
是传入的
JString
参数
args[1].d:=9.8;
b:=JVM.NewObjectA(c,mid,@args);
|
为
JValue
类型的数据设置值的语句有点特殊,是吧?我们打开
jni.pas
,查看一下
JValue
的定义,原来它是一个
packed record
,已经包括了多种数据类型,
JValue
的定义如下:
JValue = packed record
case Integer of
0: (z: JBoolean);
1: (b: JByte );
2: (c: JChar );
3: (s: JShort );
4: (i: JInt );
5: (j: JLong );
6: (f: JFloat );
7: (d: JDouble );
8: (l: JObject );
end;
|
下面再来看一下错误处理,在调试前面的例子中,大家也许看到了一旦在
Delphi
的执行过程中发生了错误,控制台就会输出一大堆错误信息,如果想要屏蔽这些信息,也就是说希望在
Delphi
中捕获错误并直接处理它,应该怎么做?也很简单,在
TJNIEnv
中提供了两个方法可以方便地处理在访问
Java
对象时发生的错误。
var
… …
ae:JThrowable;
begin
… …
ae:=JVM.ExceptionOccurred;
if ( ae<>nil ) then
begin
Writeln(Format('Exception handled in Main.cpp: %d', [longword(ae)]));
JVM.ExceptionDescribe;
JVM.ExceptionClear;
end;
… …
|
用方法
ExceptionOccurred
可以捕获
Java
抛出的错误,并存入
JThrowable
类型的变量中。用
ExceptionDescribe
可以显示出
Java
的错误信息,而
ExceptionClear
显然就是清除错误,让它不再被抛出。
至此,我们已经把从
Java
代码通过
JNI
技术访问
Delphi
本地代码的步骤做了初步的探讨。在
jni.pas
中也提供了从
Delphi
中打开
Java
虚拟机执行
Java
代码的方法,有兴趣的读者不妨自己研究一下。
posted on 2006-12-19 05:41
坏男孩 阅读(1284)
评论(1) 编辑 收藏 所属分类:
java命令学习