CTO - Abid Maqbool
CTO - Abid Maqbool

Reputation: 165

JNI Issue: Passing Java Runnable Object to JNI in Custom Gluon Attach Plugin

I am developing a custom Gluon Attach plugin and encountering issues with passing a Java Runnable object from Java to JNI. Despite reviewing the Gluon Attach plugin source code (which unfortunately didn't help much in this case), I'm unable to successfully invoke the Runnable object on the native side.

Context

This is a custom implementation of the AndroidAlertDialogService within a custom Gluon Attach plugin. I want to pass a Runnable object as the last parameter of the native method showSaveAlert3.

When the native code attempts to invoke the Runnable, I consistently run into this error:

JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug):  
jobject is an invalid JNI transition frame reference: 0x8000000000000008  

Code Overview

Java Class 1 (AndroidAlertDialogService)

package com.gluonhq.attachextended.alertdialog.impl;

import com.gluonhq.attachextended.alertdialog.AlertDialogService;

public class AndroidAlertDialogService implements AlertDialogService {

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

    @Override
    public void showSaveAlert(String title, String content, Runnable r) {
        showSaveAlert3(title, content, r);
    }

    private native static void showSaveAlert3(String title, String content, Runnable r);
}

JNI Code (alertdialog.c)

#include "util.h"

static jclass jAlertDialogServiceClass;
static jobject jDalvikAlertDialogService;

static jmethodID jAlertDialogServiceShowSaveAlert3Method;

static void initializeAlertDialogDalvikHandles() {
    jAlertDialogServiceClass = GET_REGISTER_DALVIK_CLASS(jAlertDialogServiceClass, "com/gluonhq/helloandroid/DalvikAlertDialogService");
    
    ATTACH_DALVIK();
    jmethodID jAlertDialogServiceInitMethod = (*dalvikEnv)->GetMethodID(dalvikEnv, jAlertDialogServiceClass, "<init>", "(Landroid/app/Activity;)V");

    jAlertDialogServiceShowSaveAlert3Method = (*dalvikEnv)->GetMethodID(dalvikEnv, jAlertDialogServiceClass, "showSaveAlert3", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Runnable;)V");
    
    jobject jActivity = substrateGetActivity();
    jobject jObj = (*dalvikEnv)->NewObject(dalvikEnv, jAlertDialogServiceClass, jAlertDialogServiceInitMethod, jActivity);
    jDalvikAlertDialogService = (*dalvikEnv)->NewGlobalRef(dalvikEnv, jObj);
    DETACH_DALVIK();
}

//////////////////////////
// From Graal to native //
//////////////////////////

JNIEXPORT jint JNICALL
JNI_OnLoad_alertdialog(JavaVM *vm, void *reserved)
{
    JNIEnv* graalEnv;
    ATTACH_LOG_INFO("JNI_OnLoad_alertdialog called");
#ifdef JNI_VERSION_1_8
    if ((*vm)->GetEnv(vm, (void **)&graalEnv, JNI_VERSION_1_8) != JNI_OK) {
        ATTACH_LOG_WARNING("Error initializing native AlertDialog from OnLoad");
        return JNI_FALSE;
    }
    ATTACH_LOG_FINE("[AlertDialog Service] Initializing native AlertDialog from OnLoad");
    initializeAlertDialogDalvikHandles();
    return JNI_VERSION_1_8;
#else
    #error Error: Java 8+ SDK is required to compile Attach
#endif
}

// from Java to Android

JNIEXPORT void JNICALL Java_com_gluonhq_attachextended_alertdialog_impl_AndroidAlertDialogService_showSaveAlert3  
(JNIEnv *env, jclass jClass, jstring jtitle, jstring jcontent, jobject jr) {  
    const char *titleChars = (*env)->GetStringUTFChars(env, jtitle, NULL);  
    const char *contentChars = (*env)->GetStringUTFChars(env, jcontent, NULL);  

    ATTACH_DALVIK();  
    jstring jTitleString = (*dalvikEnv)->NewStringUTF(dalvikEnv, titleChars);  
    jstring jContentString = (*dalvikEnv)->NewStringUTF(dalvikEnv, contentChars);  

    jobject globalRunnable = (*dalvikEnv)->NewGlobalRef(dalvikEnv, jr);  
    jclass rClass = (*dalvikEnv)->GetObjectClass(dalvikEnv, globalRunnable);  

    jmethodID midMyCustomMethod = (*dalvikEnv)->GetMethodID(dalvikEnv, rClass, "run", "()V");  
    (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAlertDialogService, jAlertDialogServiceShowSaveAlert3Method, jTitleString, jContentString, midMyCustomMethod);  

    DETACH_DALVIK();  
    (*env)->ReleaseStringUTFChars(env, jtitle, titleChars);  
    (*env)->ReleaseStringUTFChars(env, jcontent, contentChars);  
}

Java Class 2 (DalvikAlertDialogService)

package com.gluonhq.helloandroid;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.app.Activity;

public class DalvikAlertDialogService {

    private final Activity activity;

    public DalvikAlertDialogService(Activity activity) {
        this.activity = activity;
    }

    public void showSaveAlert3(String title, String content, Runnable r) {
        activity.runOnUiThread(() -> {
            if (!activity.isFinishing()) {
                AlertDialog.Builder dialog = new AlertDialog.Builder(activity);
                dialog.setCancelable(false);
                dialog.setTitle(title);
                dialog.setMessage(content);
                dialog.setPositiveButton("Ok", (DialogInterface dialog1, int id) -> {
                    dialog1.dismiss();
                    r.run();
                });

                AlertDialog alert = dialog.create();
                alert.show();
            }
        });
    }
}

What I've Tried

  1. Using NewGlobalRef to create a persistent reference to the Runnable object before invoking its run method.
  2. Checking the validity of the Dalvik environment (dalvikEnv).
  3. Exploring the Gluon Attach source code for existing plugins such as "DisplayService" and "StorageService." Unfortunately, those examples primarily focus on passing strings, primitives, or arrays, not complex Java objects like Runnable.

Problem Statement

Despite these attempts, I am unable to invoke the Runnable's run() method. The jobject reference seems to become invalid when used in the JNI context, resulting in the error mentioned above.


Questions

  1. What is the correct way to pass a Java Runnable object to JNI and call its run() method in this context?
  2. Are there any specific steps for handling Runnable objects in Gluon Attach that I might be missing?
  3. Could the issue be related to improper reference management (NewGlobalRef, Detach, etc.) in the Dalvik environment?

Upvotes: 0

Views: 64

Answers (1)

CTO - Abid Maqbool
CTO - Abid Maqbool

Reputation: 165

The problem facing stems from the dual JVM setup used in the GluonFX tool, which runs both GraalVM (for JavaFX) and the Android/Java JVM (for native Android interactions). Unfortunately, this architectural separation imposes strict limitations on how objects can be shared between the two JVMs.


Key Points:

  1. Dual JVM Limitation:
    • Each JVM manages its own memory and object lifecycle.
    • Passing complex Java objects like Runnable between these JVMs is not supported directly due to incompatibility in memory and reference handling.
  2. Why Simple Types Work:
    • Strings, primitives, or arrays can be passed because they are either converted into native representations or copied across JVMs.
    • Object references (like jobject) are invalid outside their owning JVM's memory space.
  3. Runnable Invocation Challenges:
    • Attempt to pass a Runnable and invoke its run method fails because the Runnable's jobject is not valid in the second JVM.
    • Using NewGlobalRef doesn’t resolve this since the reference is still bound to the originating JVM.

Solution:

1. Avoid Passing Complex Objects Directly:

Instead of passing a Runnable, pass simple callbacks encoded as primitive types, such as an integer representing an action or state.

4. Revisit Existing Gluon Attach Examples:

  • Study plugins like StorageService and DisplayService, which effectively use primitives or simple arrays for cross-VM data exchange.
  • Adapt their patterns to your plugin.

Conclusion:

The GluonFX framework's reliance on two JVMs inherently limits the ability to directly pass complex Java objects like Runnable. Instead, use primitive types or lightweight serialized formats to bridge the gap.

I have manged it by simply returning the boolean and run Runnable code there!

AndroidAlertDialogService.java

@Override
    public boolean showConfirmationAlert(String title, String content, Runnable r) {
        boolean result = showConfirmationAlert1(title, content);
        if (result) r.run();

        return result;
    }

alertdialog.c

JNIEXPORT jboolean JNICALL Java_com_gluonhq_attachextended_alertdialog_impl_AndroidAlertDialogService_showSaveAlert3
(JNIEnv *env, jclass jClass, jstring jtitle, jstring jcontent) {
    const char *titleChars = (*env)->GetStringUTFChars(env, jtitle, NULL);
    const char *contentChars = (*env)->GetStringUTFChars(env, jcontent, NULL);

    ATTACH_DALVIK();
    jstring jTitleString = (*dalvikEnv)->NewStringUTF(dalvikEnv, titleChars);
    jstring jContentString = (*dalvikEnv)->NewStringUTF(dalvikEnv, contentChars);

    jboolean result = (*dalvikEnv)->CallBooleanMethod(dalvikEnv, jDalvikAlertDialogService, jAlertDialogServiceShowSaveAlert3Method, jTitleString, jContentString);
    DETACH_DALVIK();

    (*env)->ReleaseStringUTFChars(env, jtitle, titleChars);
    (*env)->ReleaseStringUTFChars(env, jcontent, contentChars);
    
    return result;
}

DalvikAlertDialogService.java

public boolean showSaveAlert3(String title, String content) {
        AtomicReference<Boolean> isOk = new AtomicReference<>(false);
        CountDownLatch latch = new CountDownLatch(1);

        activity.runOnUiThread(() -> {
            if (!activity.isFinishing()) {
                AlertDialog.Builder dialog = new AlertDialog.Builder(activity);
                dialog.setCancelable(false);
                dialog.setTitle(title);
                dialog.setMessage(content);
                dialog.setPositiveButton("Ok", (DialogInterface dialog1, int id) -> {
                    dialog1.dismiss();
                    isOk.set(true);
                    latch.countDown();
                });

                AlertDialog alert = dialog.create();
                alert.show();
            } else {
                latch.countDown();
            }
        });

        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        return isOk.get();
    }

Ref by @josé-pereda: https://github.com/gluonhq/attach/issues/264#issuecomment-892984783

Ref by botje:

You have two JVMs that are each managing their own memory. It is simply not possible to use a jobject from the other JVM.

Upvotes: 1

Related Questions