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)與本地代碼之間的資料類型對映:

More information 本地類型, Java語言的類型 ...
本地類型 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
Close

簽章"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

參考文獻

相關書籍

外部連結

Wikiwand in your browser!

Seamless Wikipedia browsing. On steroids.

Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.

Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.