How to incorporate C code into Java projects.
Introduction.
Some nuts and bolts of how to do it.
Examples.
Java is not just a language, it is a platform. That is, it implements both the programming environment (the language per se), and the execution environment (JVM and APIs). Any implementation of the Java platform is guaranteed to support the Java programming language, virtual machine, and API. The Java platform is realised in the host environment: the host operating system, native libraries and the CPU instruction set.
When the Java platform is deployed on top of host environments, it may become desirable or necessary to allow Java applications to work closely with native code written in other languages. Programmers have begun to adopt the Java platform to build applications that were traditionally written in C and C++. Because of the existing investment in legacy code, however, Java applications will coexist with C and C++ code for many years to come.<
(from The JNI. Programmer's guide and Specification by Sheng Liang)
In the real world, we can't restrict ourselves to using nothing but Java (or whatever your favourite language is), no matter how much we'd like to.
We may need to use packages or libraries written in languages other than Java.
Some part of the system may have been written in some language other than Java.
Some parts of the system may need to have direct access to low-level system details.
One option is just to forget Java and write the system in another language, and this is often the right answer. Language choice should not just be a matter of personal preference, but should be carefully thought out for each project. By the time you finish your degree you should be familiar with several languages and capable of learning a new one in a few weeks.
But what if no language has all the libraries, capabilities and features that we need? We need to be able to make software components written in different languages work together. In practice, large systems often incorporate components written in several different languages.
Today we'll just look at one example of multi-language systems, making Java and C work together. In fact we'll only look at one direction: calling C code from Java.
JNI is designed to handle situations where you need to combine Java applications with native code. It is a two-way interface: it can support two types of native code: native libraries and native applications.
You can use the JNI to write native methods that allow Java applications to call functions implemented in native libraries. Java applications call native methods in the same way that they call methods implemented in the Java programming language. Behind the scenes, however, native methods are implemented in another language and reside in native libraries.
Although Java offers some alternatives to use of native methods, it's often necessary for a Java application to communicate with native code that resides in the same process. Here are several examples mentioned by Sheng Liang:
The Java API might not support certain host-dependent features needed by an application. An application may want to perform, for example, special file operations that are not supported by the Java API, yet it is both cumbersome and inefficient to manipulate files through another process.
You may want to access an existing native library and are not willing to pay for the overhead of copying and transmitting data across different processes. Loading the native library in the same process is much more efficient.
Having an application span multiple processes could result in unacceptable memory footprint. This is typically true if these processes need to reside on the same client machine. Loading a native library into the existing process hosting the application requires less system resources than starting a new process and loading the library into that process.
The Java Language Specification states that a method may be labelled as native (as you might label other methods abstract), which means that it is "implemented in platform-dependent code, typically written in another programming language such as C, C++, Fortran, or assembly language". But the JLS doesn't specify how to write your e.g. C code and how to connect the two pieces together. The native methods, like usual methods, can be overloaded, overridden, declared final, static, synchronized, and with any access modifier; but the native methods cannot be declared abstract (or, strictfp, if you know what this means).
The Java Native Interface (JNI) is Sun's way of connecting the implementations of native methods to its JVM. It specifies what you have to do in your C code to conform to the requirements of the JVM, and what you have to do in your Java code to tell the JVM to access the external code.
Understand that once we start going down the path to using native code we lose platform-independence (portability and safety). Our examples work with Sun's JDK on Linux. With any other combination of platform and JVM changes may (will?) be needed to both the C code and the Java code.
The process of writing a Java code (here, a single class) which makes use of native methods, consists of several steps:
Create a class (HelloWorld.java) that declares the native method.
Use javac to compile it.
Use javah -jni generate a C header file (HelloWorld.h) containing the function prototype for the native method implementation.
Write the C implementation HelloWorld.c of the native method.
Compile the C implementation into a native library, creating HelloWorld.dll (for Windows) or HelloWorld.so (for Unix/Linuxes)
Run HelloWorld in JVM. Both HelloWorld.class and HelloWorld.so are loaded at runtime.
class HelloWorld {
private native void print();
public static void main(String [] args){
new HelloWorld().print();
}
static { System.loadLibrary("HelloWorld"); }
}
The static call System.loadLibrary(String libname) is needed for the native code to be loaded into the VM at runtime; otherwise the native methods cannot be executed. The library specified by libname corresponds to a file in the local file system that is located somewhere where the JVM knows to look for library files. The actual mapping library name —> file name is system dependent. Classes with native methods typically load the corresponding library as a part of initialization of the class (hence use of the static initialization block). The use of load method is the source of the safety loss.
javac HelloWorld.java
(HelloWorld.class is generated.)
javah -jni HelloWorld
This command generates a C header file, which contains the function prototype for Java_HelloWorld_print:
..........
JNIEXPORT void JNICALL
Java_HelloWorld_print (JNIEnv *, jobject);
..........
Write a C source file HelloWorld.c for implementing the function declared in the header file:
#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"
JNIEXPORT void JNICALL
Java_HelloWorld_print(JNIEnv *env,jobject obj)
{
printf("Hello World!\n");
return;
}
When compiled, it is this native library function which will be dynamically loaded into the JVM alongside with the java class.
gcc -Wall -shared -I/usr/local/java/include \ -I/usr/local/java/include/linux HelloWorld.c -o libHelloWorld.so
abx@eudyptula:~/comp2100/labs/lab6$ java HelloWorld HelloWorld!
It is important to set your native library path correctly for your program to run. The native library path is a list of directories that the Java virtual machine searches when loading native libraries. If you do not have a native library path set up correctly, then you see an error similar to ClassNotFoundException runtime error. The environment variable for the native library path is LD_LIBRARY_PATH.
Suppose you wanted to access the system clock so as to measure how long your program takes to perform different operations. This is easy to do in C, using a library routine clock() and a macro CLOCKS_PER_SEC, both declared in the standard header file time.h.
Here's the Java code:
public class Timer {
private native int ticks();
private native int ticksPerSecond();
static {
System.loadLibrary("timer");
}
}
And here's the C code it calls.
#include <jni.h>
#include <time.h>
#include "Timer.h"
/* The external C part of a Java system stopwatch package */
JNIEXPORT jint JNICALL Java_Timer_ticks (JNIEnv *env,
jobject obj) {
return (jint) clock();
}
JNIEXPORT jint JNICALL Java_Timer_ticksPerSecond (JNIEnv *env,
jobject obj) {
return (jint) CLOCKS_PER_SEC;
}
Java routines such as ticks and ticksPerSecond are known as wrappers for the corresponding C routines.
Notice the phenomenon of name mangling — names in the Java class (e.g. ticks()) have to be matched up with names in the C code (Java_Timer_ticks()). The JNI specification defines how this relationship works.
Because of the mangling, there's no shortcut to accessing C library or system calls. At least, we can stick private on the declaration.
Now about those JNIEXPORT and JNICALL. They are the so called C macros, they are defined (as all jni types) in jni.h header file. Using these declarations, the C compiler will generate code with the correct calling convention needed for export into JVM.
What are the two parameters to the C functions? The first is a means of accessing the JVM if you need it. The second is a reference to this (the object in which the method was invoked; jobject is used for non-static native methods). In this example we don't use either of the parameters.
Argument types in the native method declaration have corresponding types in native programming languages. The JNI defines a set of C and C++ types that correspond to types in the Java programming language, such that primitive types and reference types (instances of classes) are treated differently. The mapping of primitive types is straightforward: int —> jint, char —>jchar, float —>jfloat etc. (these types are defined in the header file jni.h)
Objects to native methods are passed as opaque references, they are C pointer types that refer to internal data structures in the JVM. The exact layout of the internal data structures is hidden from the programmer. The native code manipulates the underlying objects via the appropriate JNI functions, which are available through the JNIEnv *env interface pointer. For example, the corresponding JNI type for java.lang.String is jstring. All JNI references have type jobject. Then the set of reference types ("subtypes") is defined: jstring, jclass, jobjectArray, jintArray, jcharArray etc.
In the Java class Timer (which you will use in this week's lab) there are also attributes for storing the number of 'ticks' when the clock was started and stopped, queries for returning the number of ticks and seconds between starting and stopping it, and commands to start and stop the clock. The excerpt above is just the bit that made the external calls — those routines aren't even visible to the client.
What if you want to pass parameters to the external routine? How do you know that the data representations are consistent?
Same for the return value of a function.
What if the external code has the only reference to an object? How can the garbage collector know not to clean it up?
Also from the lab, a routine to access an array element. First, the fragment of Java:
private native int c_item(int array,int index);
then the implementation in C:
/* Return the element at index i of the array a. */
JNIEXPORT jint JNICALL Java_IntegerArray_c_item(JNIEnv *env,
jobject obj, jint a, jint i){
return (jint)(((int *)a)[i]);
}
Note the casts: first, a is cast to the type int *. Then, a is used as an array, and the i'th element is accessed. Finally, the result is cast back to a jint for use by the JVM.
The Javadoc documentation of a class doesn't tells you if a method is native. A good thing.
Remember the previous warning that some of the details will be different for other operating systems and JVMs.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Timer */
#ifndef _Included_Timer
#define _Included_Timer
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Timer
* Method: ticks
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_Timer_ticks
(JNIEnv *, jobject);
/*
* Class: Timer
* Method: ticksPerSecond
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_Timer_ticksPerSecond
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
jstring NewString(JNIEnv *env, const jchar *unicodeChars,
jsize len);
jsize GetStringLength(JNIEnv *env, jstring string);
const jchar * GetStringChars(JNIEnv *env, jstring string,
jboolean *isCopy);
void ReleaseStringChars(JNIEnv *env, jstring string,
const jchar *chars);
etc. Similar sorts of methods for dealing with arrays.
To compile the system above you need to say
javac Timer.java javah -jni Timer gcc -Wall -shared -I/usr/local/java/include \ -I/usr/local/java/include/linux timer.c -o libtimer.so
You'll be using this library in the lab. For the JVM to pick up libtimer.so you have to tell it where to look. This can be done including the current directory in the environment variable LD_LIBRARY_PATH. You can set variables on a per-command basis using the env command, e.g.:
env LD_LIBRARY_PATH=. java ArrayTesterwhich is how it's done in the supplied Makefile.
So far we have limited ourselves to calling C routines from Java. What if we want the C part of a system to be able to call Java routines?
Why would we want to do this? One reason is if we were trying to write an Java interface to something like GTK+ (a graphics library), where the main loop is running on the C side, and it needs to call Java routines when various events take place.
How do we do it? Not at all in this course. The details are given in Sun's JNI documentation. The book Java Native Interface: Programmer's Guide and Specification by Sheng Liang is used to be freely available from Sun's web site .
Copyright © 2006,2007
The Australian National University
$Revision: 1.3 $ $Date: 2007/05/21 00:02:59 $
Feedback & Queries to
comp2100@cs.anu.edu.au