It can’t be argued that Java is popular and successful. It is consistently the number one language on TIOBE’s popularity list, above C which comes in as number two. This ranking is based on popularity and doesn’t mean Java is more used than C but that doesn’t change the fact that there is a lot of Java code out there.
Unlike other languages interop with Java is not what I’d call easy. It’s gotten better but much like Java itself the process it very verbose. The native way to go between C and Java is JNI (Java Native Interface). It’s written in C so thankfully you are mainly writing C code for the bridge. The bad thing is, JNI uses strings in bridged function prototypes so you lose type checking at compile time. You’ll find out here have one of the strings wrong when you try to run the application.
The C Library I’m going to use is a simple counter. You create a counter object with a given starting value. You can add, subtract, increment, decrement, and get the current value. Once done you can/need to destroy the object. This is the C library so allocation handling needs to be respected.
counter.h
#ifndef __COUNTER_H__
#define __COUNTER_H__
struct counter;
typedef struct counter counter_t;
counter_t *counter_create(int start);
void counter_destroy(counter_t *c);
void counter_add(counter_t *c, int amount);
void counter_subtract(counter_t *c, int amount);
void counter_increment(counter_t *c);
void counter_decrement(counter_t *c);
int counter_getval(counter_t *c);
#endif /* __COUNTER_H__ */
The counter object is intended to be an opaque pointer with its data hidden. The counter’s data is just an integer in a struct
at this point. While this might not be the most efficient way to handle an int
, this demonstrates working with complex (often opaque) types which typically have multiple data members.
counter.c
#include
#include "counter.h"
struct counter {
int val;
};
counter_t *counter_create(int start)
{
counter_t *c;
c = malloc(sizeof(*c));
c->val = start;
return c;
}
void counter_destroy(counter_t *c)
{
if (c == NULL)
return;
free(c);
}
void counter_add(counter_t *c, int amount)
{
if (c == NULL)
return;
c->val += amount;
}
void counter_subtract(counter_t *c, int amount)
{
if (c == NULL)
return;
c->val -= amount;
}
void counter_increment(counter_t *c)
{
if (c == NULL)
return;
c->val++;
}
void counter_decrement(counter_t *c)
{
if (c == NULL)
return;
c->val--;
}
int counter_getval(counter_t *c)
{
if (c == NULL)
return 0;
return c->val;
}
The first thing we need to do is wrap the C code in JNI C function calls. Java needs C functions exposed in a particalr way in order to call them. Implementation wise this is pretty simple because it just calls the counter object’s functions.
jni_wrapper.c
#include
#include
#include
#include "counter.h"
static const char *JNIT_CLASS = "Counter";
static jlong c_create(JNIEnv *env, jobject obj, jint start)
{
counter_t *c;
(void)env;
(void)obj;
c = counter_create((int)start);
return (jlong)c;
}
static jlong c_create_from_string(JNIEnv *env, jobject obj, jstring start)
{
const char *str;
int sval;
str = (*env)->GetStringUTFChars(env, start, 0);
sval = atoi(str);
(*env)->ReleaseStringUTFChars(env, start, str);
return c_create(env, obj, sval);
}
static void c_destroy(JNIEnv *env, jobject obj, jlong ptr)
{
(void)env;
(void)obj;
counter_destroy((counter_t *)ptr);
}
static void c_add(JNIEnv *env, jobject obj, jlong ptr, jint val)
{
(void)env;
(void)obj;
counter_add((counter_t *)ptr, (int)val);
}
static void c_subtract(JNIEnv *env, jobject obj, jlong ptr, jint val)
{
(void)env;
(void)obj;
counter_subtract((counter_t *)ptr, (int)val);
}
static void c_increment(JNIEnv *env, jobject obj, jlong ptr)
{
(void)env;
(void)obj;
counter_increment((counter_t *)ptr);
}
static void c_decrement(JNIEnv *env, jobject obj, jlong ptr)
{
(void)env;
(void)obj;
counter_decrement((counter_t *)ptr);
}
static jint c_getval(JNIEnv *env, jobject obj, jlong ptr)
{
(void)env;
(void)obj;
return counter_getval((counter_t *)ptr);
}
static jstring c_toString(JNIEnv *env, jobject obj, jlong ptr)
{
int val;
char sval[16];
jstring jval;
(void)obj;
val = counter_getval((counter_t *)ptr);
snprintf(sval, sizeof(sval), "%d", val);
return (*env)->NewStringUTF(env, sval);
}
static JNINativeMethod funcs[] = {
{ "c_create", "(I)J", (void *)&c_create },
{ "c_create_from_string", "(Ljava/lang/String;)J", (void *)&c_create_from_string },
{ "c_destroy", "(J)V", (void *)&c_destroy },
{ "c_add", "(JI)V", (void *)&c_add },
{ "c_subtract", "(JI)V", (void *)&c_subtract },
{ "c_increment", "(J)V", (void *)&c_increment },
{ "c_decrement", "(J)V", (void *)&c_decrement },
{ "c_val", "(J)I", (void *)&c_getval },
{ "c_toString", "(J)Ljava/lang/String;", (void *)&c_toString }
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv *env;
jclass cls;
jint res;
(void)reserved;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK)
return -1;
cls = (*env)->FindClass(env, JNIT_CLASS);
if (cls == NULL)
return -1;
res = (*env)->RegisterNatives(env, cls, funcs, sizeof(funcs)/sizeof(*funcs));
if (res != 0)
return -1;
return JNI_VERSION_1_8;
}
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved)
{
JNIEnv *env;
jclass cls;
(void)reserved;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK)
return;
cls = (*env)->FindClass(env, JNIT_CLASS);
if (cls == NULL)
return;
(*env)->UnregisterNatives(env, cls);
}
There is an older way to write JNI functions which doesn’t use JNI_OnLoad
. Instead the function name has a special format that includes information such as the package. The naming convention is what makes that format work. However, using the newer way presented here is much, much, easier to work with.
Now lets looks at how all that code above works.
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK)
return -1;
Here we check the version of the Java VM is a minimum version required to run. In this case version 8. However, none of the code presented here is dependant on features in version 8 and this could be JNI_VERSION_1_6
instead. I chose 8 because it’s what I have installed and can 100% test and verify.
cls = (*env)->FindClass(env, JNIT_CLASS);
if (cls == NULL)
return -1;
JNIT_CLASS
is the Java class the native (JNI) functions will be bound to. If the class is in a package this needs to be the fully qualified class name. Use ‘/’ instead of ‘.’. E.g. “com.s.counter.Counter” -> “com/s/counter/Counter”.
res = (*env)->RegisterNatives(env, cls, funcs, sizeof(funcs)/sizeof(*funcs));
if (res != 0)
return -1;
return JNI_VERSION_1_8;
Finally, we bind the native functions in the struct
to the package/class.
Only JNI_OnLoad
and JNI_OnUnload
are public. We don’t need the wrapping functions public, in the symbol table, because they are referenced through the RegisterNatives
binding function.
static return_type c_create(JNIEnv *env, jobject obj, [arg_type arg ...])
The static functions all use a similar prototype; all have the same first two arguments. That said, functions can have additional arguments passed into them. A function’s arguments correspond to “signature” member of the JNINativeMethod struct which is defined by JNI. For example:
{ "c_create", "(I)J", (void *)&c_create },
...
{ "c_subtract", "(JI)V", (void *)&c_subtract },
...
{ "c_create_from_string", "(Ljava/lang/String;)J", (void *)&c_create_from_string },
The signature is a string with a special syntax that tells Java the number of and type of arguments. As well as the return type. Since this is used by Java, each identifier corresponds to a Java type. Remember these are the Java types not the corresponding JNI type. For example Java’s long
is jlong
in JNI C code.
Z boolean
B byte
C char
S short
I int
J long
F float
D double
V void
L (full class name) Class. E.g. Ljava/lang/String;
[ array (type[]). E.g. [B
In most of the JNI counter functions env
is ignored but it can be very important in certain situations. env
in particular is used to call a host of functions to run Java objects. c_create_from_string
and c_toString
both use env
to manipulate Java strings within C. Anything you can call in Java objects can be called using JNI through the env
variable.
static jlong c_create_from_string(JNIEnv *env, jobject obj, jstring start)
...
str = (*env)->GetStringUTFChars(env, start, 0);
sval = atoi(str);
(*env)->ReleaseStringUTFChars(env, start, str);
The Java string has it’s data converted into a char
string so it can be converted to an integer. Once the string is no longer needed it is released (free
d).
A big thing to understand is, using Java objects in C can lead to issues with memory management because the garbage collector doesn’t necessarily know what’s being used. Just like any other C code what we create we need to destroy. This is precisely why after getting and working with the string data we need to release it.
static jstring c_toString(JNIEnv *env, jobject obj, jlong ptr)
...
return (*env)->NewStringUTF(env, sval);
Right here, a Java string is being created and returned to the JVM. We don’t need to destroy it ourselves because we’ve given up control of the string. As long as anything we create in JNI is given over to the JVM it will be managed by the JVM and we don’t need to worry about destroying it ourselves.
Even though we are in C the Java layer can still throw exceptions. And… exceptions don’t exist in C. And if we were using C++, these aren’t C++ exceptions either. They are Java exceptions and are within the JVM. Exceptions must be handled otherwise unexpected application termination can happen.
One way to handle exceptions is to allow them to flow up to the Java layer. When using JNI from Java, the Java code which calls the native functions can be wrapped in a try
block. This works because any exceptions will be set and handled when the application leaves the JNI layer and the JVM layer takes over again.
Another method, which I highly recommend, is to handle exceptions in the JNI code because this works with Java calling C too. Also, this way you know what threw the exception and this can lead to better flow control.
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionClear(env);
...
}
We can check if an exception was thrown and handle it. If we end up returning early from JNI, be sure to clear the exception because we’ve already handled it. Many Java programmers are used to exception and it’s also possible to create and throw exception from JNI. This could be useful for informing the JVM about the type of error that JNI encountered. Again, you don’t have to handle an exception or clear it in JNI but it’s a very good idea to do so. You should only allow exceptions to percolate up if your throwing it or if you’ve checked it and want it passed on.
An interesting aspect of JNI is the syntax. It has different syntax for C vs C++ and they’re exactly what you’d expect to see when using an object in C vs C++.
C:
cls = (*env)->FindClass(env, JNIT_CLASS);
C++:
cls = env->FindClass(JNIT_CLASS);
So far we have a C library and some JNI wrapper code that acts as a bridge between C and Java. Now we need some Java code that will use the JNI in order to use the C library. Let’s make a Java Counter class which will use the underlying C library.
Counter.java
class Counter {
private long c_counter = 0;
public Counter(int start) {
c_counter = c_create(start);
}
public Counter(String start) {
c_counter = c_create_from_string(start);
}
protected void finalize() {
c_destroy(c_counter);
}
public void add(int val) {
c_add(c_counter, val);
}
public void subtract(int val) {
c_subtract(c_counter, val);
}
public void increment() {
c_increment(c_counter);
}
public void decrement() {
c_decrement(c_counter);
}
public int getVal() {
return c_val(c_counter);
}
public String toString() {
return c_toString(c_counter);
}
static {
System.loadLibrary("counter");
}
private static native long c_create(int start);
private static native long c_create_from_string(String start);
private static native void c_destroy(long ptr);
private static native void c_add(long ptr, int val);
private static native void c_subtract(long ptr, int val);
private static native void c_increment(long ptr);
private static native void c_decrement(long ptr);
private static native int c_val(long ptr);
private static native String c_toString(long ptr);
}
This looks like just another wrapper and it is, unfortunately. JNI can expose C functions that can be called by Java but it cannot expose Java style objects. Also, while a JNI function can take and return Java objects it can only deal with objects created in Java. It cannot create a Java class. To make the counter easy and initiative a wrapper Java class (Counter
) object is created which uses the JNI functions.
Counter
calls create and destroy in the constructor and finalize functions. This allows the garbage collector to handle memory management instead of the caller.
private long c_counter = 0;
Notice that the C counter object wrapped by JNI is defined as long
. In the JNI code the JNI counter functions use jlong
. This is on purpose because JNI does not have a real way to pass pointers between C and Java. There are two solutions for this.
One is to create the object’s data in Java and have the C code fill it in. In this case the counter struct
would be a Java class. The C code (if all JNI) could work on the data within the object directly. For example, you can take this approach by having the data in the C object copied into the Java object and vice versa. This is very cumbersome, error prone, and wasteful.
Another solution (commonly accepted and used here) is to pass the memory address between the C and JVM layers. While there isn’t a direct pointer type a Java jlong
is guaranteed to be 64 bit and a pointer cannot be more than 64 bit (as of current architectures the JVM will run on) so storing the address in a Java jlong
will work. Realize that JNI uses jlong
and Java uses long
. You must use jlong
in the JNI C code because it has the 64 bit guarantee where a C long
does not. This assumes that if a 128 bit processor is developed and the JVM run on it then jlong
will be expanded in size to be a 128 bit integer. If this is not the case, then code using this approach will need to be updated. Somehow…
static {
System.loadLibrary("counter");
}
Loading the C library happens in a static context so it is only loaded once for the duration of the application. We do not and cannot have the library loaded for every Counter
object created. loadLibrary
looks for a library called “lib.ext”. Where ext varies per OS (dll on Windows, so on Linux…). It looks in a specific search path (java.libarary.path
). This takes place a run time and if the library is not found an exception will be thrown.
private static native long c_create(int start);
private static native void c_subtract(long ptr, int val);
...
Every JNI function that the class can use must be defined so Java knows what it can call. The native
attribute informs Java that this will be from a native library and it is not a Java function.
Let’s start off with a simple Java application that will use the couter we’ve made.
Main.java
class Main {
public static void main(String args[]) {
Counter c = new Counter(0);
Counter d = new Counter("10");
System.out.println("c=" + c + ", d=" + d.getVal());
c.add(4);
c.decrement();
System.out.println("c=" + c + ", d=" + d.getVal());
c.subtract(-2);
c.increment();
System.out.println("c=" + c + ", d=" + d.getVal());
d.decrement();
System.out.println("c=" + c + ", d=" + d.getVal());
}
}
This is just a simple test application which creates two counters, makes changes to them and outputs the result. We don’t need anything fancy to show how this works.
System.out.println("c=" + c + ", d=" + d.getVal());
The values are output in two different ways. Counter has a toString
function which pretty much every object has and it’s implemented through the JNI wrapper to return a string with the value. It could be implemented to return other data such as the starting value, if it was tracked, as well as the current value. Using c
in this situation causes toString
to be called and it’s output to be used.
d
has the value printed by using the getVal
function. In this case an int
is returned which also is automatically converted to a string. Be sure to document what toString
will return because if it does differ from what getVal
will show they can’t be used interminably in this context.
$ gcc counter.c jni_wrapper.c -shared -o libcounter.dylib -I$(/usr/libexec/java_home)/include -I$(/usr/libexec/java_home)/include/darwin
$ javac Counter.java Main.java
All that’s happening, is the C files are being built as static libraries and the JNI header locations are being provided. This is specific to OS X and will need some tweaks for Linux and Windows but it demonstrates how to build the example. The javac
part should be obvious.
CMake
Of course this can be built with CMake to simplify things. Also, using CMake will automatically package the Java code into a jar to make this a bit more distributable. The library isn’t put into the jar because loadLibrary
needs to load the library from the file system and cannot directly load from a jar. You could package into the jar but you would need to extract the library to a temporary location which can get messy. Pretty much every Java application I’ve seen distributes any native libraries in the same directory as the jar or in a library sub directory and sets the library search path in a wrapper script for running the application.
CMakeLists.txt
cmake_minimum_required (VERSION 3.0)
project (bridge)
find_package (Java REQUIRED)
find_package (JNI REQUIRED)
include (UseJava)
include_directories (
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
${JNI_INCLUDE_DIRS}
)
set (SOURCES
counter.c
jni_wrapper.c
)
add_library (counter SHARED ${SOURCES})
target_link_libraries (counter ${JAVA_JVM_LIBRARY})
add_jar (${PROJECT_NAME} Main.java Counter.java ENTRY_POINT Main)
This should be pretty self explanatory. First find Java and JNI, second build the library as a C library, third compile the Java code and put it into a jar.
Notice that JAVA_JVM_LIBRARY
is being used instead of JNI_LIBRARIES
when creating the library. This is because JNI_LIBRARIES
will additionally link to AWT
which is not being used.
$ mkdir build && cd build
$ JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_72.jdk/Contents/Home/ cmake ..
$ make
$ java -jar bridge.jar
For running CMake I needed to set JAVA_HOME
as part of the call because Java can be installed in multiple locations (side by side). On OS X I have the official Java 8 JDK installed but OS X also has a bundled Java 6 stub. CMake finds the correct Java because it uses the java
application to determine the path. However, it does not do this for JNI. When running CMake without JAVA_HOME
set it would find the correct Java but the incorrect JNI. This necessitates setting the environment variable so CMake can find the correct JNI (this will also find the Java at the same location).
$ java Main
Or if you built using CMake.
$ java -jar bridge.jar
There aren’t any packages in this example and we’re not dealing with packaging into JAR files so everything is in the same directory. This means there is no need to provide any additional path information.
c=0, d=10
c=3, d=10
c=6, d=10
c=6, d=9
Output is as expected when this is run.