JNI用法示例

前言:JNI簡介

Java Native Interface(JNI)是Java語言的本地編程接口,是J2SDK的一部分。在java程序中,我們可以通過JNI實現一些用java語言不便實現的功能。通常有以下幾種情況我們需要使用JNI來實現。

  • 標準的java類庫沒有提供你的應用程序所需要的功能,通常這些功能是平臺相關的。
  • 你希望使用一些已經有的類庫或者應用程序,而他們並非用java語言編寫的。
  • 程序的某些部分對速度要求比較苛刻,你選擇用匯編或者c語言來實現並在java語言中調用他們。

當然有時我們也會需要在別的語言項目中調用java類庫。

在《java核心技術》中,作者提到JNI的時候,建議不到萬不得已不要使用JNI技術,一方面它需要你把握更多的知識纔可以駕馭;另一方面,使用了JNI後你的程序就會喪失可移植性。在本文我們跳過JNI的底層機制,讀者最好先把它想象爲本地代碼和java代碼的粘合劑。關係如下圖所示:

這裏寫圖片描述

下面我們介紹幾種常見的JNI使用情景。

第一章:Windows下JAVA調用C/C++庫

我們使用的是Win7 64位系統,已經配置好了JDK和VS2010開發環境。
示例demo — http://download.csdn.net/detail/youmingyu/9702138

第一步、創建JAVA主程序

首先我們在硬盤上建立一個HelloJNIPro目錄作爲我們的工作目錄,然後編寫自己的java代碼,在java代碼中我們會聲明native方法,代碼非常簡單。如下所示

class HelloJNI
{
    public native int displayHelloJNI(int a,int b); //聲明外部實現函數
    static {
        System.loadLibrary("win64/helloJNILib"); //導入本地庫
    }

    public static void main(String[] args) {
        System.out.println(new HelloJNI().displayHelloJNI(1,2));
    }
}

注意displayHelloJNI()方法的聲明,它有一個關鍵字native,表明這個方法使用java以外的語言實現。這個方法不包括實現,因爲我們要用c/c++語言實現它。

注意System.loadLibrary("helloJNILib")這句代碼,它是在靜態初始化塊中定義的,用來裝載helloJNILib共享庫,這就是我們在後面生成的helloJNILib.dll(假如在其他的操作系統可能是其他的形式,比如Linux下爲helloJNILib.so)

最後用命令行:javac HelloJNI.java編譯生成HelloJNI.class文件,編譯最後執行也可以。

第二步、創建 .h 頭文件

這一步中我們要使用javah命令生成.h文件,這個文件要在後面的c/c++代碼中用到,運行命令行: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:    displayHelloJNI
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_HelloJNI_displayHelloJNI
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

第三步、編寫C/C++本地實現代碼

在這一步我們要用C/C++語言實現java中定義的方法,在VS中新建一個項目:helloJNILib,然後創建HelloJNI.cpp文件,內容如下

#include <jni.h> //導入jni頭文件
#include "HelloJNI.h" //導入jni頭文件
#include <stdio.h>

JNIEXPORT jint JNICALL Java_HelloJNI_displayHelloJNI(JNIEnv *env, jobject obj,jint a,jint b){
    printf("Hello JNI!\n");
    int c=a+b;
    return c;
}

注意這裏包含了jni.h和上一步得到的HelloJNI.h文件。因此你要在VS裏面設置好相關路徑,jni.h在JAVA_HOME/include裏面,方法如下。

這裏寫圖片描述

編譯生成helloJNILib.dll文件,方法參考:VS2010下靜態鏈接庫和動態鏈接庫的生成和使用,注意生成和自己Windows位數一致的dll,我的是64位。

第四步、運行JAVA程序

把上一步生成的helloJNILib.dll文件複製到工作目錄下的win64文件夾下。命令行運行:java HelloJNI命令,則可在控制檯看到如下輸出了。

這裏寫圖片描述

第二章:Windows下VS(C++)調用JAVA類

我們使用的是Win7 64位系統,已經配置好了JDK和VS2010開發環境。
示例demo — http://download.csdn.net/detail/youmingyu/9761243

第一步、開發JAVA類

java類沒有特殊要求,下面給出一個測試用例:JNITest.java,然後新建一個JNIJavaTest文件夾,將此文件放入其中(因爲java文件設置包名爲JNIJavaTest)。然後編譯生成class文件。

package JNIJavaTest;
//該類是爲了演示JNI如何訪問各種對象屬性等
public class JNITest{
    public static int COUNT = 8;//用於演示如何訪問靜態的基本類型屬性
    private String msg;//演示對象型屬性
    private int[] counts;

    public JNITest(){
        this("缺省構造函數");
    }

    //演示如何訪問構造器
    public JNITest(String msg){
        this.msg = msg;
        this.counts = null;
    }

    public String getMessage(){
        return msg;
    }

    // 該方法演示如何訪問一個靜態方法
    public static String getHelloWorld(){
        return "Hello world!";
    }

    //該方法演示參數的傳入傳出及中文字符的處理
    public String append(String str, int i){
        return str + i;
    }

    //演示數組對象的訪問
    public int[] getCounts(){
        return counts;
    }

    //演示如何構造一個數組對象
    public void setCounts(int[] counts){
        this.counts = counts;
    }

    //演示異常的捕捉
    public void throwExcp()throws IllegalAccessException{
        throw new IllegalAccessException("exception occur.");
    }

}

需要注意的是:避免在被調用的JAVA類中使用靜態final成員變量,因爲在C++中生成一個JAVA類的對象時,靜態final成員變量不會像JAVA中new對象時那樣先賦值。如果出現這種情況,在C++中調用該對象的方法時會發現該對象的靜態final成員變量值全爲0或者null(根據成員變量的類型而定)。

第三步、創建VS項目,配置環境

用VS新建一個項目,進行如下配置:

  • 設置包含目錄爲:%JAVA_HOME%\include;%JAVA_HOME%\include\win32,這是因爲需要用到jni.h和jni_md.h兩個頭文件(不設置也可以,請把這兩個文件copy到項目源文件處)。
  • 設置庫目錄爲:%JAVA_HOME%\lib,並附加依賴項jvm.lib(經測試這一步可以不做)。
  • 設置解決方案平臺同JDK一致,我們的JDK是64位,所以設置VS解決方案平臺是x64。

第三步、編寫C/C++代碼

下面以調用JNITest類中的append()函數爲例,給出一C++代碼實現例子。

#include "windows.h"
#include "jni.h"
#include <string>
#include <iostream>

using namespace std;

jstring NewJString(JNIEnv *env, LPCTSTR str);
string  JStringToCString (JNIEnv *env, jstring str);

int main(){

    //定義一個函數指針,下面用來指向JVM中的JNI_CreateJavaVM函數
    typedef jint (WINAPI *PFunCreateJavaVM)(JavaVM **, void **, void *);
    int res;
    JavaVMInitArgs vm_args;
    JavaVMOption options[3];
    JavaVM *jvm;
    JNIEnv *env;

    /*------------------------------設置初始化參數-----------------------------------------*/

    //disable JIT,這是JNI文檔中的解釋,具體意義不是很清楚 ,能取哪些值也不清楚。
    //從JNI文檔裏給的示例代碼中搬過來的
    options[0].optionString = "-Djava.compiler=NONE";
    //設置classDir,如果程序用到了第三方的JAR包,也可以在這裏麪包含進來
    options[1].optionString = "-Djava.class.path=..\\..;.";
    //設置顯示消息的類型,取值有gc、class和jni,如果一次取多個的話值之間用逗號格開,如-verbose:gc,class
    //該參數可以用來觀察C++調用JAVA的過程,設置該參數後,程序會在標準輸出設備上打印調用的相關信息
    options[2].optionString = "-verbose:NONE";

    //設置版本號,版本號有JNI_VERSION_1_1,JNI_VERSION_1_2和JNI_VERSION_1_4
    //選擇一個根你安裝的JRE版本最近的版本號即可,不過你的JRE版本一定要等於或者高於指定的版本號
    vm_args.version = JNI_VERSION_1_4;
    vm_args.nOptions = 3;
    vm_args.options = options;
    //該參數指定是否忽略非標準的參數,如果填JNI_FLASE,當遇到非標準參數時,JNI_CreateJavaVM會返回JNI_ERR
    vm_args.ignoreUnrecognized = JNI_TRUE;

    /*------------------------------創建虛擬機-----------------------------------------*/

    //加載JVM.DLL動態庫
    HINSTANCE hInstance = LoadLibrary("C:\\StudyProgram\\Java\\jdk1.8.0_45\\jre\\bin\\server\\jvm.dll");
    if (hInstance == NULL){
        return false;
    }
    //取得裏面的JNI_CreateJavaVM函數指針
    PFunCreateJavaVM funCreateJavaVM = (PFunCreateJavaVM)::GetProcAddress(hInstance, "JNI_CreateJavaVM");
    //調用JNI_CreateJavaVM創建虛擬機
    res = (*funCreateJavaVM)(&jvm, (void**)&env, &vm_args);
    if (res < 0){
        return -1;
    }

    /*------------------------------獲取java類的對象-----------------------------------------*/

    //查找test.Demo類,返回JAVA類的CLASS對象
    jclass cls = env->FindClass("test/Demo");
    //根據類的CLASS對象獲取該類的實例
    jobject obj = env->AllocObject(cls);
    //獲取類中的方法,最後一個參數是方法的簽名,通過命令:" javap -s -p 文件名 "可以獲得
    jmethodID mid = env->GetMethodID(cls, "append","(Ljava/lang/String;I)Ljava/lang/String;");

    /*------------------------------調用對象中方法-----------------------------------------*/

    //構造參數並調用對象的方法
    const char szTest[] = "國窖";
    jstring arg = NewJString(env, szTest);
    jstring msg = (jstring) env->CallObjectMethod(obj, mid, arg, 1573);
    cout<<JStringToCString(env, msg)<<endl;

    /*------------------------------清理內存-----------------------------------------*/

    //銷燬虛擬機並釋放動態庫
    jvm->DestroyJavaVM();
    ::FreeLibrary(hInstance);

    return 0;
}

//將JString轉化爲String
string  JStringToCString (JNIEnv *env, jstring str){ //(jstring str, LPTSTR desc, int desc_len)
    if(str==NULL){return "";}
    //在VC中wchar_t是用來存儲寬字節字符(UNICODE)的數據類型
    int len = env->GetStringLength(str);
    wchar_t *w_buffer = new wchar_t[len+1];
    char *c_buffer = new char[2*len+1];
    ZeroMemory(w_buffer,(len+1)*sizeof(wchar_t));
    //使用GetStringChars而不是GetStringUTFChars
    const jchar * jcharString = env->GetStringChars(str, 0);
    wcscpy(w_buffer,(const wchar_t *)jcharString);  
    env->ReleaseStringChars(str,jcharString);
    ZeroMemory(c_buffer,(2*len+1)*sizeof(char));
    //調用字符編碼轉換函數(Win32 API)將UNICODE轉爲ASCII編碼格式字符串
    len = WideCharToMultiByte(CP_ACP,0,w_buffer,len,c_buffer,2*len,NULL,NULL);
    string cstr = c_buffer;
    delete[] w_buffer;
    delete[] c_buffer;

    return cstr;
}

//將Char數組轉化爲JString
jstring NewJString(JNIEnv *env, LPCTSTR str)
{
    if(!env || !str){return 0;}
    int slen = strlen(str);
    jchar* buffer = new jchar[slen];
    int len = MultiByteToWideChar(CP_ACP,0,str,strlen(str),(LPWSTR)buffer,slen);
    if(len>0 && len < slen){
        buffer[len]=0;
    }
    jstring js = env->NewString(buffer,len);
    delete [] buffer;
    return js;
}

c++代碼中調用外部java類中的成員流程大體如下(以append()函數爲例):

  1. 設置JavaVMInitArgs類型的參數,注意JavaVMInitArgs裏的options[1]這個參數,它指的是java類的路徑,我們設它爲classDir,那麼java類的位置就爲:項目工作目錄(當前目錄)+classDir+java包名路徑。
  2. 利用LoadLibrary()函數加載jvm.dll動態庫,注意jvm.dll一般位於%JAVA_HOME%\jre\\bin\server\路徑下,請不要挪動它的位置,因爲JNI_CreateJavaVM函數內部會自動根據jvm.dll的路徑來獲取JRE的環境,如果要挪動,請一起挪動整個虛擬機需要的環境(整個jre文件夾,其實裏面大部分東西沒用,挪動時可以刪除,具體參考下文的:補充四)。
  3. 利用GetProcAddress()函數獲得JNI_CreateJavaVM函數的指針。
  4. 利用JNI_CreateJavaVM函數創建虛擬機JavaVM,並得到JNI的上下文環境JNIEnv。
  5. 利用JNIEnv->FindClass()獲得java類,注意這裏的參數是java類的(包名+類名),間隔用斜槓”/“,不要用”.“。
  6. 利用java類jclass獲取java類的對象。
  7. 利用java類對象jobject獲取Java裏相應函數,GetMethodID()函數最後一個參數是相應函數的簽名,可以通過命令:” javap -s -p 文件名 “獲得。
  8. 構造參數(如有必要),因爲java使用的參數類型和C++不一樣,有些不能強制轉型,比如Java用Unicode編碼字符,而C++不是。例子中給出了JString和CString相互轉化的方法。
  9. 利用CallObjectMethod調用java方法運算。
  10. 釋放內存。

補充一、java類其他成員的調用

1、調用JAVA中的靜態方法

//調用靜態方法  
jclass cls = env->FindClass("test/Demo");  
jmethodID mid = env->GetStaticMethodID(cls, "getHelloWorld","()Ljava/lang/String;");  
jstring msg = (jstring)env->CallStaticObjectMethod(cls, mid);      
cout<<JStringToCString(env, msg);  

2、調用JAVA中的靜態屬性

//調用靜態方法  
jclass cls = env->FindClass("test/Demo");  
jfieldID fid = env->GetStaticFieldID(cls, "COUNT","I");  
int count = (int)env->GetStaticIntField(cls, fid);     
cout<<count<<endl;  

3、調用JAVA中的帶參數構造函數

//調用構造函數  
jclass cls = env->FindClass("test/Demo");  
jmethodID mid = env->GetMethodID(cls,"<init>","(Ljava/lang/String;)V");  
const char szTest[] = "電信";  
jstring arg = NewJString(env, szTest);  
jobject demo = env->NewObject(cls,mid,arg);  
//驗證是否構造成功  
mid = env->GetMethodID(cls, "getMessage","()Ljava/lang/String;");  
jstring msg = (jstring)env->CallObjectMethod(demo, mid);   
cout<<JStringToCString(env, msg);  

4、傳入傳出數組

//傳入傳出數組  
//構造數組  
long        arrayCpp[] = {1,3,5,7,9};  
jintArray array = env->NewIntArray(5);  
env->SetIntArrayRegion(array, 0, 5, arrayCpp);  
//傳入數組  
jclass cls = env->FindClass("test/Demo");  
jobject obj = env->AllocObject(cls);  
jmethodID mid = env->GetMethodID(cls,"setCounts","([I)V");  
env->CallVoidMethod(obj, mid, array);  
//獲取數組  
mid = env->GetMethodID(cls,"getCounts","()[I");  
jintArray msg = (jintArray)env->CallObjectMethod(obj, mid, array);  
int len =env->GetArrayLength(msg);  
jint* elems =env-> GetIntArrayElements(msg, 0);  
for(int i=0; i< len; i++)  {  
    cout<<"ELEMENT "<<i<<" IS "<<elems[i]<<endl;  
}  
env->ReleaseIntArrayElements(msg, elems, 0);  

補充二、異常處理

由於調用了Java的方法,因此難免產生操作的異常信息,如JAVA函數返回的異常,或者調用JNI方法(如GetMethodID)時拋出的異常。這些異常沒有辦法通過C++本身的異常處理機制來捕捉到,但JNI可以通過一些函數來獲取Java中拋出的異常信息。

//異常處理  
jclass cls = env->FindClass("test/Demo");  
jobject obj = env->AllocObject(cls);  
jmethodID mid = env->GetMethodID(cls,"throwExcp","()V");  
env->CallVoidMethod(obj, mid);  
//獲取異常信息  
string exceptionInfo = "";  
jthrowable excp = 0;  
excp = env->ExceptionOccurred();   
if(excp)  {  
    jclass cls = env->GetObjectClass(excp);  
    env->ExceptionClear();  
    jmethodID mid = env->GetMethodID(cls, "toString","()Ljava/lang/String;");  
    jstring msg = (jstring) env->CallObjectMethod(excp, mid);  
    cout<<JStringToCString(env, msg)<<endl;    
    env->ExceptionClear();  
}

補充三、多線程

有些時候需要使用多線程的方式來訪問Java的方法。我們知道一個Java虛擬機是非常消耗系統的內存資源,差不多每個虛擬機需要內存大約在20MB左右。爲了節省資源要求每個線程使用的是同一個虛擬機,這樣在整個的JNI程序中只需要初始化一個虛擬機就可以了。

這裏面涉及到兩個概念,它們分別是虛擬機(JavaVM *jvm)和虛擬機環境(JNIEnv *env)。真正消耗大量系統資源的是jvm而不是env,jvm是允許多個線程訪問的,但是env只能被創建它本身的線程所訪問,而且每個線程必須創建自己的虛擬機環境env。主線程在初始化虛擬機的時候就創建了虛擬機環境env,爲了讓子線程能夠創建自己的env,JNI提供了兩個函數:AttachCurrentThread和DetachCurrentThread。下面代碼就是子線程訪問Java方法的框架:

// 將虛擬機通過參數傳入
DWORD WINAPI SubThreadProc(PVOID dwParam){ //PVOID:無類型指針
    JavaVM *g_jvm = (JavaVM*)dwParam;
    JNIEnv* g_env;
    (g_jvm)-> AttachCurrentThread((void**)&g_env, NULL);
    //..............
    (g_jvm)-> DetachCurrentThread();
    return 0;
}

補充四、關於項目的發佈

當要發佈使用了JNI的程序時,並不一定要求客戶要安裝一個Java運行環境,因爲可以在安裝程序中打包這個運行環境。爲了讓打包程序利於下載,這個包要比較小,因此要去除JRE(Java運行環境)中一些不必要的文件。下面給出一些必須的文件(注意這些文件之間的相對路徑不要變):

  • jre\bin\java.dll
  • jre\bin\zip.dll
  • jre\bin\server\jvm.dll
  • jre\lib\rt.jar

除此之外,根據項目使用的java類的不同,還會需要特定的文件,比如如果項目中用到了日曆,你會需要 jre\lib\tzmappings,如果你用了字符,你會需要jre\lib\charsets.jar,請根據自己的情況簡化jre環境。

另外rt.jar這個包較大,但是其中有很大一部分文件並不需要,可以根據實際的應用情況進行刪除。例如程序如果沒有用到Java Swing,就可以把涉及到Swing的文件都刪除後重新打包。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章