Wrapping a C library in Java

https://nachtimwald.com/2017/06/06/wrapping-a-c-library-in-java/

Introduction

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 We Want to Wrap

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 JNI Wrapper

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);
}

The Wrapper Explained

Loading

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_6instead. 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.

Function prototypes

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

JNIEnv *env

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 (freed).

Memory and object ownership

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.

Exceptions

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.

A note about the JNIEnv *env

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);

Java 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.

Putting it All Together

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.

Build

$ 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 loadLibraryneeds 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).

Output

$ 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.

你可能感兴趣的:(java基础)