Windows 中实现 Java 本地方法

来源:互联网 发布:南条爱乃写真集 知乎 编辑:程序博客网 时间:2024/05/22 00:26

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

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

简介

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

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

  1. 将您的所有本地方法都封装在单个类中,这个类调用单个 DLL。对于每种目标操作系统,都可以用特定于适当平台的版本替换这个 DLL。这样就可以将本地代码的影响减至最小,并有助于将以后所需的移植问题包含在内。
  2. 本地方法要简单。尽量将您的 DLL 对任何第三方(包括 Microsoft)运行时 DLL 的依赖减到最小。使您的本地方法尽量独立,以将加载您的 DLL 和应用程序所需的开销减到最小。如果需要运行时 DLL,必须随应用程序一起提供它们。

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.h。jni.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 API,GetStringUTFChars,用来根据 Java 字符串或 jstring 参数创建 C 字符串。这是必需的,因为在本地代码中不能直接读取 Java 字符串,而必须将其转换为 C 字符串或 Unicode。有关转换 Java 字符串的详细信息,请参阅标题为NLS Strings and JNI 的一篇论文。但是,jbooleanjint 值可以直接使用。

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;}

再次请注意,jintint 值是可互换的。使用相同的编译语句重新编译这个 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 );

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

 /* * 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 的类型创建的。基本类型有它们自已的数组类型,如 jintArrayjcharArray。非基本类型的数组使用 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 and JNI 的一篇论文。

示例 6 -- 返回 Java String 数组

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

   public static String[] getStrings()  {    return getStrings0();  }  private static native String[] getStrings0();

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

   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 存入数组中的。

调试

现在您已经为您的应用程序创建了一个本地 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 文件添加到这个项目中。

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

  3. 选择 Build 下拉式菜单下的 Build MyNative.dll 来建立这个项目。确保将项目的活动配置设置为调试(这通常是缺省值)。
  4. 在 Project Settings 下,设置 Debug 选项卡来调用适当的 Java 解释器,如下所示:

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

其他信息

JNI 方法和 C++

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

 (*env)-&gt;JNIMethod( env, .... ); 

更改为:

 env-&gt;JNIMethod( ... ); 

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

字符串和国家语言支持

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

小结

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

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 90后赚不到钱怎么办 处对象感觉好累怎么办 谈朋友感觉好累怎么办 家庭条件不好娶老婆难办怎么办? 异地恋闹矛盾了怎么办 有人威胁要杀我全家怎么办 分手了借我的钱怎么办 脸打架打肿了怎么办 人家不加我qq好友怎么办 一个好友被删了怎么办 dnf脸黑怎么办还有办法 我想登别人微信怎么办 昌硕工资没到怎么办 昌硕离职不批怎么办 昌硕工资不到卡怎么办 上海人去苏州工作社保怎么办 娶个个脾气暴躁的媳妇怎么办 满脸的黑头痘痘怎么办 脚底磨起泡不敢走路怎么办 老婆老是找异性朋友怎么办 喜欢的人有对象怎么办 遇到了更喜欢的怎么办 8岁骨龄11岁怎么办啊 13岁初中生有思想不听话怎么办 交朋友找对象喜欢颜值高的怎么办 儿子找对象不听父母怎么办 缺爱怎么办的搞笑回答 对象说有人追她怎么办 说了一句话媳妇非常生气怎么办 柔顺后头发太臭怎么办 积分兑换手机被骗了怎么办 老板对你的上级不满怎么办 如果老板不给工资怎么办 手机号码被标记为其他公司怎么办? 被标记为骚扰电话怎么办 手机被标记骚扰电话怎么办 360摄像头不支持5g怎么办 摄像头不支持5g网络怎么办 家里的wifi卡了怎么办 办信用卡没有座机号码怎么办 拨打电话时显示号码有误怎么办