前言
简单介绍C/C++如何调用Java,内容适合未接触过此类工程的朋友作为上手参考。
一、编译环境
1. 64位的win10系统
2. JDK:jdk-8u181-windows-x64.exe
3. IDE VS2017
二、调用步骤及Java虚拟机使用方法
一般步骤:
- 编写Java代码, 并编译
- 编写C/C++代码
- 配置lib进行编译,配置PATH添加相应的dll
使用方法:
- 创建虚拟机
- 寻找class对象, 创建对象
- 调用静态方法和成员方法
- 获取成员属性, 修改成员属性
三、编译过程
1、配置环境变量
添加环境变量JAVA_HOME,路径为Java安装路径
在path里面添加路径,路径为bin目录所在路径和jvm.dll所在路径
2、构建被调用Java类
一段简单的代码
代码如下(示例):
public class Sample2 {
public String name;
public static String sayHello(String name) {
return "Hello, " + name + "!";
}
public String sayHello() {
return "Hello, " + name + "!";
}
}
运行下面的命令编译
javac Sample2.java
可以在当前目录下看到Sample2.class文件, 编译成功.。
可以查看Sample2类中的签名
javap -s -private Sample2
结果如下
Compiled from "Sample2.java"
public class Sample2 {
public java.lang.String name;
descriptor: Ljava/lang/String;
public Sample2();
descriptor: ()V
public static java.lang.String sayHello(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
public java.lang.String sayHello();
descriptor: ()Ljava/lang/String;
}
3、创建C/C++调用Java工程(VS控制台工程x64)
c++示例代码如下:
#include <jni.h>
#include <string.h>
#include <stdio.h>
// 环境变量PATH在windows下和linux下的分割符定义
#ifdef _WIN32
#define PATH_SEPARATOR ';'
#else
#define PATH_SEPARATOR ':'
#endif
int main(void)
{
JavaVMOption options[1];
JNIEnv *env;
JavaVM *jvm;
JavaVMInitArgs vm_args;
long status;
jclass cls;
jmethodID mid;
jfieldID fid;
jobject obj;
options[0].optionString = "-Djava.class.path=D:/c_call_java/javaclass/";
memset(&vm_args, 0, sizeof(vm_args));
vm_args.version = JNI_VERSION_1_4;
vm_args.nOptions = 1;
vm_args.options = options;
// 启动虚拟机
status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
if (status != JNI_ERR)
{
// 先获得class对象
cls = env->FindClass("Sample2");
if (cls != 0)
{
// 获取方法ID, 通过方法名和签名, 调用静态方法
mid = env->GetStaticMethodID( cls, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;");
if (mid != 0)
{
const char* name = "World";
jstring arg = env->NewStringUTF(name);
jstring result = (jstring)env->CallStaticObjectMethod(cls, mid, arg);
const char* str = env->GetStringUTFChars(result, 0);
printf("Result of sayHello: %s\n", str);
env->ReleaseStringUTFChars(result, 0);
}
/*** 新建一个对象 ***/
// 调用默认构造函数
//obj = (*env)->AllocObject(env, cls);
// 调用指定的构造函数, 构造函数的名字叫做<init>
mid = env->GetMethodID( cls, "<init>", "()V");
obj = env
->NewObject( cls, mid);
if (obj == 0)
{
printf("Create object failed!\n");
}
/*** 新建一个对象 ***/
// 获取属性ID, 通过属性名和签名
fid = (env)->GetFieldID( cls, "name", "Ljava/lang/String;");
if (fid != 0)
{
const char* name = "icejoywoo";
jstring arg = (env)->NewStringUTF( name);
(env)->SetObjectField( obj, fid, arg); // 修改属性
}
// 调用成员方法
mid = (env)->GetMethodID( cls, "sayHello", "()Ljava/lang/String;");
if (mid != 0)
{
jstring result = (jstring)(env)->CallObjectMethod( obj, mid);
const char* str = (env)->GetStringUTFChars(result, 0);
printf("Result of sayHello: %s\n", str);
(env)->ReleaseStringUTFChars( result, 0);
}
}
(jvm)->DestroyJavaVM();
return 0;
}
else
{
printf("JVM Created failed!\n");
return -1;
}
}
这段代码大概做了这几件事
1.创建虚拟机JVM, 在程序结束的时候销毁虚拟机JVM
2.寻找class对象
3.创建class对象的实例
4.调用方法和修改属性
工程相关配置如下所示:
添加包含目录
添加库目录
引用库文件jvm.lib
运行结果:
Result of sayHello: Hello, World!
Result of sayHello: Hello, icejoywoo!
4、虚拟的创建
与之相关的有这样几个变量
JavaVMOption options[1];
JNIEnv *env;
JavaVM *jvm;
JavaVMInitArgs vm_args;
JavaVM就是我们需要创建的虚拟机实例
JavaVMOption相当于在命令行里传入的参数
JNIEnv在Java调用C/C++中每个方法都会有的一个参数, 拥有一个JNI的环境
JavaVMInitArgs就是虚拟机创建的初始化参数, 这个参数里面会包含JavaVMOption
下面就是创建虚拟机
options[0].optionString = "-Djava.class.path=.";
memset(&vm_args, 0, sizeof(vm_args));
vm_args.version = JNI_VERSION_1_4;
vm_args.nOptions = 1;
vm_args.options = options;
// 启动虚拟机
status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
"-Djava.class.path=."看着眼熟吧,这个就是传入当前路径,作为JVM寻找class的用户自定义路径(当然也可以不在当前路径,你可以随便修改)。
vm_args.version是Java的版本, 这个应该是为了兼容以前的JDK, 可以使用旧版的JDK, 这个宏定义是在jni.h中, 有以下四种
#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006
vm_args.nOptions的含义是, 你传入的options有多长, 我们这里就一个, 所以是1.
vm_args.options = options把JavaVMOption传给JavaVMInitArgs里面去.
然后就是启动虚拟机了status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args).
可以通过这个返回值status , 知道虚拟机是否启动成功
#define JNI_OK 0 /* success */
#define JNI_ERR (-1) /* unknown error */
#define JNI_EDETACHED (-2) /* thread detached from the VM */
#define JNI_EVERSION (-3) /* JNI version error */
#define JNI_ENOMEM (-4) /* not enough memory */
#define JNI_EEXIST (-5) /* VM already created */
#define JNI_EINVAL (-6) /* invalid arguments */
寻找class对象, 并实例化
JVM在Java中都是自己启动的,在C/C++中只能自己来启动了, 启动完之后的事情就和在Java中一样了, 不过要使用C/C++的语法.
获取class对象比较简单,FindClass(env, className).
cls = env->FindClass("Sample2");
在Java中的类名格式是java.lang.String, 但是className的格式有点不同, 不是使用’.‘作为分割, 而是’/', 即java/lang/String.
我们知道Java中构造函数有两种, 一种是默认的没有参数的, 一种是自定义的带有参数的. 对应的在C/C++中, 有两种调用构造函数的方法.
调用默认构造函数
// 调用默认构造函数
obj = env->AllocObject(cls);
构造函数也是方法, 类似调用方法的方式.
// 调用指定的构造函数, 构造函数的名字叫做
mid = env->GetMethodID(cls, "<init>", "()V");
obj = env->NewObject(cls, mid);
调用方法和修改属性
关于方法和属性是有两个ID与之对应, 这两个ID用来标识方法和属性.
jmethodID mid;
jfieldID fid;
方法分为静态和非静态的, 所以对应的有
mid = env->GetStaticMethodID(cls, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;");
mid = env->GetMethodID(cls, "sayHello", "()Ljava/lang/String;");
上面两个方法是同名的, 都叫sayHello, 但是签名不同, 所以可以区分两个方法.
JNI的函数都是有一定规律的, Static就表示是静态, 没有表示非静态.
方法的调用如下
jstring result = (jstring)env->CallStaticObjectMethod(cls, mid, arg);
jstring result = (jstring)env->CallObjectMethod(obj, mid);
我们可以看到静态方法是只需要class对象, 不需要实例的, 而非静态方法需要使用我们之前实例化的对象.
属性也有静态和非静态, 示例中只有非静态的.
获取属性ID
fid = env->GetFieldID(cls, "name", "Ljava/lang/String;");
修改属性的值
env->SetObjectField(obj, fid, arg); // 修改属性
关于jstring的说明
java的String都是使用了unicode, 是双字节的字符, 而C/C++中使用的单字节的字符.
从C转换为java的字符, 使用NewStringUTF方法
jstring arg = env->NewStringUTF(name);
从java转换为C的字符, 使用GetStringUTFChars
const char* str = env->GetStringUTFChars(result, 0);
四、总结
编译和运行
- 编译需要头文件, 头文件在这两个目录中%JAVA_HOME%\include和%JAVA_HOME%\include\win32, 第一个是与平台无关的, 第二个是与平台有关的, 由于笔者的系统是windows, 所以是win32.
- 编译的时候还要一个lib文件(jvm.lib), 是对虚拟机的支持, 保证编译通过.
- 我们可以看到在当前目录下Sample2.exe, 运行的时候需要jvm.dll(不要将其复制到当前目录下, 这样不可以运行, 会导致jvm创建失败)
- jvm.dll在%JAVA_HOME%\jre\bin\server\目录下, 所以我把这个目录加入到PATH中, 然后就可以运行
五、注意
- 动态链接库和JDK都有32位和64位的区别, 使用32位系统的朋友, 要注意这个问题, 可能导致运行或编译错误.(笔者这里使用的是64位)
- 还要注意区分C和C++代码, 在JNI中两种代码有一定的区别, 主要是env和jvm两个地方.