Our game engine Cocos2d-x
runs natively on android on its own non-Java-UI-thread
. We need to call certain Java functions from C++
via JNI
on the Android UI thread
.
For calling JNI-Functions
, we're using the JNIHelper.h/cpp from here (GitHub):
JniHelper.h, JniHelper.cpp
For example this C++ code:
auto retVal = JniHelper::callStaticStringMethod("org/utils/Facebook",
"getFacebookTokenString");
Ideally, we'd like to have all these calls happen on the Android UI thread
and pass an std::function
as a parameter which is called with the return value on the Cocos2d-x-thread
again once the function call is done.
Ideal way to call the function:
auto retVal = JniHelper::callStaticStringMethod("org/utils/Facebook",
"getFacebookTokenString", [=](std::string retVal) {
printf("This is the retval on the C++ caller thread again: %s", retVal.c_str());
});
But there are also many calls without any return value, so for those it should be easier to just call them on the java thread.
Using Android Studio 2.2 and higher, you can use the NDK to compile C and C++ code into a native library and package it into your APK using Gradle, the IDE's integrated build system. Your Java code can then call functions in your native library through the Java Native Interface (JNI) framework.
It defines a way for the bytecode that Android compiles from managed code (written in the Java or Kotlin programming languages) to interact with native code (written in C/C++). JNI is vendor-neutral, has support for loading code from dynamic shared libraries, and while cumbersome at times is reasonably efficient.
JNIEXPORT is used to make native functions appear in the dynamic table of the built binary (*. so file). They can be set to "hidden" or "default" (more info here). If these functions are not in the dynamic table, JNI will not be able to find the functions to call them so the RegisterNatives call will fail at runtime.
As @Elviss has mentioned - to post your code to main thread you should use Looper
. Actually this may be done without extra coping with JNI and creating of custom java.lang.Runnable
and posting it via complicated JNI stuff.
Android NDK offers extremely lightweight and efficient way to post your native code to the arbitrary looper. The key point is that you should provide arbitrary file descriptor to the looper and specify what file events you are interested in (input, output, so on). Under the hood looper will poll that file descriptor and once event becomes available - it runs your callback on proper thread.
There is the minimal example (no error checks and teardowns):
#include <android/looper.h>
#include <unistd.h>
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "sergik", __VA_ARGS__)
static ALooper* mainThreadLooper;
static int messagePipe[2];
static int looperCallback(int fd, int events, void* data);
void someJniFuncThatYouShouldCallOnceOnMainThread() {
mainThreadLooper = ALooper_forThread(); // get looper for this thread
ALooper_acquire(mainThreadLooper); // add reference to keep object alive
pipe(messagePipe); //create send-receive pipe
// listen for pipe read end, if there is something to read
// - notify via provided callback on main thread
ALooper_addFd(mainThreadLooper, messagePipe[0],
0, ALOOPER_EVENT_INPUT, looperCallback, nullptr);
LOGI("fd is registered");
// send few messages from arbitrary thread
std::thread worker([]() {
for(char msg = 100; msg < 110; msg++) {
LOGI("send message #%d", msg);
write(messagePipe[1], &msg, 1);
sleep(1);
}
});
worker.detach();
}
// this will be called on main thread
static int looperCallback(int fd, int events, void* data) {
char msg;
read(fd, &msg, 1); // read message from pipe
LOGI("got message #%d", msg);
return 1; // continue listening for events
}
This code produces next output:
06-28 23:28:27.076 30930-30930/? I/sergik: fd is registered
06-28 23:28:27.076 30930-30945/? I/sergik: send message #100
06-28 23:28:27.089 30930-30930/? I/sergik: got message #100
06-28 23:28:28.077 30930-30945/? I/sergik: send message #101
06-28 23:28:28.077 30930-30930/? I/sergik: got message #101
06-28 23:28:29.077 30930-30945/? I/sergik: send message #102
06-28 23:28:29.078 30930-30930/? I/sergik: got message #102
06-28 23:28:30.078 30930-30945/? I/sergik: send message #103
06-28 23:28:30.078 30930-30930/? I/sergik: got message #103
06-28 23:28:31.079 30930-30945/? I/sergik: send message #104
06-28 23:28:31.079 30930-30930/? I/sergik: got message #104
06-28 23:28:32.079 30930-30945/? I/sergik: send message #105
06-28 23:28:32.080 30930-30930/? I/sergik: got message #105
06-28 23:28:33.080 30930-30945/? I/sergik: send message #106
06-28 23:28:33.080 30930-30930/? I/sergik: got message #106
06-28 23:28:34.081 30930-30945/? I/sergik: send message #107
06-28 23:28:34.081 30930-30930/? I/sergik: got message #107
06-28 23:28:35.081 30930-30945/? I/sergik: send message #108
06-28 23:28:35.082 30930-30930/? I/sergik: got message #108
06-28 23:28:36.082 30930-30945/? I/sergik: send message #109
06-28 23:28:36.083 30930-30930/? I/sergik: got message #109
As you see from pid-tid pairs - messages are received on main thread. And of course you may send something more complicated than one-byte messages.
To run C++ code on Android UI (main) thread, you will have to use Android the looper (activity.getMainLooper() or Looper.getMainLooper() in Java):
jmethodID getMainLooperMethod = jniEnv->GetMethodID(mainActivityClass, "getMainLooper", "()Landroid/os/Looper;");
jobject mainLooper = jniEnv->CallObjectMethod(mainActivity, getMainLooperMethod);
"mainActivity" is an instance of android.app.Activity, that is passed to the JNI from Java, but you can also simply use the static getMainLooper method of the Looper class. Next you have to create an instance of Handler class (new Handler(mainLooper in Java):
jclass handlerClass = jniEnv->FindClass("android/os/Handler");
jmethodID handlerConstructor = jniEnv->GetMethodID(handlerClass, "<init>", "(Landroid/os/Looper;)V");
postMethod = jniEnv->GetMethodID(handlerClass, "post", "(Ljava/lang/Runnable;)Z");
handler = jniEnv->NewObject(handlerClass, handlerConstructor, mainLooper);
handler = jniEnv->NewGlobalRef(handler);
Be aware that you have to store the handler (jobject) to use it later. You will have to write a bit of Java to implement the Runnable interface, so this code goes in Java:
package my.package;
import java.lang.Runnable;
public class Runner implements Runnable
{
native public void run();
}
As you can see the run() method is native, so we can implement it in C++ as follows:
extern "C" JNIEXPORT void JNICALL
Java_my_package_Runner_run(JNIEnv*, jclass)
{
// here goes your native code
}
Now you have to get the Runner class and its constructor in C++:
runnerClass = jniEnv->FindClass("org/ouzelengine/Runner");
runnerClass = static_cast<jclass>(jniEnv->NewGlobalRef(runnerClass));
runnerConstructor = jniEnv->GetMethodID(runnerClass, "<init>", "()V");
Store the runnerClass (jclass) and runnerConstructor (jmethodID) somewhere for later use. The final thing you have to do is actually create the instance of the Runner class and post it to the handler:
jobject runner = jniEnv->NewObject(runnerClass, runnerConstructor);
if (!jniEnv->CallBooleanMethod(handler, postMethod, runner))
{
// something wrong happened
}
What I do in the Ouzel engines code is I create a queue of std::function's and guard it with a mutex. Whenever I need to execute a std::function on Android UI thread, I add the std::function instance to the queue and pop it from the queue and execute it in the native method (Java_my_package_Runner_run).
This is the closest you can get to writing no Java code (you will have to write 6 lines of it to implement the Runnable interface).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With