Introduction

Obviously, JNI lets you call Java functions and use Java classes in C. Typical a Java app is the one calling into C. Now Let’s say you don’t have a Java app that kicks off the process but you want your C app to still use some Java code.

Okay, I know what you’re thinking, “why”? Well, there are companies out there that provide SDKs for their products only in Java. There are also some very cool open source libraries that are written in Java. However, you may not want to write your app in Java. So you’re stuck and this is your only option.

Example application

The best way to demonstrate this is with an example application. In Wrapping a C library in Java we created a C library that was used by Java and we’ll use that here too. By the way, this is going to be a very lazy example.

main.c

#include <jni.h>

int main(int argc, char **argv)
{
	JavaVM         *vm;
	JNIEnv         *env;
	JavaVMInitArgs  vm_args;
	jint            res;
	jclass          cls;
	jmethodID       mid;
	jstring         jstr;
	jobjectArray    main_args;

	vm_args.version  = JNI_VERSION_1_8;
	vm_args.nOptions = 0;
	res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
	if (res != JNI_OK) {
		printf("Failed to create Java VMn");
		return 1;
	}

	cls = (*env)->FindClass(env, "Main");
	if (cls == NULL) {
		printf("Failed to find Main classn");
		return 1;
	}

	mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");
	if (mid == NULL) {
		printf("Failed to find main functionn");
		return 1;
	}

	jstr      = (*env)->NewStringUTF(env, "");
	main_args = (*env)->NewObjectArray(env, 1, (*env)->FindClass(env, "java/lang/String"), jstr);
	(*env)->CallStaticVoidMethod(env, cls, mid, main_args);

	return 0;
}

Within the main C function we don’t need to call the main function from the Main Java class like we are. Instead we could have, probably should have, created a Counter object and called it’s functions using JNI. Or we could have written a pure Java Counter class and used that instead of this one which calls into C. However, This is lazy example and it gets the point across that a C application can use Java objects by creating an internal JVM.

res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);

This is the key to the whole thing. C uses JNI to create an internal JVM for the Java objects to run on. The JVM is confined inside of a C application and now Java is essentially a library being used by C.

Here is a break down of what’s happening.

  1. A C application creates a JVM using JNI and runs a Java things.
  2. There is a Java application in a jar that uses the Java class.
  3. The Java class uses a set of JNI C functions to make a nice Java object.
  4. A JNI wrapper is used to expose a C object to Java.
  5. There is a object written in C and compiled as a library.

It goes C -> JNI (which starts a JVM) -> Java Function (which is the entry point to a java application) -> Java class -> JNI bridge -> C Object. This is absurd but perfectly valid and works pretty well.

Using java objects

cls = (*env)->FindClass(env, "Main");
mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");

These are the basic building blocks of JNI. First, find a Java class and get the function you want to call. You need to specify the prototype so the JVM can properly find the function. Remember, Java supports function overloading so it can’t look it up only by name. This was used in the example where Java called C for the string functions.

FindClass can find compiled Java class files on disk and well as in a JAR file.

JavaVMOption options[1];
options[0].optionString    = "-Djava.class.path=....jar";
vm_args.options            = options;
vm_args.nOptions           = 1;
vm_args.ignoreUnrecognized = JNI_TRUE;

Here we create an options object which takes a single option setting the class path to the JAR file. This needs to be the JAR file itself and not the directory it resides. Also, if you do set this you need to specify all JAR files and all class files.

jstr      = (*env)->NewStringUTF(env, "");
main_args = (*env)->NewObjectArray(env, 1, (*env)->FindClass(env, "java/lang/String"), jstr);
(*env)->CallStaticVoidMethod(env, cls, mid, main_args);

Now we load the arguments into an object so it they can be passed to the function and call it. Here, main is a static function that returns void so CallStaticVoidMethod is used. There are a variety of different JNI functions like this for calling non-void return and non-static functions.

Don’t forget exceptions are something you need to keep in mind.

Putting it All Together

We’ve already built the library and compiled the class files previously so all we need to do now is build the C application.

gcc main.c -I$(/usr/libexec/java_home)/include/ -I$(/usr/libexec/java_home)/include/darwin -L$(/usr/libexec/java_home)/jre/lib/jli -L$(/usr/libexec/java_home)/jre/lib/server/ -ljli -ljvm

The C code is compiled and linked to the JVM library. We don’t need to link any of the Java classes or jars because it will be loaded by JNI at run time. If a class cannot be found or if a function name and or signature cannot be found NULL will be returned when JNI attempts to load it. Since lookups are done using strings it is very easy to make mistakes so be extra careful about checking for typos here. And be sure you handle this situation. In case a class or jar is accidentally deleted by the user.

When compiling on OS X it’s very important to link the jli library before the jvm library if you’re not using the system provided Java JDK. If you’re using an official Oracle package linking jvm first will cause the application to try and load the system jli library (which is a stub) instead of the one provided by the package. The system jli library not only being for the wrong version of Java does not export JNI_CreateJavaVM. This is only necessary on OS X and it might not be required in the future but as of OS X 10.11 you must do this.

CMake

With CMake we want to use the jar file instead of the class files directly. main.c needs to be modified to search for and load from the jar which was covered previously. Set options[0].optionString = "-Djava.class.path=bridge.jar"; in order for the jar to be loaded from the same location as the binary.

CMakeLists.txt

cmake_minimum_required (VERSION 3.0)
project (ccounter)

find_package (JNI REQUIRED)

include_directories (
	${CMAKE_CURRENT_BINARY_DIR}
	${CMAKE_CURRENT_SOURCE_DIR}
	${JNI_INCLUDE_DIRS}
)

set (SOURCES
	main.c
)

set (JLI_LIBRARY "$ENV{JAVA_HOME}/jre/lib/jli/libjli.dylib")

add_executable (${PROJECT_NAME} ${SOURCES})
target_link_libraries (${PROJECT_NAME} ${JLI_LIBRARY} ${JAVA_JVM_LIBRARY})

We don’t need to worry about linking to the library we’ve already built or the jar file because they are loaded at run time. CMake’s FindJNI doesn’t find JLI at all so I’m adding the library based on the JAVA_HOME path directly. This will need to be tweaked for non-OS X platforms.

It’s possible to include the lib, jar, and C application all in one build file.

CMakeLists.txt

cmake_minimum_required (VERSION 3.0)
project (ccounter)

find_package (JAVA REQUIRED)
find_package (JNI REQUIRED)
include (UseJava)

include_directories (
	${CMAKE_CURRENT_BINARY_DIR}
	${CMAKE_CURRENT_SOURCE_DIR}
	${JNI_INCLUDE_DIRS}
)

set (CC_SOURCES
	main.c
)

set (JLI_LIBRARY "$ENV{JAVA_HOME}/jre/lib/jli/libjli.dylib")

add_executable (${PROJECT_NAME} ${CC_SOURCES})
target_link_libraries (${PROJECT_NAME} ${JLI_LIBRARY} ${JAVA_JVM_LIBRARY})

set (LIB_SOURCES
	counter.c
	jni_wrapper.c
)

add_library (counter SHARED ${LIB_SOURCES})
target_link_libraries (counter ${JAVA_JVM_LIBRARY})
add_jar (bridge Main.java Counter.java ENTRY_POINT Main)

While it’s possible to combine everything the better option is to use a proper source structure and have everything separated. Keep the library, Java code, and application all in their sub directory and have their own CMakeLists.txt file. Then use a top level one to chain build each component.

Finally we can build using either CMakeLists.txt file.

$ mkdir build && cd build
$ JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_72.jdk/Contents/Home/ cmake ..
$ make
$ ./ccounter

Just like when building a JNI library building an application using JNI may need JAVA_HOME specified.

Output

$ LD_LIBRARY_PATH=$(/usr/libexec/java_home)/jre/lib/jli/:$(/usr/libexec/java_home)/jre/lib/server/ ./a.out

Or if using CMake

$ ./ccounter

When building with CMake it will set the rpath to find the Java libraries so they do not need be specified explicitly as part of LD_LIBRARY_PATH. CMake makes things so much easier.

c=0, d=10
c=3, d=10
c=6, d=10
c=6, d=9

Since this just calls the Main class we get the same output which is exactly what we expect.

Conclusion

Java has a very robust and complete standard library and that’s one of it’s big strengths. While cumbersome, if you really wanted you could use this integration method to have full access to the entire Java standard library and still write in C. I don’t recommend this because the JNI marshaling is slow so you’ll negate any performance gain by using C. This tutorial in particular is mainly a demonstration of what can be done and isn’t really something you would ever see someone do. That said, the techniques outlined are general for working with JNI.