Android JNI - Call function on Android UI thread from C ++

Our game engine Cocos2d-x

initially runs on android in its own non-Java-UI-thread

. We need to call certain Java functions from C++

through JNI

to Android UI thread

.

To call JNI-Functions

we use 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 would like all of these calls to occur in Android UI thread

and passed std::function

as a parameter that is called with a return value in Cocos2d-x-thread

after the function call completes.

An ideal way to call a 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 it should be easier for them to just call them on a Java thread.

+6


source to share


3 answers


As @Elviss pointed out - in order to post your code to the main thread, you must use Looper

. In fact, it can be done without additional interaction with JNI and creating a custom one java.lang.Runnable

and publishing it through the complex JNI stuff.

The Android NDK offers an extremely lightweight and efficient way to publish your own code in a custom looper. The key point is that you must provide an arbitrary file descriptor to the looper and specify which file events you are interested in (input, output, etc.). Under the hood, the looper will poll for that file descriptor, and as soon as an event becomes available, it will execute your callback on the correct thread.

There is a minimal example (no errors and breaks):

#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 the following 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 can see from the pid-tid pairs - messages are received on the main stream. And, of course, you can send something more complex than single-byte messages.

+11


source


To run C ++ code on an Android interface (main) thread, you will need to use the Android 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 the android.app.Activity instance that is passed to JNI from Java, but you can also just use the static getMainLooper method of the Looper class. Then you need to instantiate the 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);

      

Remember that you need to save the jobject so you can use it later. You will need to write some 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 ++ like this:



extern "C" JNIEXPORT void JNICALL 
Java_my_package_Runner_run(JNIEnv*, jclass)
{
    // here goes your native code
}

      

Now you need 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");

      

Save runnerClass (jclass) and runnerConstructor (jmethodID) somewhere for later use. The last thing you need to do is create an instance of the Runner class and send it to the handler:

jobject runner = jniEnv->NewObject(runnerClass, runnerConstructor);

if (!jniEnv->CallBooleanMethod(handler, postMethod, runner))
{
    // something wrong happened
}

      

What I am doing in Ouzel code is I create a std :: function queue and guard it with a mutex. Whenever I need to execute a std :: function on an Android UI thread, I add a std :: function instance to the queue and dequeue it and execute it in its own method (Java_my_package_Runner_run).

This is the closest thing to writing Java code (you need to write 6 lines to implement the Runnable interface).

+3


source


Building on @ Sergio's answer, here is a simple wrapper NativeHandler

that can take a function, a function object, and a lambada as an argument, trying to mimic the behaviorandroid.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 a usage example, hopefully this might be helpful for anyone wanting 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();

      

0


source







All Articles