Java 本地接口
导航 高级 主题: ) |
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。
在 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 指针访问违规和除零运算),并处理这些情况,这很可能会导致未处理的异常。
例如,以下将 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
参数作为对象方法调用语义的一部分隐式传递。
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 |
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);
}
NewStringUTF
、GetStringUTFLength
、GetStringUTFChars
、ReleaseStringUTFChars
、GetStringUTFRegion
函数使用的编码不是标准 UTF-8,而是经过修改的 UTF-8。空字符 (U+0000) 和大于或等于 U+10000 的代码点在经过修改的 UTF-8 中的编码方式不同。许多程序实际上错误地使用了这些函数,并将返回或传递给函数的 UTF-8 字符串视为标准 UTF-8 字符串,而不是经过修改的 UTF-8 字符串。程序应该使用 NewString
、GetStringLength
、GetStringChars
、ReleaseStringChars
、GetStringRegion
、GetStringCritical
和 ReleaseStringCritical
函数,这些函数在小端架构上使用 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);
代码清单 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
在 POSIX 上执行的命令
chmod +x make.sh ./make.sh |
本地代码不仅可以与 Java 交互,还可以利用 Java API:java.awt.Canvas,这可以通过 Java AWT 本地接口实现。该过程几乎相同,只是稍有不同。Java AWT 本地接口仅从 J2SE 1.3 开始可用。