Building Java Native Interfaces

Although SCons supports the creation of Java Native Interface (JNI) header files via JavaH(), building and linking C or C++ JNI libraries is another matter. SCons does not report where the JNI include files or libraries are stored. We need that information to point to the header files and/or libraries needed by JNI. The following example shows how to build JNI libraries on multiple platforms using Sun's Java Development Kit (JDK).

The python code ConfigureJNI.py below first searches for a shell environment variable JAVA_HOME. If JAVA_HOME is not found, it then searches for the java compiler and uses this information to set JAVA_HOME. From the java home directory, the build environment's CPPPATH and LIBPATH are set appropriately. Additional CCFLAGS, SHLINKFLAGS, and SHLIBSUFFIX variables are updated in the build environment to allow cygwin or OS X (darwin) to properly build and link a shared library suitable for JNI.

file: ConfigureJNI.py

   1 import os
   2 import sys
   3 
   4 def walkDirs(path):
   5     """helper function to get a list of all subdirectories"""
   6     def addDirs(pathlist, dirname, names):
   7         """internal function to pass to os.path.walk"""
   8         for n in names:
   9             f = os.path.join(dirname, n)
  10             if os.path.isdir(f):
  11                 pathlist.append(f)
  12     pathlist = [path]
  13     os.path.walk(path, addDirs, pathlist)
  14     return pathlist
  15 
  16 def ConfigureJNI(env):
  17     """Configure the given environment for compiling Java Native Interface
  18        c or c++ language files."""
  19 
  20     if not env.get('JAVAC'):
  21         print "The Java compiler must be installed and in the current path."
  22         return 0
  23 
  24     # first look for a shell variable called JAVA_HOME
  25     java_base = os.environ.get('JAVA_HOME')
  26     if not java_base:
  27         if sys.platform == 'darwin':
  28             # Apple's OS X has its own special java base directory
  29             java_base = '/System/Library/Frameworks/JavaVM.framework'
  30         else:
  31             # Search for the java compiler
  32             print "JAVA_HOME environment variable is not set. Searching for java... ",
  33             jcdir = os.path.dirname(env.WhereIs('javac'))
  34             if not jcdir:
  35                 print "not found."
  36                 return 0
  37             # assuming the compiler found is in some directory like
  38             # /usr/jdkX.X/bin/javac, java's home directory is /usr/jdkX.X
  39             java_base = os.path.join(jcdir, "..")
  40             print "found."
  41 
  42     if sys.platform == 'cygwin':
  43         # Cygwin and Sun Java have different ideas of how path names
  44         # are defined. Use cygpath to convert the windows path to
  45         # a cygwin path. i.e. C:\jdkX.X to /cygdrive/c/jdkX.X
  46         java_base = os.popen("cygpath -up '"+java_base+"'").read().replace( \
  47                  '\n', '')
  48 
  49     if sys.platform == 'darwin':
  50         # Apple does not use Sun's naming convention
  51         java_headers = [os.path.join(java_base, 'Headers')]
  52         java_libs = [os.path.join(java_base, 'Libraries')]
  53     else:
  54         # windows and linux
  55         java_headers = [os.path.join(java_base, 'include')]
  56         java_libs = [os.path.join(java_base, 'lib')]
  57         # Sun's windows and linux JDKs keep system-specific header
  58         # files in a sub-directory of include
  59         if java_base == '/usr' or java_base == '/usr/local':
  60             # too many possible subdirectories. Just use defaults
  61             java_headers.append(os.path.join(java_headers[0], 'win32'))
  62             java_headers.append(os.path.join(java_headers[0], 'linux'))
  63             java_headers.append(os.path.join(java_headers[0], 'solaris'))
  64         else:
  65             # add all subdirs of 'include'. The system specific headers
  66             # should be in there somewhere
  67             java_headers = walkDirs(java_headers[0])
  68 
  69     # add Java's include and lib directory to the environment
  70     env.Append(CPPPATH = java_headers)
  71     env.Append(LIBPATH = java_libs)
  72 
  73     # add any special platform-specific compilation or linking flags
  74     if sys.platform == 'darwin':
  75         env.Append(SHLINKFLAGS = '-dynamiclib -framework JavaVM')
  76         env['SHLIBSUFFIX'] = '.jnilib'
  77     elif sys.platform == 'cygwin':
  78         env.Append(CCFLAGS = '-mno-cygwin')
  79         env.Append(SHLINKFLAGS = '-mno-cygwin -Wl,--kill-at')
  80 
  81     # Add extra potentially useful environment variables
  82     env['JAVA_HOME'] = java_base
  83     env['JNI_CPPPATH'] = java_headers
  84     env['JNI_LIBPATH'] = java_libs
  85     return 1

Example

The following example illustrates a very simple java native interface function. The java class jsrc/HelloWorld.java below attempts to load a shared library named HelloWorldImp (HelloWorldImp.dll on windows or cygwin, libHelloWorldImp.jnilib on OS X, libHelloWorldImp.so on linux). The java main function calls a native function named displayHelloWorld().

The code for displayHelloWorld() is included in the file csrc/HelloWorldImp.cpp below. displayHelloWorld() prints the all too familiar message on stdout.

file: jsrc/HelloWorld.java

class HelloWorld {
    public native void displayHelloWorld();

    static {
        System.loadLibrary("HelloWorldImp");
    }

    public static void main(String[] args) {
        new HelloWorld().displayHelloWorld();
    }
}

file: csrc/HelloWorldImp.cpp

#include <stdio.h>
#include <jni.h>
#include "HelloWorld.h"

JNIEXPORT void JNICALL
Java_HelloWorld_displayHelloWorld(JNIEnv *env, jobject obj)
{
    printf("Hello world!\n");
    return;
}

The SCons build files below are used to build this simple example. Since java classes are platform independent, they are compiled into the subdirectory "classes".

C++ classes are platform dependent so they are compiled and linked into the subdirectory "lib-platform" such as "lib-win32", "lib-cygwin", "lib-linux", "lib-darwin", etc.

SConstruct sets up the build environment and SConscript build the java and native code.

file: SConstruct

   1 import os
   2 import sys
   3 from ConfigureJNI import ConfigureJNI
   4 
   5 if sys.platform == 'win32':
   6     # MS Visual C++ is found from the registery, not the PATH
   7     env = Environment()
   8 else:
   9     # we need the path to find java
  10     env = Environment(ENV = {'PATH' : os.environ['PATH']})
  11 
  12 if not ConfigureJNI(env):
  13     print "Java Native Interface is required... Exiting"
  14     Exit(0)
  15 
  16 SConscript('SConscript', exports = 'env')

file: SConscript

   1 import os
   2 import sys
   3 Import('env')
   4 
   5 def PrependDir(dir, filelist):
   6     return [os.path.join(dir,x) for x in filelist]
   7 
   8 # compile java classes into platform independent 'classes' directory
   9 jni_classes = env.Java('classes', 'jsrc')
  10 jni_headers = env.JavaH('csrc', jni_classes)
  11 
  12 # compile native classes into platform dependent 'lib-XXX' directory
  13 # NOTE: javah dependencies do not appear to work if SConscript was called
  14 # with a build_dir argument, so we take care of the build_dir here
  15 native_dir = 'lib-' + sys.platform
  16 native_src = PrependDir(native_dir, env.Split("""HelloWorldImp.cpp"""))
  17 env.BuildDir(native_dir, 'csrc', duplicate=0)
  18 env.SharedLibrary(native_dir+'/HelloWorldImp', native_src)

Building

Create a directory and place the five files listed here in the following directory structure:

ConfigureJNI.py
SConstruct
SConscript
jsrc/HelloWorld.java
csrc/HelloWorldImp.cpp

Then run scons:

C:\Devel\jni> scons

Testing

When testing this example, remember that Java must be able to find the shared library.

On windows, the library must be in the current directory or somewhere in the PATH. To test this example on windows, build it with scons, change to the directory containing the DLL and run the java class

C:\Devel\jni> cd lib-win32

C:\Devel\jni\lib-win32> java -cp ..\classes HelloWorld
Hello World!

On linux, Java searches LD_LIBRARY_PATH for shared libraries. However, the LD_LIBRARY_PATH rarely contains the current directory, so it must be added. To test this example on linux, first update LD_LIBRARY_PATH, then change to the directory containing the shared library and run the java class.

[user@localhost ~/jni]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.

[user@localhost ~/jni]$ cd lib-linux

[user@localhost ~/jni/lib-linux]$ java -cp ../classes HelloWorld
Hello World!

On OS X, Java searches the current directory or /Library/Java/Extension for shared libraries. Note that shared JNI libraries on OS X need to have the extension .jnilib. This is taken care of by ConfigureJNI() above.

[user@localhost ~/jni]% cd lib-darwin

[user@localhost ~/jni/lib-darwin]% java -cp ../classes HelloWorld
Hello World!

Remarks

The goal of this example is to encourage cross-platform building of Java Native Interface files. Ideally, most or all of the platform-dependent setup should be taken care of in ConfigureJNI.py, rather than in SConstruct or SConscript.

I have tested the above example on windows, cygwin, linux, and OS X 10.2.

JavaNativeInterface (last edited 2009-11-25 09:13:02 by s235-128)