1. 程式人生 > >Java Native Interface(JNI)從零開始詳細教程

Java Native Interface(JNI)從零開始詳細教程

====================================================================================
首先宣告:這邊文章是我翻譯的文章(看了很多關於JNI的介紹,只有這篇個人認為最好,因此忍不住想要翻譯給國內的各位),請勿隨意轉載,尊重文章原作者。
文章原始連結:https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html
這是南洋理工大學的線上學習note,有興趣的同學可以直接看原文。如果你對E文比較頭疼的話,可以直接看下面我翻譯的,我會盡量使用平實的語言去翻譯這篇文章,另外本人水平有限,如有翻譯不當之處還請各位賜教。===================================================================================

引言

有的時候我們需要使用原生代碼(C/C++)來克服Java中的記憶體管理和效能問題,Java通過JNI機制來支援內地程式碼的使用。
想要比較好地理解JNI是比較難的,因為它包含了兩種語言和執行時機制。
在繼續之前,我應該假設你具備以下知識點和技能:
1. java
2. C/C++和gcc編譯器
3. 對於windows而言,熟悉Gygwin或者MinGW
4. 對於IDE而言,熟悉Eclipse C/C++ Development Tool (CDT)

開始

使用C來實現JNI

步驟1,編寫一個使用C實現函式的java類,HelloJNI.java:

public
class HelloJNI { static { System.loadLibrary("hello"); // Load native library at runtime // hello.dll (Windows) or libhello.so (Unixes) } // Declare a native method sayHello() that receives nothing and returns void private native void sayHello(); // Test Driver
public static void main(String[] args) { new HelloJNI().sayHello(); // invoke the native method } }

上面程式碼的靜態程式碼塊在這個類被類載入器載入的時候呼叫了System.loadLibrary()方法來載入一個native庫“hello”(這個庫中實現了sayHello函式)。這個庫在windows品臺上對應了“hello.dll”,而在類UNIX平臺上對應了“libhello.so”。這個庫應該包含在Java的庫路徑(使用java.library.path系統變量表示)上,否則這個上面的程式會丟擲UnsatisfiedLinkError錯誤。你應該使用VM的引數-Djava.library.path=path_to_lib來指定包含native庫的路徑。
接下來,我們使用native關鍵字將sayHello()方法宣告為本地例項方法,這就很明顯地告訴JVM:這個方法實現在另外一個語言中(C/C++),請去那裡尋找他的實現。注意,一個native方法不包含方法體,只有宣告。上面程式碼中的main方法例項化了一個HelloJJNI類的例項,然後呼叫了本地方法sayHello()。
下面,我們編譯HelloJNI.java成HelloJNI.class

javac HelloJNI.java

接下來,我們利用上面生成的class檔案生成用於編寫C/C++程式碼的標頭檔案,使用jdk中的javah工具完成:

javah HelloJNI

上面的命令執行完之後生成了HelloJNI.h:

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

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

我們看到,上面的標頭檔案中生成了一個Java_HelloJNI_sayHello的C函式:

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

將java的native方法轉換成C函式宣告的規則是這樣的:Java_{package_and_classname}_{function_name}(JNI arguments)。包名中的點換成單下劃線。需要說明的是生成函式中的兩個引數:
1. JNIEnv *:這是一個指向JNI執行環境的指標,後面我們會看到,我們通過這個指標訪問JNI函式
2. jobject:這裡指代java中的this物件
下面我們給出的例子中沒有使用上面的兩個引數,不過後面我們的例子會使用的。到目前為止,你可以先忽略JNIEXPORT和JNICALL這兩個玩意。
上面標頭檔案中有一個extern “C”,同時上面還有C++的條件編譯語句,這麼一來大家就明白了,這裡的函式宣告是要告訴C++編譯器:這個函式是C函式,請使用C函式的簽名協議規則去編譯!因為我們知道C++的函式簽名協議規則和C的是不一樣的,因為C++支援重寫和過載等面向物件的函式語法。
接下來,我們給出C語言的實現,以實現上面的函式:
C語言實現:

#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"

// Implementation of native method sayHello() of HelloJNI class
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
   printf("Hello World!\n");
   return;
}

將上面的程式碼儲存為HelloJNI.c。jni.h標頭檔案在 “\include” 和 “\include\win32”目錄下,這裡的JAVA_HOME是指你的JDK安裝目錄。
這段C程式碼的作用很簡單,就是在終端上列印Hello Word!這句話。
下面我們編譯這段程式碼,使用GCC編譯器:
對於windows上的MinGW:

> set JAVA_HOME=C:\Program Files\Java\jdk1.7.0_{xx}
      // Define and Set environment variable JAVA_HOME to JDK installed directory
      // I recommend that you set JAVA_HOME permanently, via "Control Panel""System""Environment Variables"
> echo %JAVA_HOME%
      // In Windows, you can refer a environment variable by adding % prefix and suffix 
> gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c
      // Compile HellJNI.c into shared library hello.dll

也可以分步編譯:

// Compile-only with -c flag. Output is HElloJNI.o
> gcc -c -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" HelloJNI.c

// Link into shared library "hello.dll"
> gcc -Wl,--add-stdcall-alias -shared -o hello.dll HelloJNI.o

下面,我們使用nm命令來檢視生成hello.dll中的函式:

> nm hello.dll | grep say
624011d8 T _Java_HelloJNI_sayHello@8

對於windows上的Cygwin:
首先,你需要講__int64定義成“long long”型別,通過-D _int64=”long long選項實現。
對於gcc-3,請包含選項-nmo -cygwin來編譯dll庫,這些庫是不依賴於Cygwin dll的。

> gcc-3 -D __int64="long long" -mno-cygwin -Wl,--add-stdcall-alias 
  -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o hello.dll HelloJNI.c

對於gcc-4,我目前還沒有找到正確的編譯選項。
==================================這部分是筆者新增的==================================
原文中只給出了windows平臺上編譯方法,下面我給出Linux等類UNIX上編譯方法:

gcc -fPIC --shared HelloJNI.c -o libhello.so -I /usr/lib/jvm/java-7-openjdk-amd64/include/

上面的命令編譯生成一個libhello.so共享庫在當前目錄下
==================================這部分是筆者新增的==================================
接下來,讓我們執行一下上面的程式碼吧:

> java HelloJNI
or
> java -Djava.library.path=. HelloJNI

有的時候,你可能需要使用-Djava.library.path來指定載入庫的位置,因為可能報出java.lang.UnsatisfiedLinkError錯誤.
==================================這部分是筆者新增的==================================
我們首先使用nm命令(關於nm請自行Google或者man)檢視libhello.so中都有那些函式:
這裡寫圖片描述
可以看到我們的sayHello函式已經在這個裡面,這說明我們編譯的基本沒有問題。
下面,我給出在我電腦上執行的效果(原文作者沒有給出):
首先我們執行java HelloJNI,看看能不能執行:
這裡寫圖片描述
果然,出現了UnsatisfiedLinkError錯誤,原因是VM去標準路徑下查詢這個庫,發現找不到,然後就掛了。因此我們還是需要使用-Djava.library.path來明確告訴VM我們的庫在哪裡(當然,你也可以將你編譯出來的庫放到系統標準路徑中,比如/usr/lib目錄下):
這裡寫圖片描述
現在OK了,因為我們明確告訴VM,我們的libhello.so就在當前目錄下,不用傻傻地去系統中找啦!!
==================================這部分是筆者新增的==================================

使用C/C++混合實現JNI

第一步:編寫一個使用原生代碼的java類:HelloJNICpp.java

public class HelloJNICpp {
   static {
      System.loadLibrary("hello"); // hello.dll (Windows) or libhello.so (Unixes)
   }

   // Native method declaration
   private native void sayHello();

   // Test Driver
   public static void main(String[] args) {
      new HelloJNICpp().sayHello();  // Invoke native method
   }
}

同樣地,我們使用javac來編譯這個程式碼:

> javac HelloJNICpp.java

步驟2:生成C/C++的標頭檔案

> javah HelloJNICpp

上面命令會生成一個HelloJNICpp.h的檔案,並且這個檔案中聲明瞭這個本地函式:

JNIEXPORT void JNICALL Java_HelloJNICpp_sayHello(JNIEnv *, jobject);

步驟3:C/C++編碼實現,HelloJNICppImpl.h, HelloJNICppImpl.cpp, 和 HelloJNICpp.c
這裡,我們使用C++來實現真正的函式(”HelloJNICppImpl.h” 和 “HelloJNICppImpl.cpp”),而使用C來和java進行互動。(譯者注:這樣就可以把JNI的程式碼邏輯和我們真正的業務邏輯分離開了!)
C++標頭檔案:”HelloJNICppImpl.h”

#ifndef _HELLO_JNI_CPP_IMPL_H
#define _HELLO_JNI_CPP_IMPL_H

#ifdef __cplusplus
        extern "C" {
#endif
        void sayHello ();
#ifdef __cplusplus
        }
#endif

#endif

C++的程式碼實現:”HelloJNICppImpl.cpp”

#include "HelloJNICppImpl.h"
#include  <iostream>

using namespace std;

void sayHello () {
    cout << "Hello World from C++!" << endl;
    return;
}

C程式碼實現和Java的互動:”HelloJNICpp.c”

#include <jni.h>
#include "HelloJNICpp.h"
#include "HelloJNICppImpl.h"

JNIEXPORT void JNICALL Java_HelloJNICpp_sayHello (JNIEnv *env, jobject thisObj) {
    sayHello();  // invoke C++ function
    return;
}

講上面的程式碼編譯成一個共享庫(在windows上是hello.dll)。
使用windows上的MinGW GCC:

> set JAVA_HOME=C:\Program Files\Java\jdk1.7.0_{xx}
> g++ -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" 
      -shared -o hello.dll HelloJNICpp.c HelloJNICppImpl.cpp

步驟4:執行java程式碼

> java HelloJNICpp
or
> java -Djava.library.path=. HelloJNICpp

java package中的JNI

在真正的產品化中,所有的java類都是有自己的包的,而不是一個預設的沒有名字的包。下面我們說明一下java中的package怎麼在JNI中使用。
步驟1:使用JNI的程式, myjni\HelloJNI.java

package myjni; // 多了包名定義

public class HelloJNI {
   static {
      System.loadLibrary("hello"); // hello.dll (Windows) or libhello.so (Unixes)
   }
   // A native method that receives nothing and returns void
   private native void sayHello();

   public static void main(String[] args) {
      new HelloJNI().sayHello();  // invoke the native method
   }
}

上面的這個類應該放在myjni目錄下。然後我們編譯這個程式碼:

// change directory to package base directory
> javac myjni\HelloJNI.java

步驟2:生成C/C++標頭檔案
如果你的java程式碼是放在一個包中的,那麼你需要使用完全限定名稱來生成C/C++標頭檔案的。你可能會需要使用-classpath選項來指定JNI程式的classpath路徑,並且可能會使用-d選項來指定生成標頭檔案的目標資料夾。

> javah --help
......

// Change directory to package base directory
> javah -d include myini.HelloJNI

在上面的例子中,我們選擇將生層的標頭檔案放在include目錄下,因此,我們輸出的就是:”include\myjni_HelloJNI.h”.這個標頭檔案聲明瞭這樣的本地函式:

JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *, jobject);

我們看到,和上面的例子相比,這裡的名字規則是這樣的:Java__methodName,同時,點號換成單下劃線。
步驟3:C程式碼實現:HelloJNI.c

#include <jni.h>
#include <stdio.h>
#include "include\myjni_HelloJNI.h"

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

編譯C程式碼:

> gcc -Wl,--add-stdcall-alias -I<JAVA_HOME>\include -I<JAVA_HOME>\include\win32 -shared -o hello.dll HelloJNI.c

執行程式碼:

> java myjni.HelloJNI

在Eclipse中開發JNI

JNI基礎知識

上面我們簡單演示了怎麼使用JNI,現在我們來系統梳理一下JNI中涉及的基本知識。
JNI定義了以下資料型別,這些型別和Java中的資料型別是一致的:
1. Java原始型別:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean這些分別對應這java的int, byte, short, long, float, double, char and boolean。
2. Java引用型別:jobject用來指代java.lang.Object,除此之外,還定義了以下子型別:
a. jclass for java.lang.Class.
b. jstring for java.lang.String.
c. jthrowable for java.lang.Throwable.
d. jarray對java的array。java的array是一個指向8個基本型別array的引用型別。於是,JNI中就有8個基本型別的array:jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray 和 jbooleanArray,還有一個就是指向Object的jobjectarray。
Native函式會接受上面型別的引數,並且也會返回上面型別的返回值。然而,本地函式(C/C++)是需要按照它們自己的方式處理型別的(比如C中的string,就是char *)。因此,需要在JNI型別和本地型別之間進行轉換。通常來講,本地函式需要:
1. 加收JNI型別的引數(從java程式碼中傳來)
2. 對於JNI型別引數,需要講這些資料轉換或者拷貝成本地資料型別,比如講jstring轉成char *, jintArray轉成C的int[]。需要注意的是,原始的JNI型別,諸如jint,jdouble之類的不用進行轉換,可以直接使用,參與計算。
3. 進行資料操作,以本地的方式
4. 建立一個JNI的返回型別,然後講結果資料拷貝到這個JNI資料中
5. returnJNI型別資料
這其中最麻煩的事莫過於在JNI型別(如jstring, jobject, jintArray, jobjectArray)和本地型別(如C-string, int[])之間進行轉換這件事情了。不過所幸的是,JNI環境已經為我們定義了很多的介面函式來做這種煩人的轉換。(譯者注:這裡就需要使用上面我們提到的JNIEnv*那個引數了!)

在Java和Native程式碼之間傳遞引數和返回值

傳遞基本型別

傳遞java的基本型別是非常簡單而直接的,一個jxxx之類的型別已經定義在本地系統中了,比如:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar 和 jboolean分別對應java的int, byte, short, long, float, double, char 和 boolean基本型別。
Java JNI 程式:TestJNIPrimitive.java

public class TestJNIPrimitive {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }

   // Declare a native method average() that receives two ints and return a double containing the average
   private native double average(int n1, int n2);

   // Test Driver
   public static void main(String args[]) {
      System.out.println("In Java, the average is " + new TestJNIPrimitive().average(3, 2));
   }
}

這個JNI程式載入了myjni.dll(windows)庫或者libmyjni.so(類UNIX)庫。並且聲明瞭一個native方法,這個方法接受兩個int型別的引數,並且返回一個double型別的返回值,這個值是兩個int型數的平均值。mian方法呼叫了average函式。
下面,我們將上面的java程式碼編譯成TestJNIPrimitive.class,進而生成C/C++標頭檔案TestJNIPrimitive.h:

> javac TestJNIPrimitive.java
> javah TestJNIPrimitive       // Output is TestJNIPrimitive.h

C實現:TestJNIPrimitive.c

標頭檔案TestJNIPrimitive.h中包含了一個函式宣告:

JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint);

可以看到,這裡的jint和jdouble分別表示java中的int和double。
jni.h(windows上是win32/jni_mh.h)標頭檔案包含了這些資料型別的定義,同時多了一個jsize的定義:

// In "win\jni_mh.h" - machine header which is machine dependent
typedef long            jint;
typedef __int64         jlong;
typedef signed char     jbyte;

// In "jni.h"
typedef unsigned char   jboolean;
typedef unsigned short  jchar;
typedef short           jshort;
typedef float           jfloat;
typedef double          jdouble;
typedef jint            jsize;

有趣的是,jint對應到C的long型別(至少是32bit的),而不是C的int型別(至少是16bit的)。於是,在C程式碼中要使用jint而不是int是很重要的。同時,CygWin不支援__int64型別。
TestJNIPrimitive.c的實現如下:

#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitive.h"

JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
          (JNIEnv *env, jobject thisObj, jint n1, jint n2) {
   jdouble result;
   printf("In C, the numbers are %d and %d\n", n1, n2);
   result = ((jdouble)n1 + n2) / 2.0;
   // jint is mapped to int, jdouble is mapped to double
   return result;
}

然後,我們編譯程式碼成一個共享庫:

// MinGW GCC under Windows
> set JAVA_HOME={jdk-installed-directory}
> gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o myjni.dll TestJNIPrimitive.c

最後,我們執行這個java程式碼:

> java TestJNIPrimitive

C++實現 TestJNIPrimitive.cpp

程式碼如下:

#include <jni.h>
#include <iostream>
#include "TestJNIPrimitive.h"
using namespace std;

JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
          (JNIEnv *env, jobject obj, jint n1, jint n2) {
   jdouble result;
   cout << "In C++, the numbers are " << n1 << " and " << n2 << endl;
   result = ((jdouble)n1 + n2) / 2.0;
   // jint is mapped to int, jdouble is mapped to double
   return result;
}

使用g++來編譯上面的程式碼:

// MinGW GCC under Windows
> g++ -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o myjni.dll TestJNIPrimitive.cpp

傳遞字串

Java JNI 程式:TestJNIString.java

public class TestJNIString {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
   // Native method that receives a Java String and return a Java String
   private native String sayHello(String msg);

   public static void main(String args[]) {
      String result = new TestJNIString().sayHello("Hello from Java");
      System.out.println("In Java, the returned string is: " + result);
   }
}

上面的程式碼聲明瞭一個native函式sayHello,這個函式接受一個java的String,然後返回一個Java string,main方法呼叫了sayHello函式。
然後,我們編譯上面的程式碼,並且生成C/C++的標頭檔案:

> javac TestJNIString.java
> javah TestJNIString

C程式碼實現:TestJNIString.c

上面的標頭檔案TestJNIString.h聲明瞭這樣的一個函式:

JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *, jobject, jstring);

JNI定義了jstring型別應對java的String型別。上面宣告中的最後一個引數jstring就是來自Java程式碼中的String引數,同時,返回值也是一個jstring型別。
傳遞一個字串比傳遞基本型別要複雜的多,因為java的String是一個物件,而C的string是一個NULL結尾的char陣列。因此,我們需要將Java的String物件轉換成C的字串表示形式:char *。
前面我們提到,JNI環境指標JNIEnv *已經為我們定義了非常豐富的介面函式用來處理資料的轉換:
1. 呼叫const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)來將JNI的jstring轉換成C的char *
2. 呼叫jstring NewStringUTF(JNIEnv*, char*)來將C的char *轉換成JNI的jstring
因此我們的C程式基本過程如下:
1. 使用GetStringUTFChars()函式來將jstring轉換成char *
2. 然後進行需要的資料處理
3. 使用NewStringUTF()函式來將char *轉換成jstring,並且返回

#include <jni.h>
#include <stdio.h>
#include "TestJNIString.h"

JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
   // Step 1: Convert the JNI String (jstring) into C-String (char*)
   const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL);
   if (NULL == inCSt) return NULL;

   // Step 2: Perform its intended operations
   printf("In C, the received string is: %s\n", inCStr);
   (*env)->ReleaseStringUTFChars(env, inJNIStr, inCStr);  // release resources

   // Prompt user for a C-string
   char outCStr[128];
   printf("Enter a String: ");
   scanf("%s", outCStr);    // not more than 127 characters

   // Step 3: Convert the C-string (char*) into JNI String (jstring) and return
   return (*env)->NewStringUTF(env, outCStr);
}

將上面的程式碼編譯成共享庫:

// MinGW GCC under Windows
> gcc -Wl,--add-stdcall-alias -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o myjni.dll TestJNIString.c

最後,執行程式碼:

> java TestJNIString
In C, the received string is: Hello from Java
Enter a String: test
In Java, the returned string is: test

JNI中的string轉換函式

上面我們展示了兩個函式,現在我們全面梳理下JNI為我們提供的函式。JNI支援Unicode(16bit字元)和UTF-8(使用1~3位元組的編碼)轉化。一般而言,我們應該在C/C++中使用UTF-8的編碼方式。
JNI系統提供瞭如下關於字串處理的函式(一共兩組,UTF8和Unicode):

// UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)
// Can be mapped to null-terminated char-array C-string
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
   // Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding.
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
   // Informs the VM that the native code no longer needs access to utf.
jstring NewStringUTF(JNIEnv *env, const char *bytes);
   // Constructs a new java.lang.String object from an array of characters in modified UTF-8 encoding.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
   // Returns the length in bytes of the modified UTF-8 representation of a string.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
   // Translates len number of Unicode characters beginning at offset start into modified UTF-8 encoding 
   // and place the result in the given buffer buf.

// Unicode Strings (16-bit character)
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
   // Returns a pointer to the array of Unicode characters
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
   // Informs the VM that the native code no longer needs access to chars.
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);
   // Constructs a new java.lang.String object from an array of Unicode characters.
jsize GetStringLength(JNIEnv *env, jstring string);
   // Returns the length (the count of Unicode characters) of a Java string.
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf);
   // Copies len number of Unicode characters beginning at offset start to the given buffer buf

GetStringUTFChars()函式可以將jstring轉成char *,這個函式會返回NULL,如果系統的內容分配失敗的話。因此,好的做法是檢查這個函式的返回是不是NULL。第三個引數是isCopy,這個引數是一個in-out引數,傳進去的是一個指標,函式結束的時候指標的內容會被修改。如果內容是JNI_TRUE的話,那麼代表返回的資料是jstring資料的一個拷貝,反之,如果是JNI_FALSE的話,就說明返回的字串就是直接指向那個String物件例項的。在這種情況下,原生代碼不應該隨意修改string中的內容,因為修改會程式碼Java中的修改。JNI系統會盡量保證返回的是直接引用,如果不能的話,那就返回一個拷貝。通常,我們很少關心修改這些string ,因此我們這裡一般傳遞NULL給isCopy引數。
必須要注意的是,當你不在需要GetStringUTFChars返回的字串的時候,一定記得呼叫ReleaseStringUTFChars()函式來將記憶體資源釋放!否則會記憶體洩露!並且上層java中的GC也不能進行!
另外,在GetStringUTFChars和ReleaseStringUTFChars不能block!
NewStringUTF()函式可以從char *字串得到jstring。
關於更詳細的描述,請參考Java Native Interface Specification:http://docs.oracle.com/javase/7/docs/technotes/guides/jni/index.html

C++實現:TestJNIString.cpp

#include <jni.h>
#include <iostream>
#include <string>
#include "TestJNIString.h"
using namespace std;

JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
   // Step 1: Convert the JNI String (jstring) into C-String (char*)
   const char *inCStr = env->GetStringUTFChars(inJNIStr, NULL);
   if (NULL == inCStr) return NULL;

   // Step 2: Perform its intended operations
   cout << "In C++, the received string is: " << inCStr << endl;
   env->ReleaseStringUTFChars(inJNIStr, inCStr);  // release resources

   // Prompt user for a C++ string
   string outCppStr;
   cout << "Enter a String: ";
   cin >> outCppStr;

   // Step 3: Convert the C++ string to C-string, then to JNI String (jstring) and return
   return env->NewStringUTF(outCppStr.c_str());
}

使用g++編譯上面的程式碼:

// MinGW GCC under Windows
> g++ -Wl,--add-stdcall-alias -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o myjni.dll TestJNIString.cpp

需要注意的是,在C++中,本地string類的函式呼叫語法不一樣。在C++中,我們使用env->來呼叫,而不是(env*)->。同時,在C++函式中不需要JNIEnv*這個引數了。

傳遞基本型別的陣列

JNI 程式碼:TestJNIPrimitiveArray.java

public class TestJNIPrimitiveArray {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }

   // Declare a native method sumAndAverage() that receives an int[] and
   //  return a double[2] array with [0] as sum and [1] as average
   private native double[] sumAndAverage(int[] numbers);

   // Test Driver
   public static void main(String args[]) {
      int[] numbers = {22, 33, 33};
      double[] results = new TestJNIPrimitiveArray().sumAndAverage(numbers);
      System.out.println("In Java, the sum is " + results[0]);
      System.out.println("In Java, the average is " + results[1]);
   }
}

C語言實現:TestJNIPrimitiveArray.c

標頭檔案TestJNIPrimitiveArray.h包含以下函式宣告:

JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_average (JNIEnv *, jobject, jintArray);

在Java中,array是指一種型別,類似於類。一共有9種java的array,8個基本型別的array和一個object的array。JNI針對java的基本型別都定義了相應的array:jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray,並且也有面向object的jobjectArray。
同樣地,你需要在JNI array和Native array之間進行轉換,JNI系統已經為我們提供了一系列的介面函式:
1. 使用jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)將jintarray轉換成C的jint[]
2. 使用jintArray NewIntArray(JNIEnv *env, jsize len)函式來分配一個len位元組大小的空間,然後再使用void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)函式講jint[]中的資料拷貝到jintArray中去。
一共有8對類似上面的函式,分別對應java的8個基本資料型別。
因此,native程式需要:
1. 接受來自java的JNI array,然後轉換成本地array
2. 進行需要的資料操作
3. 將需要返回的資料轉換成jni的array,然後返回
下面是C程式碼實現的TestJNIPrimitiveArray.c:

#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitiveArray.h"

JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage
          (JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
   // Step 1: Convert the incoming JNI jintarray to C's jint[]
   jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL);
   if (NULL == inCArray) return NULL;
   jsize length = (*env)->GetArrayLength(env, inJNIArray);

   // Step 2: Perform its intended operations
   jint sum = 0;
   int i;
   for (i = 0; i < length; i++) {
      sum += inCArray[i];
   }
   jdouble average = (jdouble)sum / length;
   (*env)->ReleaseIntArrayElements(env, inJNIArray, inCArray, 0); // release resources

   jdouble outCArray[] = {sum, average};

   // Step 3: Convert the C's Native jdouble[] to JNI jdoublearray, and return
   jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2);  // allocate
   if (NULL == outJNIArray) return NULL;
   (*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2, outCArray);  // copy
   return outJNIArray;
}

JNI基本型別的array函式

JNI基本型別的array(jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray 和 jbooleanArray)函式如下:

// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean
NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);

同樣地,在get函式和release函式之間也不能always block。

訪問Java物件變數和回撥Java方法

訪問Java物件例項的變數

JNI程式:TestJNIInstanceVariable.java

public class TestJNIInstanceVariable {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }

   // Instance variables
   private int number = 88;
   private String message = "Hello from Java";

   // Declare a native method that modifies the instance variables
   private native void modifyInstanceVariable();

   // Test Driver   
   public static void main(String args[]) {
      TestJNIInstanceVariable test = new TestJNIInstanceVariable();
      test.modifyInstanceVariable();
      System.out.println("In Java, int is " + test.number);
      System.out.println("In Java, String is " + test.message);
   }
}

這個類包含了兩個private例項變數,一個int,一個String物件。然後我們在main中呼叫本地函式modifyInstanceVariable來修改這兩個變數。
C程式碼實現:TestJNIInstanceVariable.c

#include <jni.h>
#include <stdio.h>
#include "TestJNIInstanceVariable.h"

JNIEXPORT void JNICALL Java_TestJNIInstanceVariable_modifyInstanceVariable
          (JNIEnv *env, jobject thisObj) {
   // Get a reference to this object's class
   jclass thisClass = (*env)->GetObjectClass(env, thisObj);

   // int
   // Get the Field ID of the instance variables "number"
   jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I");
   if (NULL == fidNumber) return;

   // Get the int given the Field ID
   jint number = (*env)->GetIntField(env, thisObj, fidNumber);
   printf("In C, the int is %d\n", number);

   // Change the variable
   number = 99;
   (*env)->SetIntField(env, thisObj, fidNumber, number);

   // Get the Field ID of the instance variables "message"
   jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;");
   if (NULL == fidMessage) return;

   // String
   // Get the object given the Field ID
   jstring message = (*env)->GetObjectField(env, thisObj, fidMessage);

   // Create a C-string with the JNI String
   const char *cStr = (*env)->GetStringUTFChars(env, message, NULL);
   if (NULL == cStr) return;

   printf("In C, the string is %s\n", cStr);
   (*env)->ReleaseStringUTFChars(env, message, cStr);

   // Create a new C-string and assign to the JNI string
   message = (*env)->NewStringUTF(env, "Hello from C");
   if (NULL == message) return;

   // modify the instance variables
   (*env)->SetObjectField(env, thisObj, fidMessage, message);
}

為了訪問物件中的變數,我們需要:
1. 呼叫GetObjectClass()獲得目標物件的類引用
2. 從上面獲得的類引用中獲得Field ID來訪問變數,你需要提供這個變數的名字,變數的描述符(也稱為簽名)。對於java類而言,描述符是這樣的形式:“Lfully-qualified-name;”(注意最後有一個英文半形分號),其中的包名點號換成斜槓(/),比如java的Stirng類的描述符就是“Ljava/lang/String;”。對於基本型別而言,I代表int,B代表byte,S代表short,J代表long,F代表float,D代表double,C代表char,Z代表boolean。對於array而言,使用左中括號”[“來表示,比如“[Ljava/lang/Object;”表示Object的array,“[I”表示int型的array。
3. 基於上面獲得的Field ID,使用GetObjectField() 或者 Get_primitive-type_Field()函式來從中解析出我們想要的資料
4. 使用SetObjectField() 或者 Set_primitive-type_Field()函式來修改變數
JNI中用來訪問例項變數的函式有:

jclass Ge