|
Developing a Java Native Interface (JNI)last update: 2 April 2010 |
Table of Contents
|
IntroductionThis tutorial is broken for the actual functionality it advertises. It's still useful for many points, but I lost the file, sne.c, that probably corresponds to SystemNativeError.java in some way rendering the symbols JAVA_CLASS_SystemNativeError, JAVA_CLASS_ERROR_FIELD, JAVA_CLASS_MESSAGE_FIELD, etc. missing when built. As this happened a long time ago and I don't have time to consecrate to resurrecting this tutorial, I'm just going to let it sit. I apologize for this. This tutorial will demonstrate implementing a simple Java Native Interface (JNI) on Linux with a nod to Windows issues as well. PrerequisitesThe tutorial assumes you have familiarity with C and Java programming and building on Linux—nothing very complicated at all. |
A good place to start is the Wikipedia article on this topic. JNI is a framework that connects Java via the Java Virtual Machine (JVM) to the underlying operating system or hardware through software that isn't (and cannot be reasonably) written in Java, but whose products or by-products are essential for consumption from a Java-based application (or other Java code).
A JNI is a shared-object library (on Windows, a DLL) that contains entry points reachable in Java via the JVM. If you do not understand these technologies, you may want to bone up on them, but it is not necessary to work through this tutorial and it may not be necessary for implementing your own JNI.
Your JNI can be written in most any language, most often C/C++ or even assembly, as long as it can observe certain calling conventions. On Windows, it is written as a DLL whereas on Linux and Unix it takes the final form of a shared-object library.
I'm not too interested in Windows and will mostly focus on a Linux implementation here. However, much applies to Windows which is fully supported by the JDK in this regard.
There are many other resources on the Internet such as the one you're reading. Most are as narrow and useful as mine such as the many concentrating only on the Windows DLL form.
Besides whatever file editing and source management software you like to use, all you need to develop a JNI on Linux is
In the JDK, under include/jni.h, you'll find the peculiar, helper definitions for writing C or C++ native methods. (For assembly, you use that file to guide your own definitions.) Include jni.h every time you compile your native method implementations.
The basic entry point for a JNI method implementation is:
#include "jni.h" JNIEXPORT void JNICALL Java_ClassName_MethodName( JNIEnv *env, jobject obj, jstring str ) { // native method code goes here... }
In addition to this convention, there are ways for the native code to access data passed from the JVM as well as to pass data back.
The env pointer is to a structure containing passed-in JVM information including function pointers by which to interact with the latter and access Java objects. In theory, anything that can be done in Java can be done by a JNI method implementation if less than graceful, however, the reason it is being done in (C, C++, etc.) is to shore up the inability of Java to support a native-only platform feature.
C++ has a slight leg-up on C in that access to env looks cleaner given that it is also, more or less, an object-oriented expression (see Wikipedia article). For example, to determine the calling Java class in C++ you code:
jclass class = env->FindClass( env, JAVA_CLASS_SystemNativeError );
...whereas in C you must code:
jclass class = ( *env )->FindClass( env, JAVA_CLASS_SystemNativeError );
Your friend in accessing objects as well as furnishing data back is jni.h, which ships with Sun's Java Developer Kit (JDK) and sample code you may find (like this article).
There are many additional types, practices and pitfalls, most of which I do not cover here, but some of which you will see in my code: jobject, jstring, etc. You will need to study my examples, plus read what you find on Google that concerns you.
The purpose of this document is not to show some magnificent and meaningful JNI, but expose the basic framework. Therefore, we're not necessarily going to do anything that would be best done from JNI, just something that illustrates that we can do something in C.
The important bits to take away from perusing this document are really how to structure a JNI, access the resources needed by its implementation and how to build it. In addition, we'll illustrate accessing it from Java code in an Eclipse project.
First, the structure.
Our JNI will consist of the JNI proper, a shared-object library for Linux, and some Java classes that could be included in any Java application.
While I'll show Windows DllMain.c, I'm not a Visual Studio programmer nor am I much interested in Windows issues for this article. Also, I'll show the shared-object library glue (linux_jni.c), but because I have no global data or other, complex needs, I don't even need _init() and _fini().
There isn't a great deal more to be said that an inspection of the code will not make clearer.
This is the actual JNI code where the coupling between Java method SystemCall.system() meets C. From the resulting class file (SystemCall.class), systemcall.h is generated, and included here, to create and maintain this coupling. It's correspondingly delicate, therefore. Any name or interface changes may break the compilation and even identifier naming.
Note in particular how the String argument passed in Java is handled: it's not directly used, but is retrieved in a special way (and must also be explicitly discarded in order not to leak resources).
The nucleus of this function is the call to the Linux kernel system(), which any C programmer would immediately recognize.
#include <stdlib.h> #include "errno_jni.h" #include "systemcall.h" /** * Implement system() for Java. * * @param env - the JNI environment (JNIEnv *) * @param obj - the JNI object (jobject) * @param javaCommand - the command to issue (jstring) */ JNIEXPORT jobject JNICALL Java_com_etretatlogiciels_jni_SystemCall_system( JNIEnv *env, jobject obj, jstring javaCommand ) { int err = 0; const char *command = NULL; command = ( *env )->GetStringUTFChars( env, javaCommand, 0 ); err = system( command ); ( *env )->ReleaseStringUTFChars( env, javaCommand, command ); return createErrorDetail( env, err, NULL ); }
As pointed out, this is generated by calling javah on Java class, SystemCall which contains one method, system(), itself identical to the kernel function it's fronting. It is the -o switch that makes it possible to give it a decent name (otherwise, the name is a nightmare of package elements, camel-back case classname and underscores).
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_etretatlogiciels_jni_SystemCall */ #ifndef _Included_com_etretatlogiciels_jni_SystemCall #define _Included_com_etretatlogiciels_jni_SystemCall #ifdef __cplusplus extern "C" { #endif /* * Class: com_etretatlogiciels_jni_SystemCall * Method: system * Signature: (Ljava/lang/String;)Lcom/etretatlogiciels/jni/SystemNativeError; */ JNIEXPORT jobject JNICALL Java_com_etretatlogiciels_jni_SystemCall_system (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
This is a small utility to handle creating an object of Java type SystemNativeError, to return from the JNI method. This is crucial since there are all sorts of opportunities for system() in C to fail.
#ifndef __errno_jni_h__ #define __errno_jni_h__ #include <jni.h> jobject createErrorDetail( JNIEnv *env, int error, const char *message ); #endif
#include <stdlib.h> #include <syslog.h> #include <jni.h> static const char *getErrnoComment( int error ) { int original_errno = error; char *comment = strerror( error ); return ( !comment ) ? "Unknown error" : comment; } /** Create the SystemNativeError object for our caller. * * @param env - the JNI environment (JNIEnv *) * @param error - the errno value to pass back up (int) * @param message - an additional error message to add (const char *) * * @return - the object for our caller (SystemNativeError) */ jobject createErrorDetail( JNIEnv *env, int error, const char *additional ) { jclass class = NULL; jobject errorDetail = NULL; jfieldID field = NULL; const char *message = NULL; void *string = NULL; message = getErrnoComment( error ); if( !( class = ( *env )->FindClass( env, JAVA_CLASS_SystemNativeError ) ) ) { syslog( LOG_ERR|LOG_USER, "ERROR: failed to locate Java class '%s' (JNI: errno)", JAVA_CLASS_SystemNativeError ); return NULL; } if( !( errorDetail = ( *env )->AllocObject( env, class ) ) ) { syslog( LOG_ERR|LOG_USER, "ERROR: out of memory (JNI: errno)" ); return NULL; } // -------------------------------------------------------------------------- if( !( field = ( *env )->GetFieldID( env, class, JAVA_CLASS_ERROR_FIELD, "I" ) ) ) { syslog( LOG_ERR|LOG_USER, "ERROR: failed to locate Java field '%s' in '%s' (JNI: errno)", JAVA_CLASS_ERROR_FIELD, JAVA_CLASS_SystemNativeError ); return NULL; } ( *env )->SetIntField( env, errorDetail, field, error ); // -------------------------------------------------------------------------- if( !( field = ( *env )->GetFieldID( env, class, JAVA_CLASS_MESSAGE_FIELD, "Ljava/lang/String;" ) ) ) { syslog( LOG_ERR|LOG_USER, "ERROR: failed to locate Java field '%s' in '%s' (JNI: errno)", JAVA_CLASS_MESSAGE_FIELD, JAVA_CLASS_SystemNativeError ); return NULL; } string = ( *env )->NewStringUTF( env, message ); ( *env )->SetObjectField( env, errorDetail, field, string ); // -------------------------------------------------------------------------- if( additional && *additional ) { if( !( field = ( *env )->GetFieldID( env, class, JAVA_CLASS_ADDITIONAL_FIELD, "Ljava/lang/String;" ) ) ) { syslog( LOG_ERR|LOG_USER, "ERROR: failed to locate Java field '%s' in '%s' (JNI: errno)", JAVA_CLASS_ADDITIONAL_FIELD, JAVA_CLASS_SystemNativeError ); return NULL; } string = ( *env )->NewStringUTF( env, additional ); ( *env )->SetObjectField( env, errorDetail, field, string ); } // -------------------------------------------------------------------------- ( *env )->DeleteLocalRef( env, class ); return errorDetail; }
The makefile contains all the mysteries of creating a shared-object library. There's some unnecessary extra stuff in here because of how I built this project partly on a remote Windows box and partly on Linux (keeping the files on Windows).
JDK_HOME = /home/russ/jdk1.6.0_18 CFLAGS = -I $(JDK_HOME)/include -I $(JDK_HOME)/include/linux JNI_NAME = syscall TARGETDIR = $(IDE_PATH)/lib OBJECTS = errno_jni.o sne.o system.o CLASSNAME = com.etretatlogiciels.jni.SystemCall IDE_PATH = . HEADER = systemcall.h JAVAH = $(JDK_HOME)/bin/javah SOURCEDIR = /mnt/russ/dev/hotchocolate/tutorials/jni lib$(JNI_NAME).so: $(OBJECTS) ld -shared -soname [email protected] -o [email protected] $(OBJECTS) -lc errno_jni.o: errno_jni.c errno_jni.h gcc $(CFLAGS) -fPIC -c $< system.o: system.c gcc $(CFLAGS) -fPIC -c $< clean: rm -f lib$(JNI_NAME).so.1.0 $(OBJECTS) # clean up local product plus installed product and links all-clean: clean rm -f $(TARGETDIR)/lib$(JNI_NAME).so rm -f $(TARGETDIR)/lib$(JNI_NAME).so.1 rm -f $(TARGETDIR)/lib$(JNI_NAME).so.1.0 # use target only after the Java code has been fully and correctly built gen-header: $(JAVAH) -o $(HEADER) -classpath $(IDE_PATH)/build $(CLASSNAME) install: lib$(JNI_NAME).so.1.0 cp lib$(JNI_NAME).so.1.0 $(TARGETDIR) ( cd $(TARGETDIR) ; \ ln -s ./lib$(JNI_NAME).so.1.0 lib$(JNI_NAME).so.1 ; \ ln -s ./lib$(JNI_NAME).so.1 lib$(JNI_NAME).so \ ) # use target to copy the project sources from their home locally update: cp -vR $(SOURCEDIR)/* . chmod -R u+w * # use target to erase old work, then renew file erase: rm -rf * cp -v $(SOURCEDIR)/Makefile . chmod u+w Makefile # use target to refresh all work from source refresh: erase update .PHONY: clean gen-header # vim: set tabstop=8 shiftwidth=8 noexpandtab:
My Eclipse project consists of a Java Project and one package, com.etretatlogiciels.jni with four source files. Additionally, I have a log4j.properties file and consume log4j-1.2.15.jar. I named my JNI libsyscall.so, but that is opened and consumed at runtime.
This is fairly well exposed in an illustration at the end of this document or here.
This is the class containing the native definition, SystemNativeError system( String commandline ) that will be implemented in C rather than Java.
There is also a main() method which is the one we use in order to test this facility.
package com.etretatlogiciels.jni; import org.apache.log4j.Logger; import org.apache.log4j.PropertyConfigurator; import com.etretatlogiciels.jni.JniLoader; /** * This class' purpose is to effectuate a C system call on Linux. * * @author Russell Bateman */ public class SystemCall { private static final Logger log = Logger.getLogger( SystemCall.class ); public SystemCall() { JniLoader loader = new JniLoader(); if( loader != null ) { SystemNativeError error = loader.loadJni(); String message = "Error loading JNI: " + error.getMessage() + " (" + error.getErrno() + ")"; if( error.getAdditional() != null && !error.getAdditional().isEmpty() ) message += ": " + error.getAdditional(); if( error.getErrno() != 0 ) log.error( message ); } } /** Make a system call as if in C. * * @param commandline - shell command line (String) * @return description of error (SystemNativeError) */ private native SystemNativeError system( String commandline ); /** * This method acts as if a foreign, consuming class making productive use * of our JNI. * * @param args (unused) */ public static void main( String[] args ) { PropertyConfigurator.configure( "properties/log4j.properties" ); SystemCall sc = new SystemCall(); if( sc == null ) { System.out.println( "Failed to load JNI!" ); } else { System.out.println( "Loaded JNI..." ); SystemNativeError error = sc.system( "date" ); if( error.getErrno() != 0 ) System.out.println( error.toString() ); } } }
Don't overload methods in the (Java) JNI class because if you do, javah -jni will generate even more awful names involving argument names too. Thereafter, if you modify the argument list, it breaks the method(s) in question.
Here's the format of the command:
javah -jni -d <outputdir> -o <outputname> -classpath <classpath> <fully_qualified_class>
where:
How to use it:
Switch your current working directory to the location of SystemCall.class. If you've put together your Java project using conventional wisdom, this probably isn't with the Java code, but on a parallel path. The basic command would be:
# javah -jni com.etretatlogiciels.jni.SystemCall
But we must proclaim the path to the root of the package and we want a shorter, simpler name for the generated header file, so...
# javah -jni -o systemcall.h -classpath /mnt/russ/dev/hotchocolate/tutorials/jni/build \ com.etretatlogiciels.jni.SystemCall
Copy the generated header to where it can be included by the JNI code written in C (or C++).
cp systemcall.h <C development area>
Then build away! (We actually do all of this from our makefile.)
This is the object that will, in case our main method, system(), fails, contain information on what went wrong. Even when the main method succeeds, it will contain 0, "no error," etc.
public class SystemNativeError { private int error; private String message; public final int getErrno() { return this.error; } public final String getMessage() { return this.message; } public final String toString() { return this.error + " (" + this.message + ")"; } }
Finally, this is our loader code. A JNI is in fact a shared-object library (DLL, etc.) and must be loaded when it comes time to consume interfaces out of it.
package com.etretatlogiciels.jni; import java.lang.SecurityException; import java.lang.UnsatisfiedLinkError; import org.apache.log4j.Logger; import com.etretatlogiciels.jni.OperatingSystem; /** * Use an instance of this class to load our JNI. * * @author Russell Bateman */ public class JniLoader { private static final Logger log = Logger.getLogger( JniLoader.class ); private static String DEFAULT_HOMEPATH = System.getProperty( "user.dir" ); private static String DEFAULT_PATHNAME = "lib"; private static String JNI_LIBRARY = "syscall"; private static String LINUX_SUFFIX = ".so"; private static String WINDOWS_SUFFIX = ".dll"; private static String LINUX_SEPARATOR = "/"; private static String WINDOWS_SEPARATOR= "\\"; /* Obviously, these paths cannot go unmodified if running on Windows * whose paths will be quite different. */ private String jniHomepath = DEFAULT_HOMEPATH; private String jniPathname = DEFAULT_PATHNAME; private boolean libraryLoaded = false; public JniLoader() { /* empty constructor */ } public JniLoader( String homepath, String jnipath ) { this.jniHomepath = homepath; this.jniPathname = jnipath; } public final void setJniHomepath( String path ) { this.jniHomepath = path; } public final void setJniPathname( String path ) { this.jniPathname = path; } public String getJniPathname() { return this.jniPathname; } public String getJniHomepath() { return this.jniHomepath; } /** * Load the JNI if at all possible. This must be done prior to calling * system(). * * @return a description of any error that occurs (SystemNativeError) */ public SystemNativeError loadJni() { int err = 0; String msg = null, add = null; String separator = null, prefix = null, suffix = null; SystemNativeError error = new SystemNativeError(); OperatingSystem system = OperatingSystem.system(); if( this.libraryLoaded ) // (don't go load it if already loaded) return error; if( system == OperatingSystem.LINUX ) { separator = LINUX_SEPARATOR; prefix = "lib"; suffix = LINUX_SUFFIX; } else if( system == OperatingSystem.WINDOWS ) { separator = WINDOWS_SEPARATOR; prefix = ""; suffix = WINDOWS_SUFFIX; } this.jniPathname = this.jniHomepath + separator + this.jniPathname + separator + prefix + JNI_LIBRARY + suffix; log.debug( "Opening shared-object library on " + this.jniPathname ); try { System.load( this.jniPathname ); } catch( SecurityException e ) { err = 1; msg = "security"; add = e.getMessage(); } catch( UnsatisfiedLinkError e ) { err = 2; msg = "link error"; add = e.getMessage(); } catch( Throwable e ) { err = 9; msg = "unknown"; add = e.getMessage(); } if( msg != null ) { error.setErrno( err ); error.setMessage( msg ); error.setAdditional( add ); log.error( error.getMessage() ); return error; } log.info( "JNI loaded from " + this.jniPathname ); this.libraryLoaded = true; return error; } }
Another useful utility class to help sort out platform issues.
package com.etretatlogiciels.jni; public enum OperatingSystem { LINUX, WINDOWS, UNKNOWN; private static OperatingSystem currentSystem = UNKNOWN; public static OperatingSystem system() { if( currentSystem == UNKNOWN ) { String osname = System.getProperty( "os.name" ); if( osname.startsWith( "Linux" ) ) currentSystem = LINUX; else if( osname.startsWith( "Windows" ) ) currentSystem = WINDOWS; else currentSystem = UNKNOWN; } return currentSystem; } }
Though not part of this tutorial, this file controls what is seen in the output.
# The root logger is assigned priority level DEBUG and an appender # named myAppender. log4j.rootLogger=DEBUG,myAppender # The appender's type specified as ConsoleAppender, i.e. log output # to the Eclipse console. log4j.appender.myAppender=org.apache.log4j.ConsoleAppender # The appender is assigned a layout SimpleLayout. # SimpleLayout will include only priority level of the log # statement and the log statement itself in log output. log4j.appender.myAppender.layout=org.apache.log4j.SimpleLayout
This is a snapshot of my console output at the end of this project. (Click to enlarge to full resolution.) It shows almost all the details of my Eclipse project.
This is mostly a dummy for there is nothing to do in our library. If we had global data to initialize, we were worried about threads, etc., then we'd have more code here. In this, writing a JNI is no different than writing any other shared-object library.
In fact, this isn't even necessary, at least, on Linux, because both these functions are supplied as stubs. If you don't write this code, nothing during link time will go amiss since the standard library has its own, do-nothing stubs.
int _init( void ) { return 0; } int _fini( void ) { return 0; }
This is mostly a dummy for there is nothing to do in our library. If we had global data to initialize, worried about threads, etc., then we'd have more code here. In this, writing a JNI is no different than writing any dynamically linked library.
windows_jni.h will simply define our own resources and include the Windows programming necessaries (like windows.h). Minimally, it might look like this (since we don't have any "resources" of our own to define):
Incidentally, I make no guarantees as to the utility of this Windows code.
#pragma once /* The following macros define the minimum required platform. The minimum * required platform is the earliest version of Windows, Internet Explorer etc. * that has the necessary features to run your application. The macros work by * enabling all features available on platform versions up to and including the * version specified. */ #ifndef WINVER /* Windows Vista */ # define WINVER 0x0600 #endif #ifndef _WIN32_WINNT /* Windows 98 and later */ # define _WIN32_WINNT 0x0600 #endif #ifndef _WIN32_WINDOWS # define _WIN32_WINDOWS 0x0410 #endif #ifndef _WIN32_IE /* Internet Exploder 7 and later */ # define _WIN32_IE 0x0700 #endif #define WIN32_LEAN_AND_MEAN /* exclude some rarely used stuff... */ #include
#include "windows_jni.h" BOOL APIENTRY DllMain( HMODULE hinstDLL, DWORD fdwReason, LPVOID lvpReserved ) { switch( fdwReason ) { case DLL_PROCESS_ATTACH : case DLL_THREAD_ATTACH : case DLL_THREAD_DETACH : case DLL_PROCESS_DETACH : return TRUE; } return FALSE; }
This was not relevant given how I coded this demonstration, but it seems important to note how to solve a Java classpath problem in Eclipse without perverting the actual classpath or pulling foreign files artificially into an Eclipse project in order to solve the classpath issue.
If you are consuming a JAR that itself must consume a JNI, then you must either a) ensure the parent directory of that JNI is in the Java classpath or, as I show here with the Flexera® licensing JAR that consumes a proprietary Emerson/Avocent JNI in support of the Avocent Management Platform, name that JNI's parent in the Native library location in Build Path.