Java本地接口

JNIJava Native Interface,Java本地接口)是一种编程框架,使得Java虚拟机中的Java程序可以调用本地应用/或库,也可以被其他程序调用。 本地程序一般是用其它语言(CC++汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序。[1]

设计目的和功能

有些事情Java无法处理时,JNI允许程序员用其他编程语言来解决,例如,Java标准库不支持的平台相关功能或者程序库。也用于改造已存在的用其它语言写的程序,供Java程序调用。许多基于JNI的标准库提供了很多功能给程序员使用,例如文件I/O、音频相关的功能。当然,也有各种高性能的程序,以及平台相关的API实现,允许所有Java应用程式安全并且平台独立地使用这些功能。

JNI框架允许Native方法调用Java对象,就像Java程序访问Native对象一样方便。Native方法可以创建Java对象,读取这些对象,并调用Java对象执行某些方法。当然Native方法也可以读取由Java程序自身创建的对象,并调用这些对象的方法。

注意事项

  • 在使用JNI的过程中,可能因为某些微小的BUG,对整个JVM造成很难重现和调试的错误。
  • 仅有应用程式与签名的applet可以调用JNI。
  • 依赖于JNI的应用失去了Java的平台移植性(一种解决办法是为每个平台编写专门的JNI代码,然后在Java代码中,根据操作系统加载正确的JNI代码)。
  • JNI框架并没有对 non-JVM 内存提供自动垃圾回收机制,Native代码(如汇编语言)分配的内存和资源,需要其自身负责进行显式的释放。
  • LinuxSolaris平台,如果Native代码将自身注册为信号处理器(signal handler),就会拦截发给JVM的信号。可以使用 责任链模式 让 Native代码更好地与JVM进行交互。[2]
  • Windows平台上,在SEH try/catch块中可以将结构化异常处理(SEH)用来包装Native代码,以捕获机器(CPU/FPU)生成的软中断(例如:空指针异常、被除数为0等),将这些中断在传播到JVM(中的Java代码)之前进行处理,以免造成未捕获的异常。
  • NewStringUTF、GetStringUTFLength、GetStringUTFChars、ReleaseStringUTFChars与 GetStringUTFRegion等编码函数处理的是一种修改的UTF-8,[3],实际上是一种不同的编码,某些字符并不是标准的UTF-8。 null字符(U+0000)以及不在Unicode字符平面映射中的字符(codepoints 大于等于 U+10000 的字符,例如UTF-16中的代理对 surrogate pairs),在修改的UTF-8中的编码都有所不同。 许多程序错误地使用了这些函数,将标准UTF-8字符串传入或传出这些函数,实际上应该使用修改后的编码。程序应当先使用NewString、GetStringLength、GetStringChars、ReleaseStringChars、GetStringRegion、GetStringCritical与ReleaseStringCritical等函数,这些函数在小尾序机器上使用UTF-16LE编码,在大尾序机器上使用UTF-16BE编码,然后再通过程序将 UTF-16转换为 UTF-8。
  • JNI在某些情况下可能带来很大的开销和性能损失:[4]
    • 调用 JNI 方法是很笨重的操作,特别是在多次重复调用的情况下。
    • Native 方法不会被 JVM 内联,也不会被 即时编译 优化 ,因为方法已经被编译过了。
    • Java 数组可能会被拷贝一份,以传递给 native 方法,执行完之后再拷贝回去. 其开销与数组的长度是线性相关的。
    • 如果传递一个对象给方法,或者需要一个回调,那么 Native 方法可能会自己调用JVM。 访问Java对象的属性、方法和类型时,Native代码需要类似反射的东西。签名由字符串指定,通常从JVM中查询。这非常缓慢并且容易出错。
    • Java 中的字符串(String) 也是对象,有 length 属性,并且是编码过的. 读取或者创建字符串都需要一次时间复杂度为 O(n) 的复制操作.

JNI如何工作

在JNI框架,native方法一般在单独的.c或.cpp文件中实现。当JVM调用这些函数,就传递一个JNIEnv指针,一个jobject的指针,任何在Java方法中声明的Java参数。一个JNI函数看起来类似这样:

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

env指向一个结构包含了到JVM的界面,包含了所有必须的函数与JVM交互、访问Java对象。例如,把本地数组转换为Java数组的JNI函数,把本地字符串转换为Java字符串的JNI函数,实例化对象,抛出异常等。基本上,Java程序可以做的任何事情都可以用JNIEnv做到,虽然相当不容易。

例如,下面代码把Java字符串转化为本地字符串:

//C++ code
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);
}
/*C code*/
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);
}
/*Objective-C code*/
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);
}

本地数据类型与Java数据类型可以互相映射。对于复合数据类型,如对象,数组,字符串,就必须用JNIEnv中的方法来显示地转换。

第2个参数obj引用到一个Java对象,在其中声明了本地方法。

类型映射

下表是Java (JNI)与本地代码之间的数据类型映射:

本地类型 Java语言的类型 描述 类型签名(signature)
unsigned char jboolean unsigned 8 bits Z
signed char jbyte signed 8 bits B
unsigned short jchar unsigned 16 bits C
short jshort signed 16 bits S
long jint signed 32 bits I

long long
__int64

jlong signed 64 bits J
float jfloat 32 bits F
double jdouble 64 bits D
void V

签名"L fully-qualified-class ;"是由该名字指明的类。例如,签名"Ljava/lang/String;"是类java.lang.String。带前缀[的签名表示该类型的数组,如[I表示整型数组。void签名使用V代码。

这些类型是可以互换的,如jint也可使用 int,不需任何类型转换

但是,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);
}

这种情况也适用于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;
}

JNIEnv*

JNI环境指针(JNIEnv*)作为每个映射为Java方法的本地幔数的第一个参数,使得本地幔数可以与JNI环境交互。这个JNI界面指针可以存储,但仅在当前线程中有效。其它线程必须首先调用AttachCurrentThread()把自身附加到虚拟机以获得JNI界面指针。一旦附加,本地线程运行就类似执行本地幔数的正常Java线程。本地线程直到执行DetachCurrentThread()把自身脱离虚拟机。[5]

把当前线程附加到虚拟机并获取JNI界面指针:

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

当前线程脱离虚拟机:

(*g_vm)->DetachCurrentThread (g_vm);

高级使用

本地AWT绘制

本地代码不仅可以与Java交互,也可以在Java Canvas绘图,使用Java AWT Native Interface英语Java AWT Native Interface

访问汇编代码

JNI允许直接访问汇编代码。[6] 也可以从汇编代码访问Java。[7]

Microsoft的RNI

Microsoft实现的Java虚拟机——Visual J++的类似的访问本地Windows代码的机制Raw Native InterfaceRNI)。

例子

HelloWorld

make.sh

#!/bin/sh

# openbsd 4.9
# gcc 4.2.1
# openjdk 1.7.0

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
javac HelloWorld.java
javah HelloWorld
gcc -shared libHelloWorld.c -o libHelloWorld.so
java HelloWorld

build.bat

:: Microsoft Visual Studio 2012 Visual C++ compiler
SET VC="C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC"
:: Microsoft Windows SDK for Windows 7 and .NET Framework 4 
SET MSDK="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A"
:: Java 1.7.0 update 21
SET JAVA_HOME="C:\Program Files (x86)\Java\jdk1.7.0_21"

call %VC%\vcvarsall.bat

javac HelloWorld.java
javah HelloWorld
%VC%\bin\cl /I%JAVA_HOME%\include /I%JAVA_HOME%\include\win32 /I%VC%\include /I%VC%\lib /I%MSDK%\Lib libHelloWorld.c /FelibHelloWorld.dll /LD
java HelloWorld

HelloWorld.java

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

Invocation:

$ chmod +x make.sh
$ ./make.sh

参见

  • Java AWT Native Interface英语Java AWT Native Interface
  • Gluegen英语Gluegen, a Java tool which automatically generates the Java and JNI code necessary to call C libraries from Java code
  • P/Invoke, the .NET Framework method of calling native applications
  • SWIG, a multilanguage interface-generator for C and C++ libraries that can generate JNI code
  • Java Native Access provides Java programs easy access to native shared libraries without writing boilerplate code

参考文献

  1. ^ Role of the JNI. The Java Native Interface Programmer's Guide and Specification. [2008-02-27]. (原始内容存档于2012-06-26). 
  2. ^ If JNI based application is crashing, check signal handling!. [2014-05-30]. (原始内容存档于2014-11-09). 
  3. ^ Modified UTF-8 Strings. [2014-05-30]. (原始内容存档于2020-05-03). 
  4. ^ java - What makes JNI calls slow? - Stack Overflow. [2017-01-22]. (原始内容存档于2019-10-17). 
  5. ^ The Invocation API. Sun Microsystems. http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/invocation.html页面存档备份,存于互联网档案馆
  6. ^ Invoking Assembly Language Programs from Java. Java.net. 2006-10-19 [2007-10-06]. (原始内容存档于2008-03-30). 
  7. ^ Launch Java Applications from Assembly Language Programs. Java.net. 2006-10-19 [2007-10-04]. (原始内容存档于2007-10-11). 

相关书籍

外部链接