Reputation: 165
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.
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
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);
}
#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);
}
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();
}
});
}
}
NewGlobalRef
to create a persistent reference to the Runnable
object before invoking its run
method.dalvikEnv
).Runnable
.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.
Runnable
object to JNI and call its run()
method in this context?Runnable
objects in Gluon Attach that I might be missing?NewGlobalRef
, Detach
, etc.) in the Dalvik environment?Upvotes: 0
Views: 64
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.
Runnable
between these JVMs is not supported directly due to incompatibility in memory and reference handling.jobject
) are invalid outside their owning JVM's memory space.Runnable
and invoke its run
method fails because the Runnable
's jobject
is not valid in the second JVM.NewGlobalRef
doesn’t resolve this since the reference is still bound to the originating JVM.Instead of passing a Runnable
, pass simple callbacks encoded as primitive types, such as an integer representing an action or state.
StorageService
and DisplayService
, which effectively use primitives or simple arrays for cross-VM data exchange.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