JNI_NDK Advanced Programming Guide Part 2

Posted by closer on Tue, 13 Aug 2019 06:19:52 +0200

JNI Knowledge Summarization and Use Skills

JNI Definition

JNI(Java Native Interface) means JAVA local call, which allows Java code to interact with code written in other languages. A standard mechanism for executing code under the control of a Java virtual machine.

Reviewing JNI from a Code Perspective

Organizational structure:

JNI function tables are like C++ virtual function tables. Virtual machines can run multiple function tables. JNI interface pointers only work in the current thread, pointers can not enter from one thread to another thread, but local methods can be invoked in different threads.
Let's look at the JNI header file:

JNI type and data structure:

Reference type:

Attributes, methods, value types:

jni.h 102 That's ok
struct _jfieldID;                       /* opaque structure */
typedef struct _jfieldID* jfieldID;     /* field IDs */

struct _jmethodID;                      /* opaque structure */
typedef struct _jmethodID* jmethodID;   /* method IDs */

struct JNIInvokeInterface;

typedef union jvalue {
    jboolean    z;
    jbyte       b;
    jchar       c;
    jshort      s;
    jint        i;
    jlong       j;
    jfloat      f;
    jdouble     d;
    jobject     l;
} jvalue;

Type Signatures:

Infer other things from one fact:

String The domain descriptor of the type is Ljava/lang/String;      
[ + The domain descriptor of its type + ;  
int[ ]     Its descriptor is[I  
float[ ]   Its descriptor is[F  
String[ ]  Its descriptor is[Ljava/lang/String;  
Object[ ]The domain descriptor of the type is[Ljava/lang/Object;  
int  [ ][ ] Its descriptor is[[I  
float[ ][ ] Its descriptor is[[F 
---------------------------------------------------------------------------------------------
Java Layer method                                               JNI Functional signature  
String test ( )                                         Ljava/lang/String;  
int f (int i, Object object)                            (ILjava/lang/Object;)I  
void set (byte[ ] bytes)                                ([B)V  

JNIEnv and Java VM

JNIEnv concept: It is a thread-related structure, which represents the running environment of Java in this thread.

JNIEnv and JavaVM: Pay attention to distinguishing these two concepts;

  • Java VM: Java VM is the representative of Java Virtual Machine in JNI layer. There is only one JNI in the whole world.
  • JNIEnv: JavaVM is the representative of threads. Each thread has one. There may be many JNIEnv in JNI.

JNIEnv effect:

  • Call Java functions: JNIEnv represents the Java running environment and can use JNIEnv to call the code in Java;
  • Operating Java Objects: The JNI layer of Java objects is called Jobject objects, which need to be manipulated by JNIEnv.

*** Summary: *** JNIEnv is valid only in the current thread. Local methods cannot pass JNIEnv from one thread to another. The JNIEnv passed to the local method is the same when the local method is called multiple times in the same Java thread. However, a local method can be invoked by different Java threads, so different JNIEnv can be accepted.

JNI Call Process Carding

/packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterApp.java

Actually, to learn JNI well is the source code of Android. We know that Bluetooth and Media Player all use JNI technology. Everyone should pay attention to combing the call process when looking at the source code.

  • Java code will have System.loadLibrary("**") and related native functions.
  • In the corresponding CPP source code implemented by JNI, there will be the following NativeMethod registration function and the corresponding native function in the Java code.
  • The dynamic registration of methods in JNI_OnLoad function facilitates the calls of C++ and Java. (In fact, in JNI_OnLoad, the jniRegister Native Methods method is ultimately called.)
static JNINativeMethod gMethods[] = {
    ······
    {"native_init",         "()V",                              (void *)android_media_MediaPlayer_native_init},
    // Here is native_setup: the first is the name of the java function, the second is the signature, and the third is the pointer to the jni implementation method
    {"native_setup",        "(Ljava/lang/Object;)V",            (void *)android_media_MediaPlayer_native_setup},
    {"native_finalize",     "()V",                              (void *)android_media_MediaPlayer_native_finalize},
    ······
};

// Pointer of jni implementation method
static void
android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
    ALOGV("native_setup");
    sp<MediaPlayer> mp = new MediaPlayer();
    if (mp == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
        return;
    }

    // create new listener and give it to MediaPlayer
    sp<JNIMediaPlayerListener> listener = new JNIMediaPlayerListener(env, thiz, weak_this);
    mp->setListener(listener);

    // Stow our new C++ MediaPlayer in an opaque field in the Java object.
    setMediaPlayer(env, thiz, mp);
}

// This function only registers the native methods
static int register_android_media_MediaPlayer(JNIEnv *env)
{
    // gMethods is called here. The system can get Android Runtime:. We can't get it. We can only analyze what he did when he registered.
    // Analysis: env, "android / media / media / media player" is the package + class name of MediaPlayer.java
    // gMethods
    // NELEM(gMethods) calculates how many bytes this structured array takes, and puts this size in (a macro definition, easy to reuse)
    // # define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
    // The implementation of registerNativeMethods in Android Runtime. CPP is shown in the next section of the code.
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaPlayer", gMethods, NELEM(gMethods));
}

//  The JNI_OnLoad method declared by jni.h is rewritten here, registered in JNI_OnLoad (register_android_media_MediaPlayer). During the registration process, an array of gMethods structured bodies is declared, in which the method mapping is written. JNI_OnLoad's call is to go to System. Load Library and register dynamically.
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed\n");
        goto bail;
    }
    assert(env != NULL);

    ...
    // Register_android_media_Media Player is called here
    if (register_android_media_MediaPlayer(env) < 0) {
        ALOGE("ERROR: MediaPlayer native registration failed\n");
        goto bail;
    }
    ...

    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

bail:
    return result;
}

https://android.googlesource.com/platform/libnativehelper/+/jb-mr1.1-dev-plus-aosp/JNIHelp.cpp#71

extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
    ALOGV("Registering %s's %d native methods...", className, numMethods);
    scoped_local_ref<jclass> c(env, findClass(env, className));
    if (c.get() == NULL) {
        char* msg;
        asprintf(&msg, "Native registration unable to find class '%s'; aborting...", className);
        e->FatalError(msg);
    }
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* msg;
        asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
        e->FatalError(msg);
    }
    return 0;
}

Summary of JNI Functions

function Java array type Local type Explain
GetBooleanArrayElements jbooleanArray jboolean Release Boolean Array Elements Release
GetByteArrayElements jbyteArray jbyte Release Byte Array Elements Release
GetCharArrayElements jcharArray jchar Release Short Array Elements Release
GetShortArrayElements jshortArray jshort Release Boolean Array Elements Release
GetIntArrayElements jintArray jint Release Int Array Elements Release
GetLongArrayElements jlongArray jlong Release Long Array Elements Release
GetFloatArrayElements jfloatArray jfloat Release Float Array Elements Release
GetDoubleArrayElements jdoubleArray jdouble Release Double Array Elements Release
GetObjectArrayElement custom object object
SetObjectArrayElement custom object object
GetArrayLength Get the array size
NewArray Create an array of primitive data types of specified length
GetPrimitiveArrayCritical Get a pointer to the content of the original data type. This method may make garbage collection impossible. This method may return a copy of the array, so this resource must be released.
ReleasePrimitiveArrayCritical Release a pointer to the content of the original data type. This method may make garbage collection impossible. This method may return a copy of the array, so this resource must be released.
NewStringUTF Method Conversion of jstring Type
GetStringUTFChars Method Conversion of jstring Type
DefineClass Loading classes from buffers of raw class data
FindClass This function is used to load locally defined classes. It searches for directories and zip files specified by CLASSPATH environment variables as classes with specified names
GetObjectClass Get this class from the object. This function is relatively simple, the only thing to note is that the object can not be NULL, otherwise the obtained class must return to NULL.
GetSuperclass Get the parent class or superclass. If clazz represents a class rather than an object, the function returns a superclass of the class specified by clazz. If clazz specifies a class object or represents an interface, the function returns NULL
IsAssignableFrom Determine whether clazz1 objects can be safely coerced into clazz2
Throw Throwable object
ThrowNew Construct an exception object and throw it with messages of the specified class (specified by message)
ExceptionOccurred Determine whether an exception is being thrown. Exceptions are always thrown until platform-related code calls ExceptionClear() or Java code handles the exception
ExceptionDescribe Output the exception and stack traceback to the system error reporting channel (such as stderr). This routine facilitates debugging operations
ExceptionClear Clear any exception currently thrown. If no exception is present, this routine does not produce any effect
FatalError Throws a fatal error and does not want the virtual machine to be repaired. The function has no return value
NewGlobalRef Create a new global reference to the object referenced by the obj parameter. The obj parameter can be either a global reference or a local reference. Global references are explicitly undone by calling DeleteGlobalRef().
DeleteGlobalRef Delete the global reference to which globalRef refers
DeleteLocalRef Delete the local reference to which localRef refers
AllocObject Allocate a new Java object without calling any constructor for that object. Returns a reference to the object. clazz parameters must not refer to array classes.
getObjectClass Classes returning objects
IsSameObject Testing whether two references refer to the same Java object
NewString Constructing a New java.lang.String Object Using Unicode Character Array
GetStringLength Returns the length of the Java string (number of Unicode characters)
GetStringChars Returns a pointer to an array of Unicode characters pointing to a string. This pointer is valid until ReleaseStringchars() is called
ReleaseStringChars Notify the VM platform that the relevant code does not need to access chars anymore. The parameter chars is a pointer that can be obtained from string through GetStringChars().
NewStringUTF Constructing a New java.lang.String Object Using UTF-8 Character Array
GetStringUTFLength Returns UTF-8 length of string in bytes
GetStringUTFChars Returns a pointer to the UTF-8 character array of a string. This array will remain valid until it is released by ReleaseStringUTFChars().
ReleaseStringUTFChars Notify the VM platform that the relevant code does not need to access utf again. The utf parameter is a pointer that can be obtained using GetStringUTFChars().
NewObjectArray Construct a new array that will hold objects in the class elementClass. The initial values of all elements are set to initial Element
SetArrayRegion A set of functions that copy a region of an array of primitive types from a buffer
GetFieldID Returns the attribute ID of the instance (non-static) domain of the class. The domain is specified by its name and signature. The GetField and SetField series of accessor functions use domain ID to retrieve object domains. GetFieldID() cannot be used to get the length field of an array. GetArray Length () should be used.
GetField The accessor routine series returns the value of the instance (non-static) domain of the object. The domain to be accessed is specified by the domain ID obtained by calling GetFieldID().
SetField The accessor routine series sets the value of the instance (non-static) property of the object. The attributes to be accessed are specified by the attribute ID obtained by calling SetFieldID().
GetStaticFieldID GetStaticField SetStaticField Ibid., it's just a static property.
GetMethodID Returns the method ID of a class or interface instance (non-static) method. Method can be defined in a clazz superclass or inherited from clazz. The method is determined by its name and signature. GetMethodID() initializes uninitialized classes. To get the method ID of the constructor, you should take the method name and void (V) as the return type.
CallVoidMethod
CallObjectMethod
CallBooleanMethod
CallByteMethod
CallCharMethod
CallShortMethod
CallIntMethod
CallLongMethod
CallFloatMethod
CallDoubleMethod
GetStaticMethodID Calling static methods
CallMethod
RegisterNatives Register the local method to the class specified by the clazz parameter. The methods parameter specifies an array of JNINativeMethod structures that contain the name, signature, and function pointer of the local method. The nMethods parameter specifies the number of local methods in the array.
UnregisterNatives Cancel the local method of the registered class. Class will return to the state before linking or registering the local method function. This function should not be used in regular platform-related code. Instead, it can provide a way for some programs to reload and re-link local libraries.

Summary of JNI String and Array Processing

Let's complete a small case to illustrate the knowledge points. We often use AES encryption algorithm in Java layer. Server and mobile end need to do a set of algorithms. Then we can use JNI to implement a set of algorithms and call them to multiple end. No more gossip, just code.

The following is the JNIUtils class, which mainly completes so loading, AES algorithm initialization and so on. Focus on several static native functions.

public class JNIUtils {
    public static native String getStringFormC(); //Demonstrate getting strings from C++.
    public static native byte[] getKeyValue();//The key used for AES encryption is also a byte array obtained from C++.
    public static native byte[] getIv();//Using CBC mode, a vector iv is needed to increase the strength of encryption algorithm.

    //Demonstration of string usage
    public native static String sayHello(String text);


    private static byte[]keyValue;
    private static byte[]iv;

    private static SecretKey key;
    private static AlgorithmParameterSpec paramSpec;
    private static Cipher ecipher;

    static {
        //Loading dynamic libraries
        System.loadLibrary("native-lib");
        //Configuration of AES algorithm initialization Here AES-related information can be consulted Online
        keyValue = getKeyValue();
        iv = getIv();
        if(null != keyValue && null !=iv) {
            KeyGenerator kgen;
            try {
                kgen = KeyGenerator.getInstance("AES");
                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                random.setSeed(keyValue);
                kgen.init(128,random);
                key =kgen.generateKey();
                paramSpec =new IvParameterSpec(iv);
                ecipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
                e.printStackTrace();
            }
        }
    }
    /**Encryption**/
    public static String encode(String msg) {
        String str ="";
        try {
            //Initialize this cipher with a key and a set of algorithm parameters
            ecipher.init(Cipher.ENCRYPT_MODE,key,paramSpec);
            //Encryption and conversion to hexadecimal string
            str = asHex(ecipher.doFinal(msg.getBytes()));
        } catch (BadPaddingException | InvalidAlgorithmParameterException
                | InvalidKeyException | IllegalBlockSizeException ignored) {
        }
        return str;
    }
    /**Decryption function**/
    public static String decode(String value) {
        try {
            ecipher.init(Cipher.DECRYPT_MODE,key,paramSpec);
            return new String(ecipher.doFinal(asBin(value)));
        } catch (BadPaddingException | InvalidKeyException | IllegalBlockSizeException
                | InvalidAlgorithmParameterException ignored) {
        }
        return"";
    }
    /**Conversion to hexadecimal**/
    private static String asHex(byte buf[]) {
        StringBuffer strbuf =new StringBuffer(buf.length * 2);
        int i;
        for (i = 0;i <buf.length;i++) {
            if (((int)buf[i] & 0xff) < 0x10)//Make up zero before less than ten
                strbuf.append("0");
            strbuf.append(Long.toString((int)buf[i] & 0xff, 16));
        }
        return strbuf.toString();
    }
    /**Conversion to binary**/
    private static byte[] asBin(String src) {
        if (src.length() < 1)
            return null;
        byte[]encrypted =new byte[src.length() / 2];
        for (int i = 0;i <src.length() / 2;i++) {
            int high = Integer.parseInt(src.substring(i * 2, i * 2 + 1), 16);//Take high-bit bytes
            int low = Integer.parseInt(src.substring(i * 2 + 1, i * 2 + 2), 16);//Take low byte
            encrypted[i] = (byte) (high * 16 +low);
        }
        return encrypted;
    }

Let's look at the key C++ core code:

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

#Define LOG "ndk-jni" // This is a custom LOG identifier
#Define LOGD (...) android_log_print (ANDROID_LOG_DEBUG, LOG, _VA_ARGS_)// Define LOGD Type
#Define LOGI (...) _android_log_print (ANDROID_LOG_INFO, LOG, _VA_ARGS_)// Define LOGI Types
#Define LOGW (...) _android_log_print (ANDROID_LOG_WARN, LOG, _VA_ARGS_)// Define LOGW Type
#Define LOGE (...) _android_log_print (ANDROID_LOG_ERROR, LOG, _VA_ARGS_)// Define LOGE Type
#Define LOGF (...) android_log_print (ANDROID_LOG_FATAL, LOG, _VA_ARGS_)// Define LOGF Type

const char keyValue[] = {  //Secret key
        21, 25, 21, 45, 25, 98, 55, 45, 10, 35, 45, 35,
        26, 5, 25, 65, 78, 99, 85, 45, 5, 10, 0, 11,
        35, 48, 98, 65, 32, 14, 67, 25, 36, 56, 45, 5,
        12, 15, 35, 15, 25, 14, 62, 25, 33, 45, 55, 12, 8
};

const char iv[] =  {    //16 bit enhanced iv vector for AES algorithm enhancement
        33, 32, 25, 25, 35, 27, 55, 12, 15,32,
        23, 45, 26, 32, 5,16
};


extern "C"
JNIEXPORT jstring JNICALL
Java_ndk_jesson_com_ndkdemo_JNIUtils_getStringFormC(JNIEnv *env, jclass type) {
	//NewStringUTF
    return env->NewStringUTF("This is from C++The original string");
}

/***
 * Get the array jbyte in c++.
 */
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_ndk_jesson_com_ndkdemo_JNIUtils_getKeyValue(JNIEnv *env, jclass type) {
    jbyteArray kv = env->NewByteArray(sizeof(keyValue));
    jbyte* bytes = env->GetByteArrayElements(kv,0);

    int i=0;
    for (i;i < sizeof(keyValue); ++i) {
        bytes[i] = keyValue[i];
    }
    env->SetByteArrayRegion(kv,0, sizeof(keyValue),bytes);
    env->ReleaseByteArrayElements(kv,bytes,0);
    return kv;

}

extern "C"
JNIEXPORT jbyteArray JNICALL
Java_ndk_jesson_com_ndkdemo_JNIUtils_getIv(JNIEnv *env, jclass type) {

    jbyteArray ivArray = env->NewByteArray(sizeof(iv));
    jbyte *bytes = env->GetByteArrayElements(ivArray, 0);

    int i;
    for (i = 0; i < sizeof(iv); i++){
        bytes[i] = (jbyte)iv[i];
    }

    env->SetByteArrayRegion(ivArray, 0, sizeof(iv), bytes);
    env->ReleaseByteArrayElements(ivArray,bytes,0);
    return ivArray;
}

Let's focus on the following functions:

In jni.h, there is an array definition, the size of the jsize code array (typedef jint jsize;). So you must be familiar with the following functions:

jbyteArray (*NewBooleanArray)(JNIEnv*, jsize); //Initialize arrays
jbyte* (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*);//Get array elements
void (*SetCharArrayRegion)(JNIEnv*, jcharArray, jsize, jsize, const jchar*);//Setting Array Data
void (*ReleaseByteArrayElements)(JNIEnv*, jbyteArray, jbyte*, jint);//Release memory resources occupied by arrays

Summary: In the case of AES, the Java SDK API is used to implement AES. The C++ code I demonstrated is mainly for demonstrating the use of the above functions of arrays. I hope that through this article, you can really understand some of the preliminary concepts of JNI, and I will continue to demonstrate the relevant key uses of JNI, including string, exception, multi-threading. Welcome to my NDK development series.

Topics: Java Android MediaPlayer Attribute