Search code examples
javacmultithreadingjava-native-interfacepthreads

SIGSEGV when calling Java method from native pthread


In a Java project that uses C code via JNI I have a piece of native C code that obtains references to an object and one of its methods, then starts a native thread, passing these references to it in a struct. When the thread tries to call the method, the code crashes with a SIGSEGV. Calling that same method from the main thread works.

Doing some research I learned that the env reference is only valid within the thread and that any other native thread must be attached first. I did this but the code still crashes on the first call to the method.

Strangely, when I call the same method from the main thread before I create the other thread (just uncomment the line in the main code), things work for some time. The output thread loops for some 10,000 times before it crashes.

The method call is to DataOutputStream.writeShort(). The thread in question is the only one writing to the DataOutputStream. However, the DataOutputStream is connected to a DataInputStream.

Simplified native code:

static void write_output(struct output_state *s) {
    int i;
    jint sample;
    for (i = 0; i < 2 * s->result_len; i += 2) {
        sample = (s->result[i] << 8) + s->result[i+1];
        (*(s->env))->CallVoidMethod(s->env, s->tunerOut, s->writeShort, sample);
    }
}

static void *output_thread_fn(void *arg)
{
    struct output_state *s = arg;
    (*(s->jvm))->AttachCurrentThread(s->jvm, &(s->env), NULL);
    while (!do_exit) {
        // use timedwait and pad out under runs
        safe_cond_wait(&s->ready, &s->ready_m);
        pthread_rwlock_rdlock(&s->rw); //sync access to s with producer thread
        write_output(s);
        pthread_rwlock_unlock(&s->rw);
    }
    (*(s->jvm))->DetachCurrentThread(s->jvm);
    return 0;
}

JNIEXPORT jboolean JNICALL Java_eu_jacquet80_rds_input_SdrGroupReader_open
  (JNIEnv *env, jobject self) {
    jclass clsSelf = (*env)->GetObjectClass(env, self);
    jfieldID fTunerOut = (*env)->GetFieldID(env, clsSelf, "tunerOut", "Ljava/io/DataOutputStream;");
    jobject tunerOut = (*env)->GetObjectField(env, self, fTunerOut);
    jclass cls = (*env)->GetObjectClass(env, tunerOut);
    jmethodID writeShortID = (*env)->GetMethodID(env, cls, "writeShort", "(I)V");

    if (!writeShortID || !cls)
        return 0;

    (*env)->GetJavaVM(env, &(output.jvm));
    output.tunerOut = tunerOut;
    output.writeShort = writeShortID;

    // (*env)->CallVoidMethod(env, tunerOut, writeShortID, 0); // just for testing

    pthread_create(&controller.thread, NULL, controller_thread_fn, (void *)(&controller));
    usleep(100000);
    pthread_create(&output.thread, NULL, output_thread_fn, (void *)(&output));
    pthread_create(&demod.thread, NULL, demod_thread_fn, (void *)(&demod));
    pthread_create(&dongle.thread, NULL, dongle_thread_fn, (void *)(&dongle));

    return 1;
}

Are JNI references such as jclass, jfieldID, jobject and jmethodID subject to the same limitations as JNIEnv, i.e. valid only within the same thread?

Suspecting this, I moved the JNI reference stuff from open() to output_thread(), right after the call to AttachCurrentThread(). However, I still need to pass a jobject reference (self) across thread borders, and the call to GetObjectClass() crashes.

What is the correct way to create a thread native code and have that thread call a particular method of a given class instance?


Solution

  • Turns out my suspicion was correct: jobject and jclass references are indeed local, i.e. valid only within the same thread and only until the current native method returns. See http://developer.android.com/training/articles/perf-jni.html#local_and_global_references.

    My approach of moving the reference-related code to the thread function was correct, except that I need to first convert self into a global reference by calling NewGlobalRef().

    Updated code:

    static void write_output(struct output_state *s) {
        int i;
        jint sample;
        for (i = 0; i < 2 * s->result_len; i += 2) {
            sample = (s->result[i] << 8) + s->result[i+1];
            (*(s->env))->CallVoidMethod(s->env, s->tunerOut, s->writeShort, sample);
        }
    }
    
    static void *output_thread_fn(void *arg)
    {
        struct output_state *s = arg;
        (*(s->jvm))->AttachCurrentThread(s->jvm, &(s->env), NULL);
        jclass clsSelf = (*(s->env))->GetObjectClass(s->env, s->self);
        jfieldID fTunerOut = (*(s->env))->GetFieldID(s->env, clsSelf, "tunerOut", "Ljava/io/DataOutputStream;");
        s->tunerOut = (*(s->env))->GetObjectField(s->env, s->self, fTunerOut);
        jclass cls = (*(s->env))->GetObjectClass(s->env, s->tunerOut);
        s->writeShort = (*(s->env))->GetMethodID(s->env, cls, "writeShort", "(I)V");
        while (!do_exit) {
            // use timedwait and pad out under runs
            safe_cond_wait(&s->ready, &s->ready_m);
            pthread_rwlock_rdlock(&s->rw); //sync access to s with producer thread
            write_output(s);
            pthread_rwlock_unlock(&s->rw);
        }
        (*(s->jvm))->DetachCurrentThread(s->jvm);
        return 0;
    }
    
    JNIEXPORT jboolean JNICALL Java_eu_jacquet80_rds_input_SdrGroupReader_open
      (JNIEnv *env, jobject self) {
        jclass clsSelf = (*env)->GetObjectClass(env, self);
    
        if (!writeShortID || !cls)
            return 0;
    
        output.self = (*env)->NewGlobalRef(env, self);
        (*env)->GetJavaVM(env, &(output.jvm));
    
        (*env)->CallVoidMethod(env, tunerOut, writeShortID, 0); // just for testing
    
        pthread_create(&controller.thread, NULL, controller_thread_fn, (void *)(&controller));
        usleep(100000);
        pthread_create(&output.thread, NULL, output_thread_fn, (void *)(&output));
        pthread_create(&demod.thread, NULL, demod_thread_fn, (void *)(&demod));
        pthread_create(&dongle.thread, NULL, dongle_thread_fn, (void *)(&dongle));
    
        return 1;
    }
    

    One thing still missing is a call to DeleteGlobalRef() when the output thread is done. This is to make sure the global reference is released when it is no longer needed, so that the garbage collector can pick it up.