Implementing the “Observer-Subscriber” pattern using JNI callbacks on Android (NDK)

  • Tutorial

Implementation in Android (NDK) JNI callbacks, " Observer -Subscriber" pattern with NDK and callback, self-written EventBus or Rx


... It got me "inside there are no user serviceable parts." I want to see what is there.
- Russian matryoshka to the very depths. True, Orozco? Juan did not begin to look what a Russian matryoshka is.
- Yes, it's garbage, Professor Gu. Who needs it - to mess with it?
"The End of the Rainbows" Vinge Vernor

There are quite a few Android applications that combine C ++ and Java code. Java implements business logic, and C ++ does all the work of computation, which is often found in audio processing. The audio stream is processed somewhere inside, and the brake with gas and clutch, and the data for all the funny pictures are brought up.
Well, since ReactiveX is already familiar, then, so to say, not to change a hand, and to work with the JNI dungeon in familiar ways, there is a regular need to implement the “Observer” pattern in projects with NDK. Well, at the same time clear code forarchaeologiststhose who are not lucky to "understand someone else's code" increases.
So, the best way to learn something is to do it yourself.
Suppose we love and know how to write our bikes. And what we get as a result:


  • Something like a postback from C ++ code to subscribing;
  • management of processing in the native code, that is, we can not be driven about the calculations, when there are no subscribers and there is no one to send them;
  • You may need to transfer data between different JVMs;
  • and in order not to get up twice, at the same time sending messages inside the project's threads.

Full working code is available on GitHub . The article contains only excerpts from it.


A bit of theory and history


Recently I was at the RX mitap and I was amazed at the number of questions about: how fast ReactiveX is and how it works in general.
For ReactiveX, I can only say that for Java its speed is very dependent on how reasonably used it is, with proper use of its speed is quite enough.
Our bike is much more lightweight, but if you need, for example, a message queue (as flowable), then you have to write it yourself. For that, you know that all the glitches are just yours.
A bit of theory: the observer pattern“Subscriber” is a mechanism that allows an object to receive notifications about changes in the state of other objects and thus monitor them. It is done to reduce connectivity and dependencies between software components, which allows them to be more efficiently used and tested. A bright representative in which the language concept is built on this all - Smalltalk, all based on the idea of ​​sending messages. Influenced by Objective-C.


Implementation


Let's try in the best traditions of DIY, so to speak, “to flash with a LED”. If you are using JNI, in the Android NDK world, you can query the Java method asynchronously, in any stream. This is what we use to build our “Observer”.


The demo project is built on a new project template from Android Studio.


This is an auto-generated method. He commented:


// Used to load the 'native-lib' library on application startup.static {
    System.loadLibrary("native-lib");
}

And now yourself. The first step is the method nsubscribeListener.


privatenativevoidnsubscribeListener(JNIListener JNIListener);

It allows C ++ code to get a link to java-code to include a callback to an object that implements the interface JNIListener.


publicinterfaceJNIListener{
    voidonAcceptMessage(String string);
    voidonAcceptMessageVal(int messVal);
}

In the implementation of its methods and values ​​will be transferred.


For effective caching of links to the virtual machine, we also save the link to the object. We get for it a global link.


Java_ua_zt_mezon_myjnacallbacktest_MainActivity_nsubscribeListener(JNIEnv *env, jobject instance,
                                                                   jobject listener) {
    env->GetJavaVM(&jvm); //store jvm reference for later call
    store_env = env;
    jweak store_Wlistener = env->NewWeakGlobalRef(listener);

Immediately calculate and save references to methods. This means fewer operations are required to make a callback.


jclass clazz = env->GetObjectClass(store_Wlistener);
jmethodID store_method = env->GetMethodID(clazz, "onAcceptMessage", "(Ljava/lang/String;)V");
jmethodID store_methodVAL = env->GetMethodID(clazz, "onAcceptMessageVal", "(I)V");

Subscriber data is stored as class entries ObserverChain.


classObserverChain{
public:
    ObserverChain(jweak pJobject, jmethodID pID, jmethodID pJmethodID);
    jweak store_Wlistener=NULL;
    jmethodID store_method = NULL;
    jmethodID store_methodVAL = NULL;
};

Store the subscriber in the store_Wlistener_vector dynamic array.


ObserverChain *tmpt = new ObserverChain(store_Wlistener, store_method, store_methodVAL);
store_Wlistener_vector.push_back(tmpt);

Now how messages from a Java-code will be transferred.


A message sent to the nonNext method will be sent to all subscribers.


privatenativevoidnonNext(String message);

Implementation:


Java_ua_zt_mezon_myjnacallbacktest_MainActivity_nonNext(JNIEnv *env, jobject instance,
                                                                jstring message_) {
    txtCallback(env, message_);
}

Function txtCallback (env, message_); sends messages to all subscribers.


voidtxtCallback(JNIEnv *env, const _jstring *message_){
    if (!store_Wlistener_vector.empty()) {
        for (int i = 0; i < store_Wlistener_vector.size(); i++) {
            env->CallVoidMethod(store_Wlistener_vector[i]->store_Wlistener,
                                store_Wlistener_vector[i]->store_method, message_);
        }
    }
}

For forwarding messages from C ++ or C code, use the test_string_callback_fom_c function


voidtest_string_callback_fom_c(char *val)

It checks right from the start whether there are any subscribers at all.


if (store_Wlistener_vector.empty())
    return;

It is easy to see that the same txtCallback function is used for sending messages:


voidtest_string_callback_fom_c(char *val){
    if (store_Wlistener_vector.empty())
        return;
    __android_log_print(ANDROID_LOG_VERBOSE, "GetEnv:", " start Callback  to JNL [%d]  \n", val);
    JNIEnv *g_env;
    if (NULL == jvm) {
        __android_log_print(ANDROID_LOG_ERROR, "GetEnv:", "  No VM  \n");
        return;
    }
    //  double check it's all ok
    JavaVMAttachArgs args;
    args.version = JNI_VERSION_1_6; // set your JNI version
    args.name = NULL; // you might want to give the java thread a name
    args.group = NULL; // you might want to assign the java thread to a ThreadGroupint getEnvStat = jvm->GetEnv((void **) &g_env, JNI_VERSION_1_6);
    if (getEnvStat == JNI_EDETACHED) {
        __android_log_print(ANDROID_LOG_ERROR, "GetEnv:", " not attached\n");
        if (jvm->AttachCurrentThread(&g_env, &args) != 0) {
            __android_log_print(ANDROID_LOG_ERROR, "GetEnv:", " Failed to attach\n");
        }
    } elseif (getEnvStat == JNI_OK) {
        __android_log_print(ANDROID_LOG_VERBOSE, "GetEnv:", " JNI_OK\n");
    } elseif (getEnvStat == JNI_EVERSION) {
        __android_log_print(ANDROID_LOG_ERROR, "GetEnv:", " version not supported\n");
    }
    jstring message = g_env->NewStringUTF(val);//
    txtCallback(g_env, message);
    if (g_env->ExceptionCheck()) {
        g_env->ExceptionDescribe();
    }
    if (getEnvStat == JNI_EDETACHED) {
        jvm->DetachCurrentThread();
    }
}

In the MainActivity create two textview and one EditView.


<TextViewandroid:id="@+id/sample_text_from_Presenter"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_weight="1"android:padding="25dp"android:text="Hello World!from_Presenter"app:layout_constraintBottom_toTopOf="parent"app:layout_constraintEnd_toStartOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><TextViewandroid:id="@+id/sample_text_from_act"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_weight="1"android:text="Hello World from_ac!"app:layout_constraintBottom_toTopOf="parent"app:layout_constraintStart_toStartOf="@+id/sample_text_from_Presenter"app:layout_constraintTop_toTopOf="parent" /><EditTextandroid:id="@+id/edit_text"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Hello World!"app:layout_constraintBottom_toTopOf="parent"app:layout_constraintStart_toStartOf="@+id/sample_text_from_act"app:layout_constraintTop_toTopOf="parent" />

In OnCreate, we associate the View with variables and describe that when the text changes in EditText, a message will be sent to subscribers.


mEditText = findViewById(R.id.edit_text);
mEditText.addTextChangedListener(new TextWatcher() {
    @OverridepublicvoidbeforeTextChanged(CharSequence charSequence, int i, int i1, int i2){
    }
    @OverridepublicvoidonTextChanged(CharSequence charSequence, int i, int i1, int i2){
        nonNext(charSequence.toString());
    }
    @OverridepublicvoidafterTextChanged(Editable editable){
    }
});
tvPresenter = (TextView) findViewById(R.id.sample_text_from_Presenter);
tvAct = (TextView) findViewById(R.id.sample_text_from_act);

We get and register subscribers:


mPresenter = new MainActivityPresenterImpl(this);
nsubscribeListener((MainActivityPresenterImpl) mPresenter);
nlistener = new JNIListener() {
    @OverridepublicvoidonAcceptMessage(String string){
        printTextfrActObj(string);
    }
    @OverridepublicvoidonAcceptMessageVal(int messVal){
    }
};
nsubscribeListener(nlistener);

It looks like this:



The full working code is available on GitHub https://github.com/NickZt/MyJNACallbackTest
Tell in the comments what to describe in more detail.


Also popular now: