JNI development process and handling of reference data types

Today, let's take a look at Java JNI, first look at the definition given by Wikipedia,

JNI, Java Native Interface, Java Native Interface, is a programming framework that enables Java programs in the Java virtual machine to call native applications or libraries, or be called by other programs. Native programs are typically written in other languages ​​(C, C++, or assembly language) and compiled into programs based on the native hardware and operating system.

This article is to analyze the steps of calling C++ programs from Java and the problem of accessing arrays and strings in JNI development.

Let's take a look at the development steps of JNI in Android. Simply write a Demo and see the effect:

Demo.png

Call method:

button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, HelloWorld.sayHello("JNIEnjoy!"), Toast.LENGTH_LONG).show();
            }
});

Click SUM to sum the arrays and print out the two-dimensional array passed by Native

SUM.png

Log.png

calling code:

btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                btn2.setText(String.valueOf(ArrayJni.arraySum(get())));

                int[][] arr = ArrayJni.getArray(3);
                for (int i = 0; i < 3; i++) {
                    for (int j = 0; j < 3; j++) {
                        Log.d("JNILOG", String.valueOf(arr[i][j]));
                    }
                }
            }
});

private int[] get() {
        int[] array = new int[10];
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }
        return array;
}

Next, look at the JNI development steps:

1. JNI development steps

The first step is to establish a JNI Class in the Java layer. The keyword native declaration is required where the Native method needs to be called. The method sayHello needs to be implemented at the bottom layer, which will be implemented in C in this Demo.

public class HelloWorld {

    public static native String sayHello(String name);
}

The second step, Make Project, this will generate a class file under app/build/intermediates/classes/debug, as shown in the figure below, of course, the file Hello World.class is needed.

Make Class.png

The third step, switch the directory to app\build\intermediates\classes\debug in the terminal, generate the .h header file through the command javah -jni juexingzhe.com.hello.HelloWorld, juexingzhe.com.hello is the package name, and it needs to be replaced with the small partner's own package name. juexingzhe.com.hello.HelloWorld.h file.

Make h.png

Looking at the contents of the file, the default generated function name rules are:

Java_包名_类名_Native方法名

Among them, JNIEnv is thread-related, that is, there is a JNIEnv pointer in each thread, each JNIEnv is thread-specific, and thread A cannot call thread B's JNIEnv.

jclass is the class of HelloWorld. Because the method in this example is static, the default generated is jclass. If the method is not static, the default generated will pass in jobject, pointing to the object instance when the native method is called.

The jstring is the parameter passed in when the method is defined.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class juexingzhe_com_hello_HelloWorld */

#ifndef _Included_juexingzhe_com_hello_HelloWorld
#define _Included_juexingzhe_com_hello_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     juexingzhe_com_hello_HelloWorld
 * Method:    sayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_juexingzhe_com_hello_HelloWorld_sayHello
  (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

The fourth step is to create a new jni folder in the main directory, and cut the .h file generated above.

New JNI Folder.png

The fifth step, it is finally time to write C code. Note that in the header file, C and C++ are written differently.

C中(*env)->NewStringUTF(env, "string)

C++中env->NewStringUTF("string")

The final juexingzhe.com.hello.HelloWorld.cfile is as follows:

#include "juexingzhe_com_hello_HelloWorld.h"
#include <stdio.h>
/* Header for class juexingzhe_com_hello_HelloWorld */

/*
 * Class:     juexingzhe_com_hello_HelloWorld
 * Method:    sayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_juexingzhe_com_hello_HelloWorld_sayHello
        (JNIEnv *env, jclass jcls, jstring jstr)
{
    const char *c_str = NULL;
    char buff[128] = { 0 };
    c_str = (*env)->GetStringUTFChars(env, jstr, NULL);
    if (c_str == NULL)
    {
        printf("out of memory.\n");
        return NULL;
    }
    sprintf(buff, "hello %s", c_str);
    (*env)->ReleaseStringUTFChars(env, jstr, c_str);
    return (*env)->NewStringUTF(env, buff);
}

There are a few points to note about the above code, please refer to the following string processing.

After the above five steps to write code, it is almost the same. There is another question, how to transfer the Java layer to this C file? This requires the sixth step to configure ndk

The sixth step, configure ndk, add defaultConfig in build.gradle under the module package, where moduleName is the name of the final packaged so library

ndk {
     moduleName 'HelloWorld'
}

The final android task is as follows

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "juexingzhe.com.hello"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        ndk {
            moduleName 'HelloWorld'
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

You also need to add the following sentence to gradle.properties in the project directory

android.useDeprecatedNdk=true

The .so file can be generated by re-Make Project. There is no platform configured here, so the so library of all platforms will be generated by default, including arm/x86/mips, etc.

Make SO.png

In the seventh step, you need to load the .so file in the Java layer and add it to HelloWorld.Java written in the first step, where HelloWorld is the name of the so library generated by the above NDK configuration.

public class HelloWorld {

    static {
        System.loadLibrary("HelloWorld");
    }

    public static native String sayHello(String name);
}

2. String processing

Let's review the contents of the .c file above:

#include "juexingzhe_com_hello_HelloWorld.h"
#include <stdio.h>
/* Header for class juexingzhe_com_hello_HelloWorld */

/*
 * Class:     juexingzhe_com_hello_HelloWorld
 * Method:    sayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_juexingzhe_com_hello_HelloWorld_sayHello
        (JNIEnv *env, jclass jcls, jstring jstr)
{
    const char *c_str = NULL;
    char buff[128] = { 0 };
    c_str = (*env)->GetStringUTFChars(env, jstr, NULL);
    if (c_str == NULL)
    {
        printf("out of memory.\n");
        return NULL;
    }
    sprintf(buff, "hello %s", c_str);
    (*env)->ReleaseStringUTFChars(env, jstr, c_str);
    return (*env)->NewStringUTF(env, buff);
}
  • 1. The jstring type is a string that points to the inside of the JVM. Unlike the basic type, it cannot be used directly in the C code. It needs to access the string data structure inside the JVM through the JNI function.

  • 2. Take a brief look at GetStringUTFChars(env, jstr, &isCopy), jstr is the string pointer passed by Java to the native code, isCopy takes the values ​​JNI_TRUE and JNI_FALSE, if the value is JNI_TRUE, it means returning a copy of the JVM internal source string, And allocate memory space for the newly generated string. If the value is JNI_FALSE, it means that the pointer to the source string inside the JVM is returned, which means that the content of the source string can be modified through the pointer. This is not recommended, because doing so breaks the rule that Java strings cannot be modified. But we don't care what this value is during development. Usually, this parameter can be filled with NULL.

  • 3. Java uses Unicode encoding by default, while C/C++ uses UTF encoding by default, so when manipulating strings in native code, you must use appropriate JNI functions to convert jstrings into C-style strings. JNI supports the conversion of strings between Unicode and UTF-8 encodings. GetStringUTFChars can convert a jstring pointer (pointing to the Unicode character sequence inside the JVM) into a C string in UTF-8 format. In the above example, in the sayHello function, we correctly obtained the string content inside the JVM through GetStringUTFChars

  • 4. Exception checking. After calling GetStringUTFChars, a security check needs to be performed, because the JVM needs to allocate memory for the newly born string. If the allocation fails, NULL will be returned and an OutOfMemoryError exception will be thrown. In Java, if an exception is encountered and the program is not caught, it will stop running immediately. However, when JNI encounters an unhandled exception, it will not change the running process of the program, and it will continue to go down, so all operations on this string in the future are dangerous. So if NULL, you need to return to skip the following code.

  • 5. Release the string. C is different from Java, you need to manually release the memory, and notify the JVM that this memory is no longer needed through the ReleaseStringUTFChars function. Note that GetXXX and ReleaseXXX should be called together.

  • 6. Calling the NewStringUTF function will construct a new java.lang.String string object, which will be automatically converted to the Unicode encoding supported by Java. If the JVM cannot allocate enough memory to construct the java.lang.String, NewStringUTF will throw an OutOfMemoryError and return NULL.

Of course, JNI provides a lot of functions for manipulating strings, so I won't explain them one by one here. The main thing is to pay attention to memory allocation and cross-threading issues.

3. Array processing

Arrays are similar to the above strings, and there is no way to operate them directly. You need to obtain the corresponding pointer from the JVM through the JNI function or copy it to the memory buffer for operation.

Follow the above steps to add an example of an array, look at the Java code, two Native functions, one for summation and one for obtaining a two-dimensional array.

public class ArrayJni {

    static {
        System.loadLibrary("HelloWorld");
    }

    //求和
    public static native int arraySum(int[] array);

    //获取二维数组
    public static native int[][] getArray(int size);

}

Next, let’s take a look at the C code of arraySum. The parameter defined by the Java layer is an array of int type, which corresponds to jintArray in Native. Get the length of the parameter array through GetArrayLength, and then copy the parameter array to the memory buffer buffer through GetIntArrayRegion, and then you can The summation operation is performed. Remember to free the memory when the operation is complete.

/*
 * Class:     juexingzhe_com_hello_ArrayJni
 * Method:    arraySum
 * Signature: ([I)I
 */
JNIEXPORT jint JNICALL Java_juexingzhe_com_hello_ArrayJni_arraySum
        (JNIEnv *env, jclass jcls, jintArray jarr)
{
    jint i, sum = 0, len;
    jint *buffer;
    //1.获取数组长度
    len = (*env)->GetArrayLength(env, jarr);

    //2.分配缓冲区
    buffer = (jint*) malloc(sizeof(jint) * len);
    memset(buffer, 0, sizeof(jint) * len);

    //3.拷贝Java数组中所有元素到缓冲区
    (*env)->GetIntArrayRegion(env, jarr, 0, len, buffer);

    //4.求和
    for (int i = 0; i < len; ++i) {
        sum += buffer[i];
    }

    //5.释放内存
    free(buffer);

    return sum;
}

Let's look at the code for generating a two-dimensional array. Everyone knows that each element in the two-dimensional array is actually a one-dimensional array, so you need to construct a reference to the one-dimensional array first, use FindClass, and then construct a two-dimensional array through NewObjectArray.

Construct a one-dimensional array through NewIntArray, then SetIntArrayRegion assigns int type array elements, of course, there is also the GetIntArrayRegion function, which can copy all elements in the Java array to the C buffer.

Two-dimensional arrays are assigned values ​​through SetObjectArrayElement.

In order to avoid creating a large number of JNI local references in the loop and causing overflow of the JNI reference table, DeleteLocalRef must be called every time in the outer loop to remove the newly created jintArray reference from the reference table. In JNI, only jobject and subclasses belong to reference variables, which will occupy the space of the reference table. Jint, jfloat, jboolean, etc. are all basic type variables and will not occupy the reference table space, that is, they do not need to be released. The maximum space for the reference table is 512. If it exceeds this range, the JVM will hang up.

/*
 * Class:     juexingzhe_com_hello_ArrayJni
 * Method:    getArray
 * Signature: (I)[[I
 */
JNIEXPORT jobjectArray JNICALL Java_juexingzhe_com_hello_ArrayJni_getArray
        (JNIEnv *env, jclass jcls, jint size)
{
    jobjectArray result;
    jclass onearray;

    //1.获取一维数组引用
    onearray = (*env)->FindClass(env, "[I");
    if (onearray == NULL){
        return NULL;
    }

    //2.构造二维数组
    result = (*env)->NewObjectArray(env, size, onearray, NULL);
    if (result == NULL){
        return NULL;
    }

    //3.构造一维数组
    for (int i = 0; i < size; ++i) {

        int j;
        jint buffer[256];
        //构造一维数组
        jintArray array = (*env)->NewIntArray(env, size);
        if (array == NULL){
            return NULL;
        }
        //准备数据
        for (int j = 0; j < size; ++j) {
            buffer[j] = i + j;
        }

        //设置一维数组数据
        (*env)->SetIntArrayRegion(env, array, 0, size, buffer);

        //赋值一维数组给二维数组
        (*env)->SetObjectArrayElement(env, result, i, array);

        //删除一维数组引用
        (*env)->DeleteLocalRef(env, array);
    }

    return result;
}

Similarly, there are many functions for array operations, and it is impossible to describe each of them here. Those who need it can search by themselves, and the difference will not be too big.

4. Summary

This article is just a summary of a little understanding of Android development JNI, including the steps of JNI development, the processing of strings and arrays, and there is no way to directly manipulate the data of reference types during the development of JNI Native. It is necessary to obtain the JVM through the functions provided by JNI. Some of the provided functions will copy the original data, some will return the pointer of the original data, and make different choices according to your needs.

There may be some more content on JNI later, such as Native calling the object method fields of the Java layer, etc. Friends who need it are welcome to pay attention.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325388218&siteId=291194637