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的文件都删除后重新打包。

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