用JNI进行Java编程

关于本教程

本教程是关于什么的?

Java 本机接口(Java Native Interface (JNI))是一个本机编程接口,它是 Java 软件开发工具箱(Java Software Development Kit (SDK))的一部分。JNI 允许 Java 代码使用以其它语言(譬如 C C++)编写的代码和代码库。Invocation APIJNI 的一部分)可以用来将 Java 虚拟机(JVM)嵌入到本机应用程序中,从而允许程序员从本机代码内部调用 Java 代码。

本教程涉及 JNI 最常见的两个应用:从 Java 程序调用 C/C++,以及从 C/C++ 程序调用 Java 代码。我们将讨论 Java 本机接口的这两个基本部分以及可能出现的一些更高级的编程难题。

我应该学习本教程吗?

本教程将带您去了解使用 Java 本机接口的所有步骤。您将学习如何从 Java 应用程序内部调用本机 C/C++ 代码以及如何从本机 C/C++ 应用程序内部调用 Java 代码。

所有示例都是使用 JavaC C++ 代码编写的,并可以移植到 Windows 和基于 UNIX 的平台上。要完全理解这些示例,您必须有一些 Java 语言编程经验。此外,您还需要一些 C C++ 编程经验。严格来说,JNI 解决方案可以分成 Java 编程任务和 C/C++ 编程任务,由不同的程序员完成每项任务。然而,要完全理解 JNI 是如何在两种编程环境中工作的,您必须能够理解 Java C/C++ 代码。

我们还将讲述一些高级主题,包括本机方法的异常处理和多线程。要充分理解本教程,您应该熟悉 Java 平台的安全性模型,并有一些多线程应用程序开发的经验。

这里将关于高级主题的节从较基本的循序渐进 JNI 简介中划分出来。现在,初级 Java 程序员可以先学习本教程的前两部分,掌握之后再开始学习高级主题。

请参阅参考资料,那里有关于本文所提到的教程、文章和其它一些参考书目的清单。

工具与组件

要运行本教程中的示例,您需要下列工具与组件:

  • Java 编译器:随 SDK 一起提供的 javac.exe
  • Java 虚拟机(JVM:随 SDK 一起提供的 java.exe
  • 本机方法 C 文件生成器:随 SDK 一起提供的 javah.exe
  • 定义 JNI 库文件和本机头文件jni.h C 头文件、jvm.lib jvm.dll jvm.so 文件,这些文件都是随 SDK 一起提供的。
  • 能够创建共享库的 C C++ 编译器。最常见的两个 C 编译器是用于 Windows Visual C++ 和用于基于 UNIX 系统的 cc

虽然您可以使用自己喜欢的任何开发环境,但我们将在本教程中使用示例是用随 SDK 一起提供的标准工具和组件编写的。请参阅参考资料来下载 SDK、完整的源文件以及对于完成本教程不可缺少的其它工具。本教程具体地解释了 Sun JNI 实现,该实现被认为是 JNI 解决方案的标准。本教程中没有讨论其它 JNI 实现的详细信息。

其它注意事项

Java 2 SDK 中,JVM 和运行时支持位于名为 jvm.dllWindows)或 libjvm.soUNIX)的共享库文件中。在 Java 1.1 JDK 中,JVM 和运行时支持位于名为 javai.dllWindows)或 libjava.soUNIX)的共享库文件中。版本 1.1 的共享库包含运行时以及类库的一些本机方法,但在版本 1.2 中已经不包含运行时,并且本机方法被放在 java.dll libjava.so 中。对于以下 Java 代码,这一变化很重要:

  • 代码是用非 JNI 本机方法编写的(因为使用了 JDK 1.0 中旧的本机方法接口)
  • 通过 JNI Invocation 接口使用了嵌入式 JVM

在两种情况下,在您的本机库能与版本 1.2 一起使用之前,都必须重新链接它们。注:这个变化应该不影响 JNI 程序员实现本机方法只有通过 Invocation API调用 JVM JNI 代码才会受到影响。

如果使用随 SDK/JDK 一起提供的 jni.h 文件,则头文件将使用 SDK/JDK 安装目录中的缺省 JVMjvm.dll libjvm.so)。支持 JNI Java 平台的任何实现都会这么做,或允许您指定 JVM 共享库;然而,完成这方面操作的细节可能会因具体 Java 平台/JVM 实现而有所不同。实际上,许多 JVM 实现根本不支持 JNI

关于作者

Scott Stricker IBM Global Services 下属的 Business Innovation Services 的企业应用程序开发人员。他的专长是面向对象技术,尤其是 Java C++ 编程。

Scott 拥有美国辛辛那提大学计算机科学理学学士学位。他是“Sun 认证的 Java 2 程序员与开发者(Sun Certified Java 2 Programmer and Developer。可通过 sstricke@us.ibm.com 联系 Scott

Java调用C++代码

概述

当无法用 Java 语言编写整个应用程序时,JNI 允许您使用本机代码。在下列典型情况下,您可能决定使用本机代码:

  • 希望用更低级、更快的编程语言去实现对时间有严格要求的代码。
  • 希望从 Java 程序访问旧代码或代码库。
  • 需要标准 Java 类库中不支持的依赖于平台的特性。

Java 代码调用 C/C++ 的六个步骤

Java 程序调用 C C ++ 代码的过程由六个步骤组成。我们将在下面几页中深入讨论每个步骤,但还是先让我们迅速地浏览一下它们。

  1. 编写 Java 代码。我们将从编写 Java 类开始,这些类执行三个任务:声明将要调用的本机方法;装入包含本机代码的共享库;然后调用该本机方法。
  2. 编译 Java 代码。在使用 Java 类之前,必须成功地将它们编译成字节码。
  3. 创建 C/C++ 头文件C/C++ 头文件将声明想要调用的本机函数说明。然后,这个头文件与 C/C++ 函数实现(请参阅步骤 4)一起来创建共享库(请参阅步骤 5)。
  4. 编写 C/C++ 代码。这一步实现 C C++ 源代码文件中的函数。C/C++ 源文件必须包含步骤 3 中创建的头文件。
  5. 创建共享库文件。从步骤 4 中创建的 C 源代码文件来创建共享库文件。
  6. 运行 Java 程序。运行该代码,并查看它是否有用。我们还将讨论一些用于解决常见错误的技巧。

步骤 1:编写 Java 代码

我们从编写 Java 源代码文件开始,它将声明本机方法(或方法),装入包含本机代码的共享库,然后实际调用本机方法。

这里是名为 Sample1.java Java 源代码文件的示例:


   
   
    
     
   
   
 1. public class Sample1
   
   
 2. {
   
   
 3.   public native int intMethod(int n);
   
   
 4.   public native boolean booleanMethod(boolean bool);
   
   
 5.   public native String stringMethod(String text);
   
   
 6.   public native int intArrayMethod(int[] intArray);
   
   
 7.
   
   
 8.   public static void main(String[] args)
   
   
 9.   {
   
   
10.     System.loadLibrary("Sample1");
   
   
11.     Sample1 sample = new Sample1();
   
   
12.     int     square = sample.intMethod(5);
   
   
13.     boolean bool   = sample.booleanMethod(true);
   
   
14.     String  text   = sample.stringMethod("JAVA");
   
   
15.     int     sum    = sample.intArrayMethod(
   
   
16.                         new int[]{1,1,2,3,5,8,13} );
   
   
17.
   
   
18.     System.out.println("intMethod: " + square);
   
   
19.     System.out.println("booleanMethod: " + bool);
   
   
20.     System.out.println("stringMethod: " + text);
   
   
21.     System.out.println("intArrayMethod: " + sum);
   
   
22.   }
   
   
23. }
   
   

这段代码做了些什么?

首先,请注意对 native 关键字的使用,它只能随方法一起使用。native 关键字告诉 Java 编译器:方法是用 Java 类之外的本机代码实现的,但其声明却在 Java 中。只能在 Java 类中声明本机方法,而不能实现它,所以本机方法不能拥有方法主体。

现在,让我们逐行研究一下代码:

  • 从第 3 行到第 6 行,我们声明了四个 native 方法。
  • 在第 10 行,我们装入了包含这些本机方法的实现的共享库文件。(到步骤 5 时,我们将创建该共享库文件。)
  • 最终,从第 12 行到第 15 行,我们调用了本机方法。注:这个操作和调用非本机 Java 方法的操作没有差异。

:基于 UNIX 的平台上的共享库文件通常含有前缀“lib”。在本例中,第 10 行可能是 System.loadLibrary("libSample1");。请一定要注意您在步骤 5:创建共享库文件中生成的共享库文件名。

步骤 2:编译 Java 代码

接下来,我们需要将 Java 代码编译成字节码。完成这一步的方法之一是使用随 SDK 一起提供的 Java 编译器 javac。用来将 Java 代码编译成字节码的命令是:

javac Sample1.java
   
   

步骤 3:创建 C/C++ 头文件

第三步是创建 C/C++ 头文件,它定义本机函数说明。完成这一步的方法之一是使用 javah.exe,它是随 SDK 一起提供的本机方法 C 存根生成器工具。这个工具被设计成用来创建头文件,该头文件为在 Java 源代码文件中所找到的每个 native 方法定义 C 风格的函数。这里使用的命令是:

 javah Sample1 
  
  

Sample1.java 上运行 javah.exe 的结果

下面的 Sample1.h 是对我们的 Java 代码运行 javah 工具所生成的 C/C++ 头文件:

 1. /* DO NOT EDIT THIS FILE - it is machine generated */
   
   
 2. #include <jni.h>
   
   
 3. /* Header for class Sample1 */
   
   
 4. 
   
   
 5. #ifndef _Included_Sample1
   
   
 6. #define _Included_Sample1
   
   
 7. #ifdef __cplusplus
   
   
 8. extern "C" {
   
   
 9. #endif
   
   
10.
   
   
11. JNIEXPORT jint JNICALL Java_Sample1_intMethod
   
   
12.   (JNIEnv *, jobject, jint);
   
   
13.
   
   
14. JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
   
   
15.   (JNIEnv *, jobject, jboolean);
   
   
16. 
   
   
17. JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
   
   
18.  (JNIEnv *, jobject, jstring);
   
   
19.
   
   
20. JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
   
   
21.  (JNIEnv *, jobject, jintArray);
   
   
22. 
   
   
23. #ifdef __cplusplus
   
   
24. }
   
   
25. #endif
   
   
26. #endif
   
   

  
  
   
    
  
  

关于 C/C++ 头文件

正如您可能已经注意到的那样,Sample1.h 中的 C/C++ 函数说明和 Sample1.java 中的 Java native 方法声明有很大差异。JNIEXPORT JNICALL 是用于导出函数的、依赖于编译器的指示符。返回类型是映射到 Java 类型的 C/C++ 类型。附录 AJNI 类型中完整地说明了这些类型。

除了 Java 声明中的一般参数以外,所有这些函数的参数表中都有一个指向 JNIEnv jobject 的指针。指向 JNIEnv 的指针实际上是一个指向函数指针表的指针。正如将要在步骤 4 中看到的,这些函数提供各种用来在 C C++ 中操作 Java 数据的能力。

jobject 参数引用当前对象。因此,如果 C C++ 代码需要引用 Java 函数,则这个 jobject 充当引用或指针,返回调用的 Java 对象。函数名本身是由前缀Java_加全限定类名,再加下划线和方法名构成的。

步骤 4:编写 C/C++ 代码

当谈到编写 C/C++ 函数实现时,有一点需要牢记:说明必须和 Sample1.h 的函数声明完全一样。我们将研究用于 C 实现和 C++ 实现的完整代码,然后讨论两者之间的差异。

C 函数实现

以下是 Sample1.c,它是用 C 编写的实现:

 1. #include "Sample1.h"
   
   
 2. #include <string.h>
   
   
 3. 
   
   
 4. JNIEXPORT jint JNICALL Java_Sample1_intMethod
   
   
 5.   (JNIEnv *env, jobject obj, jint num) {
   
   
 6.    return num * num;
   
   
 7. }
   
   
 8. 
   
   
 9. JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
   
   
10.   (JNIEnv *env, jobject obj, jboolean boolean) {
   
   
11.   return !boolean;
   
   
12. }
   
   
13.
   
   
14. JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
   
   
15.   (JNIEnv *env, jobject obj, jstring string) {
   
   
16.     const char *str = (*env)->GetStringUTFChars(env, string, 0);
   
   
17.     char cap[128];
   
   
18.     strcpy(cap, str);
   
   
19.     (*env)->ReleaseStringUTFChars(env, string, str);
   
   
20.     return (*env)->NewStringUTF(env, strupr(cap));
   
   
21. }
   
   
22. 
   
   
23. JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
   
   
24.   (JNIEnv *env, jobject obj, jintArray array) {
   
   
25.     int i, sum = 0;
   
   
26.     jsize len = (*env)->GetArrayLength(env, array);
   
   
27.     jint *body = (*env)->GetIntArrayElements(env, array, 0);
   
   
28.     for (i=0; i<len; i++)
   
   
29.     {        sum += body[i];
   
   
30.     }
   
   
31.     (*env)->ReleaseIntArrayElements(env, array, body, 0);
   
   
32.     return sum;
   
   
33. }
   
   
34. 
   
   
35. void main(){}
   
   

C++ 函数实现

以下是 Sample1.cppC++ 实现)

 1. #include "Sample1.h"
   
   
 2. #include <string.h>
   
   
 3.
   
   
 4.JNIEXPORT jint JNICALL Java_Sample1_intMethod
   
   
 5.  (JNIEnv *env, jobject obj, jint num) {
   
   
 6.   return num * num;
   
   
 7. }
   
   
 8.
   
   
 9. JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
   
   
10.   (JNIEnv *env, jobject obj, jboolean boolean) {
   
   
11.   return !boolean;
   
   
12. }
   
   
13.
   
   
14. JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
   
   
15.   (JNIEnv *env, jobject obj, jstring string) {
   
   
16.     const char *str = env->GetStringUTFChars(string, 0);
   
   
17.     char cap[128];
   
   
18.     strcpy(cap, str);
   
   
19.     env->ReleaseStringUTFChars(string, str);
   
   
20.     return env->NewStringUTF(strupr(cap));
   
   
21. }
   
   
22. 
   
   
23. JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
   
   
24.   (JNIEnv *env, jobject obj, jintArray array) {
   
   
25.     int i, sum = 0;
   
   
26.     jsize len = env->GetArrayLength(array);
   
   
27.     jint *body = env->GetIntArrayElements(array, 0);
   
   
28.     for (i=0; i<len; i++)
   
   
29.     {        sum += body[i];
   
   
30.     }
   
   
31.     env->ReleaseIntArrayElements(array, body, 0);
   
   
32.     return sum;
   
   
33. }
   
   
34.
   
   
35. void main(){}
   
   

C C++ 函数实现的比较

C C++ 代码几乎相同;唯一的差异在于用来访问 JNI 函数的方法。在 C 中,JNI 函数调用由(*env)->作前缀,目的是为了取出函数指针所引用的值。在 C++ 中,JNIEnv 类拥有处理函数指针查找的内联成员函数。下面将说明这个细微的差异,其中,这两行代码访问同一函数,但每种语言都有各自的语法。

C 语法:

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

C++ 语法:

jsize len =env->GetArrayLength(array);

步骤 5:创建共享库文件

接下来,我们创建包含本机代码的共享库文件。大多数 C C++ 编译器除了可以创建机器代码可执行文件以外,也可以创建共享库文件。用来创建共享库文件的命令取决于您使用的编译器。下面是在 Windows Solaris 系统上执行的命令。

Windows

cl -Ic:/jdk/include -Ic:/jdk/include/win32 -LD Sample1.c -FeSample1.dll

Solaris

cc -G -I/usr/local/jdk/include -I/user/local/jdk/include/solaris Sample1.c -o Sample1.so

步骤 6:运行 Java 程序

最后一步是运行 Java 程序,并确保代码正确工作。因为必须在 Java 虚拟机中执行所有 Java 代码,所以需要使用 Java 运行时环境。完成这一步的方法之一是使用 java,它是随 SDK 一起提供的 Java 解释器。所使用的命令是:

java Sample1
   
   

当运行 Sample1.class 程序时,应该获得下列结果:

PROMPT>java Sample1
   
   
intMethod: 25
   
   
booleanMethod: false
   
   
stringMethod: JAVA
   
   
intArrayMethod: 33
   
   

   
   
    
     
   
   
PROMPT>
   
   

故障排除

当使用 JNI Java 程序访问本机代码时,您会遇到许多问题。您会遇到的三个最常见的错误是:

  • 无法找到动态链接。它所产生的错误消息是:java.lang.UnsatisfiedLinkError。这通常指无法找到共享库,或者无法找到共享库内特定的本机方法。
  • 无法找到共享库文件。当用 System.loadLibrary(String libname) 方法(参数是文件名)装入库文件时,请确保文件名拼写正确以及没有指定扩展名。还有,确保库文件的位置在类路径中,从而确保 JVM 可以访问该库文件。
  • 无法找到具有指定说明的方法。确保您的 C/C++ 函数实现拥有与头文件中的函数说明相同的说明。

结束语

Java 调用 C C++ 本机代码(虽然不简单)是 Java 平台中一种良好集成的功能。虽然 JNI 支持 C C++,但 C++ 接口更清晰一些并且通常比 C 接口更可取。

正如您已经看到的,调用 C C++ 本机代码需要赋予函数特殊的名称,并创建共享库文件。当利用现有代码库时,更改代码通常是不可取的。要避免这一点,在 C++ 中,通常创建代理代码或代理类,它们有专门的 JNI 所需的命名函数。然后,这些函数可以调用底层库函数,这些库函数的说明和实现保持不变。

C/C++调用Java代码

概述

JNI 允许您从本机代码内调用 Java 类方法。要做到这一点,通常必须使用 Invocation API 在本机代码内创建和初始化一个 JVM。下列是您可能决定从 C/C++ 代码调用 Java 代码的典型情况:

  • 希望实现的这部分代码是平台无关的,它将用于跨多种平台使用的功能。
  • 需要在本机应用程序中访问用 Java 语言编写的代码或代码库。
  • 希望从本机代码利用标准 Java 类库。

C/C++ 程序调用 Java 代码的四个步骤

C/C++ 调用 Java 方法过程的四个步骤如下:

  1. 编写 Java 代码。这个步骤包含编写一个或多个 Java 类,这些类实现(或调用其它方法实现)您想要访问的功能。
  2. 编译 Java 代码。在能够使用这些 Java 类之前,必须成功地将它们编译成字节码。
  3. 编写 C/C++ 代码。这个代码将创建和实例化 JVM,并调用正确的 Java 方法。
  4. 运行本机 C/C++ 应用程序。将运行应用程序以查看它是否正常工作。我们还将讨论一些用于处理常见错误的技巧。

步骤 1:编写 Java 代码

我们从编写一个或多个 Java 源代码文件开始,这些文件将实现我们想要本机 C/C++ 代码使用的功能。

下面显示了一个 Java 代码示例 Sample2.java

 1. public class Sample2
   
   
 2. {
   
   
 3.   public static int intMethod(int n) {
   
   
 4.       return n*n;
   
   
 5.   }
   
   
 6.
   
   
 7.   public static boolean booleanMethod(boolean bool) {
   
   
 8.        return !bool;
   
   
 9.   }
   
   
10. }
   
   

注:Sample2.java 实现了两个 static Java 方法:intMethod(int n) booleanMethod(boolean bool)(分别在第 3 行和第 7 行)。static 方法是一种不需要与对象实例关联的类方法。调用 static 方法要更容易些,因为不必实例化对象来调用它们。

步骤 2:编译 Java

接下来,我们将 Java 代码编译成字节码。完成这一步的方法之一是使用随 SDK 一起提供的 Java 编译器 javac。使用的命令是:

javac Sample1.java
   
   

步骤 3:编写 C/C++ 代码

即使是在本机应用程序中运行,所有 Java 字节码也必须在 JVM 中执行。因此 C/C++ 应用程序必须包含用来创建和初始化 JVM 的调用。为了方便我们,SDK 包含了作为共享库文件(jvm.dll jvm.so)的 JVM,这个库文件可以嵌入到本机应用程序中。

让我们先从浏览一下 C C++ 应用程序的整个代码开始,然后对两者进行比较。

带有嵌入式 JVM C 应用程序

Sample2.c 是一个带有嵌入式 JVM 的简单的 C 应用程序:

 1. #include <jni.h>
   
   
 2.
   
   
 3. #ifdef _WIN32
   
   
 4. #define PATH_SEPARATOR ';'
   
   
 5. #else
   
   
 6. #define PATH_SEPARATOR ':'
   
   
 7. #endif
   
   
 8.
   
   
 9. int main()
   
   
10. {
   
   
11.   JavaVMOption options[1];
   
   
12.   JNIEnv *env;
   
   
13.   JavaVM *jvm;
   
   
14.   JavaVMInitArgs vm_args;
   
   
15.   long status;
   
   
16.   jclass cls;
   
   
17.   jmethodID mid;
   
   
18.   jint square;
   
   
19.   jboolean not;
   
   
20.
   
   
21.   options[0].optionString = "-Djava.class.path=.";
   
   
22.   memset(&vm_args, 0, sizeof(vm_args));
   
   
23.   vm_args.version = JNI_VERSION_1_2;
   
   
24.   vm_args.nOptions = 1;
   
   
25.   vm_args.options = options;
   
   
26.   status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
   
   
27.
   
   
28.   if (status != JNI_ERR)
   
   
29.   {
   
   
30.     cls = (*env)->FindClass(env, "Sample2");
   
   
31.     if(cls !=0)
   
   
32.     { mid = (*env)->GetStaticMethodID(env, cls, "intMethod", "(I)I");
   
   
33.       if(mid !=0)
   
   
34.       { square = (*env)->CallStaticIntMethod(env, cls, mid, 5);
   
   
35.                            printf("Result of intMethod: %d/n", square);
   
   
36.       }
   
   
37.
   
   
38.       mid = (*env)->GetStaticMethodID(env, cls, "booleanMethod", "(Z)Z");
   
   
39.       if(mid !=0)
   
   
40.       { not = (*env)->CallStaticBooleanMethod(env, cls, mid, 1);
   
   
41.         printf("Result of booleanMethod: %d/n", not);
   
   
42.       }
   
   
43.     }
   
   
44.
   
   
45.     (*jvm)->DestroyJavaVM(jvm);
   
   
46.     return 0;
   
   
47/   }
   
   
48.   else
   
   
49.   return -1;
   
   
50. }
   
   

带有嵌入式 JVM C++ 应用程序

Sample2.cpp 是一个带有嵌入式 JVM C++ 应用程序:

 1. #include <jni.h>
   
   
 2.
   
   
 3. #ifdef _WIN32
   
   
 4. #define PATH_SEPARATOR ';'
   
   
 5. #else
   
   
 6. #define PATH_SEPARATOR ':'
   
   
 7. #endif
   
   
 8.
   
   
 9. int main()
   
   
10. {
   
   
11.       JavaVMOption options[1];
   
   
12.       JNIEnv *env;
   
   
13.       JavaVM *jvm;
   
   
14.       JavaVMInitArgs vm_args;
   
   
15.       long status;
   
   
16.       jclass cls;
   
   
17.       jmethodID mid;
   
   
18.       jint square;
   
   
19.       jboolean not;
   
   
20.
   
   
21.       options[0].optionString = "-Djava.class.path=.";
   
   
22.       memset(&vm_args, 0, sizeof(vm_args));
   
   
23.       vm_args.version = JNI_VERSION_1_2;
   
   
24.       vm_args.nOptions = 1;
   
   
25.       vm_args.options = options;
   
   
26.       status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
   
   
27.
   
   
28.       if (status != JNI_ERR)
   
   
29.   {
   
   
30.         cls = (*env)->FindClass(env, "Sample2");
   
   
31.         if(cls !=0)
   
   
32.     {   mid = (*env)->GetStaticMethodID(env, cls, "intMethod", "(I)I");
   
   
33.             if(mid !=0)
   
   
34.             {  square = (*env)->CallStaticIntMethod(env, cls, mid, 5);
   
   
35.                   printf("Result of intMethod: %d/n", square);
   
   
36.             }
   
   
37.
   
   
38.             mid = (*env)->GetStaticMethodID(env, cls, "booleanMethod", "(Z)Z");
   
   
39.             if(mid !=0)
   
   
40.             {  not = (*env)->CallStaticBooleanMethod(env, cls, mid, 1);
   
   
41.                   printf("Result of booleanMethod: %d/n", not);
   
   
42.             }
   
   
43.     }
   
   
44.
   
   
45.         (*jvm)->DestroyJavaVM(jvm);
   
   
46.    return 0;
   
   
47.   }
   
   
48.       else
   
   
49.         return -1;
   
   
50. }
   
   

C C++ 实现的比较

C C++ 代码几乎相同;唯一的差异在于用来访问 JNI 函数的方法。在 C 中,为了取出函数指针所引用的值,JNI 函数调用前要加一个 (*env)-> 前缀。在 C++ 中,JNIEnv 类拥有处理函数指针查找的内联成员函数。因此,虽然这两行代码访问同一函数,但每种语言都有各自的语法,如下所示。

C 语法:

cls = (*env)->FindClass(env, "Sample2");

C++ 语法:

cls = env->FindClass("Sample2");

C 应用程序更深入的研究

我们刚才编写了许多代码,但它们都做些什么呢?在执行步骤 4 之前,让我们更深入地研究一下 C 应用程序的代码。我们将先浏览一些必要的步骤,包括准备本机应用程序以处理 Java 代码、将 JVM 嵌入本机应用程序,然后从该应用程序内找到并调用 Java 方法。

包括 jni.h 文件

我们从 C 应用程序中所包括的 jni.h C 头文件开始,如下面的代码样本中所示:

#include <jni.h>
   
   

jni.h 文件包含在 C 代码中所需要的 JNI 的所有类型和函数定义。

声明变量

接下来,声明所有希望在程序中使用的变量。JavaVMOption options[] 具有用于 JVM 的各种选项设置。当声明变量时,确保所声明的 JavaVMOption options[] 数组足够大,以便能容纳您希望使用的所有选项。在本例中,我们使用的唯一选项就是类路径选项。因为在本示例中,我们所有的文件都在同一目录中,所以将类路径设置成当前目录。可以设置类路径,使它指向任何您希望使用的目录结构。

以下代码声明了用于 Sample2.c 的变量:

JavaVMOption options[1];

JNIEnv *env;

JavaVM *jvm;

JavaVMInitArgs vm_args;

注:

  • JNIEnv *env 表示 JNI 执行环境。
  • JavaVM jvm 是指向 JVM 的指针。我们主要使用这个指针来创建、初始化和销毁 JVM
  • JavaVMInitArgs vm_args 表示可以用来初始化 JVM 的各种 JVM 参数。

设置初始化参数

JavaVMInitArgs 结构表示用于 JVM 的初始化参数。在执行 Java 代码之前,可以使用这些参数来定制运行时环境。正如您所见,这些选项是一个参数而 Java 版本是另一个参数。按如下所示设置了这些参数:

vm_args.version = JNI_VERSION_1_2;
   
   
vm_args.nOptions = 1;
   
   
vm_args.options = options;
   
   

设置类路径

接下来,为 JVM 设置类路径,以使它能找到所需要的 Java 类。在这个特定示例中,因为 Sample2.class Sample2.exe 都位于同一目录中,所以将类路径设置成当前目录。我们用来为 Sample2.c 设置类路径的代码如下所示:

options[0].optionString = "-Djava.class.path=."; 
   
   
// same text as command-line options for the java.exe JVM
   
   

vm_args 留出内存

在可以使用 vm_args 之前,必需为它留出一些内存。一旦设置了内存,就可以设置版本和选项参数了,如下所示:

    memset(&vm_args, 0, sizeof(vm_args));  // set aside enough memory for vm_args
   
   
    vm_args.version = JNI_VERSION_1_2;         // version of Java platform
   
   
    vm_args.nOptions = 1;                      // same as size of options[1]
   
   
    vm_args.options = options;
   
   

创建 JVM

处理完所有设置之后,现在就准备创建 JVM 了。先从调用方法开始:

JNI_CreateJavaVM(JavaVM **jvm, void** env, JavaVMInitArgs **vm_args)
   
   

如果成功,则这个方法返回零,否则,如果无法创建 JVM,则返回 JNI_ERR

查找并装入 Java

一旦创建了 JVM 之后,就可以准备开始在本机应用程序中运行 Java 代码。首先,需要使用 FindClass() 函数查找并装入 Java 类,如下所示:

cls = (*env)->FindClass(env, "Sample2");
   
   

cls 变量存储执行 FindClass() 函数后的结果。如果找到该类,则 cls 变量表示该 Java 类的句柄。如果不能找到该类,则 cls 将为零。

查找 Java 方法

接下来,我们希望用 GetStaticMethodID() 函数在该类中查找某个方法。我们希望查找方法 intMethod,它接收一个 int 参数并返回一个 int。以下是查找 intMethod 的代码:

mid = (*env)->GetStaticMethodID(env, cls, "intMethod", "(I)I");
   
   

mid 变量存储执行 GetStaticMethodID() 函数后的结果。如果找到了该方法,则 mid 变量表示该方法的句柄。如果不能找到该方法,则 mid 将为零。

请记住,在本示例中,我们正在调用 static Java 方法。那就是我们使用 GetStaticMethodID() 函数的原因。GetMethodID() 函数与 GetStaticMethodID() 函数的功能一样,但它用来查找实例方法。

如果正在调用构造器,则方法的名称为“<init>”。要了解更多关于调用构造器的知识,请参阅错误处理。要了解更多关于用来指定参数类型的代码以及关于如何将 JNI 类型映射到 Java 原始类型的知识,请参阅附录。

调用 Java 方法

最后,我们调用 Java 方法,如下所示:

square = (*env)->CallStaticIntMethod(env, cls, mid, 5);
   
   

CallStaticIntMethod() 方法接受 cls(表示类)、mid(表示方法)以及用于该方法一个或多个参数。在本例中参数是 int 5

您还会遇到 CallStaticXXXMethod() CallXXXMethod() 之类的方法。这些方法分别调用静态方法和成员方法,用方法的返回类型(例如,ObjectBooleanByteCharIntLong 等等)代替变量 XXX

步骤 4:运行应用程序

现在准备运行这个 C 应用程序,并确保代码正常工作。当运行 Sample2.exe 时,应该可以得到如下结果:

PROMPT>Sample2
   
   
Result of intMethod: 25
   
   
Result of booleanMethod: 0
   
   

   
   
    
     
   
   
PROMPT>
   
   

故障排除

JNI Invocation API 有点麻烦,因为它是用 C 语言定义的,而 C 语言基本上不支持面向对象编程。结果是,它很容易遇到问题。下面是一份检查表,它可能有助于您避免一些较常见的错误。

  • 请总是确保正确设置了引用。例如,当使用 JNI_CreateJavaVM() 方法创建 JVM 时,确保它返回零。还请确保,在使用 FindClass() GetMethodID() 方法之前,它们的引用设置不是零。
  • 请检查方法名是否拼写正确以及是否适当地转换了方法说明。还请确保,对静态方法使用了 CallStaticXXXMethod() 以及对成员方法使用了 CallXXXMethod()
  • 确保使用任何 Java 类所需的特殊的参数或选项来初始化 JVM。例如,如果 Java 类需要大量内存,则可能需要增加堆的最大大小选项。
  • 请总是确保正确设置了类路径。使用嵌入式 JVM 的本机应用程序必须能够找到 jvm.dll jvm.so 共享库。

结束语

尽管从 C 调用 Java 方法确实需要相当高级的类面向对象编程技术,但这对经验丰富的 C 程序员而言相对比较简单。尽管 JNI 支持 C C++,但 C++ 接口要更清晰些,通常比 C 接口更可取。

要记住很重要的一点是:可以用单个 JVM 来装入和执行多个类和方法。如果每次从本机代码与 Java 交互都创建和销毁 JVM,则会浪费资源并降低性能。

高级主题

概述

Java 程序内调用本机代码破坏了 Java 程序的可移植性和安全性。尽管已编译的 Java 字节码保持了很好的可移植性,但必须为您打算用来运行该应用程序的每个平台重新编译本机代码。另外,由于本机代码在 JVM 之外执行,所以约束它的安全性协议不必和 Java 代码的相同。

从本机程序调用 Java 代码也很复杂。因为 Java 语言是面向对象的,所以从本机应用程序调用 Java 代码通常涉及面向对象技术。有些本机语言不支持面向对象编程或只是有限地支持面向对象编程(譬如 C),使用这些语言调用 Java 方法可能会产生问题。在本节中,我们将讨论使用 JNI 所带来的若干复杂性,并研究解决它们的方法。

Java 字符串 vs. C 字符串

Java 字符串是作为 16 Unicode 字符存储的,而 C 字符串是作为一组 8 位且以空字符为结束的字符存储的。JNI 提供了几个有用的函数,它们用于在 Java 字符串和 C 字符串之间进行转换并操作这两种字符串。下面的代码片段演示了如何将 C 字符串转换成 Java 字符串:

 1. /* Convert a C string to a Java String. */
   
   
 2. char[]  str  = "To be or not to be./n";
   
   
 3. jstring jstr = (*env)->NewStringUTF(env, str);
   
   

接下来,我们研究将 Java 字符串转换成 C 字符串的代码。请注意第 5 行对 ReleaseStringUTFChars() 函数的调用。当您不再使用 Java 字符串时,应该使用这个函数来释放它们。当本机代码不再需要引用字符串时,请总是确保释放它们。不这样做可能导致内存泄漏。

 1. /* Convert a Java String into a C string. */
   
   
 2. char buf[128; 
   
   
 3. const char *newString = (*env)->GetStringUTFChars(env, jstr, 0);
   
   
 4. ...
   
   
 5. (*env)->ReleaseStringUTFChars(env, jstr, newString);
   
   

Java 数组 vs. C 数组

与字符串类似,Java 数组和 C 数组在内存中的表示不同。幸运的是,一组 JNI 函数可以提供指向数组中元素的指针。下图显示了如何将 Java 数组映射到 JNI C 类型。

C 类型 jarray 表示通用数组。在 C 语言中,所有数组类型实际上只是 jobject 的同义类型。但是,在 C++ 语言中,所有的数组类型都继承了 jarrayjarray 又依次继承了 jobject。有关所有 C 类型对象的继承图,请参阅附录 AJNI 类型。

使用数组

通常,处理数组时,首先想到要做的是确定其大小。为了做到这一点,应该使用 GetArrayLength() 函数,它返回一个表示数组大小的 jsize

接下来,会想要获取一个指向数组元素的指针。可以使用 GetXXXArrayElement() SetXXXArrayElement() 函数(根据数组的类型替换方法名中的 XXXObjectBooleanByteCharIntLong 等等)来访问数组中的元素。

当本机代码完成了对 Java 数组的使用时,必须调用函数 ReleaseXXXArrayElements() 来释放它。否则,可能导致内存泄漏。下面的代码段显示了如何循环遍历一个整型数组的所有元素:

 1. /* Looping through the elements in an array. */
   
   
 2. int* elem = (*env)->GetIntArrayElements(env, intArray, 0);
   
   
 3. for (i=0; I < (*env)->GetIntArrayLength(env, intArray); i++)
   
   
 4.    sum += elem[i]
   
   
 5. (*env)->ReleaseIntArrayElements(env, intArray, elem, 0);
   
   

局部引用 vs. 全局引用

当使用 JNI 编程时,会需要使用对 Java 对象的引用。缺省情况下,JNI 创建局部引用以确保它们可以被垃圾收集。由于这一点,您可能会因为尝试存储一个本地引用,以便稍后重用它而无意间编写出非法代码,如下面的代码样本所示:

 1. /* This code is invalid! */
   
   
 2. static jmethodID mid;
   
   
 3. 
   
   
 4. JNIEXPORT jstring JNICALL
   
   
 5. Java_Sample1_accessMethod(JNIEnv *env, jobject obj)
   
   
 6. {
   
   
 7.    ...
   
   
 8.      cls = (*env)->GetObjectClass(env, obj);
   
   
 9.    if (cls != 0)
   
   
10.       mid = (*env)->GetStaticMethodID(env, cls, "addInt", "(I)I");
   
   
11.    ...
   
   
12. }
   
   

因为第 10 行的错误,所以这个代码段是非法的。mid methodID,并且 GetStaticMethodID() 返回 methodID。但是,返回的 methodID 是一个局部引用,而您不应该将一个局部引用赋给全局引用。而 mid 是一个全局引用。

Java_Sample1_accessMethod() 返回之后,mid 引用就不再有效,因为赋给它现在超出作用域以外的局部引用。尝试使用 mid 将导致错误结果或 JVM 崩溃。

创建全局引用

要纠正这个问题,需要创建和使用全局引用。全局引用将在显式释放之前一直有效,您必须记住去显式地释放它。没有释放引用可能导致内存泄漏。

使用 NewGlobalRef() 创建全局引用,并用 DeleteGlobalRef() 删除它,如下面的代码样本所示:

 1. /* This code is valid! */
   
   
 2. static jmethodID mid;
   
   
 3. 
   
   
 4. JNIEXPORT jstring JNICALL
   
   
 5. Java_Sample1_accessMethod(JNIEnv *env, jobject obj)
   
   
 6. {
   
   
 7.    ...
   
   
 8.      cls = (*env)->GetObjectClass(env, obj);
   
   
 9.    if (cls != 0)
   
   
10.    {
   
   
11.       mid1 = (*env)->GetStaticMethodID(env, cls, "addInt", "(I)I");
   
   
12.       mid = (*env)->NewGlobalRef(env, mid1);
   
   
13.    ...
   
   
14. }
   
   

错误处理

Java 程序中使用本机方法,就以某种基本的方式破坏了 Java 安全性模型。因为 Java 程序在一个受控的运行时系统(JVM)中运行,所以 Java 平台设计师决定通过检查常见运行时系统错误(如数组下标、越界错误、空指针错误)来帮助程序员。从另一方面讲,由于 C C++ 不使用此类错误检查,所以本机方法程序员必须自己处理所有错误情况,而在运行时,这些错误可以在 JVM 中被捕获。

例如,对于 Java 程序而言,通过抛出一个异常来向 JVM 报告出错是常见和正确的操作。C 没有异常,因此必须使用 JNI 的异常处理函数。

JNI 的异常处理函数

有两种方法用来在本机代码中抛出异常:可以调用 Throw() 函数或 ThrowNew() 函数。在调用 Throw() 之前,首先需要创建一个 Throwable 类型的对象。可以通过调用 ThrowNew() 跳过这一步,因为这个函数为您创建了该对象。在下面的示例代码片段中,我们使用这两个函数抛出 IOException

 1. /* Create the Throwable object. */
   
   
 2. jclass cls = (*env)->FindClass(env, "java/io/IOException");
   
   
 3. jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
   
   
 4. jthrowable e = (*env)->NewObject(env, cls, mid);
   
   
 5. 
   
   
 6. /* Now throw the exception */
   
   
 7. (*env)->Throw(env, e);
   
   
 8. ...
   
   
 9. 
   
   
10. /* Here we do it all in one step and provide a message*/
   
   
11. (*env)->ThrowNew(env,
   
   
12.                  (*env)->FindClass("java/io/IOException"),
   
   
13.                  "An IOException occurred!");
   
   

Throw() ThrowNew() 函数并不中断本机方法中的控制流。直到本机方法返回,在 JVM 中才会将异常实际抛出。在 C 中,一旦碰到错误条件,不能使用 Throw() ThrowNew() 函数立即退出方法,而在 Java 中,这可以使用 throw 语句来退出方法。相反,需要在 Throw() ThrowNew() 函数之后立即使用 return 语句,以便在出错点退出本机方法。

JNI 的异常捕获函数

当从 C C++ 调用 Java 时,也可能需要捕获异常。许多 JNI 函数都能抛出希望捕获的异常。ExceptionCheck() 函数返回 jboolean 以表明是否抛出了异常,而 ExceptionOccured() 方法返回指向当前异常的 jthrowable 引用(或者返回 NULL,如果未抛出异常的话)。

如果正在捕获异常,可能要处理异常,在这种情况下需要在 JVM 中清除该异常。可以使用 ExceptionClear() 函数来进行这个操作。ExceptionDescribed() 函数用来显示异常的调试消息。

本机方法中的多线程

在使用 JNI 工作时,您将遇到的更高级的问题之一是在本机方法中使用多线程。即使是在不需要支持多线程的系统上运行时,Java 平台也是作为多线程系统来实现的;因此您有责任确保本机函数是线程安全的。

Java 程序中,可以通过使用 synchronized 语句实现线程安全的代码。synchronized 语句的语法使您能够获取对象上的锁。只要在 synchronized 块中,就可以执行任何数据操作,而不必担心其它线程会悄悄进入并访问您锁定的对象。

JNI 使用 MonitorEnter() MonitorExit() 函数提供类似的结构。对于传递到 MonitorEnter() 函数中的对象,您会得到一个用于该对象的监视器(锁),并在使用 MonitorExit() 函数释放它之前一直持有该锁。对于您锁定的对象而言,MonitorEnter() MonitorExit() 函数之间的所有代码保证是线程安全的。

本机方法中的同步

下表显示了如何在 JavaC C++ 中同步一块代码。正如您所见,这些 C C++ 函数类似于 Java 代码中的 synchronized 语句。

随本机方法一起使用 synchronized

确保本机方法同步的另一种方法是:当在 Java 类中声明 native 方法时使用 synchronized 关键字。

使用 synchronized 关键字将确保任何时候从 Java 程序调用 native 方法,它都将是 synchronized。尽管用 synchronized 关键字来标记线程安全的本机方法是个好想法,但通常最好总是在本机方法实现中实现同步。这样做的主要原因如下:

  • C C++ 代码和 Java 本机方法声明不同,因此,如果方法声明有变动(即,如果一旦除去了 synchronized 关键字),此方法可能马上不再是线程安全的了。
  • 如果有人对使用该函数的其它本机方法(或其它 C C++ 函数)进行编码,他们可能并没有意识到该本机实现不是线程安全的。
  • 如果将函数作为普通的 C 函数在 Java 程序之外使用,则它不是线程安全的。

其它同步技术

Object.wait()Object.notify() Object.notifyAll() 方法也支持线程同步。因为所有 Java 对象都将 Object 类作为父类,所以所有 Java 对象都有这些方法。您可以象调用其它方法一样,从本机代码调用这些方法,并以 Java 代码中相同的方式来使用它们,以实现线程同步。

结束语和参考资料

结束语

Java 本机接口是 Java 平台中一种设计良好和良好集成的 API。它被设计成用来使您能将本机代码合并到 Java 程序中,也为您提供了一种在用其它编程语言编写的程序中使用 Java 代码的方式。

使用 JNI 总会破坏 Java 代码的可移植性。当从 Java 程序调用本机方法时,需要为每个您打算运行程序的平台分发本机共享库文件。另一方面,从本机程序调用 Java 代码实际上可以改进应用程序的可移植性。

参考资料

下载

文章和教程

  • 要了解更多关于用 C/C++ 语言编程和用 Java 语言编程之间的差异(从 C/C++ 程序员的角度),请参阅教程 Introduction to Java for C/C++ programmersdeveloperWorks1999 4 月)
  • 最近的一篇文章Weighing in on Java native compilationdeveloperWorks2002 1 月)使用用来进行比较的基准测试程序从正反两方面研究了 Java 本机接口。
  • 了解更多关于 Java 本机接口的内容,包括 Java 2 SDK 中的 JNI 的增强
  • 要进一步学习 Java 编程,请参阅 developerWorks 上的关于 Java 编程的教程列出的所有教程。
  • IBM developerWorks 上的 Java 技术专区,关于 Java 编程的每个方面,您都可以找到数百篇文章。

推荐书籍

反馈意见

请将您关于本教程的反馈意见发送给我们。我们热切期待您的反馈意见!

附录

附录 AJNI 类型

JNI 使用几种映射到 Java 类型的本机定义的 C 类型。这些类型可以分成两类:原始类型和伪类(pseudo-classes)。在 C 中,伪类作为结构实现,而在 C++ 中它们是真正的类。

Java 原始类型直接映射到 C 依赖于平台的类型,如下所示:

C 类型 jarray 表示通用数组。在 C 中,所有的数组类型实际上只是 jobject 的同义类型。但是,在 C++ 中,所有的数组类型都继承了 jarrayjarray 又依次继承了 jobject。下列表显示了 Java 数组类型是如何映射到 JNI C 数组类型的。

这里是一棵对象树,它显示了 JNI 伪类是如何相关的。

附录 BJNI 方法说明编码

用下表指定的编码将本机 Java 方法参数类型表示或转换成本机代码。

  • 类类型 L 表达式结尾的分号是类型表达式的终止符,而不是多个表达式之间的分隔符。
  • 必须用正斜杠(/)而不是点(.)来将包和类名称隔开。要指定数组类型,用左方括号([)。例如,Java 方法:

boolean print(String[] parms, int n)

的转换说明如下:

([Ljava/lang/Sting;I)Z

 

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值