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!