以前在某娱乐公司工作的时候,做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);

分析了这么多,你晕了吗? :)