Developing a Java Native Interface (JNI)

last update: 2 April 2010

Table of Contents

What is a JNI?
Toolstack and resources
JNI calling conventions
JNIEnv *env
Other types and practices
Structure
C code
Java project
The Linux source code "project"
system.c
systemcall.h
errno_jni_h
errno_jni_c
Makefile
The Java (Eclipse) project
SystemCall.java
Notes on running javah
SystemNativeError.java
JniLoader.java
OperatingSystem.java
Error logging
log4j.properties
Console output
Appendix: _init() and _fini()
Appendix: Windows code and issues (DllMain())
Appendix: Denoting a native library

Introduction

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

Prerequisites

The tutorial assumes you have familiarity with C and Java programming and building on Linux—nothing very complicated at all.

What is a Java Native Interface (JNI)?

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.

Documents and links

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.

Toolstack and resources

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.

JNI calling conventions

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.

JNIEnv *env

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

Other types and practices

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.

Structure

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.

C code

Java project

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.

The Linux source code "project"

system.c:

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

systemcall.h:

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

errno_jni.h (and errno_jni.c):

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

errno_jni.c

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

Makefile:

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:

The Java (Eclipse) project

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.

SystemCall.java:

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

Notes on running javah

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

More Java code

SystemNativeError.java

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

JniLoader.java:

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

OperatingSystem.java:

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

Error logging

properties/log4j.properties:

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

Console output

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.

Appendix: _init() and _fini()

linux_jni.c:

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

Appendix: Windows code and issues

windows_jni.h and DllMain.c:

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

Appendix: Denoting a native library

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.