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.
- A C application creates a JVM using JNI and runs a Java things.
- There is a Java application in a jar that uses the Java class.
- The Java class uses a set of JNI C functions to make a nice Java object.
- A JNI wrapper is used to expose a C object to Java.
- 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.