Android Game Development Practice (1) NDK and JNI Development 04

Keywords: Android Java jvm Gradle

Android Game Development Practice (1) NDK and JNI Development 04

With the previous several articles NDK and JNI development-related foundation to pave the way, and then through the code to explain the specific operation of this area and some important details. Then, continue to learn NDK and JNI summary.

Author: AlphaGL. Copyright is reserved. The original link is welcome to be reproduced.

Portal:
Android Game Development Practice (1) NDK and JNI Development 01
Android Game Development Practice (1) NDK and JNI Development 02
Android Game Development Practice (1) NDK and JNI Development 03

Java VM and JNIEnv

Two important data structures JavaVM and JNIEnv are defined in the jni.h header file, and their implementations are different in C and C++ (through if defined (cplusplus) macro definitions). Essentially, they are pointers that encapsulate the list of JNI functions.

JavaVM

Is the representation of the java virtual machine in the jni layer. In Android, a JVM allows only one java VM object. You can share a java VM object between threads.

JavaVM statement

Java VM implementations for C language environment and C++ language environment are different in jni.

The C version of Java VM is declared as:

typedef const struct JNIInvokeInterface* JavaVM;

struct JNIInvokeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;

    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

The C++ version of Java VM is declared as:

typedef _JavaVM JavaVM;

struct _JavaVM {
    const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThread(this, p_env, thr_args); }
    jint DetachCurrentThread()
    { return functions->DetachCurrentThread(this); }
    jint GetEnv(void** env, jint version)
    { return functions->GetEnv(this, env, version); }
    jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
Java VM acquisition

(1)jni dynamic registration mode. When loading dynamic link libraries, the JVM calls JNI_OnLoad(JavaVM* vm, void* reserved) and passes in the JavaVM pointer:

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {

}

(2) Create in local code by calling jint JNI_Create Java VM (Java VM**, JNIEnv**, void*).

JNIEnv

Simply put, JNIEnv provides an interface for all JNI function calls. You cannot share the same JNIEnv variable between threads, only the thread that created it is valid. If you want to access the JVM in other threads, you need to call AttachCurrentThread or AttachCurrentThreadAsDaemon to bind the current thread to the JVM. Then get JNIEnv through GetEnv of JavaVM object.

JNIEnv statement

Similar to Java VM, JNIEnv has different declarations in C and C++ languages.

The C version of Java VM is declared as:

typedef const struct JNINativeInterface* JNIEnv;

struct JNINativeInterface {
        jint        (*GetVersion)(JNIEnv *);
        ···
}

The C++ version of Java VM is declared as:

typedef _JNIEnv JNIEnv;

struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); }

    ...
}

jobject, jclass, jmethod ID and jfield ID

jobject:
JNI is a mapping of the original java.lang.Object. You can get a job object by calling NewObject. For example:

env->NewObject(jclass clazz, jmethodID methodID, ...)

jclass:
JNI is a mapping of the original java.lang.Class. The jclass object can be obtained by calling FindClass. For example:

jclass intArrayClass = env->FindClass("[I");

jmethodID:
Gets the method id of the corresponding class member method. You can get it by calling GetMethodID. For example:

jmethodID myMethodId = env->(jclass clazz, const char *name, const char *sig);

jfieldID:
Gets the field id of the corresponding class member variable. You can get it by calling GetField id. For example:

jfieldID nameFieldId = env->GetFieldID(jclass clazz, const char *name, const char *sig)

Local library calls

JNI loads the code in the local library. The steps are outlined below (and recommended by Android):
(1) Call System.load Library to load dynamic library in static block of java class. If the name of dynamic library is libcocos2dx.so, then the call is:

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

(2) Implement JNIEXPORT jint JNICALL JNI_OnLoad (Java VM* vm, void* reserved) in local code; method.

(3) In this JNI_OnLoad method, all local implementation methods are registered by calling env-> Register Natives (jclass clazz, const JNI Native Method * methods, jint Methods). It is recommended that methods be declared static so that they do not occupy the space of the symbol table on the device.

JNI communication

The communication process of JNI is actually the process of data transfer between native Java and underlying C/C++. In brief, data transmission can be divided into the following categories:

  • Passing basic data types (such as int, float, etc.)
  • Delivery objects (for example: String, Object, custom class MyObject, etc.)
  • Pass-through arrays (for example: int [], String [], etc.)
  • Pass collection objects (e.g. ArrayList, HashMap, etc.)

The invocation methods can be divided into:
(1)java calls native methods
(2)native calls java static methods, non-static methods (member methods), and obtains member variables of java classes.

Below according to the different ways of implementation combined with the above points, through an example code to illustrate how to achieve the specific below.
(1) The way of static registration
The structure of the project is as follows: (Here only the main items are listed)

JNISample1  
  │── build.gradle
  │── CMakeLists.txt 
  └── app 
      ├── build.gradle
      ├── CMakeLists.txt
      └── src 
          ├── cpp
          │    ├── JNIUtils.h
          │    └── JNIUtils.cpp
          └── com.alphagl.main
                    ├── JNIUtils.java
                    ├── MainActivity.Java
                    └── Person.java

The code is as follows: (Here we simplify, remove some comments and the unit test part of the code)
MainActivity.java:

package com.alphagl.main;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {

    static {
        System.loadLibrary("native-lib");
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.i("MainActivity", "getStringFromJNI ============= " + JNIUtils.getStringFromJNI());
        Log.i("MainActivity", "getIntArrayFromJNI ============= " + JNIUtils.getIntArrayFromJNI()[0] + "," + JNIUtils.getIntArrayFromJNI()[1]);
        JNIUtils.setPersonToJNI(new Person(18, "jobs"));
        Log.i("MainActivity", "getPersonFromJNI ============= " + JNIUtils.getPersonFromJNI().getAge()+ "," + JNIUtils.getPersonFromJNI().getName());
    }
}

Person.java:(Encapsulated custom objects)

package com.alphagl.main;

import android.util.Log;

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void printPerson() {
        Log.d("MainActivity", "age ======== " + age + "," + "name ======== " + name);
    }
}

JNIUtils.java:

package com.alphagl.main;

public class JNIUtils {
    public static native String getStringFromJNI();
    public static native int[] getIntArrayFromJNI();
    public static native void setPersonToJNI(Person person);
    public static native Person getPersonFromJNI();
}

JNIUtils.h:

#include <jni.h>
#include <stdio.h>

#ifndef _Included_com_alphagl_main_JNIUtils
#define _Included_com_alphagl_main_JNIUtils
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jstring JNICALL Java_com_alphagl_main_JNIUtils_getStringFromJNI
  (JNIEnv *, jclass);


JNIEXPORT jintArray JNICALL Java_com_alphagl_main_JNIUtils_getIntArrayFromJNI
  (JNIEnv *, jclass);


JNIEXPORT void JNICALL Java_com_alphagl_main_JNIUtils_setPersonToJNI
  (JNIEnv *, jclass, jobject);


JNIEXPORT jobject JNICALL Java_com_alphagl_main_JNIUtils_getPersonFromJNI
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

JNIUtils.cpp

#include "JNIUtils.h"
#include <android/log.h>

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "MainActivity", __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "MainActivity", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROE, "MainActivity", __VA_ARGS__)


JNIEXPORT jstring JNICALL Java_com_alphagl_main_JNIUtils_getStringFromJNI (JNIEnv *env, jclass jcls) {
    LOGD(" ====================== getStringFromJNI");
    // Construct a String string
    return env->NewStringUTF("Hello from jni");
}


JNIEXPORT jintArray JNICALL Java_com_alphagl_main_JNIUtils_getIntArrayFromJNI (JNIEnv *env, jclass jcls) {
    LOGD(" ====================== getIntArrayFromJNI");
    // Construct an int [] array
    jintArray intArray = env->NewIntArray(2);
    int size[]={640, 960};
    // Assign values to int [] arrays
    env->SetIntArrayRegion(intArray, 0, 2, size);

    return intArray;
}


JNIEXPORT void JNICALL Java_com_alphagl_main_JNIUtils_setPersonToJNI (JNIEnv *env, jclass jcls, jobject jobj) {
    LOGD(" ====================== setPersonToJNI");
    jclass jperson = env->GetObjectClass(jobj);
    if (jperson != NULL) {
        // Get the age field id of the Person object
        jfieldID ageFieldId = env->GetFieldID(jperson, "age", "I");
        // Get the name field id of the Person object
        jfieldID nameFieldId = env->GetFieldID(jperson, "name", "Ljava/lang/String;");

        // Get Person's age member variable
        jint age = env->GetIntField(jobj, ageFieldId);
        // Get Person's name member variable
        jstring name = (jstring)env->GetObjectField(jobj, nameFieldId);

        const char *c_name = env->GetStringUTFChars(name, NULL);

        // Print age and name variables of Person objects passed from Java
        LOGD("age ===== %d, name ===== %s", age, c_name);
    }

    // The following is to construct Java objects from JNI and call member methods in Java classes for demonstration purposes only
    // Get the class of Person object
    jclass jstu = env->FindClass("com/alphagl/main/Person");
    // Getting the method id of the construction method of Person object
    jmethodID personMethodId = env->GetMethodID(jperson, "<init>", "(ILjava/lang/String;)V");
    // Construct a String string
    jstring name = env->NewStringUTF("bill");

    // Construct a Person object
    jobject  jPersonObj = env->NewObject(jstu, personMethodId, 30, name);
    // Get the method id of the printPerson member method of the Person object
    jmethodID jid = env->GetMethodID(jstu, "printPerson", "()V");
    // Calling java's printPerson method
    env->CallVoidMethod(jPersonObj, jid);
}


JNIEXPORT jobject JNICALL Java_com_alphagl_main_JNIUtils_getPersonFromJNI(JNIEnv *env, jclass jcls) {
    LOGD(" ====================== getPersonFromJNI");
    // Get the class of Person object
    jclass jstudent = env->FindClass("com/alphagl/main/Person");
    // Getting the method id of the construction method of Person object
    jmethodID studentMethodId = env->GetMethodID(jstudent, "<init>", "(ILjava/lang/String;)V");
    // Construct a String string
    jstring name = env->NewStringUTF("john");
    // Construct a Person object
    jobject  jstudentObj = env->NewObject(jstudent, studentMethodId, 20, name);

    return jstudentObj;
}

Here again, as defined in the JNIUtils.java class above, how to generate the corresponding C/C++ method declaration according to the method signature of the object. This part is about Android Game Development Practice (1) NDK and JNI Development 01 As mentioned, we can use javah to generate header files from compiled. class.
The common practice is:

In Android Studio, you can:
Tools - > External Tools - > Add

(1) The path of javah
(2) Command line parameters
(3) Path of header file generation


In declaring the class of the native method, right-click to execute javah.

(2) The way of dynamic registration
The structure of the project is as follows: (Here only the main items are listed)

JNISample2  
  │── build.gradle
  │── CMakeLists.txt 
  └── app 
      ├── build.gradle
      ├── CMakeLists.txt
      └── src 
          ├── cpp
          │   └── JNIUtils.cpp
          │    
          └── com.alphagl.main
                    ├── JNIUtils.java
                    ├── MainActivity.Java
                    └── Person.java

Here's a look at the different parts of the code, JNIUtils.cpp.
JNIUtils.cpp:

#include <jni.h>
#include <string>
#include <android/log.h>

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "MainActivity", __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "MainActivity", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROE, "MainActivity", __VA_ARGS__)

#define CLASSNAME "com/alphagl/main/JNIUtils"

static jstring getStringFromJNI_native(JNIEnv *env, jclass jcls) {
    LOGD(" ====================== getStringFromJNI");
    // Construct a String string
    return env->NewStringUTF("Hello from jni");
}

static jarray getIntArrayFromJNI_native(JNIEnv *env, jclass jcls) {
    LOGD(" ====================== getIntArrayFromJNI");
    // Construct an int [] array
    jintArray intArray = env->NewIntArray(2);
    int size[]={640, 960};
    // Assign values to int [] arrays
    env->SetIntArrayRegion(intArray, 0, 2, size);

    return intArray;
}

static void setJniPerson_native(JNIEnv *env, jclass jcls, jobject jobj) {
    LOGD(" ====================== setPersonToJNI");
    jclass jperson = env->GetObjectClass(jobj);
    if (jperson != NULL) {
        // Get the age field id of the Person object
        jfieldID ageFieldId = env->GetFieldID(jperson, "age", "I");
        // Get the name field id of the Person object
        jfieldID nameFieldId = env->GetFieldID(jperson, "name", "Ljava/lang/String;");

        // Get Person's age member variable
        jint age = env->GetIntField(jobj, ageFieldId);
        // Get Person's name member variable
        jstring name = (jstring)env->GetObjectField(jobj, nameFieldId);

        const char *c_name = env->GetStringUTFChars(name, NULL);

        // Print age and name variables of Person objects passed from Java
        LOGD("age ===== %d, name ===== %s", age, c_name);
    }

    // The following is to construct Java objects from JNI and call member methods in Java classes for demonstration purposes only
    // Get the class of Person object
    jclass jstu = env->FindClass("com/alphagl/main/Person");
    // Getting the method id of the construction method of Person object
    jmethodID personMethodId = env->GetMethodID(jperson, "<init>", "(ILjava/lang/String;)V");
    // Construct a String string
    jstring name = env->NewStringUTF("bill");

    // Construct a Person object
    jobject  jPersonObj = env->NewObject(jstu, personMethodId, 30, name);
    // Get the method id of the printPerson member method of the Person object
    jmethodID jid = env->GetMethodID(jstu, "printPerson", "()V");
    // Calling java's printPerson method
    env->CallVoidMethod(jPersonObj, jid);
}

static jobject getJniPerson_native(JNIEnv *env, jclass jcls) {
    LOGD(" ====================== getPersonFromJNI");
    // Get the class of Person object
    jclass jstudent = env->FindClass("com/alphagl/main/Person");
    // Getting the method id of the construction method of Person object
    jmethodID studentMethodId = env->GetMethodID(jstudent, "<init>", "(ILjava/lang/String;)V");
    // Construct a String string
    jstring name = env->NewStringUTF("john");
    // Construct a Person object
    jobject  jstudentObj = env->NewObject(jstudent, studentMethodId, 20, name);

    return jstudentObj;
}

static JNINativeMethod gMethods[] = {
        {"getStringFromJNI", "()Ljava/lang/String;", (void*)getStringFromJNI_native},
        {"getIntArrayFromJNI", "()[I", (void*)getIntArrayFromJNI_native},
        {"setPersonToJNI", "(Lcom/alphagl/main/Person;)V", (void*)setJniPerson_native},
        {"getPersonFromJNI", "()Lcom/alphagl/main/Person;", (void*)getJniPerson_native}
};

static jint registerNativeMethods(JNIEnv *env, const char* className, JNINativeMethod *gMethods, int numMethods) {
    jclass jcls;
    jcls = env->FindClass(className);
    if (jcls == NULL) {
        return JNI_FALSE;
    }

    if (env->RegisterNatives(jcls, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

static jint registerNative(JNIEnv *env) {
    return registerNativeMethods(env, CLASSNAME, gMethods, sizeof(gMethods) / sizeof(gMethods[0]));
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv *env = NULL;
    if ((vm->GetEnv((void**)&env, JNI_VERSION_1_6)) != JNI_OK) {
        return JNI_ERR;
    }

    if (!registerNative(env)) {
        return JNI_ERR;
    }

    return JNI_VERSION_1_6;
}

The final results of implementation are as follows:

Comparisons of the two implementations:
(1) In dynamic registration, methods in the form of Java_packageName_className_methodName may not be declared.
(2) In dynamic registration, we need to rewrite the JNI_OnLoad method, call RegisterNatives manually to register the local method, and declare it in JNI NativeMethod.
(3) Dynamic registration, obviously this way is more flexible, but the code requirements are higher, recommended to use this way.

The above sample code has been uploaded to Github and can be viewed by yourself if necessary.
https://github.com/cnsuperx/android-jni-example

JNI debugging

If the LLVM environment is installed, simply turn on the Jni Debuggable option. Environmental construction can be referred to Android Game Development Practice (1) NDK and JNI Development 03.

Then you can set breakpoints directly in C or C++ code.

QQ Group of Technical Exchange: 528655025
Author: AlphaGL
Source: http://www.cnblogs.com/alphagl/
Copyright is reserved. The original link is welcome to be reproduced.

Posted by drock on Tue, 02 Apr 2019 23:21:29 -0700