c/c++调用Java
来源:互联网 发布:斯托克斯矩阵 编辑:程序博客网 时间:2024/04/30 03:59
Java跨平台的特性使Java越来越受开发人员的欢迎,但也往往会听到不少的抱怨:用Java开发的图形用户窗口界面每次在启动的时候都会跳出一个控制台窗口,这个控制台窗口让本来非常棒的界面失色不少。怎么能够让通过Java开发的GUI程序不弹出J
Java跨平台的特性使Java越来越受开发人员的欢迎,但也往往会听到不少的抱怨:用Java开发的图形用户窗口界面每次在启动的时候都会跳出一个控制台窗口,这个控制台窗口让本来非常棒的界面失色不少。怎么能够让通过Java开发的GUI程序不弹出Java的控制台窗口呢?
其实现在很多流行的开发环境例如JBuilder、Eclipse都是使用纯Java开发的集成环境。这些集成环境启动的时候并不会打开一个命令窗口,因为它使用了JNI(Java Native Interface)的技术。
通过这种技术,开发人员不一定要用命令行来启动Java程序,可以通过编写一个本地GUI程序直接启动Java程序,这样就可避免另外打开一个命令窗口,让开发的Java程序更加专业。
JNI允许运行在虚拟机的Java程序能够与其它语言(例如C和C++)编写的程序或者类库进行相互间的调用。同时JNI提供的一整套的API,允许将Java虚拟机直接嵌入到本地的应用程序中。
本文将介绍如何在C/C++中调用Java方法,并结合可能涉及到的问题介绍整个开发的步骤及可能遇到的难题和解决方法。本文所采用的工具是Sun公司创建的 Java Development Kit (JDK) 版本 1.3.1,以及微软公司的Visual C++ 6开发环境。
环境搭建
为了让本文以下部分的代码能够正常工作,我们必须建立一个完整的开发环境。首先需要下载并安装JDK 1.3.1,其下载地址为“http://java.sun.com”。假设安装路径为C:\JDK。下一步就是设置集成开发环境,通过Visual C++ 6的菜单Tools→Options打开选项对话框。
将目录C:\JDK\include和C:\JDK\include\win32加入到开发环境的Include Files目录中,同时将C:\JDK\lib目录添加到开发环境的Library Files目录中。这三个目录是JNI定义的一些常量、结构及方法的头文件和库文件。集成开发环境已经设置完毕,同时为了执行程序需要把Java虚拟机所用到的动态链接库所在的目录C:\JDK \jre\bin\classic设置到系统的Path环境变量中。
这里需要提出的是,某些开发人员为了方便直接将JRE所用到的DLL文件直接拷贝到系统目录下。这样做是不行的,将导致初始化Java虚拟机环境失败(返回值-1),原因是Java虚拟机是以相对路径来寻找所用到的库文件和其它一些相关文件的。
至此整个JNI的开发环境设置完毕,为了让此次JNI旅程能够顺利进行,还必须先准备一个Java类。在这个类中将用到Java中几乎所有有代表性的属性及方法,如静态方法与属性、数组、异常抛出与捕捉等。我们定义的Java程序(Demo.java)如下,本文中所有的代码演示都将基于该Java程序,代码如下:
package jni.test; /** * 该类是为了演示JNI如何访问各种对象属性等 * @author liudong */ public class Demo { //用于演示如何访问静态的基本类型属性public static int COUNT = 8; //演示对象型属性 public String msg; private int[] counts; public Demo() { this("缺省构造函数"); } /** * 演示如何访问构造器 */ public Demo(String msg) { System.out.println("<init>:" + msg); this.msg = msg; this.counts = null; } /** * 该方法演示如何访问一个访问以及中文字符的处理 */ public String getMessage() { return msg; } /** * 演示数组对象的访问 */ public int[] getCounts(){ return counts; } /** * 演示如何构造一个数组对象 */ public void setCounts(int[] counts){ this.counts = counts; } /** * 演示异常的捕捉 */ public void throwExcp()throws IllegalAccessException{throw new IllegalAccessException("exception occur."); } }
初始化虚拟机
本地代码在调用Java方法之前必须先加载Java虚拟机,而后所有的Java程序都在虚拟机中执行。为了初始化Java虚拟机,JNI提供了一系列的接口函数Invocation API。通过这些API可以很方便地将虚拟机加载到内存中。
创建虚拟机可以用函数
jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args)
对于这个函数有一点需要注意的是,在JDK 1.1中第三个参数总是指向一个结构JDK1_ 1InitArgs, 这个结构无法完全在所有版本的虚拟机中进行无缝移植。在JDK 1.2中已经使用了一个标准的初始化结构JavaVMInitArgs来替代JDK1_1InitArgs。下面我们分别给出两种不同版本的示例代码。 在JDK 1.1初始化虚拟机:
#include <jni.h> int main() { JNIEnv *env; JavaVM *jvm; JDK1_1InitArgs vm_args; jint res;/* IMPORTANT: 版本号设置一定不能漏 */ vm_args.version = 0x00010001;/*获取缺省的虚拟机初始化参数*/ JNI_GetDefaultJavaVMInitArgs(&vm_args); /* 添加自定义的类路径 */ sprintf(classpath,"%s%c%s", vm_args.classpath,PATH_SEPARATOR, USER_CLASSPATH); vm_args.classpath = classpath; /*设置一些其他的初始化参数*/ /* 创建虚拟机 */ res = JNI_CreateJavaVM(&jvm,&env,&vm_args); if (res < 0) { fprintf(stderr, "Can't create Java VM\n"); exit(1); } /*释放虚拟机资源*/ (*jvm)->DestroyJavaVM(jvm); } JDK 1.2初始化虚拟机: /* invoke2.c */ #include <jni.h> int main() { int res; JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption options[3]; vm_args.version=JNI_VERSION_1_2;//这个字段必须设置为该值 /*设置初始化参数*/ options[0].optionString= "-Djava.compiler=NONE"; options[1].optionString = "-Djava.class.path=."; options[2].optionString= "-verbose:jni";//用于跟踪运行时的信息 /*版本号设置不能漏*/ vm_args.version = JNI_VERSION_1_2; vm_args.nOptions = 3; vm_args.options = options; vm_args.ignoreUnrecognized= JNI_TRUE; res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); if (res < 0){ fprintf(stderr,"Can't create Java VM\n"); exit(1); } (*jvm)->DestroyJavaVM(jvm); fprintf(stdout,"Java VM destory.\n"); }
为了保证JNI代码的可移植性,建议使用JDK 1.2的方法来创建虚拟机。JNI_CreateJavaVM函数的第二个参数JNIEnv *env,就是贯穿整个JNI始末的一个参数,因为几乎所有的函数都要求一个参数就是JNIEnv *env。 访问类方法 初始化了Java虚拟机后,就可以开始调用Java的方法。要调用一个Java对象的方法必须经过几个步骤: 1.获取指定对象的类定义(jclass) 有两种途径来获取对象的类定义:第一种是在已知类名的情况下使用FindClass来查找对应的类。但是要注意类名并不同于平时写的Java代码,例如要得到类jni.test.Demo的定义必须调用如下代码:
jclass cls = (*env)->FindClass(env,"jni/test/Demo");//把点号换成斜杠
然后通过对象直接得到其所对应的类定义:
jclass cls = (*env)-> GetObjectClass(env, obj);//其中obj是要引用的对象,类型是jobject
2.读取要调用方法的定义(jmethodID) 我们先来看看JNI中获取方法定义的函数:
jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig);jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env,jclass class, const char *name, const char *sig);
这两个函数的区别在于GetStaticMethodID是用来获取静态方法的定义,GetMethodID则是获取非静态的方法定义。这两个函数都需要提供四个参数:env就是初始化虚拟机得到的JNI环境;第二个参数class是对象的类定义,也就是第一步得到的obj;第三个参数是方法名称;最重要的是第四个参数,这个参数是方法的定义。
因为我们知道Java中允许方法的多态,仅仅是通过方法名并没有办法定位到一个具体的方法,因此需要第四个参数来指定方法的具体定义。但是怎么利用一个字符串来表示方法的具体定义呢?JDK中已经准备好一个反编译工具javap,通过这个工具就可以得到类中每个属性、方法的定义。下面就来看看jni.test.Demo的定义:
打开命令行窗口并运行 javap -s -p jni.test.Demo 得到运行结果如下:
Compiled from Demo.java public class jni.test.Demoextends java.lang.Object{ public static int COUNT; /* I */ public java.lang.String msg; /* Ljava/lang/String; */ private int counts[]; /* [I */ public jni.test.Demo(); /* ()V */ public jni.test.Demo(java.lang.String); /* (Ljava/lang/String;)V */ public java.lang.String getMessage(); /* ()Ljava/lang/String; */ public int getCounts()[]; /* ()[I */ public void setCounts(int[]); /* ([I)V */ public void throwExcp() throwsjava.lang.IllegalAccessException; /* ()V */ static {}; /* ()V */ }
我们看到类中每个属性和方法下面都有一段注释。注释中不包含空格的内容就是第四个参数要填的内容(关于javap具体参数请查询JDK的使用帮助)。下面这段代码演示如何访问jni.test.Demo的getMessage方法:
/* 假设我们已经有一个jni.test.Demo的实例obj */ jmethodID mid; jclass cls = (*env)-> GetObjectClass (env, obj);//获取实例的类定义 mid=(*env)->GetMethodID(env,cls,"getMessage"," ()Ljava/lang/String;");/*如果mid为0表示获取方法定义失败*/jstring msg = (*env)-> CallObjectMethod(env, obj, mid);/* 如果该方法是静态的方法那只需要将最后一句代码改为以下写法即可: jstring msg = (*env)-> CallStaticObjectMethod(env, cls, mid); */
3.调用方法 为了调用对象的某个方法,可以使用函数CallMethod或者CallStaticMethod(访问类的静态方法),根据不同的返回类型而定。这些方法都是使用可变参数的定义,如果访问某个方法需要参数时,只需要把所有参数按照顺序填写到方法中就可以。在讲到构造函数的访问时,将演示如何访问带参数的构造函数。 访问类属性 访问类的属性与访问类的方法大体上是一致的,只不过是把方法变成属性而已。 1.获取指定对象的类(jclass) 这一步与访问类方法的第一步完全相同,具体使用参看访问类方法的第一步。 2.读取类属性的定义(jfieldID) 在JNI中是这样定义获取类属性的方法的:
jfieldID (JNICALL *GetFieldID)(JNIEnv *env, jclass clazz,const char *name, const char *sig);jfieldID (JNICALL *GetStaticFieldID)(JNIEnv *env, jclass clazz, const char *name, const char *sig);
这两个函数中第一个参数为JNI环境;clazz为类的定义;name为属性名称;第四个参数同样是为了表达属性的类型。前面我们使用javap工具获取类的详细定义的时候有这样两行:
public java.lang.String msg;/* Ljava/lang/String; */
其中第二行注释的内容就是第四个参数要填的信息,这跟访问类方法时是相同的。 3.读取和设置属性值
有了属性的定义要访问属性值就很容易了。有几个方法用来读取和设置类的属性,它们是:GetField、SetField、GetStaticField、SetStaticField。比如读取Demo类的msg属性就可以用GetObjectField,而访问COUNT用GetStaticIntField,相关代码如下:
jfieldID field =(*env)->GetFieldID(env,obj,"msg"," Ljava/lang/String;"); jstring msg = (*env)-> GetObjectField(env, cls, field); //msg就是对应Demo的msg jfieldID field2 = (*env)->GetStaticFieldID(env,obj,"COUNT","I");jint count =(*env)->GetStaticIntField(env,cls,field2);
访问构造函数 很多人刚刚接触JNI的时候往往会在这一节遇到问题,查遍了整个jni.h看到这样一个函数NewObject,它应该是可以用来访问类的构造函数。但是该函数需要提供构造函数的方法定义,其类型是jmethodID。从前面的内容我们知道要获取方法的定义首先要知道方法的名称,但是构造函数的名称怎么来填写呢?其实访问构造函数与访问一个普通的类方法大体上是一样的,惟一不同的只是方法名称不同及方法调用时不同而已。访问类的构造函数时方法名必须填写“”。下面的代码演示如何构造一个Demo类的实例:
jclass cls = (*env)->FindClass(env, "jni/test/Demo"); /** 首先通过类的名称获取类的定义,相当于Java中的Class.forName方法 */ if (cls == 0) <error handler> jmethodID mid = (*env)->GetMethodID(env,cls,"<init>","(Ljava/lang/String;)V "); if(mid == 0) <error handler> jobject demo = jenv->NewObject(cls,mid,0); /** 访问构造函数必须使用NewObject的函数来调用前面获取的构造函数的定义上面的代码我们构造了一个Demo的实例并传一个空串null */
数组处理 创建一个新数组 要创建一个数组,我们首先应该知道数组元素的类型及数组长度。JNI定义了一批数组的类型jArray及数组操作的函数NewArray,其中就是数组中元素的类型。例如,要创建一个大小为10并且每个位置值分别为1-10的整数数组,编写代码如下:
int i = 1;jintArray array;//定义数组对象(*env)-> NewIntArray(env, 10);for(; i<= 10; i++)(*env)->SetIntArrayRegion(env, array, i-1, 1, &i);
访问数组中的数据 访问数组首先应该知道数组的长度及元素的类型。现在我们把创建的数组中的每个元素值打印出来,代码如下:
int i;/* 获取数组对象的元素个数 */int len =(*env)->GetArrayLength(env, array); /* 获取数组中的所有元素 */ jint* elems =(*env)-> GetIntArrayElements(env, array, 0); for(i=0; i< len; i++) printf("ELEMENT %d IS %d\n",i, elems[i]);
中文处理 中文字符的处理往往是让人比较头疼的事情,特别是使用Java语言开发的软件,在JNI这个问题更加突出。由于Java中所有的字符都是Unicode编码,但是在本地方法中,例如用VC编写的程序,如果没有特殊的定义一般都没有使用Unicode的编码方式。为了让本地方法能够访问Java中定义的中文字符及Java访问本地方法产生的中文字符串,我定义了两个方法用来做相互转换。 方法一,将Java中文字符串转为本地字符串
/** 第一个参数是虚拟机的环境指针 第二个参数为待转换的Java字符串定义 第三个参数是本地存储转换后字符串的内存块 第三个参数是内存块的大小 */int JStringToChar(JNIEnv *env, jstring str,LPTSTR desc, int desc_len) { int len = 0; if(desc==NULL||str==NULL) return -1;//在VC中wchar_t是用来存储宽字节字符(UNICODE)的数据类型wchar_t *w_buffer = new wchar_t[1024];ZeroMemory(w_buffer,1024*sizeof(wchar_t));//使用GetStringChars而不是GetStringUTFCharswcscpy(w_buffer,env->GetStringChars(str,0));env->ReleaseStringChars(str,w_buffer); ZeroMemory(desc,desc_len);//调用字符编码转换函数(Win32 API)将UNICODE转为ASCII编码格式字符串len = WideCharToMultiByte(CP_ACP,0,w_buffer,1024,desc,desc_len,NULL,NULL); //len = wcslen(w_buffer); if(len>0 && len<desc_len) desc[len]=0; delete[] w_buffer; return strlen(desc); }
方法二,将C的字符串转为Java能识别的Unicode字符串
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),buffer,slen); if(len>0 && len < slen) buffer[len]=0; jstring js =env->NewString(buffer,len); delete [] buffer; return js; }
异常
由于调用了Java的方法,因此难免产生操作的异常信息。这些异常没有办法通过C++本身的异常处理机制来捕捉到,但JNI可以通过一些函数来获取Java中抛出的异常信息。之前我们在Demo类中定义了一个方法throwExcp,下面将访问该方法并捕捉其抛出来的异常信息,代码如下:
/** 假设我们已经构造了一个Demo的实例obj,其类定义为cls */ jthrowable excp = 0;/* 异常信息定义 */ jmethodID mid=(*env)->GetMethodID(env,cls,"throwExcp","()V"); /*如果mid为0表示获取方法定义失败*/ jstring msg = (*env)-> CallVoidMethod(env, obj, mid); /* 在调用该方法后会有一个IllegalAccessException的异常抛出 */ excp = (*env)->ExceptionOccurred(env); if(excp){ (*env)->ExceptionClear(env); //通过访问excp来获取具体异常信息 /* 在Java中,大部分的异常信息都是扩展类java.lang.Exception,因此可以访问excp的toString或者getMessage来获取异常信息的内容。访问这两个方法同前面讲到的如何访问类的方法是相同的。 */ }
线程和同步访问 有些时候需要使用多线程的方式来访问Java的方法。我们知道一个Java虚拟机是非常消耗系统的内存资源,差不多每个虚拟机需要内存大约在20MB左右。为了节省资源要求每个线程使用的是同一个虚拟机,这样在整个的JNI程序中只需要初始化一个虚拟机就可以了。所有人都是这样想的,但是一旦子线程访问主线程创建的虚拟机环境变量,系统就会出现错误对话框,然后整个程序终止。 其实这里面涉及到两个概念,它们分别是虚拟机(JavaVM *jvm)和虚拟机环境(JNIEnv *env)。真正消耗大量系统资源的是jvm而不是env,jvm是允许多个线程访问的,但是env只能被创建它本身的线程所访问,而且每个线程必须创建自己的虚拟机环境env。 这时候会有人提出疑问,主线程在初始化虚拟机的时候就创建了虚拟机环境env。为了让子线程能够创建自己的env,JNI提供了两个函数:AttachCurrentThread和DetachCurrentThread。下面代码就是子线程访问Java方法的框架:
DWORD WINAPI ThreadProc(PVOID dwParam) { JavaVM jvm = (JavaVM*)dwParam;/* 将虚拟机通过参数传入 */ JNIEnv* env; (*jvm)-> AttachCurrentThread(jvm, (void**)&env, NULL); .........(*jvm)-> DetachCurrentThread(jvm); }
时间 关于时间的话题是我在实际开发中遇到的一个问题。当要发布使用了JNI的程序时,并不一定要求客户要安装一个Java运行环境,因为可以在安装程序中打包这个运行环境。为了让打包程序利于下载,这个包要比较小,因此要去除JRE(Java运行环境)中一些不必要的文件。 但是如果程序中用到Java中的日历类型,例如java.util.Calendar等,那么有个文件一定不能去掉,这个文件就是[JRE]\lib\tzmappings。它是一个时区映射文件,一旦没有该文件就会发现时间操作上经常出现与正确时间相差几个小时的情况。下面是打包JRE中必不可少的文件列表(以Windows环境为例),其中[JRE]为运行环境的目录,同时这些文件之间的相对路径不能变。 文件名 目录
hpi.dll [JRE]\binioser12.dll [JRE]\binjava.dll [JRE]\binnet.dll [JRE]\binverify.dll [JRE]\binzip.dll [JRE]\binjvm.dll [JRE]\bin\classicrt.jar [JRE]\libtzmappings [JRE]\lib
由于rt.jar有差不多10MB,但是其中有很大一部分文件并不需要,可以根据实际的应用情况进行删除。例如程序如果没有用到Java Swing,就可以把涉及到Swing的文件都删除后重新打包。 (T117)
参考地址:http://www.ccidnet.com/2005/0413/237901.shtml
C++调用JAVA主要用到了SUN公司的JNI技术, JNI是Java Native Interface的 缩写。从Java 1.1开始,Java Native Interface (JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。相关资料见http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/jniTOC.html
开发环境安装及配置
1.1 安装JDK
到SUN公司网站可以下载到最新版的JDK。下载下来后开始安装,一路选择默认配置即可,本文档中假定安装的是JDK1.4,安装目录为C:\j2sdk1.4.2_15。
1.2 配置VC6.0
通过Visual C++ 6的菜单Tools→Options打开选项对话框。在Directories标签页下添加JDK的相关目录到Include和目录下。
开发测试用到的JAVA类
2.1 开发JAVA类
在硬盘的任意地方新建一个名叫test的文件夹,本文档示例中将test文件夹建立在C盘根目录,然后在里面新建一个名称叫Demo.java的JAVA文件,将下面测试用的代码粘贴到该文件中。
package test; /** * 该类是为了演示JNI如何访问各种对象属性等 */ public class Demo { //用于演示如何访问静态的基本类型属性 public static int COUNT = 8; //演示对象型属性 private String msg; private int[] counts; public Demo() { this("缺省构造函数"); } /** * 演示如何访问构造器 */ public Demo(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."); } }
2.2 编译JAVA类
运行CMD控制台程序进入命令行模式,输入命令javac -classpath c:\ c:\test\Demo.java,-classpath参数指定classpath的路径,这里就是test目录所在的路径。(注意:如果你没有将JDK的环境变量设置好,就需要先进入JDK的bin目录下,如下图所示。)
2.3 查看方法的签名
我们知道Java中允许方法的多态,仅仅是通过方法名并没有办法定位到一个具体的方法,因此需要一个字符串来唯一表示一个方法。但是怎么利用一个字 符串来表示方法的具体定义呢?JDK中已经准备好一个反编译工具javap,通过这个工具就可以得到类中每个属性、方法的签名。在CMD下运行javap -s -p -classpath c:\ test.Demo即可看到属性和方法的签名。如下图红色矩形框起来的字符串为方法String append(String str, int i)的签名。
在VC中调用JAVA类
3.1 快速调用JAVA中的函
在VC中新建一个控制台程序,然后新建一个CPP文件,将下面的代码添加到该文件中。运行该文件,即可得到Demo类中String append(String str, int i)函数返回的字符串。
#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"; //设置classpath,如果程序用到了第三方的JAR包,也可以在这里面包含进来 options[1].optionString = "-Djava.class.path=.;c:\\"; //设置显示消息的类型,取值有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:\\j2sdk1.4.2_15\\jre\\bin\\client\\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; } //查找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, 12); cout<<JStringToCString(env, msg); //销毁虚拟机并释放动态库 jvm->DestroyJavaVM(); ::FreeLibrary(hInstance); return 0; } 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,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; } 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),buffer,slen); if(len>0 && len < slen) { buffer[len]=0; } jstring js = env->NewString(buffer,len); delete [] buffer; return js; }
3.2 调用步骤分析及注意事项
a、加载jvm.dll动态库,然后获取里面的JNI_CreateJavaVM函数。这个步骤也可以通过在VC工程的LINK标签页里添加对jvm.lib的连接,然后在环境变量里把jvm.dll所在的路径加上去来实现。但后面这种方法在部署的时候会比前一个方法麻烦。 b、利用构造好的参数,调用JNI_CreateJavaVM函数创建JVM。JNI_CreateJavaVM函数内部会自动根据jvm.dll的路径来获取JRE的环境,所以千万不要把jvm.dll文件拷贝到别的地方,然后再通过LoadLibrary函数导入。 c、JVM创建成功后,JNI_CreateJavaVM函数会传出一个JNI上下文环境对象(JNIEnv),利用该对象的相关函数就可以调用JAVA类的属性和方法了。 d、以上面的代码为例:先调用JNIEnv的FindClass方法,该函数传入一个参数,该参数就是java类的全局带包名的名称,如上面示例中的test/Demo表示test包中的Demo类。这个方法会在你创建JVM时设置的classpath路径下找相应的类,找到后就会返回该类的class对象。 Class是JAVA中的一个类,每个JAVA类都有唯一的一个静态的Class对象,Class对象包含类的相关信息。为了使FindClass方法能找到你的类,请确保创建JVM时-Djava.class.path=参数设置正确。注意:系统环境变量中的CLASSPATH对这里创建JVM没有影响,所以不要以为系统CLASSPATH设置好了相关路径后这里就不用设置了。 e、利用FindClass返回的class对象,调用GetMethodID函数可以获得里面方法的ID,在这里GetMethodID函数传入了三个参数:第一个参数是class对象,因为方法属于某个具体的类;第二个参数是方法的名称;第三个参数是方法的签名,这个签名可以在前面3.3中介绍的方法获得。 f、利用class对象,可以通过调用AllocObject函数获得该class对象对应类的一个实例,即Demo类的对象。 g、利用上面获取的函数ID和Demo类的对象,就可以通过CallObjectMethod函数调用相应的方法,该函数的参数跟printf函数的参数一样,个数是不定的。第一个参数是类的对象;第二个参数是要调用的方法的ID;后面的参数就是需要传给调用的JAVA类方法的参数,如果调用的JAVA类方法没有参数,则调用CallObjectMethod时传前两个参数就可以了。 h、从上面的示例中可以看到,在调用JAVA的方法前,构造传入的字符串时,用到了NewJString函数;在调用该方法后,对传出的字符串调用了JstringToCString函数。这是由于Java中所有的字符都是Unicode编码,但是在本地方法中,例如用VC编写的程序,如果没有特殊的定义一般都没有使用Unicode的编码方式。为了让本地方法能够访问Java中定义的中文字符及Java访问本地方法产生的中文字符串,定义了两个方法用来做相互转换。 i、避免在被调用的JAVA类中使用静态final成员变量,因为在C++中生成一个JAVA类的对象时,静态final成员变量不会像JAVA中new对象时那样先赋值。如果出现这种情况,在C++中调用该对象的方法时会发现该对象的静态final成员变量值全为0或者null(根据成员变量的类型而定)。
3.3 调用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);
3.4 调用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.5 调用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);
3.6 传入传出数组
//传入传出数组 //构造数组 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);
3.7 异常处理
由于调用了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); out<<JStringToCString(env, msg)<<endl; env->ExceptionClear(); }
参考地址:http://www.360doc.com/content/10/0209/10/59141_15507749.shtml
- java调用C语言
- java调用c
- 实现java调用c
- Java调用C/C++
- C调用JAVA
- C/C++调用Java
- java调用C
- java调用c、c++
- Android C调用Java
- Java调用C JNI
- java调用c程序
- Java调用C/C++
- Java调用C语言
- android java 调用 c
- java 调用C/C++
- jni c调用java
- C调用Java
- Java调用C语言
- 轮播图
- 从ASP.NET Web API 2 (C#)开始说起
- 小程序wx:key的作用
- Unity3d:Occlusion Culling
- c# donald gerald robert
- c/c++调用Java
- cordova/ionic alipay 支付宝插件
- 数据库设计原则
- 【BZOJ 2084】[Poi2010]Antisymmetry manacher
- 我的2016年个人总结
- 原码、反码、补码、负数的移位
- DB2常用函数详解(一):字符串函数
- [51Nod 1189阶乘分数]数学
- Launcher介绍