Chap10:在 Windows 中实现 Java 本地方法

来源:互联网 发布:虚拟运营商 移动网络 编辑:程序博客网 时间:2024/06/02 18:58

本文为在 32 Windows 平台上实现 Java 本地方法提供了实用的示例、步骤和准则。这些示例包括传递和返回常用的数据类型。

本文中的示例使用 Sun Microsystems公司创建的 JavaDevelopmentKit (JDK)版本 1.1.6  Java本地接口 (JNI)规范 C语言编写的本地代码是用 MicrosoftVisualC++编译器编译生成的。

简介

本文提供调用本地 C代码的 Java 代码示例,包括传递和返回某些常用的数据类型。本地方法包含在特定于平台的可执行文件中。就本文中的示例而言,本地方法包含在Windows 32位动态链接库 (DLL)中。

不过我要提醒您,对 Java外部的调用通常不能移植到其他平台上,在 applet中还可能引发安全异常。实现本地代码将使您的 Java应用程序无法通过 100% Java 测试。但是,如果必须执行本地调用,则要考虑几个准则:

1.     将您的所有本地方法都封装在单个类中,这个类调用单个 DLL。对于每种目标操作系统,都可以用特定于适当平台的版本替换这个 DLL。这样就可以将本地代码的影响减至最小,并有助于将以后所需的移植问题包含在内。

2.     本地方法要简单。尽量将您的 DLL对任何第三方(包括 Microsoft)运行时 DLL的依赖减到最小。使您的本地方法尽量独立,以将加载您的 DLL和应用程序所需的开销减到最小。如果需要运行时 DLL,必须随应用程序一起提供它们。

 

,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,

1.Java调用C

对于调用 C函数的 Java 方法,必须在 Java 类中声明一个本地方法。在本部分的所有示例中,我们将创建一个名为 MyNative的类,并逐步在其中加入新的功能。这强调了一种思想,即将本地方法集中在单个类中,以便将以后所需的移植工作减到最少。

 

示例 1 --传递参数

在第一个示例中,我们将三个常用参数类型传递给本地函数:Stringintboolean。本例说明在本地 C 代码中如何引用这些参数


public class MyNative

{

  public void showParms( String s, int i, boolean b )

  {

    showParms0( s, i , b );

  }

  private native void showParms0( String s, int i, boolean b );

  static

  {

    System.loadLibrary( "MyNative" );

  }

}


请注意,本地方法被声明为专用的,并创建了一个包装方法用于公用目的。这进一步将本地方法同代码的其余部分隔离开来,从而允许针对所需的平台对它进行优化。static子句加载包含本地方法实现的 DLL

下一步是生成 C代码来实现 showParms0方法。此方法的 C 函数原型是通过对 .class文件使用 javah 实用程序来创建的,而 .class 文件是通过编译 MyNative.java文件生成的。这个实用程序可在 JDK中找到。下面是 javah的用法:

 javac MyNative.java(将 .java编译为 .class

 javah -jni MyNative(生成 .h文件) 

 

这将生成一个 MyNative.h文件,其中包含一个本地方法原型,如下所示:

/*

 * Class:     MyNative

 * Method:    showParms0

 * Signature: (Ljava/lang/String;IZ)V

 */

JNIEXPORT void JNICALL Java_MyNative_showParms0

  (JNIEnv *, jobject, jstring, jint, jboolean);

 

第一个参数是调用JNI方法时使用的JNI Environment 指针。第二个参数是指向在此 Java代码中实例化的 Java对象 MyNative的一个句柄。其他参数是方法本身的参数。请注意,MyNative.h包括头文件jni.hjni.h包含JNI API和变量类型(包括jobjectjstringjintjboolean,等等)的原型和其他声明。

本地方法是在文件 MyNative.c中用 C语言实现的:

#include <stdio.h>

#include "MyNative.h"

JNIEXPORT void JNICALL Java_MyNative_showParms0

  (JNIEnv *env, jobject obj, jstring s, jint i, jboolean b)

{

  const char* szStr = (*env)->GetStringUTFChars( env, s, 0 );

  printf( "String = [%s]\n", szStr );

  printf( "int = %d\n", i );

  printf( "boolean = %s\n", (b==JNI_TRUE ? "true" : "false") );

  (*env)->ReleaseStringUTFChars( env, s, szStr );

}

 

JNI APIGetStringUTFChars,用来根据 Java 字符串或 jstring参数创建 C字符串。这是必需的,因为在本地代码中不能直接读取 Java字符串,而必须将其转换为 C字符串或 Unicode。有关转换 Java字符串的详细信息,请参阅标题为 NLS Strings andJNI的一篇论文。但是,jboolean jint值可以直接使用。

MyNative.dll是通过编译 C源文件创建的。下面的编译语句使用 Microsoft Visual C++编译器:

 cl -Ic:\jdk1.1.6\include -Ic:\jdk1.1.6\include\win32 -LD MyNative.c

      -FeMyNative.dll 

 

其中 c:\jdk1.1.6 JDK的安装路径。

MyNative.dll已创建好,现在就可将其用于 MyNative类了。
可以这样测试这个本地方法:在 MyNative类中创建一个 main方法来调用 showParms方法,如下所示:

   public static void main( String[] args )

   {

     MyNative obj = new MyNative();

     obj.showParms( "Hello", 23, true );

     obj.showParms( "World", 34, false );

   }

 

当运行这个 Java应用程序时,请确保 MyNative.dll位于 Windows PATH环境变量所指定的路径中或当前目录下。当执行此 Java程序时,如果未找到这个 DLL,您可能会看到以下的消息:

 java MyNative 

 Can't find class MyNative 

 

这是因为 static子句无法加载这个 DLL,所以在初始化 MyNative类时引发异常。Java解释器处理这个异常,并报告一个一般错误,指出找不到这个类。
如果用 -verbose命令行选项运行解释器,您将看到它因找不到这个 DLL而加载 UnsatisfiedLinkError异常。

如果此 Java程序完成运行,就会输出以下内容:

 java MyNative 

 String = [Hello] 

 int = 23

 boolean = true 

 String = [World] 

 int

      = 34 

 

boolean = false示例2 --返回一个值

本例将说明如何在本地方法中实现返回代码。
将这个方法添加到 MyNative类中,这个类现在变为以下形式:

public class MyNative

{

  public void showParms( String s, int i, boolean b )

  {

    showParms0( s, i , b );

  }

  public int hypotenuse( int a, int b )

  {

    return hyptenuse0( a, b );

  }

  private native void showParms0( String s, int i, boolean b );

  private native int  hypotenuse0( int a, int b );

  static

  {

    System.loadLibrary( "MyNative" );

  }

  /* 测试本地方法 */

  public static void main( String[] args )

  {

    MyNative obj = new MyNative();

    System.out.println( obj.hypotenuse(3,4) );

    System.out.println( obj.hypotenuse(9,12) );

  }

}

 

公用的 hypotenuse方法调用本地方法 hypotenuse0来根据传递的参数计算值,并将结果作为一个整数返回。这个新本地方法的原型是使用 javah生成的。请注意,每次运行这个实用程序时,它将自动覆盖当前目录中的 MyNative.h。按以下方式执行 javah

 javah -jni MyNative 

 

生成的 MyNative.h现在包含 hypotenuse0原型,如下所示:

/*

 * Class:     MyNative

 * Method:    hypotenuse0

 * Signature: (II)I

 */

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *, jobject, jint, jint);

 

该方法是在 MyNative.c源文件中实现的,如下所示:

#include <stdio.h>

#include <math.h>

#include "MyNative.h"

JNIEXPORT void JNICALL Java_MyNative_showParms0

  (JNIEnv *env, jobject obj, jstring s, jint i, jboolean b)

{

  const char* szStr = (*env)->GetStringUTFChars( env, s, 0 );

  printf( "String = [%s]\n", szStr );

  printf( "int = %d\n", i );

  printf( "boolean = %s\n", (b==JNI_TRUE ? "true" : "false") );

  (*env)->ReleaseStringUTFChars( env, s, szStr );

}

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *env, jobject obj, jint a, jint b)

{

  int rtn = (int)sqrt( (double)( (a*a) + (b*b) ) );

  return (jint)rtn;

}

 

再次请注意,jint int值是可互换的。
使用相同的编译语句重新编译这个 DLL

 cl -Ic:\jdk1.1.6\include -Ic:\jdk1.1.6\include\win32 -LD MyNative.c

      -FeMyNative.dll 

 

现在执行 java MyNative将输出 5 15 作为斜边的值。

示例 3 --静态方法

您可能在上面的示例中已经注意到,实例化的 MyNative对象是没必要的。实用方法通常不需要实际的对象,通常都将它们创建为静态方法。本例说明如何用一个静态方法实现上面的示例。更改 MyNative.java中的方法签名,以使它们成为静态方法:

  public static int hypotenuse( int a, int b )

  {

    return hypotenuse0(a,b);

  }

  ...

  private static native int  hypotenuse0( int a, int b );

 

现在运行 javahhypotenuse0创建一个新原型,生成的原型如下所示:

/*

 * Class:     MyNative

 * Method:    hypotenuse0

 * Signature: (II)I

 */

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *, jclass, jint, jint);

 

C 源代码中的方法签名变了,但代码还保持原样:

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *env, jclass cls, jint a, jint b)

{

  int rtn = (int)sqrt( (double)( (a*a) + (b*b) ) );

  return (jint)rtn;

}

 

本质上,jobject参数已变为 jclass参数。此参数是指向 MyNative.class的一个句柄。main方法可更改为以下形式:

  public static void main( String[] args )

  {

    System.out.println( MyNative.hypotenuse( 3, 4 ) );

    System.out.println( MyNative.hypotenuse( 9, 12 ) );

  }

 

因为方法是静态的,所以调用它不需要实例化 MyNative对象。本文后面的示例将使用静态方法。

示例 4 --传递数组

本例说明如何传递数组参数。本例使用一个基本类型,boolean,并将更改数组元素。下一个示例将访问 String(非基本类型)数组。将下面的方法添加到 MyNative.java源代码中:

  public static void setArray( boolean[] ba )

  {

    for( int i=0; i < ba.length; i++ )

      ba[i] = true;

    setArray0( ba );

  }

  ...

  private static native void setArray0( boolean[] ba );

 

在本例中,布尔型数组被初始化为 true,本地方法将把特定的元素设置为 false。同时,在 Java源代码中,我们可以更改 main以使其包含测试代码:

    boolean[] ba = new boolean[5];

    MyNative.setArray( ba );

    for( int i=0; i < ba.length; i++ )

      System.out.println( ba[i] );

 

在编译源代码并执行 javah以后,MyNative.h头文件包含以下的原型:

/*

 * Class:     MyNative

 * Method:    setArray0

 * Signature: ([Z)V

 */

JNIEXPORT void JNICALL Java_MyNative_setArray0

  (JNIEnv *, jclass, jbooleanArray);

 

请注意,布尔型数组是作为单个名为 jbooleanArray的类型创建的。
基本类型有它们自已的数组类型,如 jintArray jcharArray
非基本类型的数组使用 jobjectArray类型。下一个示例中包括一个 jobjectArray。这个布尔数组数组元素是通过JNI方法 GetBooleanArrayElements 来访问的。
针对每种基本类型都有等价的方法。这个本地方法是如下实现的:

JNIEXPORT void JNICALL Java_MyNative_setArray0

  (JNIEnv *env, jclass cls, jbooleanArray ba)

{

  jboolean* pba = (*env)->GetBooleanArrayElements( env, ba, 0 );

  jsize len = (*env)->GetArrayLength(env, ba);

  int i=0;

  // 更改偶数数组元素

  for( i=0; i < len; i+=2 )

    pba[i] = JNI_FALSE;

  (*env)->ReleaseBooleanArrayElements( env, ba, pba, 0 );

}

 

指向布尔型数组的指针可以使用 GetBooleanArrayElements获得。
数组大小可以用 GetArrayLength方法获得。使用ReleaseBooleanArrayElements方法释放数组。现在就可以读取和修改数组元素的值了。jsize声明等价于 jint(要查看它的定义,请参阅 JDK include目录下的jni.h头文件)。

示例 5 --传递Java String数组

本例将通过最常用的非基本类型,Java String,说明如何访问非基本对象的数组。字符串数组被传递给本地方法,而本地方法只是将它们显示到控制台上。
MyNative
类定义中添加了以下几个方法:

  public static void showStrings( String[] sa )

  {

    showStrings0( sa );

  }

  private static void showStrings0( String[] sa );

 

并在 main方法中添加了两行进行测试:

  String[] sa = new String[] { "Hello,", "world!", "JNI", "is", "fun." };

  MyNative.showStrings( sa );

 

本地方法分别访问每个元素,其实现如下所示。

JNIEXPORT void JNICALL Java_MyNative_showStrings0

  (JNIEnv *env, jclass cls, jobjectArray sa)

{

  int len = (*env)->GetArrayLength( env, sa );

  int i=0;

  for( i=0; i < len; i++ )

  {

    jobject obj = (*env)->GetObjectArrayElement(env, sa, i);

    jstring str = (jstring)obj;

    const char* szStr = (*env)->GetStringUTFChars( env, str, 0 );

    printf( "%s ", szStr );

    (*env)->ReleaseStringUTFChars( env, str, szStr );

  }

  printf( "\n" );

}

 

数组元素可以通过GetObjectArrayElement访问。

在本例中,我们知道返回值是 jstring类型,所以可以安全地将它从 jobject类型转换为 jstring类型。字符串是通过前面讨论过的方法打印的。有关在 Windows中处理 Java字符串的信息,请参阅标题为 NLS Strings andJNI的一篇论文。

示例 6 --返回Java String数组

最后一个示例说明如何在本地代码中创建一个字符串数组并将它返回给 Java调用者。MyNative.java中添加了以下几个方法:

  public static String[] getStrings()

  {

    return getStrings0();

  }

  private static native String[] getStrings0();

 

更改 main以使 showStrings getStrings的输出显示出来:

  MyNative.showStrings( MyNative.getStrings() );

 

实现的本地方法返回五个字符串。

JNIEXPORT jobjectArray JNICALL Java_MyNative_getStrings0

  (JNIEnv *env, jclass cls)

{

  jstring      str;

  jobjectArray args = 0;

  jsize        len = 5;

  char*        sa[] = { "Hello,", "world!", "JNI", "is", "fun" };

  int          i=0;

  args = (*env)->NewObjectArray(env, len, (*env)->FindClass(env, "java/lang/String"), 0);

  for( i=0; i < len; i++ )

  {

    str = (*env)->NewStringUTF( env, sa[i] );

    (*env)->SetObjectArrayElement(env, args, i, str);

  }

  return args;

}

 

字符串数组是通过调用 NewObjectArray创建的,同时传递了 String类和数组长度两个参数Java String 是使用 NewStringUTF创建的。String元素是使用 SetObjectArrayElement存入数组中的。

 

 

2.调试

在您已经为您的应用程序创建了一个本地 DLL,但在调试时还要牢记以下几点。如果使用 Java调试器 java_g.exe,则还需要创建 DLL的一个调试版本。这只是表示必须创建同名但带有一个 _g后缀的 DLL 版本。就 MyNative.dll 而言,使用 java_g.exe 要求在 Windows PATH 环境指定的路径中有一个 MyNative_g.dll文件。在大多数情况下,这个 DLL可以通过将原文件重命名或复制为其名称带缀 _g的文件。

现在,Java调试器不允许您进入本地代码,但您可以在 Java环境外使用 C 调试器(如 Microsoft Visual C++)调试本地方法。首先将源文件导入一个项目中。
将编译设置调整为在编译时将 include目录包括在内:

 c:\jdk1.1.6\include;c:\jdk1.1.6\include\win32 

 

将配置设置为以调试模式编译 DLL。在 Project Settings中的 Debug 下,将可执行文件设置为 java.exe(或者 java_g.exe,但要确保您生成了一个 _g.dll文件)。程序参数包括包含 main的类名。如果在 DLL中设置了断点,则当调用本地方法时,执行将在适当的地方停止。

下面是设置一个 Visual C++ 6.0项目来调试本地方法的步骤。

1.      Visual C++中创建一个 Win32 DLL项目,并将 .c .h 文件添加到这个项目中。



·   Tools下拉式菜单的 Options设置下设置 JDK include目录。下面的对话框显示了这些目录。

 

·  选择 Build下拉式菜单下的 Build MyNative.dll来建立这个项目。确保将项目的活动配置设置为调试(这通常是缺省值)。

·   Project Settings下,设置 Debug选项卡来调用适当的 Java解释器,如下所示:

 

当执行这个程序时,忽略 java.exe中找不到任何调试信息的消息。当调用本地方法时,在 C代码中设置的任何断点将在适当的地方停止 Java程序的执行。

 

3.其他信息

JNI 方法和 C++

上面这些示例说明了如何在 C源文件中使用JNI方法。如果使用 C++,则请将相应方法的格式从:

 (*env)->JNIMethod( env, .... ); 

 

更改为:

 env->JNIMethod( ... ); 

 

C++中,JNI函数被看作是 JNIEnv类的成员方法。

字符串和国家语言支持

本文中使用的技术用 UTF方法来转换字符串。使用这些方法只是为了方便起见,如果应用程序需要国家语言支持 (NLS),则不能使用这些方法。有关在 Windows NLS环境中处理 Java 字符串正确方法,请参标题为 NLS Strings andJNI的一篇论文。

4.小结

本文提供的示例用最常用的数据类据(如 jint jstring)说明了如何实现本地方法,并讨论了 Windows特定的几个问题,如显示字符串。本文提供的示例并未包括全部JNIJNI还包括其他参数类型,如 jfloatjdoublejshortjbyte jfieldID,以及用来处理这些类型的方法。有关这个主题的详细信息,请参阅 Sun Microsystems提供的 Java本地接口规范。

5.关于作者:    David Wendt IBM WebSphere Studio的一名程序员,该工作室位于北卡罗莱纳州的 Research Triangle Park。可以通过 wendt@us.ibm.com与他联系。