Search code examples
javacjava-native-interface

JNIEnv::NewObject() throwing java.lang.InstantiantionException


I am attempting to call JNIEnv::NewObject() in some JNI code when a C function returns a non-zero error code.

The order of events looks like:

  1. Call C function.
  2. If return code is non-zero, call a helper function which throws a custom excpetion.

The class I am trying to construct so that I can throw it is:

public final class HseException extends Exception {
    private static final long serialVersionUID = 8995408998818557762L;

    private final int errno;
    private final Context ctx;

    /* Only called from C */
    HseException(final String message, final int errno, final Context ctx) {
        super(message);

        this.errno = errno;
        this.ctx = ctx;
    }

    public Context getContext() {
        return this.ctx;
    }

    public int getErrno() {
        return this.errno;
    }

    public static enum Context {
        NONE
    }
}

In my code I am caching the jclass and jmethodID for the class and the constructor in a global struct, but the code looks like:

    globals.com.micron.hse.HseException.class =
        (*env)->FindClass(env, "com/micron/hse/HseException");
    globals.com.micron.hse.HseException.init = (*env)->GetMethodID(
        env,
        globals.com.micron.hse.HseException.class,
        "<init>",
        "(Ljava/lang/String;ILcom/micron/hse/HseException$Context;)V");

    globals.com.micron.hse.HseException.Context.class =
        (*env)->FindClass(env, "com/micron/hse/HseException$Context");
    globals.com.micron.hse.HseException.Context.NONE = (*env)->GetStaticFieldID(
        env,
        globals.com.micron.hse.HseException.Context.class,
        "NONE",
        "Lcom/micron/hse/HseException$Context;");

Note that the above code is located in the JNI_OnLoad() function of my library. This function completes without error, so this tells me that at least my classes and methods are being loaded correctly.

Lastly here is my helper function where I throw my custom exception type:

/* hse_err_t is a scalar type.
 * hse_strerror() creates a string out of that scalar.
 * hse_err_to_ctx() gets the enum context value embedded within the scalar.
 * hse_err_to_errno() gets the errno value embedded within the scalar.
 */
jint
throw_new_hse_exception(JNIEnv *env, hse_err_t err)
{
    assert(env);
    assert(err);

    const size_t needed_sz = hse_strerror(err, NULL, 0);
    char        *buf = malloc(needed_sz + 1);
    if (!buf)
        return (*env)->ThrowNew(
            env,
            globals.java.lang.OutOfMemoryError.class,
            "Failed to allocate memory for error buffer");

    hse_strerror(err, buf, needed_sz + 1);

    const jstring message = (*env)->NewStringUTF(env, buf);
    free(buf);
    if ((*env)->ExceptionCheck(env))
        return JNI_ERR;

    const int              rc = hse_err_to_errno(err);
    const enum hse_err_ctx ctx = hse_err_to_ctx(err);

    jfieldID err_ctx_field = NULL;
    switch (ctx) {
        case HSE_ERR_CTX_NONE:
            err_ctx_field = globals.com.micron.hse.HseException.Context.NONE;
            break;
    }

    assert(err_ctx_field);

    const jobject err_ctx_obj = (*env)->GetStaticObjectField(
        env, globals.com.micron.hse.HseException.Context.class, err_ctx_field);
    if ((*env)->ExceptionCheck(env))
        return JNI_ERR;

    const jobject hse_exception_obj = (*env)->NewObject(
        env,
        globals.com.micron.hse.HseException.class,
        globals.com.micron.hse.HseException.init,
        message,
        rc,
        err_ctx_obj);
    if ((*env)->ExceptionCheck(env))
        return JNI_ERR;

    return (*env)->Throw(env, (jthrowable)hse_exception_obj);
}

I know for a fact that the (*env)->NewObject() call is what is raising the exception because an exception check before and after will tell me so. The (*env)->NewStringUTF() call is successful and contains the string it should contain. The context field is also retrieved successfully.

What I am not understanding is why I am getting an InstantiationException. The Throws section of the JNIEnv::NewObject() is marked as the following:

THROWS:
InstantiationException: if the class is an interface or an abstract class.

OutOfMemoryError: if the system runs out of memory.

Any exceptions thrown by the constructor.

My class is not an interface nor is it an abstract class, so where could this exception be generated from? The weird thing is that I swear this worked before, but since I am writing these Java bindings from scratch, I have just been overwriting commits and force pushing to my branch.

Any help is appreciated. Unfortunately getMessage() on the exception returns null which just isn't helpful at all. There is no message from the JVM telling me potentially what I have done wrong either.

One detail that could be helpful is that when I try to call JNIEnv::ThrowNew() (after putting a (Ljava/lang/String;)V constructor in the same HseException class, jni_ThrowNew() segfaults, and I cannot understand why. The class is valid when I stash the jclass, and I know for a fact that the memory it is stashed in isn't overwritten in any way, since I have checked the pointer.

The repo where all this code lives is: https://github.com/hse-project/hse-java. Unfinished product, but at least it is buildable and tests can be ran. In the event that someone decides to clone the repo and build it, I will repeat the directions here:

meson build
ninja -C build
meson test -C build -t 0 KvsTest # I am using this test to exercise the code path

My goal tomorrow will be to try to reproduce the issue in a smaller manner. I may also try to peer into the OpenJDK code assuming that is where the JNI interfaces live. Figure if I look hard enough, I might find the line of code which generates the exception.

Edit: I did a test where in my current code, I added a main function and a native function whose only purpose is to throw an exception from C. The code looks something like:

private static native void throwException();

public static void main(String[] args) {
    System.load("/path/to/.so");
    throwException();
}

The implementation of the native function is:

void Java_com_micron_hse_Hse_throwException
  (JNIEnv *env, jclass hse_cls)
{
    (void)hse_cls;

    /* Generate error */
    hse_err_t err = hse_kvdb_txn_begin(NULL, NULL);

    throw_new_hse_exception(env, err);
}

This printed the following after executing java -jar path/to/jar:

Exception in thread "main" com.micron.hse.HseException: lib/binding/kvdb_interface.c:1046: Invalid argument (22)
        at com.micron.hse.Hse.throwException(Native Method)
        at com.micron.hse.Hse.main(Hse.java:28)

That is exactly what I expect to be printed, so now I would say I am even more lost than when I started. For some reason in the context of my tests, the InstantiationException is raised. Not sure if an application using the JAR would hit the same issue or if it is just a test context thing.

Edit 2:

Changed the main method from the previous edit to the following which is pretty much exactly what my test does:

    public static void main(String[] args) throws HseException {
        try {
            loadLibrary(Paths.get("/home/tpartin/Projects/hse-java/build/src/main/c/libhsejni-2.so"));

            init();

            final Kvdb kvdb = Kvdb.open(Paths.get("/media/hse-tests"));
            final Kvs kvs = kvdb.kvsOpen("kvs");

            kvs.delete((byte[])null);

            kvs.close();
            kvdb.close();
        } finally {
            // fini();
        }
    }

And was able throw the exception from C appropriately. This must mean that something is wrong with my test environment somehow.

Edit 3: Another clue. On one test, this issue generates the InstantiationException. On another test, this issue segfaults in jni_NewObject.


Solution

  • My issue was that I was holding onto jclass et al. references for too long.

    Prior question: Why I should not reuse a jclass and/or jmethodID in JNI?

    Java docs: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#global_and_local_references

    All Java objects returned by JNI functions are local references.

    Thanks to Andrew Henle for pointing this out in the comments of the question. I have highlighted his comment in this answer, and will mark it is as the answer.