0

I'm trying to use the JNI in an Android app. My code correctly compiles and the application launches, however when I want to call a Java method (from the calling class) in my C++ code, the app promptly crashes saying it cannot find the method.

You will have to excuse the code, it is a bit messy because I've tried so many things, it is also mixed with React-Native stuff, but the JNI crash has nothing to do with it.

The java module:

package com.jsirnwalletcore;

import androidx.annotation.NonNull;
import androidx.annotation.Keep;
import android.util.Log;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Base64;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

@Keep
class WalletCoreModule extends ReactContextBaseJavaModule {
  public static final String NAME = "WalletCore";
  private static native void initialize(long jsiPtr);
  private SharedPreferences prefs;

  public WalletCoreModule(ReactApplicationContext context) {
    super(context);
    
    this.prefs = context.getSharedPreferences("WALLETCORE", Context.MODE_PRIVATE);
  }

  @NonNull
  @Override
  public String getName() {
    return NAME;
  }

  public String getSeed() {
    return "HARDCODED SEED";
  }

  @ReactMethod(isBlockingSynchronousMethod = true)
  public boolean install() {
    try {
      String seed = getSeed();
      Log.w(NAME, "ROPO CALLING GET SEED: " + seed);

      System.loadLibrary("jsi-rn-wallet-core");

      ReactApplicationContext context = getReactApplicationContext();

      initialize(context.getJavaScriptContextHolder().get());
      return true;
    } catch(Exception exception) {
      return false;
    }
  }
}

The cpp-adapter.cpp:

#include <jni.h>
#include "installer.hpp"
#include "logs.h"
#include <string>
#include "pthread.h"
#include <sys/types.h>
#include <jsi/jsi.h>

JavaVM *java_vm;
jclass java_class;
jobject java_object;

/**
 * A simple callback function that allows us to detach current JNI Environment
 * when the thread
 * See https://stackoverflow.com/a/30026231 for detailed explanation
 */

void DeferThreadDetach(JNIEnv *env)
{
    static pthread_key_t thread_key;

    // Set up a Thread Specific Data key, and a callback that
    // will be executed when a thread is destroyed.
    // This is only done once, across all threads, and the value
    // associated with the key for any given thread will initially
    // be NULL.
    static auto run_once = []
    {
        const auto err = pthread_key_create(&thread_key, [](void *ts_env)
                                            {
            if (ts_env) {
                java_vm->DetachCurrentThread();
            } });
        if (err)
        {
            // Failed to create TSD key. Throw an exception if you want to.
        }
        return 0;
    }();

    // For the callback to actually be executed when a thread exits
    // we need to associate a non-NULL value with the key on that thread.
    // We can use the JNIEnv* as that value.
    const auto ts_env = pthread_getspecific(thread_key);
    if (!ts_env)
    {
        if (pthread_setspecific(thread_key, env))
        {
            // Failed to set thread-specific value for key. Throw an exception if you want to.
        }
    }
}

/**
 * Get a JNIEnv* valid for this thread, regardless of whether
 * we're on a native thread or a Java thread.
 * If the calling thread is not currently attached to the JVM
 * it will be attached, and then automatically detached when the
 * thread is destroyed.
 *
 * See https://stackoverflow.com/a/30026231 for detailed explanation
 */
JNIEnv *GetJniEnv()
{
    JNIEnv *env = nullptr;
    // We still call GetEnv first to detect if the thread already
    // is attached. This is done to avoid setting up a DetachCurrentThread
    // call on a Java thread.

    // g_vm is a global.
    auto get_env_result = java_vm->GetEnv((void **)&env, JNI_VERSION_1_6);
    if (get_env_result == JNI_EDETACHED)
    {
        if (java_vm->AttachCurrentThread(&env, NULL) == JNI_OK)
        {
            DeferThreadDetach(env);
        }
        else
        {
            // Failed to attach thread. Throw an exception if you want to.
        }
    }
    else if (get_env_result == JNI_EVERSION)
    {
        // Unsupported JNI version. Throw an exception if you want to.
    }
    return env;
}

void tempInstall(jsi::Runtime &rt)
{
    auto testFn = jsi::Function::createFromHostFunction(
        rt,
        jsi::PropNameID::forAscii(rt, "testFn"),
        0,
        [](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value
        {
            JNIEnv *jniEnv = GetJniEnv();

            java_class = jniEnv->GetObjectClass(java_object);
            jmethodID get = jniEnv->GetMethodID(java_class, "getSeed", "()Ljava/lang/String;");
            // jobject result = jniEnv->CallObjectMethod(java_object, get);
            // const char *str = jniEnv->GetStringUTFChars((jstring)result, NULL);
            // LOGW("ROPO GOT: %s", str);
            return {};
        });

    rt.global().setProperty(rt, "testFn", std::move(testFn));
}

extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved)
{
    java_vm = jvm;

    LOGW("ROPO JNI_OnLoad CALLED");

    return JNI_VERSION_1_6;
}

extern "C" JNIEXPORT void JNICALL
Java_com_jsirnwalletcore_WalletCoreModule_initialize(JNIEnv *env, jobject thiz, jlong jsi)
{
    auto rt = reinterpret_cast<jsi::Runtime *>(jsi);
    // jclass clazz = env->GetObjectClass(thiz);

    // jmethodID getNameMID = env->GetMethodID(clazz, "getName", "()Ljava/lang/String;");
    // jstring jClassName = (jstring)env->CallObjectMethod(thiz, getNameMID);
    // std::string classString = jstringToString(env, jClassName);
    // LOGW("###########ROPO############");
    // LOGW("RETRIEVED STRING: %s", classString.c_str());
    // LOGW("###########ROPO############");

    // jmethodID retrieveMethodID = env->GetMethodID(clazz, "getSeed", "()Ljava/lang/String;");
    // if (retrieveMethodID == NULL)
    // {
    //     LOGW("ROPO methodID not found!");
    // }
    // else
    // {
    //     jstring myString = (jstring)env->CallObjectMethod(thiz, retrieveMethodID);
    //     std::string retrievedString = jstringToString(env, myString);
    //     LOGW("###########ROPO############");
    //     LOGW("RETRIEVED STRING: %s", retrievedString.c_str());
    //     LOGW("###########ROPO############");
    // }

    install(*rt);
    tempInstall(*rt);

    // env->GetJavaVM(&java_vm);
    java_object = env->NewGlobalRef(thiz);
}

In my app, I call the testFn, basically when it reaches the line that calls env->getMethodID, there is an error saying the method is not found and then the app promptly crashes.

04-10 04:04:11.707 22870 22916 F com.example: java_vm_ext.cc:579] JNI DETECTED ERROR IN APPLICATION: JNI NewObjectV called with pending exception java.lang.NoSuchMethodError: no non-static method "Ljava/lang/Class;.getSeed()Ljava/lang/String;"

Any idea what might be wrong?

Oscar Franco
  • 5,691
  • 5
  • 34
  • 56
  • I'm no expert in React but did you initialize the JNI Library by calling your `install()` method anywhere? I see you have added `System.loadLibrary("jsi-rn-wallet-core");` in that method. – Darshan Apr 10 '22 at 06:07
  • yes, as stated in the question, the testFn is bound and called correctly, that's why I get the error message... – Oscar Franco Apr 10 '22 at 06:17
  • Can you try moving the `java_object = env->NewGlobalRef(thiz);` before `tempInstall(*rt)`? I'm not sure but either the `java_object` is clearly uninitialized when `tempInstall()` is called or complete class name is not formed when calling `getSeed` – Darshan Apr 10 '22 at 06:23
  • get seed is called much later, via a button in the UI. But I've moved it anyways, makes no difference. – Oscar Franco Apr 10 '22 at 06:29
  • 1
    `initialize` is a static method so `thiz` is not a handle to `WalletCoreModule` object instance so `GetObjectClass` returns the wrong class and so on. – user7860670 Apr 10 '22 at 07:02
  • Oh jesus.. you are right @user7860670... thanks a lot! – Oscar Franco Apr 10 '22 at 07:06

1 Answers1

0

As pointed out by @user7860670 in the comments, the problem was that I declared the native method as static.

private static native void initialize(long jsiPtr);

Whereas it should be an instance method:

private native void initialize(long jsiPtr);

Even if the correct class name was being returned, the static nature of it caused it to crash.

Big oof that the JNI throws no real helpful hint.

Oscar Franco
  • 5,691
  • 5
  • 34
  • 56
  • *Big oof that the JNI throws no real helpful hint.* Absolutely not true. You didn't bother to check the return value from `GetObjectClass()` nor did you ever check if any of your JNI calls resulted in a Java exception. – Andrew Henle Apr 10 '22 at 14:16
  • I checked A LOT of things, I'm just not well versed in things Java/JNI. But in any case, it was my mistake. – Oscar Franco Apr 10 '22 at 18:05