Search code examples
javacallbackjava-native-interface

JNI Callbacks failed to invoke


I'm currently writing a JNI wrapper for the Tox chat library. Many things are handled through callbacks (like receiving messages, friend requests and such). In order to handle these, I already implemented something that worked (until now, and I can't figure out why it stopped working):

In the main Class, I define a method that allows setting a callback like this:

    /**
 * Native call to tox_callback_friendrequest
 * 
 * @param messengerPointer
 *            pointer to the internal messenger struct
 * @param callback
 *            the callback to set for receiving friend requests
 */
private native void tox_onfriendrequest(long messengerPointer,
        OnFriendRequestCallback callback);

/**
 * Method used to set a callback method for receiving friend requests. Any
 * time a friend request is received on this Tox instance, the
 * {@link OnFriendRequestCallback#execute(String, String)} method will be
 * executed.
 * 
 * @param callback
 *            the callback to set for receiving friend requests
 * @throws ToxException
 *             if the instance has been killed
 */
public void setOnFriendRequestCallback(OnFriendRequestCallback callback)
        throws ToxException {
    lock.lock();
    try {
        checkPointer();

        tox_onfriendrequest(this.messengerPointer, callback);
    } finally {
        lock.unlock();
    }
}

The public java method simply calls the following native Code:


JNIEXPORT void JNICALL Java_im_tox_jtoxcore_JTox_tox_1onfriendrequest(
        JNIEnv * env, jobject obj, jlong messenger, jobject callback) {
    tox_jni_globals_t *_messenger = (tox_jni_globals_t *) messenger;
    if (_messenger->frqc) {
        if (_messenger->frqc->jobj) {
            (*env)->DeleteGlobalRef(env, _messenger->frqc->jobj);
        }
        free(_messenger->frqc);
    }

    friendrequest_callback_t *data = malloc(sizeof(friendrequest_callback_t));
    data->env = env;
    data->jobj = (*env)->NewGlobalRef(env, callback);
    (*env)->DeleteLocalRef(env, callback);
    _messenger->frqc = data;
    tox_callback_friendrequest(_messenger->tox, (void *) callback_friendrequest,
            data);
}

static void callback_friendrequest(uint8_t *pubkey, uint8_t *message,
        uint16_t length, void *ptr) {
    friendrequest_callback_t *data = ptr;

    jclass clazz = (*data->env)->GetObjectClass(data->env, data->jobj);
    jmethodID meth = (*data->env)->GetMethodID(data->env, clazz, "execute",
            "(Ljava/lang/String;[B)V");

    char buf[ADDR_SIZE_HEX] = { 0 };
    addr_to_hex(pubkey, buf);
    jstring _pubkey = (*data->env)->NewStringUTF(data->env, buf);
    jbyteArray _message = (*data->env)->NewByteArray(data->env, length);
    (*data->env)->SetByteArrayRegion(data->env, _message, 0, length, message);

    printf("definitely happening"); //printf debugging. This IS being printed.
    fflush(stdout);
    (*data->env)->CallVoidMethod(data->env, data->jobj, meth, _pubkey, message); //this does not seem to be called
}

This code IS rather complicated, but here's what it does: First, it checks if there already IS a callback set for this action, and if so, removes it. Then it creates a new Global Reference to the Callback object that it received, and saves that reference as well as the JNIEnv-pointer to the messenger struct. In the end, it invokes the callback method supplied by the Tox-API, and registers the callback static void callback_friendrequest

In the opaque void pointer ptr, we store the JNIEnv, and the Callback object. The callback Class looks like this:

package im.tox.jtoxcore.test;

import im.tox.jtoxcore.JTox;
import im.tox.jtoxcore.ToxException;
import im.tox.jtoxcore.callbacks.OnFriendRequestCallback;

public class TestOnFriendRequestCallback extends OnFriendRequestCallback {

    public TestOnFriendRequestCallback(JTox jtox) {
        super(jtox);
    }

    @Override
    public void execute(String publicKey, byte[] message) {
        String msg;
        if (message == null) {
            msg = "No mesage";
        } else {
            try {
                msg = JTox.getByteString(message);
            } catch (ToxException e) {
                msg = "ERROR: Unable to get message";
            }
        }
        System.out.println("We received a friend request from " + publicKey
                + " with the following message: " + msg);

        try {
            jtox.confirmRequest(publicKey);
            System.out.println("confirmed!");
        } catch (ToxException e) {
            System.out.println("Unable to confirm friend request");
        }
    }
}

Now, when I send a friend request via another client, after registering the callback, it prints "definitely happening", which is a debug statement I introduced. The CallVoidMetod call is NOT executed, as it is definitely neither printing that I received a FriendRequest, nor that it was confirmed.

To me, this means, that the callback was registered successfully, however for some reason, the Java method is never invoked.

The full code is available on GitHub: https://github.com/Tox/jToxcore


Solution

  • I suspect that the reason that this is failing is because you are storing the Java env pointer within your object named data. The env pointer cannot be cached this way, and is probably invalid when the callback in being invoked. For some more info on how to deal with this, see this question.