C和C++与Java的互相调用
2015年06月05日 C++Java

因为adnroid项目中用到了获取mac地址做为唯一码的功能,c和c++在android却没有对应的api,还好有jni这个东西,用java写好获取mac地址的代码,用c/c++调用代码就可以直接获取mac字符串了。

JNI(Java Native Interface)Java本地接口,详情点击此处,就是java用来与本地的已经编译的语言交互的接口。这里使用的环境是windows 7 64,gcc 4.6.2,jdk 1.6

首先看在Java中调用c/c++:

  1. 创建Java类(JavaJni),创建要交互的本地方法,使用native关键字,加载我们要调用的动态库。

  2. 编译创建的Java类,无错生成.class文件。

  3. 通过JDK中javah工具生成本地方法声明的头文件。(这里没有使用包名,正确方法是javah 包名.类名)

  4. 生成本地声明的头文件之后,编写对应的c或者c++文件,实现具体的本地方法。

  5. 编译生成动态链接库,windows下是dll文件,linux是so文件。这里是使用windows下的MinGW编译生成dll文件。(因为windows下jni的默认是使用vc的调用约定, 所以这步使用gcc生成dll文件时候需要注意不同平台调用约定的实现,下文会说)。

  6. 生成动态库后,将其放入java.library.path所指定的路径中,可能通过getProperty方法查看java.library.path所在的路径,将其放入任意一个即可加载。

  7. 执行java JavaJni,查看结果。

    下面写一个测试,具体实现一下上面的步骤:

    a. 创建Java类(JavaJni)如下:

    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
    public class JavaJni
    {
    public static void main (String [] args)
    {
    JavaJni jni = new JavaJni();
    //本地方法测试
    jni.jniVoidTest();
    System.out.println("===============================");
    System.out.println(jni.jniStringTest());
    System.out.println("===============================");
    System.out.println(jni.jniIntTest());
    System.out.println("===============================");
    System.out.println(jni.jniWithString("From JavaJni"));
    System.out.println("===============================");
    int[] array = jni.jniArray();
    for (int i = 0; i < array.length; i++)
    {
    System.out.println(array[i]);
    }
    System.out.println("===============================");
    int[] temp = {1, 2, 3, 4, 5, 6, 7};
    System.out.println(jni.jniWithArray(temp, temp.length));
    System.out.println("===============================");
    }

    //类初始化时候加载动态库
    static
    {
    System.loadLibrary("JavaJni");
    }

    //本地方法声明 使用native关键字
    native void jniVoidTest();
    native String jniStringTest();
    native int jniIntTest();
    native String jniWithString(String str);
    native int[] jniArray();
    native boolean jniWithArray(int[] array, int len);
    }

    native关键字声明本地dll中要调用的方法,静态块在类初始化的时候加载动态库。主方法中调用本地方法做一个简单的测试,六个不同的返回值,不同参数的测试方法。

    b.编译创建的Java类:

    1
    javac JavaJni.java

    c. 通过javah工具生成本地方法声明的头文件,由于这里没有指定包名,直接使用类名:

    1
    javah JavaJni(注意这里是JavaJni,没有后缀)

    这里会生成一个JavaJni.h的本地方法的头文件声明,类似如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class JavaJni */
    #ifndef _Included_JavaJni
    #define _Included_JavaJni
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
    * Class: JavaJni
    * Method: jniVoidTest
    * Signature: ()V
    */
    JNIEXPORT void JNICALL Java_JavaJni_jniVoidTest
    (JNIEnv *, jobject);

    #ifdef __cplusplus
    }
    #endif
    #endif

    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

    d. 如注释所说,这是机器生成的方法声明,直接使用即可。现在要对应头文件创建JavaJni.c文件实现每个声明的本地方法,这里做简单的实现:

    ```c
    #include <stdio.h>
    #include <assert.h>
    #include "JavaJni.h"

    #define BUFF_SIZE 5

    JNIEXPORT void JNICALL Java_JavaJni_jniVoidTest(JNIEnv *env, jobject obj)
    {
    printf(">>: Excute [jniVoidTest] success\n");
    }
    JNIEXPORT jstring JNICALL Java_JavaJni_jniStringTest(JNIEnv *env, jobject obj)
    {
    char *static_str = "excute [jniStringTest] success\n";
    //创建可供返回的jstring
    jstring jstr = (*env)->NewStringUTF(env, static_str);
    return jstr;
    }
    JNIEXPORT jint JNICALL Java_JavaJni_jniIntTest(JNIEnv *env, jobject obj)
    {
    printf("excute [jniIntTest] success\n");
    return 1024;
    }
    JNIEXPORT jstring JNICALL Java_JavaJni_jniWithString(JNIEnv *env, jobject obj, jstring str)
    {
    const char *temp = NULL;
    //由传入的参数获取c类型的字符串
    temp = (*env)->GetStringUTFChars(env, str, 0);
    //只做测试,简单的写一个断言,其他的要做对应的参数为空处理
    assert(temp);
    printf(temp);
    //减引用计数
    (*env)->ReleaseStringUTFChars(env, str, temp);
    char *r_str = "excute [jniWithString] success\n";

    jstring jstr = (*env)->NewStringUTF(env, r_str);
    return jstr;
    }
    JNIEXPORT jintArray JNICALL Java_JavaJni_jniArray(JNIEnv *env, jobject obj)
    {
    jint tmp_buffer[BUFF_SIZE] = {0};
    for (int i = 0; i < BUFF_SIZE; i++)
    {
    tmp_buffer[i] = i + 4;
    }
    jintArray int_array = (*env)->NewIntArray(env, BUFF_SIZE);
    //将数所设设置到将要返回给java端的数组中去
    (*env)->SetIntArrayRegion(env, int_array, 0, BUFF_SIZE, tmp_buffer);
    return int_array;
    }
    JNIEXPORT jboolean JNICALL Java_JavaJni_jniWithArray(JNIEnv *env, jobject obj, jintArray array, jint len)
    {
    //此处使用从java处copy数组数据到此,不对java处的数据做修改
    jint tmp_array[len];
    //由java处的数组获取数组数据,之后不要忘记减少引用计数
    (*env)->GetIntArrayRegion(env, array, 0, len, tmp_array);

    for (int i = 0; i < len; i++)
    {
    printf("%d\t", tmp_array[i]);
    }
    printf("\n");
    }

    关键部分都做了注释,注意的地方就是:NewStringXXX之后不要忘记ReleaseString减少对应的引用计数以防止内存泄露。

    e. 编译上一步所写的实现文件生成动态库,这里环境是windows,所以使用gcc生成dll文件。

1
gcc -Wl, --kill-at -std=c99 -shared -o JavaJni.dll JavaJni.c

参数一个一个看:

-std=c99 这里是使用C语言的c99标准,包括for循环的初始化部分变量声明和变长数组。

-shared 链接器选项,用于生成一个共享库目标文件。也就是生成这个动态库的选项。

-o 指定输出(地球人都知道)

-Wl, 这个选项是把这个后面的选项传递给链接器。后面的–kill-at才是真正起作用的。

由于JNI加载dll的中导出的函数名在windows下是与MSVC相同的。而JNI的调用约定使用的是__stdcall,gcc下生成的dll导出的函数名是func_name@shift_value的形式。MSVC下生成的dll导出的函数名是func_name,这里出现了差异,那么JNI在使用gcc生成的dll的时候就会出现找不到对应的函数名的错误,链接错误,是因为导出的函数名不同的原因。所以才会使用Wl, –kill-at给链接器的选项,使生成的dll导出的函数名是func_name这种形式,kill-at的意思也就是去掉@之后的部分。[这里如有说得不对,请指正,非常感激!]这里是一个不同的调用约定,在不同编译器下导出函数名:

还可以使用另一个链接器的选项,-Wl,–add-stdcall-alias即同时导出func_name@shift_valuefunc_name二种函数名的。

这里生成动态库的时候可能还会出现jni.h找不到的情况,在MinGW下最简单的方法就是把JDK目录下的include下的所有文件都拷贝到MinGW的include目录中(包括win32目录下的头文件都复制到MinGW下的include文件夹中)。这样gcc就可以找到jni.h了。

f. 生成dll之后加入java.library.path中,如果不知道具体的目录在哪,可以放到system32,或者自己看一下java.library.path,如下:

1
2
3
4
5
6
7
8
class OutJavaPath
{
public static void main (String [] args)
{
String path = System.getProperty("java.library.path");
System.out.println(path);
}
}

得到path的值之后,把dll放到合适的目录。

g. 执行JavaJni(java JavaJni)。到这里就会看到console里的输出。如下:

到这里就是使用Java本地接口JNI调用本地语言实现的实现的整个过程。