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.
-- Jeff Kuhn <jeffrey.kuhn@yale.edu>
