Be native with Java Native Interface

Be native with Java Native Interface

Java Native Interface (JNI) is a foreign function framework designed to call native applications. In other words, via JNI one can invoke a function written in C or C++ and vice versa. In this article, we discuss how to use JNI and interface between a C program and a Java program. So the Java program can call functions written in C.

If you ever read JDK source code, you often find methods like below:

public static native long currentTimeMillis();

As you can see methods like currentTimeMillis has not implementation. This is because the method implementation is done in another language, most probably C. The method is just a function prototype or declaration that calls an external function.

Hence, if we call currentTimeMillis in a program, an external function will be invoked, then calculates the result and returns it back to the Java class. All of this is done with the help of JNI.

In the following section, we explain in step by step details on how to use JNI to create native functions implemented in C. Then invoke them in a Java program.

For this purpose, our C compiler of choice is GCC and for Java, we use OpenJDK 21. Although, the process is the same in any JDK higher than 7. For JDK older than 8, refer to this tutorial.

For the purpose of this tutorial, we create two methods, printHello and multiple, in Java that call two respective functions in a C program with the same name.

Java implementation

First we start with writing our small Java program as follows:

public class JniExample {
    static {
        System.loadLibrary("hello");
    }

    public native void printHello();

    public native int multiple(int x, int y);


    public static void main(String[] args) {
        JniExample jniExample = new JniExample();
        jniExample.printHello();

        int result = jniExample.multiple(2, 9);
        System.out.println(String.format("What got from native: %s", result));
    }
}

The above code is very straightforward. In the static block, we load a binary library which is basically the compiled version of the C program in the library form. Then we declared two method prototypes that represent of the C functions.

Header file generation

In order to start writing the C program, we need to generate the appropriate header file for it. That is the main reason why we started with writing the Java code first. To generate the C header file, need to run the following command:

$ javac JniExample.java -h .

After running the above content, JniExample.h should be generated with the following content:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniExample */

#ifndef _Included_JniExample
#define _Included_JniExample
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JniExample
 * Method:    printHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JniExample_printHello
  (JNIEnv *, jobject);

/*
 * Class:     JniExample
 * Method:    multiple
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_JniExample_multiple
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

As you can see the function names have changed in the header files. Java has added, Java_JniExample_ prefix to them. Obviously, this is Java packaging which is separated by underscore. The first portion is Java and the second one is the class name of the example. Keep in mind for the implementation of these two functions, need to use the same name.

Another interesting thing to note both declared functions have two additional parameters added in the generated header which are JNIEnv *env, jobject thisObj.

The env pointer is a structure that contains the interface to the JVM. It includes all of the functions necessary to interact with the JVM and to work with Java objects.

The argument obj is a reference to the Java object inside which this native method has been declared.

For this tutorial we don’t need to bother with env and obj as the C program is not interacting with the Java code.

C code implementation

Now that we have the header file, the next step is to implement the C program as follows:

#include <stdio.h>
#include <stdlib.h>
#include <jni.h>
#include "JniExample.h"

JNIEXPORT void JNICALL Java_JniExample_printHello(JNIEnv *env, jobject thisObj) 
{
	printf("Hello World from Native method\n");
   return;
}

JNIEXPORT jint JNICALL Java_JniExample_multiple(JNIEnv *env, jobject thisObj, jint x, jint y) 
{
	int result = x * y;
	printf("In native function. The result is: %d\n", result);
	return result;
}

Gluing all pieces together

Now that both C and Java implementations are ready, the only remaining thing is to compile and run the project. This could be tricky as this step is very dependent on the OS and its configuration. Here we cover how to compile the project for macOS and Linux.

But before jumping to OS-specific configuration the first thing is to find whether JAVA_HOMEvariable is set in the terminal or not. This is done by running echo $JAVA_HOME. If it is not, need to find the JDK path. This is usually done by a simple search. After that have to export the variable as follows:

$ export JAVA_HOME=/jdj/path

MacOS

To compile the code in macOS, we need to generate .dylib file from my C code by running this command:

$ gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib Example.c

Keep in mind that in the Java code I’m importing hello library but the generated file has the name libhello. Well, that is not an issue, the JDK automatically figures that out.

And finally to make sure everything is working, we need to run the Java code:

$ java -Djava.library.path=. JniExample

Linux

For Linux, we need to generated .sofile. To do so need to compile the C code like below:

$ gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so Example.c

And then run the Java code:

$ java -Djava.library.path=. JniExample

References

Inline/featured images credits