当前位置: 首页 > 图灵资讯 > 技术篇> 在 Windows 中实现 Java 本地方法

在 Windows 中实现 Java 本地方法

来源:图灵教育
时间:2024-02-28 17:14:54

本文为在 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 测试。但是,如果必须执行本地调用,则应考虑几个标准:

将您所有的本地方法封装在单个类别中,此类调用单个类别 DLL。对于每个目标操作系统,可以用特定于适当平台的版本替换 DLL。这样可以最大限度地减少本地代码的影响,并帮助包括未来所需的移植问题。 本地方法要简单。尽量将您的 DLL 对任何第三方(包括) Microsoft)运行时 DLL 尽量减少依赖。使您的本地方法尽可能独立,以便加载您 DLL 并尽量减少应用程序所需的费用。若需要操作时 DLL,它们必须与应用程序一起提供。 Java 调用 C

对于调用 C 函数的 Java 方法,必须在 Java 该类声明了一种本地方法。在本部分的所有示例中,我们将创建一个名称 MyNative 类别,并逐渐添加新的功能。这强调了一个想法,即将本地方法集中在单一类别中,以尽量减少未来所需的移植工作。

示例 1 -- 传递参数

在第一个例子中,我们将三种常用参数类型传递给本地函数:String、int 和 boolean。本例说明在本地 C 如何在代码中引用这些参数?

public class MyNative { public void showParms( String s, int i, boolean b ) { showParms( s, i , b ); } private native void showParms( 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: showParms * Signature: (Ljava/lang/String;IZ)V */ JNIEXPORT void JNICALL Java_MyNative_showParms (JNIEnv *, jobject, jstring, jint, jboolean);

第一个参数是调用 JNI 使用方法 JNI Environment 指针。第二个参数是指在这里 Java 实例化代码 Java 对象 MyNative 一个句柄。其它参数是方法本身的参数。请注意,MyNative.h 包括头文件 jni.h。jni.h 包含 JNI API 和变量类型(包括jobjectct)、jstring、jint、jboolean,等等)原型及其它声明。

本地方法是文件 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 一篇论文。但是,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 hyptenuse( a, b ); } private native void showparms0( String s, int i, boolean b ); private native int hypotenuse( 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 调用本地方法的方法 hypotenuse 根据传输的参数计算值,并将结果作为整数返回。本地新方法的原型是使用 javah 生成。请注意,这个实用程序每次运行时,都会自动覆盖当前目录中的内容。 MyNative.h。以下方式执行 javah:

javah -jni MyNative

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

/* * Class: MyNative * Method: hypotenuse * Signature: (II)I */ JNIEXPORT jint JNICALL Java_MyNative_hypotenuse (JNIEnv *, jobject, jint, jint);

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

#include <stdio.h> #include <math.h> #include "MyNative.h" JNIEXPORT void JNICALL Java_MyNativeshowParms00 (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_hypotenuse (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 hypotenuse(a,b); } ... private static native int hypotenuse( int a, int b );

现在运行 javah 为 hypotenuse 创建新的原型,生成的原型如下:

/* * Class: MyNative * Method: hypotenuse * Signature: (II)I */ JNIEXPORT jint JNICALL Java_MyNative_hypotenuse (JNIEnv *, jclass, jint, jint);

C 源代码中的方法签名发生了变化,但代码仍然保持不变:

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse (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_setArray000 (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_MyNativeshowStrings000 (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 以使 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 存储在数组中。

调试

现在你已经为你的应用程序创建了一个本地应用程序 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 本项目调试本地方法的步骤。

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

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

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

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

JNI的其他信息 方法和 C++

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

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

更改为:

env->JNIMethod( ... );

在 C++ 中,JNI 函数被认为是 JNIEnv 类成员方法。

字符串和国家语言支持

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

小结

本文提供的示例使用最常用的数据类据(如 jint 和 jstring)解释了如何实现本地方法,并讨论了如何实现本地方法 Windows 显示字符串等具体问题。本文提供的示例不包括所有示例 JNI,JNI 还包括其他参数类型,如 jfloat、jdouble、jshort、jbyte 和 jfieldID,以及处理这些类型的方法。请参考本主题的详细信息 Sun Microsystems 提供的 Java 本地界面规范。