0

In my kotlin app, I'm working with the SuperpoweredAdvancedAudio SDK playing some music. Superpowered is a C++ library that's used via JNI.

As part of the code, I need to call from my C++ code back into Kotlin.

When playing songs, there's an event loop which must be called periodically (I'm calling every 100ms) to check for events. I setup this via looping using an ALooper attached to the main thread that is triggered by a std::thread worker that loops every 100ms. (mainly cribbed from https://stackoverflow.com/a/44812074/1204250)

I can call Kotlin from C++ if it's from within a Kotlin -> JNI C++ -> Kotlin flow, however when running inside the looper, the error:

JNI DETECTED ERROR IN APPLICATION: JNI GetStaticFieldID called with pending exception java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available` occurs.

As an example, if I do the Kotlin -> JNI C++ -> Kotlin flow, this works:

Kotlin "MusicPlayer.kt":

package com.example;

object MusicPlayer {
    fun startup() {

        // bunch of initialization code...

        initAudio()      
    }

    // JNI external fun
    private external fun initAudio();

    // callback that is called from C++
    fun trackFinishedCallback(trackId: String) {
        Timber.d("kotlin got callback trackFinished with id: $trackId")
    }
}

C++ JNI "InternalPlayer.cpp":

#include <jni.h>
#include <string>
#include <android/log.h>
#include <thread>

extern "C" JNIEXPORT void
Java_com_example_MusicPlayer_initAudio(
        JNIEnv *env,
        jobject kotlinMusicPlayer) {

    jclass clazz = env->FindClass("com/example/MusicPlayer");
    jmethodID methodId = env->GetMethodID(clazz, "trackFinishedCallback", "(Ljava/lang/String;)V");
    jstring result;
    result = (*env).NewStringUTF("Foo bar");
    env->CallVoidMethod(kotlinMusicPlayer, methodId, result);
}

Running this code works and I get a kotlin got callback trackFinished with id: Foo bar message in the log.

However, replacing the C++ code with work when calling the Kotlin code from within hte context of the original JNI call, but will fail when inside the loop.

The main error is:

A/example_androi: java_vm_ext.cc:570] JNI DETECTED ERROR IN APPLICATION: JNI GetStaticFieldID called with pending exception java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available

A full log of the errors can be found here: https://pastebin.com/PKGeBjXK

C++ JNI "InternalPlayer.cpp":

#include <jni.h>
#include <string>
#include <android/log.h>
#include <thread>
#include <chrono>
#include <android/looper.h>
#include <unistd.h>
#include <sstream>

NativeMusicPlayer* nativeMusicPlayer;

extern "C" JNIEXPORT void
Java_com_example_MusicPlayer_initAudio(
        JNIEnv *env,
        jobject kotlinMusicPlayer) {

    nativeMusicPlayer = new NativeMusicPlayer();
}

class NativeMusicPlayer {
private:
    JNIEnv *jniEnv;
    jobject kotlinMusicPlayer;
    jclass clazz;
    jmethodID trackFinishedCallbackMethodId;

public: 
    ALooper* mainThreadLooper;
    int messagePipe[2];

    NativeMusicPlayer(JNIEnv *jniEnv, jobject kotlinMusicPlayer) {
        this->jniEnv = jniEnv;
        this->kotlinMusicPlayer = kotlinMusicPlayer;
        this->clazz = this->jniEnv->FindClass("com/example/MusicPlayer");
        this->trackFinishedCallbackMethodId = this->jniEnv->GetMethodID(clazz, "trackFinishedCallback", "(Ljava/lang/String;)V");

        // This will work, so calling from when still in the context of the original JNI call works. 
        // calling from within the loop below doesn't work.
        jstring currentTrackIdJString = (this->jniEnv)->NewStringUTF("Foo Bar");
        this->jniEnv->CallVoidMethod(this->kotlinMusicPlayer, this->trackFinishedCallbackMethodId, currentTrackIdJString);

        // setup a timer to call on the main thread, 
        mainThreadLooper = ALooper_forThread();
        ALooper_acquire(mainThreadLooper);
        pipe(messagePipe);
        ALooper_addFd(mainThreadLooper, messagePipe[0], 0, ALOOPER_EVENT_INPUT, processEventsCallback, this);

        std::thread worker([this]() {
            while (true) {
                char triggerMessage = 0;
                write(messagePipe[1], &triggerMessage, 1);
                std::this_thread::sleep_for(std::chrono::milliseconds(100));

            }
        });
        worker.detach();
    }

    static int processEventsCallback(int fd, int events, void* data) {
        auto *player = (NativeMusicPlayer *)data;
        char msg;
        read(fd, &msg, 1);
        player->processEvents();
        return 1; // continue listening for events
    }

    void processEvents() {
        // process events code. From here we'll try to call Kotlin via JNI.  
        // It doesn't work.
        if (track_is_finished) {
            // This line works...
            jstring currentTrackIdJString = (this->jniEnv)->NewStringUTF("Foo Bar");
            // This line causes the crash.
            this->jniEnv->CallVoidMethod(this->kotlinMusicPlayer, this->trackFinishedCallbackMethodId, currentTrackIdJString);
        }
    }
}

I think it might have to do with losing some context when leaving the initial JNI call, so one of the objects I'm storing in the NativeMusicPlayer is invalid. Initially I thought it was the JNIEnv *jniEnv, however using some judicious logging (since breakpoints in JNI don't seem to work in Android Studio BumbleBee) I the failusre is on the CallVoidMethod call, not the prior NewStringUTF call that uses the jniEnv.

Any ideas? It'd be fine to do this another way as long as I can call the Kotlin callback from within looping code attached to the main thread.

Thanks!

SuperDuperTango
  • 1,398
  • 1
  • 14
  • 32
  • 2
    It **is** the fact you're caching the `JNIEnv` value - you can **not** do that. See https://stackoverflow.com/questions/23195798/cache-jni-environment-in-a-multithreading-application among quite a few others. – Andrew Henle Apr 01 '22 at 00:27

1 Answers1

0

You can cache JNIEnv if are going to use it only on the same thread and you are sure that this thread is never detached from JVM. But in your case, it looks like you use JNIEnv on the main thread only and it's the correct way of doing it.

According to the error log, one of JNI calls throws an exception. And most likely it's the FindClass call. Though it's really weird for it to throw such an exception because you are clearly calling your native code from a thread that should have a dex class loader and not the bootstrap one.

So I would do the following:

  • Replace jniEnv->FindClass("com/example/MusicPlayer") with jniEnv->GetObjectClass(kotlinMusicPlayer) to avoid searching for a class and just get it directly instead
  • Check for jniEnv->ExceptionOccurred() after each JNI call. You can use such macro for convenience:
    #define JNI_CALL(call) \
        call; \
        if (jniEnv->ExceptionOccurred()) { \
            jniEnv->ExceptionDescribe(); \
            jniEnv->ExceptionClear(); \
            throw std::runtime_error("JNI call \"" #call "\" failed"); \
        }

And call it as JNI_CALL(jniEnv->CallVoidMethod(...))