Search code examples
javaccallbackjava-native-interfaceinvoke

Java Invocation API: Call the C function back from the java code


I have a C (navive) program and a jar file with the main() method. From my native program I am initializing the JVM, and calling the main() method. I have no problems with this, everything is completely fine. But then I wanted to call back a C function from my java code.

The C function is defined in the native code in the same module as the one, that have created the JVM. The header is auto-generated, and the body is as simple as this:

JNIEXPORT void JNICALL Java_eu_raman_chakhouski_NativeUpdaterBus_connect0(JNIEnv* env, jclass clazz)
{
    return;
}

So, from the java code I'm calling NativeUpdaterBus.connect0(), continuosly getting an UnsatisfiedLinkError. I have no System.loadLibrary() calls in my java code, because I thought, that there will be no problems calling the native code back from the java code if the target module is (possibly?) already loaded.

Well, maybe my approach is completely incorrect, but I can't see any obvious defects, maybe you could help?

What possibly could help (but I didn't tried any of these approaches, because I'm still not quite sure)

  • Use a kind of a "trampoline" dynamic library with these JNI methods, load it from the java code, then marshal native calls through it.
  • Define a java.lang.Runnable's anonymous inheritor, created with jni_env->DefineClass() but this involves some bytecode trickery.
  • Use an another, less invasive approach, like sockets, named pipes, etc. But in my case I'm using only one native process, so this might be an overkill.

I'm using OpenJDK 11.0.3 and Windows 10. My C program is compiled with the Microsoft cl.exe 19.16.27031.1 for x64 (Visual Studio 2017).


Solution

  • One possibility, as others have already mentioned, is to create a shared library (.dll) and call it from the native code and from Java to exchange data.

    However, if you want to callback to a C function defined in the native code in the same module as the one the JVM originally created, you can use RegisterNatives.

    Simple Example

    • C program creates JVM
    • it calls a Main of a class
    • the Java Main calls back a C function named connect0 in the calling C code
    • to have a test case the native C function constructs a Java string and returns it
    • the Java side prints the result

    Java

    package com.software7.test;
    
    public class Main {
        private native String connect0() ;
    
        public static void main(String[] args) {
            Main m = new Main();
            m.makeTest(args);
        }
    
        private void makeTest(String[] args) {
            System.out.println("Java: main called");
            for (String arg : args) {
                System.out.println(" -> Java: argument: '" + arg + "'");
            }
            String res = connect0(); //callback into native code
            System.out.println("Java: result of connect0() is '" + res + "'"); //process returned String
        }
    }
    

    C Program

    One can create the Java VM in C as shown here (works not only with cygwin but still with VS 2019) and then register with RegisterNatives native C callbacks. So using the function invoke_class from the link above it could look like this:

    #include <stdio.h>
    #include <windows.h>
    #include <jni.h>
    #include <stdlib.h>
    #include <stdbool.h>
    
    ... 
    
    void invoke_class(JNIEnv* env) {
        jclass helloWorldClass;
        jmethodID mainMethod;
        jobjectArray applicationArgs;
        jstring applicationArg0;
    
        helloWorldClass = (*env)->FindClass(env, "com/software7/test/Main");
    
        mainMethod = (*env)->GetStaticMethodID(env, helloWorldClass, "main", "([Ljava/lang/String;)V");
    
        applicationArgs = (*env)->NewObjectArray(env, 1, (*env)->FindClass(env, "java/lang/String"), NULL);
        applicationArg0 = (*env)->NewStringUTF(env, "one argument");
        (*env)->SetObjectArrayElement(env, applicationArgs, 0, applicationArg0);
    
        (*env)->CallStaticVoidMethod(env, helloWorldClass, mainMethod, applicationArgs);
    }
    
    jstring connect0(JNIEnv* env, jobject thiz);
    
    static JNINativeMethod native_methods[] = {
            { "connect0", "()Ljava/lang/String;", (void*)connect0 },
    };
    
    jstring connect0(JNIEnv* env, jobject thiz) {
        printf("C: connect0 called\n");
        return (*env)->NewStringUTF(env, "Some Result!!");
    }
    
    static bool register_native_methods(JNIEnv* env) {
        jclass clazz = (*env)->FindClass(env, "com/software7/test/Main");
        if (clazz == NULL) {
            return false;
        }
        int num_methods = sizeof(native_methods) / sizeof(native_methods[0]);
        if ((*env)->RegisterNatives(env, clazz, native_methods, num_methods) < 0) {
            return false;
        }
        return true;
    }
    
    
    int main() {
        printf("C: Program starts, creating VM...\n");
    
        JNIEnv* env = create_vm();
        if (env == NULL) {
            printf("C: creating JVM failed\n");
            return 1;
        }
        if (!register_native_methods(env)) {
            printf("C: registering native methods failed\n");
            return 1;
        }
        invoke_class(env);
    
        destroy_vm();
        getchar();
        return 0;
    }
    

    Result

    resgister natives

    Links

    Creating a JVM from a C Program: http://www.inonit.com/cygwin/jni/invocationApi/c.html

    Registering Native Methods: https://docs.oracle.com/en/java/javase/11/docs/specs/jni/functions.html#registering-native-methods