Time for action – calling back Java from native code

Let's continue our Store by calling back the interface we defined from native code:

  1. In com_packtpub_store_Store.cpp, declare method descriptors with type jmethodID for each callback, which is going to be cached:
    ...
    static Store gStore;
    
    static jclass StringClass;
    static jclass ColorClass;
    
    static jmethodID MethodOnSuccessInt;
    static jmethodID MethodOnSuccessString;
    static jmethodID MethodOnSuccessColor;
    ...
  2. Then, cache all the callback descriptors in JNI_OnLoad(). This can be done in two main steps:

    Getting a Class descriptor with the JNI method FindClass(). One can find a class descriptor, thanks to its absolute package path, here: com./packtpub/store/Store.

    Retrieving a method descriptor from the class descriptor with GetMethodID(). To differentiate several overloaded methods, the signatures retrieved earlier with javap must be specified:

    ...
    JNIEXPORT jint JNI_OnLoad(JavaVM* pVM, void* reserved) {
        JNIEnv *env;
        if (pVM->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
            abort();
        }
        ...
        // Caches methods.
        jclass StoreClass = env->FindClass("com/packtpub/store/Store");
        if (StoreClass == NULL) abort();
    
        MethodOnSuccessInt = env->GetMethodID(StoreClass, "onSuccess",
                "(I)V");
        if (MethodOnSuccessInt == NULL) abort();
    
        MethodOnSuccessString = env->GetMethodID(StoreClass, "onSuccess",
                "(Ljava/lang/String;)V");
        if (MethodOnSuccessString == NULL) abort();
    
        MethodOnSuccessColor = env->GetMethodID(StoreClass, "onSuccess",
                "(Lcom/packtpub/store/Color;)V");
        if (MethodOnSuccessColor == NULL) abort();
        env->DeleteLocalRef(StoreClass);
    
        // Store initialization.
        gStore.mLength = 0;
        return JNI_VERSION_1_6;
    }
    ...
  3. Notify the Java Store (that is, pThis) when an integer is successfully inserted in setInteger(). To invoke a Java method on a Java object, simply use CallVoidMethod() (which means that the called Java method returns void). To do so, we need:
    • An object instance
    • A method signature
    • Effective parameters to pass, if applicable (here, an integer value)
      ...
      JNIEXPORT void JNICALL Java_com_packtpub_store_Store_setInteger
        (JNIEnv* pEnv, jobject pThis, jstring pKey, jint pInteger) {
          StoreEntry* entry = allocateEntry(pEnv, &gStore, pKey);
          if (entry != NULL) {
              entry->mType = StoreType_Integer;
              entry->mValue.mInteger = pInteger;
      
              pEnv->CallVoidMethod(pThis, MethodOnSuccessInt,
                      (jint) entry->mValue.mInteger);
          }
      }
      ...
  4. Repeat the operation for strings. There is no need to generate a Global reference when allocating the returned Java string as it is used immediately in the Java callback. We can also destroy the Local reference to this string right after usage, but JNI will take care of that when returning from the native callback:
    ...
    JNIEXPORT void JNICALL Java_com_packtpub_store_Store_setString
      (JNIEnv* pEnv, jobject pThis, jstring pKey, jstring pString) {
        // Turns the Java string into a temporary C string.
        StoreEntry* entry = allocateEntry(pEnv, &gStore, pKey);
        if (entry != NULL) {
            entry->mType = StoreType_String;
            ...
    
            pEnv->CallVoidMethod(pThis, MethodOnSuccessString,
                    (jstring) pEnv->NewStringUTF(entry->mValue.mString));
        }
    }
    ...
  5. Finally, repeat the operation for colors:
    ...
    JNIEXPORT void JNICALL Java_com_packtpub_store_Store_setColor
      (JNIEnv* pEnv, jobject pThis, jstring pKey, jobject pColor) {
        // Save the Color reference in the store.
        StoreEntry* entry = allocateEntry(pEnv, &gStore, pKey);
        if (entry != NULL) {
            entry->mType = StoreType_Color;
            entry->mValue.mColor = pEnv->NewGlobalRef(pColor);
    
            pEnv->CallVoidMethod(pThis, MethodOnSuccessColor,
                    (jstring) entry->mValue.mColor);
        }
    }
    ...

What just happened?

Launch the application and insert an integer, a string, or color entry. A successful message is displayed with the inserted value. The native code called the Java side thanks to the JNI Reflection API. This API is not only useful to execute a Java method, it is also the only way to process jobject parameters passed to a native method. However, if calling C/C++ code from Java is rather easy, performing Java operations from C/C++ is a bit more involving!

Although a bit repetitive and verbose, calling any Java method should always be as trivial as this:

  • Retrieve the class descriptor from those we want to call methods (here, the Store Java object):
    jclass StoreClass = env->FindClass("com/packtpub/store/Store");
  • Retrieve the method descriptors for the callback we want to call (such as the Method class in Java). These method descriptors are retrieved from the class descriptor, which owns it (like a Class in Java):
    jmethodID MethodOnSuccessInt = env->GetMethodID(StoreClass,
                                                    "onSuccess", "(I)V");
  • Optionally, cache the descriptors so that they can be used immediately in future native calls. Again, JNI_OnLoad() makes it easy to cache JNI descriptors before any native call is made. Descriptors whose names end with Id, such as jmethodID, can be freely cached. They are not references that can be leaked, or have to be made global on the opposite to jclass descriptors.

    Tip

    Caching descriptors is definitely good practice, as retrieving Fields or Methods through the JNI reflection may cause some overhead.

  • Invoke methods with the necessary parameters on an object. The same method descriptor can be reused on any object instance of the corresponding class:
    env->CallVoidMethod(pThis, MethodOnSuccessInt, (jint) myInt); 

Whatever method you need to call on a Java object, the same process always applies.

More on the JNI Reflection API

Once you know the Reflection API, you know most of the JNI. Here are some of the provided methods that may be useful:

  • FindClass() retrieves a (Local) reference to a Class descriptor object according to its absolute path:
    jclass FindClass(const char* name)
  • GetObjectClass() has the same purpose, except that FindClass() finds class definitions according to their absolute path, whereas the other finds the class directly from an object instance (such as getClass() in Java):
    jclass GetObjectClass(jobject obj)
  • The following methods allow you to retrieve JNI descriptors for methods and fields, and either static or instance members. These descriptors are IDs and not references to Java objects. There is no need to turn them into Global references. These methods require the method or field name and a signature to differentiate overloads. Constructor descriptors are retrieved in the same way as methods, except that their name is always <init> and they have a void return value:
    jmethodID GetMethodID(jclass clazz, const char* name,
                          const char* sig) 
    jmethodID GetStaticMethodID(jclass clazz, const char* name,
                                const char* sig)
    
    jfieldID GetStaticFieldID(jclass clazz, const char* name,
                              const char* sig)
    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
  • There is a second set of methods to retrieve field values using their corresponding descriptors. There is one getter and one setter method per primitive type, plus another for objects:
    jobject GetObjectField(jobject obj, jfieldID fieldID)
    <primitive> Get<Primitive>Field(jobject obj, jfieldID fieldID)
    
    void SetObjectField(jobject obj, jfieldID fieldID, jobject value)
    void Set<Primitive>Field(jobject obj, jfieldID fieldID,
                             <jprimitive> value)
  • The same goes for methods according to their return values:
    jobject CallObjectMethod(JNIEnv*, jobject, jmethodID, ...)
    
    <jprimitive> Call<Primitive>Method(JNIEnv*, jobject, jmethodID, ...);
  • Variants of these methods exist with an A and V postfix. The behavior is identical, except that arguments are specified respectively using a va_list (that is, variable argument list) or jvalue array (jvalue being a union of all JNI types):
    jobject CallObjectMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jobject CallObjectMethodA(JNIEnv*, jobject, jmethodID, jvalue*);

Have a look at jni.h in the Android NDK include directory to see all the possibilities by the JNI reflective API.

Debugging JNI

The goal of JNI calls is often performance. Thus, JNI does not perform advanced checking when its API methods are invoked. Hopefully, there exists an extended checking mode, which performs advanced checks and gives feedback in the Android Logcat.

To activate it, run the following command from a command prompt:

adb shell setprop debug.checkjni 1

The extended checking mode is available for applications started after this flag is set, until it is set to 0, or until the device is rebooted. For rooted devices, the whole device can be started with this mode with the following commands:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

If everything works properly, a message Late-enabling – Xcheck:jni appears in the Logcat when your application starts. Then, check the Logcat regularly to find its JNI warning or error.

Debugging JNI

Synchronizing Java and native threads

Parallel programming is a mainstream subject nowadays. Android makes no exception since the introduction of multicore processors. You can do the threading entirely on the Java side (with the Java Thread and Concurrency APIs), on the native side (with the POSIX PThread API, which is provided by the NDK), and, more interestingly, between the Java and native side using JNI.

In this part, we will create a background thread, the watcher, which keeps a constant eye on what is inside the data store. It iterates through all entries and then sleeps for a fixed amount of time. When the watcher thread finds a key of a specific type predefined in the code, it acts accordingly. For this first part, we are just going to clip integer values to a predefined range.

Of course, threads need synchronization. The native thread is going to access and update the store only when a user understands the UI thread, and does not modify it. The native thread is created in C/C++ but the UI thread is a Java thread. We are going to use JNI monitors to synchronize both of them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.174.195