3

I have an Android app, and need to use a C library. I am using JNI to interface with it. That library uses a struct (lets call it foo). foo works with an initial set of parameters, and among them pointers to C functions that it uses to request more data from my app, incorporating that data into the computational process. Once it has everything it needs, it returns a result via a C callback function, to which it also needs a pointer. I need to hook all those C callback functions to my app to get more data from user, return that data back into foo and finally display the results to the user in my app through final callback function.

I created foo_callbacks - just defined static C functions that I pass into foo upon initialization, and in those functions I use JNI to call my app again (haven't tested this yet, but I also keep a reference to jvm and get JNIEnv from that and attach to current thread, like this).

But is what's happening:

  1. Call to JNI to initialize foo with pointers to static funcitons from foo_callbacks. I keep a static reference to foo.
  2. Separate call to JNI starts computational process using existing foo object
  3. But when foo needs to use a callback function I passed earlier, I get this message: A/libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x4 in tid 14244.

Upon Googling, it seems the memory I am trying to access isn't owned by my app anymore. So I think those references to callback funcitons are not valid anymore. So my question is, how do I keep a native object in memory between JNI calls? Or is there another way to approach this issue? Thanks.

Here's some sample code:

FooManager.java

...
static {
    System.loadLibrary("FooLib");
}

//initialized Foo library
private native int fooLibInit();

//start the process 
public native int fooStart(String message);

//continue the process after a delay 
public native int fooContinue(String message);

//retrieve milliseconds to schedule next event
public native long fooGetMsToNextEvent();

//method that gets called from native code
public static long getCurrentTime(){
    return System.currentTimeMillis();
}

//method that gets called from native code, returning results
public static void deliverResult(String result){
    //display result to the user
}

...

FooScheduler.java

...
public static void kickItOff(String message){
    FooManager.fooLibInit();
    long timeToWait = FooManager.fooGetMsToNextEvent();
    //this call figures out what step it is and gets some data
    SchedulerUtil.scheduleCallBack(timeToWait);
}

//this is a callback function that gets called after given about of time by SchedulerUtil
public static void callBack(int step, String message){
    if(step == 1)
        FooManager.fooStart(message)
    else FooManager.fooContinue(message);
}
...

FooLib.cpp

#include <string.h>
#include <jni.h>
#include <android/log.h>

extern "C" {
    #include "blackbox.h"
    #include "foo_wrapper.h"
}

extern "C" {

    static jboolean isJni();
    //struct defined in blackbox.h
    static foo foo_obj;

    JNIEXPORT jint
    JNI_OnLoad(JavaVM *vm, void *reserved) {
        //defined in foo_wrapper.h
        set_java_env(vm);
        return JNI_VERSION_1_6;
    }

    JNIEXPORT jint JNICALL
    Java_com_myapp_fooInit(JNIEnv * env, jobject obj){
        //foo_get_global_time_wrapper and foo_return_result_wrapper functions is defined in foo_wrapper.h.
        //those pointers are actually a member variables of foo_obj,
        //they gets assigned in the fooInit() so foo_obj can use them later. fooInit is defined in blackbox.h
        int resultInit = fooInit(&foo_obj, foo_get_global_time_wrapper, foo_return_result_wrapper);
        return resultInit;
    }

    JNIEXPORT jint JNICALL
    Java_com_myapp_fooStart(JNIEnv * env, jobject obj, jstring message){
        jboolean copy = isJni();
        const char *firstCharPointer = env->GetStringUTFChars(message, &copy);

        //here is where the foo_get_global_time_wrapper function is called, and
        //
        //I am getting A/libc: Fatal signal 11 (SIGSEGV) error.
        //
        //fooStart is defined in blackbox.h
        int resultCode = fooStart(&foo_obj, (uint8*)firstCharPointer, strlen(firstCharPointer));
        return resultCode;
    }

    JNIEXPORT jint JNICALL
    Java_com_myapp_fooContinue(JNIEnv * env, jobject obj, jstring message){
        jboolean copy = isJni();
        const char *firstCharPointer = env->GetStringUTFChars(chunk, &copy);

        //here blackbox produces results based on the first and second messages that were passed in and calls foo_return_result_wrapper with results
        //fooContinue is defined in blackbox.h
        int resultCode = fooContinue(&foo_obj, (uint8*)firstCharPointer, strlen(firstCharPointer));
        return resultCode;
    }


    static jboolean isJni(){
        return JNI_TRUE;
    }
}

foo_wrapper.c

#include "foo_wrapper.h"
#include <jni.h>
#include <string.h>

static JavaVM *JVM;

extern uint32 foo_get_global_time_wrapper() {
    JNIEnv *env;
    int result = (*JVM)->GetEnv(JVM, (void **) &env, JNI_VERSION_1_6);
    if (result != JNI_OK) {
        LOGI("couldnt get JVM.");
        return 1;
    }

    jclass clazz = (*env)->FindClass(env, "com/myapp/FooManager");
    jmethodID mid = (*env)->GetStaticMethodID(env, clazz, "getCurrentTime", "()J");
    long milliseconds;
    (*env)->CallStaticObjectMethod(env, clazz, mid, milliseconds);
    return milliseconds;
}

extern int foo_return_result_wrapper(const uint8 *start, uint16 length) {
    JNIEnv *env;
    int result = (*JVM)->GetEnv(JVM, (void **) &env, JNI_VERSION_1_6);
    if (result != JNI_OK) {
        LOGI("couldnt get JVM.");
        return 1;
    }

    jstring result = //get jstring from parameters start and length;

    jclass clazz = (*env)->FindClass(env, "com/myapp/FooManager");
    jmethodID mid = (*env)->GetStaticMethodID(env, clazz, "deliverResult", "(LJava.lang.String;)J");

    jobject obj =   (*env)->CallStaticObjectMethod(clazz, mid, result);
    return 0;
}

extern void set_java_env(JavaVM *vm) {
    JVM = vm;
}

Please note, this isn't tested code - it basically a much simpler version of what I am trying to do.

Community
  • 1
  • 1
C0D3LIC1OU5
  • 8,600
  • 2
  • 37
  • 47
  • There's no such thing as a class in C. Do you mean a C++ class or do you mean something different entirely? – Brick Oct 20 '15 at 23:30
  • That helps some, but I still cannot understand your actual flow. Some code might help. Specific issues: 1. Not clear how you "keep a static reference to `foo` in Java given that it is a C struct. 2. Not clear how the memory for `foo` itself is managed on the native side. 3. Not clear what `foo_callbacks` is at all - is that a file or some struct? 4. Not really clear how your starting an calling your JVM - I followed the link, but the details matter a lot, so *exactly* how you're doing it could be important. – Brick Oct 21 '15 at 04:04
  • @Brick Fair enough, I'll post sample code in a bit – C0D3LIC1OU5 Oct 21 '15 at 04:38
  • @Brick ok, I've added some sample code. – C0D3LIC1OU5 Oct 21 '15 at 15:43
  • If you look at logcat output, you will find the stack trace. You can analyze it with [ndk-stack](https://developer.android.com/intl/zh-tw/ndk/guides/ndk-stack.html) tool and find the exact line of your code that causes the crash. – Alex Cohn Oct 21 '15 at 19:44
  • I tried to follow this through, but I could not find a clear answer. Generally it's not good to jump back and forth so much if you can avoid it - And in this format it's too difficult to keep the flow. I'm suspicious of three things: 1. I don't see where you start the JVM. 2. I'm not sure about the cast from (char*) to (uint8*). 3. I'm not sure that `GetStringUTFChars` gives you a null-terminated string compatible with `strlen`. That's just initial things that I would check, and I would do it as Alex Cohn suggested in the debugger and/or with a ton of print statements. Good luck! – Brick Oct 22 '15 at 03:14
  • I don't know what kind of restrictions you have, but I would have tried an asynchronous communication between C and Java. This would reduce the number of JNI communications. Why don't you launch your ndk code in a background service, so your variable is in memory all the time, and send `broadcasts` between C and Java? – Paschalis Oct 22 '15 at 07:55
  • I don't get it, why don't you just pass the JNIenv* to the *foo_get_global_time_wrapper* function? It would be foo_get_global_time_wrapper(JNIenv*) – Gábor Buella Oct 24 '15 at 14:04
  • Thanks guys, I've figured this out. It was a multithreading issue, I didn't realize JNI code was executing on a different thread under the hood. I am not sure what exactly was causing errors, but adding `synchronized` keyword to native function definitions resolved error I ran into. – C0D3LIC1OU5 Oct 26 '15 at 17:13

2 Answers2

1

This is the problem that Services were designed to solve. There are three flavors: Foreground Services that interact with the user; Background Services that do stuff in the background; and Bound Services (client/server) that exist for as long as a client app is bound to them. Implement your JNI code as a Bound Service with a thin wrapper of Java on top of it; then you will get your persistence.

DragonLord
  • 6,395
  • 4
  • 36
  • 38
-1

It was a multithreading issue. There is no guarantee that JNI will execute native code on the same thread as Java. Making all native functions synchronized resolved the issue.

//initialized Foo library
private native synchronized int fooLibInit();

//start the process 
public native synchronized int fooStart(String message);

//continue the process after a delay 
public native synchronized int fooContinue(String message);

//retrieve milliseconds to schedule next event
public native synchronized long fooGetMsToNextEvent();

//method that gets called from native code
public static synchronized long getCurrentTime(){
    return System.currentTimeMillis();
}

//method that gets called from native code, returning results
public static synchronized void deliverResult(String result){
    //display result to the user
}
C0D3LIC1OU5
  • 8,600
  • 2
  • 37
  • 47
  • 2
    Under Dalvik, the native code called from Java-language code runs on the same thread. If you were seeing one thread stepping on another it's because you had two different threads in your app, not one thread splitting at the JNI call. The SIGSEGV at address 0x4 looks like a null-pointer dereference; nothing to do with memory going away. FWIW you can easily identify threads by writing a log message and looking at the thread ID... `adb logcat -v threadtime` if you don't want to configure Android Studio. I have no idea why `synchronized` would make any difference. – fadden May 04 '16 at 00:09