C和C++与Java互相调用(续)
2015年06月15日 C++Java

上一篇说的是java的本地调用,即使用jni完成java调用c/c++编写的代码,这回仍然是使用jni,不过是倒过来,使用c/c++调用java编写的代码。本次使用环境是:centos5.4gcc4.9.0jdk1.6_45

  1. 配置相关环境。

    配置jdk环境,加入JAVA_HOME环境变量,即jdk目录加入path。

    配置加载jvm所需要的libjvm.so动态库,所以需要指定LD_LIBRARY_PATH这个环境变量,也就是libjvm.so这个动态库所在的路径, 通常都是:

    1
    export LD_LIBRARY_PATH=${JAVA_HOME}/jre/lib/i386/client:$LD_LIBRARY_PATH

    这样程序运行的时候如果在lib里找不到jvm这个动态库,就会去LD_LIBRARY_PATH所指定路径下去找。c/c++相关编译环境linux下自带。

  2. 因为是c/c++调用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
    class CCallJavaTest
    {
    //类成员
    public String mTestStr;
    private static final int CONST_VAL = 10;
    //静态方法
    public static String staticCallTest(int val)
    {
    System.out.println("[staticCallTest] called");
    return new Integer(val).toString();
    }

    //成员方法
    public int getVal()
    {
    System.out.println("[getVal] called");
    return CONST_VAL + 10;
    }
    public String getTestStr()
    {
    return this.mTestStr;
    }
    }
    1
    javac CCallJavaTest.java

    无错生成.class文件,这里只是一个简单的测试,所以没有指定包名,如果指定包名,下文中使用jni在寻找java类的时候就需要全名(包名/类名这种形式,下文会说明)。

  3. 查看刚才写的测试类的签名,下面用jni获取类内部类型(包括方法和成员)会用到,这里用到一个jdk里自带的一个分析工具,叫做javap,终端里直接键入javap -help查看帮助。

    -s选项是将内部的类型的签名打印出来,-private选项是输出所有的类与成员。这里就使用这二个选项,将输出重定向到一个文件中,以便之后查看:

    1
    javap -private -s CCallJavaTest > CCallJavaTest.sig

    查看sig文件会看到类似如下内容:

    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
    class CCallJavaTest extends java.lang.Object{

    public java.lang.String mTestStr;

      Signature: Ljava/lang/String;

    private static final int CONST_VAL;

      Signature: I

    CCallJavaTest();

      Signature: ()V

    public static java.lang.String staticCallTest(int);

      Signature: (I)Ljava/lang/String;

    public int getVal();

      Signature: ()I

    public java.lang.String getTestStr();

      Signature: ()Ljava/lang/String;

    }

    Signature部分就是我们需要的部分。

  4. 编写c代码(这里使用c语言)通过jni调用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
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    #include <stdlib.h>
    #include <string.h>
    #include <assert.h>

    int main(int argc, char *argv[])
    {
        //Java虚拟机
        JavaVM *jvm = NULL;
        //Jni运行环境
        JNIEnv *env = NULL;
        //虚拟机初始化参数
        JavaVMInitArgs jvm_args;
        //java虚拟机参数, 这里只使用classpath这一项,所以长度为1
        JavaVMOption opt[1];
        //设置初始化参数(classpath 为当前目录)
        opt[0].optionString = "-Djava.class.path=.";
        //初始化虚拟机初始化参数
        memset(&jvm_args, 0, sizeof(jvm_args));
        //设置jni环境版本
        jvm_args.version = JNI_VERSION16;
        //参数的个数
        jvm_args.nOptions = 1;
        //虚拟机参数
        jvm_args.options = opt;
        //创建java虚拟机 返回0为启动成功,其他数值的意义在头文件中有定义
        long jvm_stat = JNI_CreateJavaVM(&jvm, (void **)&env, &jvm_args);

        //错误处理
        if (jvm_stat)
        {
            fprintf(stderr, "%s\n", "JVM Create Error!");
            exit(-1);
        }
        //找到类定义
        jclass cls = (*env)->FindClass(env, "CCallJavaTest");

        //找到做相应处理
        if (cls != 0)
        {
            //获取静态方法的ID, 通过方法签名和方法的名字
            jmethodID static_method = (*env)->GetStaticMethodID(env, cls,
                                "staticCallTest", "(I)Ljava/lang/String");
            //找到方法
            if (static_method)
            {
                jint val = 1024;
                //调用静态方法并获取返回值
                jstring return_str = (jstring)(*env)->CallStaticObjectMethod(env,
                                                       cls, static_method, val);

                const char trans_str = (env)->GetStringUTFChars(env, return_str, 0);
                fprintf(stdout, "%s\n", trans_str);

                //释放内存(减引用)
                (*env)->ReleaseStringUTFChars(env, return_str, 0);
            }

            //通过新建对象调用方法, 调用默认构造方法
            jobject obj = (*env)->AllocObject(env, cls);

            if (!obj)
            {
                fprintf(stderr, "%s\n", "Alloc New Object Error!");
                exit(-1);
            }

            //调用类成员方法
            jmethodID member_method = (*env)->GetMethodID(env, cls, "getVal", "()I");
            if (member_method)
            {
                //调用成员方法
                jint res = (jint)(*env)->CallObjectMethod(env, obj, member_method);
                fprintf(stdout, "[%s] %d\n", "Call Member Method", (int)res);
            }

            //获取类成员
            jfieldID member_field = (jfieldID)(*env)->GetFieldID(env, cls,
                                        "mTestStr", "Ljava/lang/String;");

            if (member_field)
            {
                //对象成员的新的成员值
                const char *modify_str = "new class member";

                //分配待传参数
                jstring arg_str = (*env)->NewStringUTF(env, modify_str);

                //设置新的成员值
                (*env)->SetObjectField(env, obj, member_field, arg_str);

            }   
            //查看新的string的值
            member_method = (*env)->GetMethodID(env, cls, "getTestStr",
                                            "()Ljava/lang/String;");

            if (member_method)
            {
                jstring new_set_str = (jstring)(*env)->CallObjectMethod(env,
                                                            obj, member_method);
                const char new_c_str = (env)->GetStringUTFChars(env, new_set_str, 0);
                fprintf(stdout, "[new set str]: %s\n", new_c_str);
                (*env)->ReleaseStringUTFChars(env, new_set_str, 0);
            }
            //使用完成销毁虚拟机
            (*jvm)->DestroyJavaVM(jvm);
        }

        return 0;
    }

    注释已经算详细了,大体的步骤:

    1)初始化jni环境,因为都是基于jni来操作的,初始化创建并加载java虚拟机(jvm),因为java生成的字节码需要在jvm中运行。因为就有一个java类在当前目录中,所以JavaVMOption的长度为1,只指定了一个classpath参数。通过JNI_CreateJavaVM函数创建jvm。

    2)创建了jvm之后就可以执行java代码了,但是要找到java的类,才能调用方法,获取成员,所以接下来就是找到要装载的java类。通过jni提供的FindClass方法传入java的类名,找到对应的类。(C语言调用:(*env)->FindClass(env, “ClassName”);C++调用:env->FindClass(“ClassName”);)返回的jclass不为空则找到了对应的类。通过找到的类就可以进行访问成员,调用方法操作了。

  5. 现在得到了java类,还得继续通过类获取方法,(这里就用到javap所生成出来的类文件的签名了)

    1)静态方法。静态方法是不需要实例化对象的,它是类级的。可以通过类名.静态方法名来调用。jni提供了GetStaticMethodID函数通过方法名和方法签名来获取类的静态方法,提供了CallStaticObjectMethod函数来调用获取到的方法。GetStaticMethodID返回一个jni的包装类型jmethodID,对应方法的ID,再把这个ID传给CallStaticObjectMethod函数,通过这个获取到的ID来调用对应的方法。代码中的第42行,GetStaticMethodID函数的最后一个参数就是这个方法的签名,括号里的是方法的形参类型,后面的是方法的返回类型,这里使用的就是javap直接分析出来的对应的签名(区别方法重载)。代码中42-54行就是通过类获取静态方法再调用并获得返回值的过程。

    2)成员方法。需要通过对象来调用。通过实例对象.成员方法的形式来调用。所以这里要调用成员方法就必须先产生一个类的实例,再通过这个类的实例去调用成员方法。jni提供了AllocObject来调用java类的默认构造方法。返回一个jobject的jni包装的对象类型。通过GetMethodID函数获取类的成员方法。(如果不是默认构造方法,就可以使用普通的GetMethodID方法获取构造方法如:

    1
    jmethodID constructor_method = (*env)->GetMethodID(env, cls, "<init>", "(I)");

    类似这样,再使用jni的NewObject函数得到了这个类的对象。)这里使用的方法签名也同样是javap工具得来的。得到jmethodID后通过jni提供的CallObjectMethod传入对象与方法ID,调用这个方法并获取返回值。代码58-72行就是生成新对象,调用成员方法的过程。

    3)成员变量。同样是需要实例对象来进行获取,修改等操作。jni提供一个叫做GetFieldID的函数,传入类名,成员变量名称,还有其变量类型(javap生成的签名),返回一个jni的jFieldID的包装类型。获取其ID之后,通过SetObjectField函数将新的值设置给这个成员变量。函数依次传入,对象、成员变量ID,将要设置的值。设置完成,再次使用GetMethodID调用对应的get方法查看其值是否改变。代码中74-97就是获取成员变量并设置新值,再次查看的过程。

    以上过程都调用都完成之后,使用创建出的jvm指针销毁jvm,释放加载的虚拟机。

  6. gcc下编译代码查看结果:

    1
    2
    gcc -Wall -o c_call_java c_call_java.c -I${JAVA_HOME}/include \
    -I${JAVA_HOME}/include/linux -L${JAVA_HOME}/jre/lib/i386/client/ -ljvm

    -I指定头文件目录,这里是jni的头文件目录(另一个办法是把里面的h文件都拷贝到/usr/local/inlcude中),-L指定要链接的库的目录,-l选项直接指定库的名称,这里要链接的是$JAVA_HOME/jre/lib/i386/client/下的libjvm.so动态库。

    编译无错生成c_call_java可执行文件。终端下运行输出:

    1
    2
    3
    [getVal] called
    [Call Member Method] 20
    [new set str]: new class member

    c通过jni成功调用了java代码。以上就是在没有线程的情况下,c/c++调用java的jni的一般操作方法。