Fatal error when calling native method more than once

0

I've recently started to build my own math library using intrinsics in C++. I finally finished creating Java bindings through the JNI interface and ironed out all of the bugs.

Strangely enough, when i call a Vec3f's add(Vec3f) method more than once in my java code, a fatal exception occurs. Since the log output is fairly unintuitive, I thought a more experienced developer might know more about this than I do.

I will only share one class since all of them are based around the same principles.

Vec3f c++:

#include"com_math_Vec3f.h" // includes jni.h

#include<immintrin.h>

static bool initialized = false;
static jclass this_class;
static jfieldID values_id;
static jmethodID init_id;

static void initialize(JNIEnv *env)
{
    if (!initialized)
    {
        initialized = true;

        this_class = env->FindClass("Lcom/math/Vec3f;");

        values_id = env->GetFieldID(this_class, "values", "[F");
        init_id = env->GetMethodID(this_class, "<init>", "(FFF)V");
    }
}

/*
 * Class:     com_math_Vec3f
 * Method:    add
 * Signature: (Lcom/math/Vec3f;)Lcom/math/Vec3f;
 */
JNIEXPORT jobject JNICALL Java_com_math_Vec3f_add
  (JNIEnv *env, jobject this_object, jobject v)
{
    initialize(env);

    jfloatArray v1 = reinterpret_cast<jfloatArray>(env->GetObjectField(this_object, values_id));
    jfloatArray v2 = reinterpret_cast<jfloatArray>(env->GetObjectField(v, values_id));

    jfloat *v1_a = env->GetFloatArrayElements(v1, nullptr);
    jfloat *v2_a = env->GetFloatArrayElements(v2, nullptr);

    __m128 m1 = _mm_set_ps(v1_a[2], v1_a[1], v1_a[0], 0.0f);
    __m128 m2 = _mm_set_ps(v2_a[2], v2_a[1], v2_a[0], 0.0f);

    __m128 sum = _mm_add_ps(m1, m2);

    jobject res = env->NewObject(this_class, init_id, sum[1], sum[2], sum[3]);

    return res;
}

Vec3f java:

import com.math.libloader.LibLoader;

public class Vec3f {

    static {
        LibLoader.init();
    }

    private float[] values;

    public Vec3f(float x, float y, float z) {
        values = new float[]{ x, y, z };
    }

    public Vec3f() {
        this(0.0f, 0.0f, 0.0f);
    }

    public Vec3f(Vec3f v) {
        this(v.values[0], v.values[1], v.values[2]);
    }

    public native Vec3f add(Vec3f v);

    public float getX() {
        return values[0];
    }

    public float getY() {
        return values[1];
    }

    public float getZ() {
        return values[2];
    }
}

I've cut out a lot of unnecessary code from these classes

LibLoader java (My dll is placed in a jar on the classpath):

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class LibLoader {

    private static final int BUFF_SIZE = 10000;

    static {
        InputStream stream = ClassLoader.getSystemResourceAsStream("math.dll");

        if (stream == null) {
            System.err.println("Error loading native libs for math.jar");
            System.exit(-1);
        }

        File tempDir = new File("math-temp");
        tempDir.mkdirs();
        tempDir.deleteOnExit();

        File tempFile = new File(tempDir, "math.dll");
        try {
            tempFile.createNewFile();
            tempFile.deleteOnExit();
        } catch (IOException e) {
            System.err.println("Error loading native libs for math.jar. Cannot create temp file");
            System.exit(-1);
        }

        try (FileOutputStream outStream = new FileOutputStream(tempFile)) {
            while (stream.available() > 0) {
                outStream.write(stream.readNBytes(BUFF_SIZE));
            }

            stream.close();
            outStream.close();
        } catch (IOException e) {
            System.err.println("Error loading native libs for math.jar. Internal error");
            System.exit(-1);
        }

        System.load(tempFile.getAbsolutePath());
    }

    public static void init() {}

    private LibLoader() {}
}

And finally, my test code (Main java):

import com.math.Vec3f;

public class Main {

    public static void main(String[] args) {
        Vec3f v1 = new Vec3f(-4.3f, 0.7f, 6.1f);
        Vec3f v2 = new Vec3f(3.5f, -6.7f, 3.1f);

        Vec3f t1 = v1.add(v2);
        Vec3f t2 = v1.add(v2); // fatal exception occurs here
    }
}

Also, in case it is of any use to anyone, an excerpt of the log:

---------------  S U M M A R Y ------------

Command Line: -Djava.library.path=C:\Dev\java\Projects\Math\dist\jni; -Dfile.encoding=Cp1252 --module-path=C:\Dev\java\Projects\Math Test\bin;C:\Dev\java\Projects\Math Test\libs\math.jar -Djdk.module.main=com.test com.test/main.Main

Host: Intel(R) Core(TM) i7-3520M CPU @ 2.90GHz, 4 cores, 7G,  Windows 10 , 64 bit Build 18362 (10.0.18362.778)
Time: Wed May 20 00:17:23 2020 W. Europe Daylight Time elapsed time: 0 seconds (0d 0h 0m 0s)

---------------  T H R E A D  ---------------

Current thread (0x000001dd8c4cd000):  JavaThread "main" [_thread_in_vm, id=20616, stack(0x0000002906300000,0x0000002906400000)]

Stack: [0x0000002906300000,0x0000002906400000],  sp=0x00000029063ff2c0,  free space=1020k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V  [jvm.dll+0x3ce5f2]
V  [jvm.dll+0x3cc3e4]
C  [math.dll+0x7596]

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j  com.math.Vec3f.add(Lcom/math/Vec3f;)Lcom/math/Vec3f;+0 com.math
j  main.Main.main([Ljava/lang/String;)V+41 com.test
v  ~StubRoutines::call_stub

siginfo: EXCEPTION_ACCESS_VIOLATION (0xc0000005), reading address 0x0000000000000005

While reading about function arguments in the JNI, I came across some warnings about the returned objects' lifetimes which I honestly didn't really understand.

My pure c++ math library looks almost identical to this one (the math at the very least is identical), but it does not produce the same error.

If anyone could help me out here I'd really appreciate it, Thanks!

java
c++
java-native-interface
asked on Stack Overflow May 19, 2020 by J. Lengel

1 Answer

2

There might be more issues but the first issue I noticed is that you cannot cache the result of FindClass the way you do. You need to use NewGlobalRef if you want to keep the class ID around across JNI method calls.

answered on Stack Overflow May 19, 2020 by Konrad Rudolph • edited May 19, 2020 by Konrad Rudolph

User contributions licensed under CC BY-SA 3.0