跳转到内容

Java 本地接口

100% developed
来自维基教科书,开放书籍,开放世界

导航 高级 主题:v  d  e )


Java 本地接口 (JNI) 使在 Java 虚拟机 (JVM) 中运行的 Java 代码能够调用和被其他语言(如 C、C++ 和汇编)编写的本机应用程序(特定于硬件和操作系统平台的程序)和库调用。

JNI 可以用于

  • 实现或使用特定于平台的功能。
  • 实现或使用标准 Java 类库不支持的功能。
  • 使用另一种编程语言编写的现有应用程序可供 Java 应用程序访问。
  • 让本机方法以与 Java 代码使用这些对象相同的方式使用 Java 对象(本机方法可以创建 Java 对象,然后检查和使用这些对象来执行其任务)。
  • 让本机方法检查和使用由 Java 应用程序代码创建的对象。
  • 用于时间关键的计算或操作,例如解决复杂的数学方程(本机代码可能比 JVM 代码更快)。

另一方面,依赖 JNI 的应用程序会失去 Java 提供的平台可移植性。因此,您需要为每个平台编写 JNI 代码的单独实现,并让 Java 在运行时检测操作系统并加载正确的实现。许多标准库类依赖 JNI 为开发人员和用户提供功能(文件 I/O、声音功能...)。在标准库中包含性能和平台敏感的 API 实现允许所有 Java 应用程序以安全且与平台无关的方式访问此功能。只有应用程序和签名的小程序可以调用 JNI。应谨慎使用 JNI。使用 JNI 时出现的细微错误可能会以非常难以重现和调试的方式使整个 JVM 不稳定。错误检查是必须的,否则它有可能使 JNI 端和 JVM 崩溃。

此页面只解释如何从 JVM 调用本机代码,而不是如何从本机代码调用 JVM。

从 JVM 调用本机代码

[编辑 | 编辑源代码]

在 JNI 框架中,本机函数在单独的 .c 或 .cpp 文件中实现。C++ 提供了一个与 JNI 相比略微更简单的接口。当 JVM 调用函数时,它会传递一个 JNIEnv 指针、一个 jobject 指针以及 Java 方法声明的任何 Java 参数。JNI 函数可能如下所示

 JNIEXPORT void JNICALL Java_ClassName_MethodName
   (JNIEnv *env, jobject obj)
 {
     /*Implement Native Method Here*/
 }

env 指针是一个结构,其中包含与 JVM 的接口。它包含所有与 JVM 交互和处理 Java 对象所需的函数。示例 JNI 函数是将本机数组转换为/从 Java 数组、将本机字符串转换为/从 Java 字符串、实例化对象、抛出异常等。基本上,Java 代码可以执行的任何操作都可以使用 JNIEnv 完成,尽管要容易得多。

在 Linux 和 Solaris 平台上,如果本机代码将自己注册为信号处理程序,它可能会拦截针对 JVM 的信号。应使用信号链接以允许本机代码更好地与 JVM 交互。在 Windows 平台上,可以使用结构化异常处理 (SEH) 将本机代码包装在 SEH try/catch 块中,以便在将中断传播回 JVM(即 Java 端代码)之前捕获机器(CPU/FPU)生成的软件中断(例如 NULL 指针访问违规和除零运算),并处理这些情况,这很可能会导致未处理的异常。

C++ 代码

[编辑 | 编辑源代码]

例如,以下将 Java 字符串转换为本机字符串

 extern "C"
 JNIEXPORT void JNICALL Java_ClassName_MethodName
   (JNIEnv *env, jobject obj, jstring javaString)
 {
     //Get the native string from javaString
     const char *nativeString = env->GetStringUTFChars(javaString, 0);

     //Do something with the nativeString

     //DON'T FORGET THIS LINE!!!
     env->ReleaseStringUTFChars(javaString, nativeString);
 }

JNI 框架不为本机侧代码执行的非 JVM 内存资源分配提供任何自动垃圾回收。因此,本机侧代码(例如 C、C++ 或汇编语言)必须承担显式释放其本身获取的任何此类内存资源的责任。

 JNIEXPORT void JNICALL Java_ClassName_MethodName
   (JNIEnv *env, jobject obj, jstring javaString)
 {
     /*Get the native string from javaString*/
     const char *nativeString = (*env)->GetStringUTFChars(env, javaString, 0);

     /*Do something with the nativeString*/

     /*DON'T FORGET THIS LINE!!!*/
     (*env)->ReleaseStringUTFChars(env, javaString, nativeString);
 }

需要注意的是,C++ JNI 代码在语法上比 C JNI 代码更简洁,因为像 Java 一样,C++ 使用对象方法调用语义。这意味着在 C 中,env 参数使用 (*env)-> 解引用,并且 env 必须显式传递给 JNIEnv 方法。在 C++ 中,env 参数使用 env-> 解引用,并且 env 参数作为对象方法调用语义的一部分隐式传递。

Objective-C 代码

[编辑 | 编辑源代码]
 JNIEXPORT void JNICALL Java_ClassName_MethodName(JNIEnv *env, jobject obj, jstring javaString)
 {
     /*DON'T FORGET THIS LINE!!!*/
     JNF_COCOA_ENTER(env);

     /*Get the native string from javaString*/
     NSString* nativeString = JNFJavaToNSString(env, javaString);

     /*Do something with the nativeString*/

     /*DON'T FORGET THIS LINE!!!*/
     JNF_COCOA_EXIT(env);
 }

JNI 还允许直接访问汇编代码,甚至不需要经过 C 桥接。

类型映射

[编辑 | 编辑源代码]

本地数据类型可以映射到/从 Java 数据类型。对于对象、数组和字符串等复合类型,本地代码必须通过调用 JNIEnv 中的方法来显式转换数据。下表显示了 Java (JNI) 和本地代码之间类型的映射。

本地类型 JNI 类型 描述 类型签名
unsigned char jboolean 无符号 8 位 Z
signed char jbyte 有符号 8 位 B
unsigned short jchar 无符号 16 位 C
short jshort 有符号 16 位 S
long jint 有符号 32 位 I

long long
__int64

jlong 有符号 64 位 J
float jfloat 32 位 F
double jdouble 64 位 D

此外,签名 "L fully-qualified-class ;" 表示由该名称唯一指定的类;例如,签名 "Ljava/lang/String;" 指的是类 java.lang.String。此外,在签名前面加上 [ 将构成该类型的数组;例如,[I 表示 int 数组类型。最后,void 签名使用 V 代码。在这里,这些类型可以互换。您可以在通常使用 int 的地方使用 jint,反之亦然,无需任何类型转换。

但是,Java 字符串和数组到本地字符串和数组之间的映射不同。如果在需要 char * 的地方使用 jstring,代码可能会使 JVM 崩溃。

JNIEXPORT void JNICALL Java_ClassName_MethodName
        (JNIEnv *env, jobject obj, jstring javaString) {
    // printf("%s", javaString);        // INCORRECT: Could crash VM!

    // Correct way: Create and release native string from Java string
    const char *nativeString = (*env)->GetStringUTFChars(env, javaString, 0);
    printf("%s", nativeString);
    (*env)->ReleaseStringUTFChars(env, javaString, nativeString);
}

NewStringUTFGetStringUTFLengthGetStringUTFCharsReleaseStringUTFCharsGetStringUTFRegion 函数使用的编码不是标准 UTF-8,而是经过修改的 UTF-8。空字符 (U+0000) 和大于或等于 U+10000 的代码点在经过修改的 UTF-8 中的编码方式不同。许多程序实际上错误地使用了这些函数,并将返回或传递给函数的 UTF-8 字符串视为标准 UTF-8 字符串,而不是经过修改的 UTF-8 字符串。程序应该使用 NewStringGetStringLengthGetStringCharsReleaseStringCharsGetStringRegionGetStringCriticalReleaseStringCritical 函数,这些函数在小端架构上使用 UTF-16LE 编码,在大端架构上使用 UTF-16BE 编码,然后使用 UTF-16 到标准 UTF-8 的转换例程。

代码与 Java 数组类似,如下面的示例所示,该示例计算数组中所有元素的总和。

JNIEXPORT jint JNICALL Java_IntArray_sumArray
        (JNIEnv *env, jobject obj, jintArray arr) {
    jint buf[10];
    jint i, sum = 0;
    // This line is necessary, since Java arrays are not guaranteed
    // to have a continuous memory layout like C arrays.
    env->GetIntArrayRegion(arr, 0, 10, buf);
    for (i = 0; i < 10; i++) {
        sum += buf[i];
    }
    return sum;
}

当然,它远不止这些。

JNI 环境指针 (JNIEnv*) 作为参数传递给每个映射到 Java 方法的本地函数,允许在本地方法中与 JNI 环境进行交互。这个 JNI 接口指针可以存储,但只在当前线程中有效。其他线程必须首先调用 AttachCurrentThread() 将自身附加到 VM 并获取 JNI 接口指针。附加后,本地线程就像在本地方法中运行的普通 Java 线程一样。本地线程保持附加到 VM,直到它调用 DetachCurrentThread() 将自身分离。

要附加到当前线程并获取 JNI 接口指针

JNIEnv *env;
(*g_vm)->AttachCurrentThread (g_vm, (void **) &env, NULL);

要从当前线程分离

(*g_vm)->DetachCurrentThread (g_vm);

HelloWorld

[编辑 | 编辑源代码]
Computer code 代码清单 10.1:HelloWorld.java
public class HelloWorld {
 private native void print();

 public static void main(String[] args) {
  new HelloWorld().print();
 }

 static {
  System.loadLibrary("HelloWorld");
 }
}

HelloWorld.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorld
 * Method:    print
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_print
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

libHelloWorld.c

 #include <stdio.h>
 #include "HelloWorld.h"

 JNIEXPORT void JNICALL
 Java_HelloWorld_print(JNIEnv *env, jobject obj)
 {
     printf("Hello World!\n");
     return;
 }

make.sh

#!/bin/sh

# openbsd 4.9
# gcc 4.2.1
# openjdk 1.7.0
JAVA_HOME=$(readlink -f /usr/bin/javac | sed "s:bin/javac::")
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
javac HelloWorld.java
javah HelloWorld
gcc -I${JAVA_HOME}/include -shared libHelloWorld.c -o libHelloWorld.so
java HelloWorld
Computer code 在 POSIX 上执行的命令
chmod +x make.sh
./make.sh

高级用法

[编辑 | 编辑源代码]

本地代码不仅可以与 Java 交互,还可以利用 Java API:java.awt.Canvas,这可以通过 Java AWT 本地接口实现。该过程几乎相同,只是稍有不同。Java AWT 本地接口仅从 J2SE 1.3 开始可用。


华夏公益教科书