谈笑有鸿儒,往来无白丁

在恰当的时间、地点以恰当的方式表达给恰当的人...  阅读的时候请注意分类,佛曰我日里面是谈笑文章,其他是各个分类的文章,积极的热情投入到写博的队伍中来,支持blogjava做大做强!向dudu站长致敬>> > 我的微博敬请收听

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 坏男孩 阅读(1287) 评论(1)  编辑  收藏 所属分类: java命令学习

FeedBack:
# re: 用Delphi开发JNI(Java Native Interface)应用
2006-12-22 14:46 | ghost
东西真多,慢慢看  回复  更多评论
  

只有注册用户登录后才能发表评论。


网站导航: