06 December 2004

Executing native system commands with Java Native Interface.

As an alternative to the Java Runtime API, I would like to share another technique of performing native system calls from a Java program. This technique uses the Java Native Interface(JNI) which makes it possible for Java to communicate with platform-specific native libraries such as shared objects(.so) in Unix/Linux and dynamic-link libraries(DLL) in Windows. The advantage of using this technique is that native commands are passed using strings as if it is being entered into the commandline instead of instead of Runtime API's exec() method which needs some few guessing or trial-and-errors in order to get native system calls from Java working properly.

The only disadvantage for JNI is that this technique is platform-specific, meaning, what works in Linux is not or not guaranteed to work in Windows or any other platforms. Therefore, breaking the Java's concept of Write-Once-Run-Anywhere. So it's always been strongly advised that this technique should only be performed by experienced developers.

Let's start with a scenario in which there's a requirement for a Java program to execute an external native program that is written in C, The command that we are going to send to the native environment is "hostname --ip-address" this is a Linux command that retrieves the ip-address from /etc/hosts file if configured properly.

First, let's define the header file for the C program that will actually perform the native system call.

Listing 1: RunExternal.h



#include <stdio.h>
#include <string.h>



#ifndef __RUN_EXTERNAL_H
#define __RUN_EXTERNAL_H


// input:
// buffer = storage for output of external script
// sizeOfBuffer = sizeof(buffer), for max length checking..
// cmdstring = command / script to be executed
// output:
// ptr to buffer for success
// NULL for error
char *RunExternalScript(char *buffer, int sizeOfBuffer, const char *cmdstring);

#endif




Next, we will create the implementation code for RunExternal.h file.

Listing 2: RunExternal.c



#include <stdio.h>
#include <string.h>
#include "RunExternal.h"

// input:
// buffer = storage for output of external script
// sizeOfBuffer = sizeof(buffer), for max length checking..
// cmdstring = command / script to be executed
// output:
// ptr to buffer for success
// NULL for error
char *RunExternalScript(char *buffer, int sizeOfBuffer, const char *cmdstring)
{
FILE *fp;
char tmp[1024];
int buffer_length;
int tmp_length;

// do a fork(), exec(cmdstring), get a stdio file ptr (read-mode)
if (!(fp=popen(cmdstring, "r")))
{
fprintf(stderr, "Error in opening pipe for %s\n", cmdstring);
return NULL;
}

// initialize buffer = ""
buffer[0]=0;

while (!feof(fp))
{
// exit loop if NULL is read .. usually at EOF.
if (!fgets(tmp, sizeof(tmp), fp))
{
break;
}

// check for buffer overflow
buffer_length=strlen(buffer);
tmp_length=strlen(tmp);

// printf("debug: L=(%d, %d)appending tmp=<%s> to buffer=<%s>\n", buffer_length, tmp_length, tmp, buffer);

if ((buffer_length + tmp_length) >= sizeOfBuffer)
{
fprintf(stderr, "Buffer overflow.\n");
pclose(fp);
return NULL;
}

// append output to buffer, including newlines and other characters retrieved..
strcat(buffer, tmp);
}
pclose(fp);

return buffer;
}






Now that we have written the implementation the program that will call any native programs, let's build the JNI bridge by creating a native-aware class.

Listing 3: NativeCommander.java



/**
*
* Created Dec 6, 2004 9:15:39 AM
* @author Jared Odulio
*
* Description:
* This classes will perform native system calls that will act as a
* helper for CORBA servants too.
*
*
*/
public class NativeCommander {

static{
System.loadLibrary("runner");
}
/**
*
*/
public NativeCommander() {
super();
// TODO Auto-generated constructor stub
}

/**
* The entry point of native call from the Java side.
* @param cmdString the command string to be passed.
* @return the string result of the execution.
*/
public native String runCommand(String cmdString);

}



Before generating a JNI header file that above code must be compiled first, just simply do this command:



javah -jni NativeCommander.java



After compiling, a header file (.h) will be generated with the name net_smart_smspro_jni_NativeCommander.h that looks like this:

Listing 4: net_smart_smspro_jni_NativeCommander.h



/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class net_smart_smspro_jni_NativeCommander */

#ifndef _Included_net_smart_smspro_jni_NativeCommander
#define _Included_net_smart_smspro_jni_NativeCommander
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: net_smart_smspro_jni_NativeCommander
* Method: runCommand
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_net_smart_smspro_jni_NativeCommander_runCommand
(JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif




Java provides a logical way of mapping its data type to the target native data type see Mapping Between Java and Native Types for more details. Notice that jstring return type and a jstring argument has been included in the header file generation.

The next step is the implementation of the JNI-generated header file that includes the usage of the RunExternal.h implementation as well.

Listing 5: NativeCmdr.c



/*
* Created Dec. 6, 2004
* Author: Jared Odulio
*
* Description:
*
* Implementation module use to run native system calls for Java programs.
*
*/

#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include "RunExternal.h"
#include "net_smart_smspro_jni_NativeCommander.h"

JNIEXPORT jstring JNICALL Java_net_smart_smspro_jni_NativeCommander_runCommand
(JNIEnv *env, jobject obj, jstring cmd){

char buffer[8196];
char *exec;
char result[8196];
jstring retval;

const char *cmdstring = (*env)->GetStringUTFChars(env, cmd, 0);
exec = RunExternalScript(buffer, sizeof(buffer), cmdstring);
strcpy(result, exec);
(*env)->ReleaseStringUTFChars(env, cmd, cmdstring);
retval = (*env)->NewStringUTF(env, result);

return retval;

}




In the code above is the code that provides inter-communication between Java and a C program. We are almost complete and the next procedure is to compile the C programs to a shared object in order for the Java to access it. To compile in Linux see listing below:

gcc -I/opt/j2sdk1.4.2_04/include/ -I/opt/j2sdk1.4.2_04/include/linux/ *.c -o librunner.so -shared

In Linux, in order for Java's System.loadLibrary(String lib) to actually load, naming conventions has to be followed, a shared object should have "lib" as prefix and ".so" as extension. In this case, "runner" is the name of our library. The "-I" option is a flag indicating the include directories to be used by this compilation in addition to the include directories that has been defined within the native environment.

Finally, we have now our librunner.so compiled, we need to create a main Java program to test our native system calls.

Listing 6: MainJNI.java




public class MainJNI{

public static void main(String[] args){

NativeCommander nc = new NativeCommander();
//issue of the native command here are more straightfoward than Runtime API
String results = nc.runCommand("hostname --ip-address");
System.out.println("What I got: " + results);

}
}



Let's compile this class:

javac MainJNI.java

And then let's execute this class:

java -Djava.library.path=<LIB_DIR> MainJNI

where LIB_DIR is where the compile .so file is located.

If everything is done properly, a result will be displayed something like:


What I got: 127.0.0.1

or an actual IP address defined in the /etc/hosts.

This is one of the simple ways of unleashing the power of JNI, other stuff can include manipulating microcontrollers, industrial instrumentations, extending MSF COM APIs to be used for Java applications and the rest is in your imagination.


2 comments:

Warren said...

what would be the advantages of using JNI instead of direct Runtime call?

Jared said...

One of the advantages is you will be able to issue native commands "as-is" regardless of how many options needed to be passed and no need to initialize InputStream for the execution's result output so there's no need to code and flag whether the output is an "stdout" or "stderr".