Android JNI - Call function on Android UI thread from C++
Asked Answered
B

4

14

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.

Blowbyblow answered 28/6, 2017 at 16:38 Comment(2)
As far as I see native-to-java call should be issued on some non-main thread? And then you want to post call to main thread and wait until the result becomes ready?Dialectology
Thanks for the questions, I thought about it some more and edited the question with how I'd imagine it to work ideally. Does it make sense?Blowbyblow
D
16

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.

Dialectology answered 28/6, 2017 at 20:34 Comment(7)
That looks very cool, thank you! With main thread you mean the Java-Main-Thread, right? You are now exchanging strings only? Do I miss something? In the text you said one could execute arbitrary code.Blowbyblow
@Blowbyblow Yes I meant java main thread (actually main thread is the same for both java and native code). You may call registration function from Activity.onCreate() e.g. This example doesn't exchanges anything :-). It just sends numbers 100...109 from worker thread to main (UI) one. They may be threated as message IDs. If you need - you can send some serialized objects. The only thing is to design serialization-write-read-deserialization flow.Dialectology
If I understand this correctly, I call someJniFuncThatYouShouldCallOnceOnMainThread() as the first thing I do in android_main(). Then, whenever I send a message via write, the looperCallback get's executed on the same thread as the java ui thread. In other words, JNI code which is supposed to be executed on the java ui thread, can be executed in the looperCallback ?Haemostat
@Viktor Sehr yes, it is essentially what this snippet allows you to do. Strictly saying UI or main thread is the same thing for both native code and Java. At some point it is executing native machine code, at some other - Java bytecode. But nevertheless it is the same thread from kernel's perspective.Dialectology
@Dialectology Hmm so if I understand this correctly, I could as well execute main-thread related stuff in my while(true){...} loop located inside android_main()? Meanwhile, the android_app->onAppCmd etc callbacks are executed on other threads? (I do have some jni-exception which seem to be related to not running jni commands on the java main thread)Haemostat
I am trying to do this in C and I am getting undefined reference to 'ALooper_forThread' I don't know if it is getting android/looper.hUnionize
I'm in the same situation as @ViktorSehr, if someone could clarify that.Leaven
P
3

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).

Pairs answered 28/6, 2017 at 19:46 Comment(5)
why do you want to make your life harder? just call a java callback method (with any your data passed as params) from your c++ background thread and from your java callback post it to he UI thread by calling Handler#post / Handler#sendMessage / whateverPyrrha
My engine is C++, so I want as little Java as I can get because Java resides in the "user's part" of the app, not my engine.Pairs
There is absolutely no need to utilize JNI for interacting with main looper. There is NDK header <android/looper.h> that exposes interface to the native looper. See my answer.Dialectology
Thanks! I think you approach is a lot better. I already implemented it into the Ouzel engine. Thank you!Pairs
I used to use JNI for this job, but I soon found out that cleaning up local JNI references is a nightmare from a C++-owned thread. The answer that doesn't use JNI is a better idea.Teakwood
G
3

Based on @Sergio's answer, here is a simple wrapper NativeHandler which can take function, function object and lambda as argument, try to mimic the behavior of android.os.Handler

class NativeHandler {
public:
    static constexpr auto TAG = "NativeHandler";
    static NativeHandler* forCurrentThread() {
        return new NativeHandler;
    }

    template<typename FUNC, typename... ARGS>
    bool post(FUNC&& func, ARGS&&... args) {
        auto callable = new Callable(func, std::forward<ARGS>(args)...);
        write(_pipeFDS[1], &callable, sizeof(decltype(callable)));
        return true;
    }

    NativeHandler(const NativeHandler&) = delete;
    NativeHandler(NativeHandler&&) = delete;
    NativeHandler& operator=(const NativeHandler&) = delete;
    NativeHandler& operator=(NativeHandler&&) = delete;
    virtual ~NativeHandler() {
        ALooper_removeFd(_looper, _pipeFDS[0]);
        ALooper_release(_looper);
        close(_pipeFDS[0]);
        close(_pipeFDS[1]);
    }

private:
    class Callable {
    public:
        void call() {
            if (_function) _function();
        }

        template<typename FUNC, typename... ARGS>
        Callable(FUNC func, ARGS... args) : _function(std::bind(func, args...)) {}

        Callable() = delete;
        Callable(const Callable&) = delete;
        Callable(Callable&&) = delete;
        Callable operator=(const Callable&) = delete;
        Callable operator=(Callable&&) = delete;
        virtual ~Callable() {}
    private:
        std::function<void()> _function;
    };

    NativeHandler() {
        if (pipe(_pipeFDS) != 0) {
            throw std::bad_alloc();
        }
        _looper = ALooper_forThread();
        ALooper_acquire(_looper);
        if (ALooper_addFd(_looper, _pipeFDS[0], ALOOPER_POLL_CALLBACK,
                          ALOOPER_EVENT_INPUT, _looperCallback, nullptr) == -1) {
            throw std::bad_alloc();
        }
    };

    ALooper* _looper;
    int _pipeFDS[2];
    static int _looperCallback(int fd, int events, void* data) {
        void* buf = new char[sizeof(Callable*)];
        ssize_t nr = read(fd, buf, sizeof(Callable*));
        Callable* callable = *((Callable**)buf);
        __android_log_print(ANDROID_LOG_INFO, "Callable", "read size is %d %p", nr, callable);
        callable->call();
        delete[] buf;
        return 1;
    }
};

And then the use example, hope it might be helpful for anyone who want similar behavior with android java api handler in JNI.

void f(char c, short s) {
    __android_log_print(ANDROID_LOG_DEBUG, NativeHandler::TAG, "%s c = %c, s = %d", __FUNCTION__, c, s);
}

struct Task {
    void operator()(int i, double d) {
        __android_log_print(ANDROID_LOG_DEBUG, NativeHandler::TAG, "Task i = %d, d = %f", i, d);
    }
};

// ...
auto handler = NativeHandler::forCurrentThread();
std::thread worker([handler]() {
    handler->post([](int i, double d, void* p) {
        __android_log_print(ANDROID_LOG_DEBUG, "NativeHandler", "i = %d, d = %f, p = %p", i, d, p);
    }, 100, -123.4, nullptr);

    handler->post(f, 'c', 128);
    handler->post(Task(), 123, 3.1415926);
});
worker.detach();
Gothart answered 29/6, 2019 at 6:18 Comment(3)
Very nice, thanks for posting that code! I have made some adjustments in the _looperCallback. First, I determine the buffer size only once: std::size_t size = sizeof(Callable*);. Instead of allocating a buffer on the heap I just use stack memory: char buffer[size]; read(fd, static_cast<void*>(buffer), size);. There's only an address that is exchanged so stack memory should be fine. Then I replaced the C-style cast: Callable* callable = *(reinterpret_cast<Callable**>(buffer));.Caste
Note also that there is a memory leak: Every call to post allocates memory on the heap that is never freed again. To fix this, add delete callable; at the end of _looperCallback.Caste
This code would be better if you could drop the ancient pipe and use event_fd instead. It's made for this and supported since Android 4Englebert
J
1

Another option is to use the Arcana.cpp C++ library, which includes an Android Looper based "scheduler." In its simplest form, you can use it like this:

#include <arcana/threading/task_schedulers.h>

void SomeFunctionCalledFromUIThread()
{
  // Note: The '64' below is the max size of the callables passed to the scheduler.
  // This is done to reduce allocations and make schedulers more efficient.
  auto looper_scheduler = arcana::looper_scheduler<64>::get_for_current_thread();

  // Get on a background thread to test getting back on the UI thread.
  std::thread worker([looper_scheduler = std::move(looper_scheduler)]() {
    looper_scheduler([]() {
      // Do something on the UI (looper) thread
    });
  });
}

Schedulers are a general construct in Arcana.cpp, and are also used in the low overhead cross platform async task system, so if you opt to use that then you can do typical async task programming with this scheduler:

#include <arcana/threading/task_schedulers.h>
#include <arcana/threading/task.h>
#include <arcana/threading/dispatcher.h>

// Schedulers need to outlive task chains, so imagine m_looper_scheduler and m_background_dispatcher are created and stored from some constructor.
// "Dispatchers" in Arcana.cpp are a class of schedulers that own their own work queue.

arcana::task<void, std::exception_ptr> SomeFunctionCalledFromUIThread() {
  return arcana::make_task(m_background_dispatcher, arcana::cancellation::none(), []() {
    // Do something on a background thread (via background_dispatcher).
  }).then(m_looper_scheduler, arcana::cancellation::none(), []() {
    // Do something on the UI thread (via looper_scheduler).
  });
}

If you want to get more adventurous, C++ coroutines can be used with tasks, or directly with schedulers:

#include <arcana/threading/task_schedulers.h>
#include <arcana/threading/task.h>
#include <arcana/threading/dispatcher.h>
#include <arcana/threading/coroutine.h>

arcana::task<void, std::exception_ptr> SomeFunctionCalledFromUIThread() { 
  auto looper_scheduler = arcana::looper_scheduler<64>::get_for_current_thread();
  arcana::background_dispatcher<64> background_dispatcher;

  // Code executing here is on the UI thread (since the function is called from the UI thread).

  co_await arcana::switch_to(background_dispatcher);

  // Code executing here is on a background thread.

  co_await arcana::switch_to(looper_scheduler);

  // Code executing here is back on the UI thread.
}

You can read more about Arcana.cpp schedulers, tasks, and coroutines here: https://github.com/microsoft/arcana.cpp/blob/master/Source/Arcana.Tasks.md

Joinder answered 6/4, 2020 at 21:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.