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). 

相關書籍

外部連結