[技术小贴士]: 用C/C++调用java
以前在某娱乐公司工作的时候,做RTB(Real Time Bidding)广告用到了AES,当时是用C++实现的。后来部门作鸟兽散,孔雀东南飞,我当时的组长去了某DSP公司当CTO。有一天我的前组长突然叫我帮他把一个java的AES包转成C++。既然有现成的java实现,如果用C++去重新实现一次逻辑,显然是太麻烦啦。最简单的方案就是直接用C++去调用java代码。于是自己折腾了一番,终于悟出了一点道道。
首先,C/C++程序需要包含头文件$JAVA_HOME/include/jni.h。在使用C/C++调用java的过程中,需要使用java虚拟机对象JavaVM和java环境对象JNIEnv。首先需要初始化java环境,创建java虚拟机,然后再去获取.class文件中的对象、方法,待使用完之后,最后再销毁java虚拟机,结束进程。
我们可以把java虚拟机对象和java环境对象设定为全局对象,封装到一个初始化函数中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static JavaVM *jvm;
static JNIEnv *env;
void JVM_Init()
{
JavaVMInitArgs vm_args;
JavaVMOption options[1];
vm_args.version = JNI_VERSION_1_2;
vm_args.ignoreUnrecognized = JNI_TRUE;
vm_args.nOptions = 0;
char classpath[1024] = "-Djava.class.path=";
char *env_classpath = getenv("CLASSPATH");
if (env_classpath) {
options[0].optionString = strcat(classpath, env_classpath);
vm_args.nOptions++;
}
if (vm_args.nOptions > 0) {
vm_args.options = options;
}
// 创建java虚拟机
jint res = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
if (res < 0) {
printf("Create Java VM error, code = %d/n", res);
exit(-1);
}
}
void JVM_Destroy()
{
jvm->DestroyJavaVM();
env = NULL;
jvm = NULL;
}
初始化之后,获取了jvm和env句柄,就可以去获取java对象了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取java对象(对应磁盘上的SomeObj.class)
jclass cls = env->FindClass("SomeObj");
// 获取静态方法,其中第三个参数,方法签名,可以用javap -s <className> 命令获取
jmethodID mid = env->GetStaticMethodID(cls, "MethodName", "MethodSig");
// 获取方法
jmethodID mid = env->GetMethodID(cls, "MethodName", "MethodSig");
// 获取成员变量,成员变量的签名同样可以通过javap -s 查看
jfieldID fid = env->GetFieldID(cls, "fieldName", "fieldSig");
// 调用静态方法
jobject obj = env->CallStaticObjectMethod(cls, mid, ...);
// 调用方法
jobject obj = env->CallObjectMethod(cls, mid, ...);
另外,由于java和C/C++字符串的内存布局不一致,java动态分配的内存由java虚拟机回收,而C/C++分配的内存由调用者手动回收,所以,在写jni程序的时候,需要处理java和C/C++字符串的转换工作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
jstring ch2jstring(const char* cstr)
{
size_t len = strlen(str);
jclass strClass = env->FindClass("java/lang/String");
jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
jbyteArray bytes = env->NewByteArray(len);
env->SetByteArrayRegion(bytes, 0, len, (jbyte*)cstr);
jstring encoding = env->NewStringUTF("utf-8");
return (jstring)env->NewObject(strClass, ctorID, bytes, encoding);
}
/* 注意,这里返回的字符串需要手动调用free回收 */
const char *jstring2ch(jstring jstr)
{
char* rtn = NULL;
jclass clsString = env->FindClass("java/lang/String");
jstring strEncode = env->NewStringUTF("utf-8");
jmethodID mid = env->GetMethodID(clsString, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr= (jbyteArray)env->CallObjectMethod(jstr, mid, strEncode);
jsize alen = env->GetArrayLength(barr);
jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE);
if (alen > 0) {
rtn = (char*)malloc(alen + 1);
memcpy(rtn, ba, alen);
rtn[alen] = 0;
}
env->ReleaseByteArrayElements(barr, ba, 0);
return rtn;
}
如果您是一位C++工程师,那么,到这里就可以结束了。然后如果你是一位C工程师,那么这里仅仅只是开始而已。我一直觉得使用纯C的人才是真正的神。当然啦,不是说用C++的工程师都很逊,而是写C++的真正牛人,在C++工程师中所占的比例是极小的。而绝大多数C++工程师,他们夜以继日所干的事情,无非就是不断地在他们的代码中埋下更多的隐形炸弹和性能炸弹,例如默认拷贝函数、大对象的值传递等等等等。用纯粹的C写代码是有好处的,一个最最直观的优势,就是你能很方便地使用python,go,lua等语言直接调用你的C代码,而如果你想要调用的语言是C++,那你可得折腾一番啦。
闲侃时间结束,我们来深入到jni.h中看看C和C++调用java的区别:
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* JNI Invocation Interface.
*/
struct JNIInvokeInterface_;
struct JavaVM_;
#ifdef __cplusplus
typedef JavaVM_ JavaVM;
#else
typedef const struct JNIInvokeInterface_ *JavaVM;
#endif
我们看到,在jni.h中,如果是用C++编译器,那么JavaVM其实是JavaVM_,而如果是C编译器,则是JNIInvokeInterface_ *,先来看看JavaVM_,由于是C++编译环境,所以有一个this指针,那么在这里只需要向C函数传入this指针就可以了,所以才有了functions->DestroyJavaVM(this)
这样的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct JavaVM_ {
const struct JNIInvokeInterface_ *functions;
#ifdef __cplusplus
jint DestroyJavaVM() {
return functions->DestroyJavaVM(this);
}
jint AttachCurrentThread(void **penv, void *args) {
return functions->AttachCurrentThread(this, penv, args);
}
jint DetachCurrentThread() {
return functions->DetachCurrentThread(this);
}
jint GetEnv(void **penv, jint version) {
return functions->GetEnv(this, penv, version);
}
jint AttachCurrentThreadAsDaemon(void **penv, void *args) {
return functions->AttachCurrentThreadAsDaemon(this, penv, args);
}
#endif
};
而如果是在C编译器环境中,由于没有this指针,我们只能够自己显式地手动传入”this”指针,就像用C来实现C++一样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct JNIInvokeInterface_ {
void *reserved0;
void *reserved1;
void *reserved2;
#if !TARGET_RT_MAC_CFM && defined(__ppc__)
void* cfm_vectors[4];
#endif /* !TARGET_RT_MAC_CFM && defined(__ppc__) */
jint (JNICALL *DestroyJavaVM)(JavaVM *vm);
jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args);
jint (JNICALL *DetachCurrentThread)(JavaVM *vm);
jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version);
jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);
#if TARGET_RT_MAC_CFM && defined(__ppc__)
void* real_functions[5];
#endif /* TARGET_RT_MAC_CFM && defined(__ppc__) */
};
我们看到,这里面的所有函数,第一个参数都是JavaVM *vm
,这就是”this”指针,因此,如果我们是在C中去调用java,需要自己传入jvm这个二级指针(** 注意,如果是C++中,jvm是一个一级指针,而在C中,jvm是一个二级指针,请注意前面对JavaVM类型的typedef **),因此,对应的代码应该是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static JavaVM *jvm;
static JNIEnv *env;
// 创建java虚拟机
jint res = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
// 获取class,注意要手动传入env(即C++中的this指针)
jclass clsString = (*env)->FindClass(env, "java/lang/String");
// ...
/* 销毁虚拟机(注意,JavaVM *jvm表面看上去是一级指针,
其实JavaVM在C环境下是JNIInvokeInterface_ *,
所以jvm实际是个二级指针,因此,(*jvm)后面应该跟->操作符,
而不是.操作符 )
*/
(*jvm)->DestroyJavaVM(jvm);
分析了这么多,你晕了吗? :)