JAVA Native Interface (JNI)
1. Introduction
At times, it is necessary to use native (non-Java) codes (e.g., C/C++) to overcome the memory management and performance constraints in Java. Java supports native codes via the Java Native Interface (JNI).
JNI is difficult, as it involves two languages and runtimes.
I shall assume that you are familiar with:
- Java.
- C/C++ and the GCC Compiler (Read "GCC and Make").
- (For Windows) Cygwin or MinGW (Read "How to Setup Cygwin and MinGW").
2. Getting Started
2.1 JNI with C
Step 1: Write a Java Class HelloJNI.java that uses C Codes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class HelloJNI { // Save as HelloJNI.java
static {
System.loadLibrary("hello"); // Load native library hello.dll (Windows) or libhello.so (Unixes)
// at runtime
// This library contains a native method called sayHello() } // Declare an instance native method sayHello() which receives no parameter and returns void private native void sayHello(); // Test Driver public static void main(String[] args) { new HelloJNI().sayHello(); // Create an instance and invoke the native method } } |
The static initializer invokes System.loadLibrary()
to load the native library "hello
" (which contains a native method called sayHello()
) during the class loading. It will be mapped to "hello.dll
" in Windows; or "libhello.so
" in Unixes/Mac OS X. This library shall be included in Java's library path (kept in Java system variable java.library.path
). You could include the library into Java's library path via VM argument -Djava.library.path=/path/to/lib
. The program will throw a UnsatisfiedLinkError
if the library cannot be found in runtime.
Next, we declare the method sayHello()
as a native instance method, via keyword native
which denotes that this method is implemented in another language. A native method does not contain a body. The sayHello()
shall be found in the native library loaded.
The main()
method allocates an instance of HelloJNI
and invoke the native method sayHello()
.
Step 2: Compile the Java Program HelloJNI.java & Generate the C/C++ Header File HelloJNI.h
Starting from JDK 8, you should use "javac -h
" to compile the Java program AND generate C/C++ header file called HelloJNI.h
as follows:
> javac -h . HelloJNI.java
The "-h dir
" option generates C/C++ header and places it in the directory specified (in the above example, '.'
for the current directory).
Before JDK 8, you need to compile the Java program using javac
and generate C/C++ header using a dedicated javah
utility, as follows. The javah
utility is no longer available in JDK 10.
> javac HelloJNI.java > javah HelloJNI
Inspect the header file HelloJNI.h
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif |
The header declares a C function Java_HelloJNI_sayHello
as follows:
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
The naming convention for the C function is Java_{package_and_classname}_{function_name}(JNI_arguments)
. The dot in package name is replaced by underscore.
The arguments are:
JNIEnv*
: reference to JNI environment, which lets you access all the JNI functions.jobject
: reference to "this
" Java object.
We are not using these arguments in this hello-world example, but will be using them later. Ignore the macros JNIEXPORT
and JNICALL
for the time being.
The extern "C"
is recognized by C++ compiler only. It notifies the C++ compiler that these functions are to be compiled using C's function naming protocol instead of C++ naming protocol. C and C++ have different function naming protocols as C++ support function overloading and uses a name mangling scheme to differentiate the overloaded functions. Read "Name Mangling".
Step 3: Implementing the C Program HelloJNI.c
1 2 3 4 5 6 7 8 9 10 |
// Save as "HelloJNI.c"
#include <jni.h> // JNI header provided by JDK
#include <stdio.h> // C Standard IO Header
#include "HelloJNI.h" // Generated
// Implementation of the native method sayHello()
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
return;
} |
Save the C program as "HelloJNI.c
".
The JNI header "jni.h
" provided by JDK is available under the "<JAVA_HOME>\include
" and "<JAVA_HOME>\include\win32
" (for Windows) or "<JAVA_HOME>\include\linux
" (for Ubuntu) [Check Mac OS X] directories, where <JAVA_HOME>
is your JDK installed directory (e.g., "c:\program files\java\jdk10.0.x
" for Windows).
The C function simply prints the message "Hello world!" to the console.
Step 4: Compile the C program HelloJNI.c
Finding the right compiler for your operating platform (Windows, Mac OS X, Ubuntu), for your JDK (32-bit, 64-bit), and figuring out the correct compiler options is the hardest part to get the JNI working!!!
(Windows) 64-bit JDK
We are going to use Cygwin. You need to take note of the followings for Windows:
- Windows/Intel uses these instruction sets: x86 is a 32-bit instruction set; i868 is an enhanced version of x86 (also 32-bit); x86_64 (or amd64) is a 64-bit instruction set.
- A 32-bit compiler can run on 32-bit or 64-bit (backward compatible) Windows, but 64-bit compiler can only run on 64-bit Windows.
- A 64-bit compiler could produce target of 32-bit or 64-bit.
- If you use Cygwin's GCC, the target could be native Windows or Cygwin. If the target is native Windows, the code can be distributed and run under Windows. However, if the target is Cygwin, to distribute, you need to distribute Cygwin runtime environment (
cygwin1.dll
). This is because Cygwin is a Unix emulator under Windows. - The above explains for the many versions of GCC under Cygwin.
For 64-bit JDK, you need to find a compiler that produces target of 64-bit native Windows. This is provided by MinGW-W64. You can install MinGW-W64 under Cygwin, by selecting packages "mingw64-x86_64-gcc-core
" (C compiler) and "mingw64-x86_64-gcc-g++
" (C++ compiler). The executables are "x86_64-w64-mingw32-gcc
" (C Compiler) and "x86_64-w64-mingw32-g++
" (C++ Compiler), respectively.
First, set the environment variable JAVA_HOME
to point the JDK installed directory (e.g., "c:\program files\java\jdk10.0.x
"). Follow the steps HERE.
Next, use the following commands to compile HelloJNI.c
into hello.dll
. In Windows, we reference the environment variable JAVA_HOME
as %JAVA_HOME%
in the command.
> x86_64-w64-mingw32-gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c
The compiler options used are:
-IheaderDir
: for specifying the header directory. In this case "jni.h
" (in "%JAVA_HOME%\include
") and "jni_md.h
" (in "%JAVA_HOME%\include\win32"
), whereJAVA_HOME
is an environment variable set to the JDK installed directory.-shared
: to generate share library.-o outputFilename
: for setting the output filename "hello.dll
".
You can also compile and link in two steps:
// Compile-only "HelloJNI.c" with -c flag. Output is "HElloJNI.o"
> x86_64-w64-mingw32-gcc -c -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" HelloJNI.c
// Link "HelloJNO.o" into shared library "hello.dll"
> x86_64-w64-mingw32-gcc -shared -o hello.dll HelloJNI.o
You need check the resultant file type via the "file
" utility, which indicates "Hello.dll
" is a 64-bit (x86_64) native Windows DLL.
> file hello.dll hello.dll: PE32+ executable (DLL) (console) x86-64, for MS Windows
Try nm
, which lists all the symbols in the shared library and look for the sayHello()
function. Check for the function name Java_HelloJNI_sayHello
with type "T"
(defined).
> nm hello.dll | grep say 00000000624014a0 T Java_HelloJNI_sayHello
(Windows) 32-bit JDK [Obsolete?]
For 32-bit JDK, you need to find a 32/64-bit compiler that produces target of 32-bit native Windows. This is provided by MinGW-W64 (and the older MinGW). You can install MinGW-W64 under Cygwin, by selecting packages "mingw64-i686-gcc-core
" (C compiler) and "mingw64-i686-gcc-g++
" (C++ compiler). The executables are "i886-w64-mingw32-gcc
" (C Compiler) and "i686-w64-mingw32-g++
" (C++ Compiler), respectively.
First, set the environment variable JAVA_HOME
to point the JDK installed directory (e.g., "c:\program files\java\jdk9.0.x
"). Follow the steps HERE.
Next, use the following command to compile HelloJNI.c
into hello.dll
:
> i886-w64-mingw32-gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c
The compiler options used are:
-Wl
: The-Wl
to pass linker option--add-stdcall-alias
to preventUnsatisfiedLinkError
(symbols with a stdcall suffix (@nn
) will be exported as-is and also with the suffix stripped). (Some people suggested to use-Wl,--kill-at
.)-I
: for specifying the header files directories. In this case "jni.h
" (in "%JAVA_HOME%\include
") and "jni_md.h
" (in "%JAVA_HOME%\include\win32"
), where%JAVA_HOME%
is an environment variable set to the JDK installed directory.-shared
: to generate share library.-o
: for setting the output filename "hello.dll
".-D
__int64
="long long"
: define the type (add this option in front if error "unknown type name '__int64'")
(Ubuntu) 64-bit JDK
- Set environment variable
JAVA_HOME
to point to the JDK installed directory (which shall contains theinclude
subdirectory to be used in the next step):$ export JAVA_HOME=/your/java/installed/dir$ echo $JAVA_HOME
- Compile the C program
HelloJNI.c
into share modulelibhello.so
usinggcc
, which is included in all Unixes:$ gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c
- Run the Java Program:
$ java -Djava.library.path=. HelloJNI
(Mac OS X) 64-Bit JDK
- Set environment variable
JAVA_HOME
to point to the JDK installed directory (which shall contains theinclude
subdirectory to be used in the next step):$ export JAVA_HOME=/your/java/installed/dir // for my machine @ /Library/Java/JavaVirtualMachines/jdk1.8.0_xx.jdk/Contents/Home $ echo $JAVA_HOME
- Compile the C program
HelloJNI.c
into dynamic share modulelibhello.dylib
usinggcc
, which is included in all Unixes/Mac OS:$ gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.c
- Run the Java Program:
$ java -Djava.library.path=. HelloJNI
Step 4: Run the Java Program
> java HelloJNI
You may need to explicitly specify the Java library path of the "hello.dll
" (Windows), "libHello.so
" (Unixes), "libhello.dylib
" (Mac OS X) via VM option -Djava.library.path=/path/to/lib
, as below. In this example, the native library is kept in the current directory '.'
.
> java -Djava.library.path=. HelloJNI
2.2 JNI with C++
Instead of a C program, we can use a C++ program (called HelloJNI.cpp
) for the above example.
1 2 3 4 5 6 7 8 9 10 11 |
// Save as "HelloJNI.cpp"
#include <jni.h> // JNI header provided by JDK
#include <iostream> // C++ standard IO header
#include "HelloJNI.h" // Generated
using namespace std;
// Implementation of the native method sayHello()
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
cout << "Hello World from C++!" << endl;
return;
} |
Compile the C++ programs into shared library as follows. See "JNI with C" section for explanation.
(Windows) 64-bit JDK
On Cygwin, you need to install mingw64-x86-gcc-g++
package.
> x86_64-w64-mingw32-g++ -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.cpp
(Ubuntu) 64-bit JDK
$ g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.cpp
(Mac OS X) 64-bit JDK
[TODO]
Run the Java Program
> java HelloJNI
or
> java -Djava.library.path=. HelloJNI
Notes: If you encounter "java.lang.UnsatisfiedLinkError: hello.dll: Can't find dependent libraries
", you need to find a "DLL dependency walker" to track down the dependent libraries. Search for the libraries (under Cygwin) and include the libraries in the environment variable PATH
. In my case, the dependent library is "libstdc++-6.dll
" located at "cygwin64\usr\x86_64-w64-mingw32\sys-root\mingw\bin
".
2.3 JNI with C/C++ Mixture
Step 1: Write a Java Class that uses Native Codes - HelloJNICpp.java
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class HelloJNICpp {
static {
System.loadLibrary("hello"); // hello.dll (Windows) or libhello.so (Unixes)
}
// Native method declaration
private native void sayHello();
// Test Driver
public static void main(String[] args) {
new HelloJNICpp().sayHello(); // Invoke native method
}
} |
Step 2: Compile the Java Program & Generate the C/C++ Header file HelloJNICpp.h
javac -h . HelloJNICpp
The resultant header file "HelloJNICpp.h
" declares the native function as:
JNIEXPORT void JNICALL Java_HelloJNICpp_sayHello(JNIEnv *, jobject);
Step 3: C/C++ Implementation - HelloJNICppImpl.h, HelloJNICppImpl.cpp, and HelloJNICpp.c
We shall implement the program in C++ (in "HelloJNICppImpl.h
" and "HelloJNICppImpl.cpp
"), but use a C program ("HelloJNICpp.c
") to interface with Java.
C++ Header - "HelloJNICppImpl.h
"
1 2 3 4 5 6 7 8 9 10 11 12 |
#ifndef _HELLO_JNI_CPP_IMPL_H #define _HELLO_JNI_CPP_IMPL_H #ifdef __cplusplus extern "C" { #endif void sayHello (); #ifdef __cplusplus } #endif #endif |
C++ Implementation - "HelloJNICppImpl.cpp
"
1 2 3 4 5 6 7 8 9 |
#include "HelloJNICppImpl.h" #include <iostream> using namespace std; void sayHello () { cout << "Hello World from C++!" << endl; return; } |
C Program interfacing with Java - "HelloJNICpp.c
"
1 2 3 4 5 6 7 8 |
#include <jni.h>
#include "HelloJNICpp.h"
#include "HelloJNICppImpl.h"
JNIEXPORT void JNICALL Java_HelloJNICpp_sayHello (JNIEnv *env, jobject thisObj) {
sayHello(); // invoke C++ function
return;
} |
Compile the C/C++ programs into shared library ("hello.dll
" for Windows).
(Windows) 64-bit JDK
On Cygwin, you need to install mingw64-x86-gcc-g++
package.
> x86_64-w64-mingw32-g++ -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNICpp.c HelloJNICppImpl.cpp
(Ubuntu) 64-bit JDK
$ g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNICpp.c HelloJNICppImpl.cpp
Step 4: Run the Java Program
> java HelloJNICpp
or
> java -Djava.library.path=. HelloJNICpp
Notes: If you encounter "java.lang.UnsatisfiedLinkError: hello.dll: Can't find dependent libraries
", you need to find a DLL dependency walker to track down the dependent libraries. Search for the libraries (under Cygwin) and include the libraries in the PATH
. In my case, the dependent library is "libstdc++-6.dll
" located at "cygwin64\usr\x86_64-w64-mingw32\sys-root\mingw\bin
".
2.4 JNI in Package
For production, all Java classes shall be kept in proper packages, instead of the default no-name package.
Step 1: JNI Program - myjni\HelloJNI.java
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package myjni;
public class HelloJNI {
static {
System.loadLibrary("hello"); // hello.dll (Windows) or libhello.so (Unixes)
}
// A native method that receives nothing and returns void
private native void sayHello();
public static void main(String[] args) {
new myjni.HelloJNI().sayHello(); // invoke the native method
}
} |
This JNI class is kept in package "myjni
" - to be saved as "myjni\HelloJNI.java
".
Step 2: Compile the JNI program & Generate C/C++ Header
// change directory to package base directory
> javac -h include myjni\HelloJNI
The output of compilation is "myjni\HelloJNI.class
".
In this example, we decided to place the header file under a "include
" sub-directory. The generated output is "include\myjni_HelloJNI.h
".
The header file declares a native function:
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *, jobject);
Take note of the native function naming convention: Java_<fully-qualified-name>_methodName
, with dots replaced by underscores.
Step 3: C Implementation - HelloJNI.c
1 2 3 4 5 6 7 8 |
#include <jni.h> #include <stdio.h> #include "include\myjni_HelloJNI.h" JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) { printf("Hello World!\n"); return; } |
Compile the C program:
// for Windows 64-bit JDK
> x86_64-w64-mingw32-gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c
You can now run the JNI program:
> java -Djava.library.path=. myjni.HelloJNI
2.5 JNI in Module (JDK 9)
[TODO]
2.6 JNI in Eclipse [To Check]
Writing JNI under Eclipse is handy for development Android apps with NDK.
You need to install Eclipse and Eclipse CDT (C/C++ Development Tool) Plugin. Read "Eclipse for C/C++" on how to install CDT.
Step 1: Create a Java Project
Create a new Java project (says HelloJNI
), and the following Java class "HelloJNI.java
":
public class HelloJNI {
static {
System.loadLibrary("hello"); // hello.dll (Windows) or libhello.so (Unixes)
}
// Declare native method
private native void sayHello();
// Test Driver
public static void main(String[] args) {
new HelloJNI().sayHello(); // Allocate an instance and invoke the native method
}
}
Step 2: Convert the Java Project to C/C++ Makefile Project
Right-click on the "HelloJNI
" Java project ⇒ New ⇒ Other... ⇒ Convert to a C/C++ Project (Adds C/C++ Nature) ⇒ Next.
The "Convert to a C/C++ Project" dialog appears. In "Project type", select "Makefile Project" ⇒ In "Toolchains", select "MinGW GCC" ⇒ Finish.
Now, you can run this project as a Java as well as C/C++ project.
Step 3: Generate C/C++ Header File (Pre JDK-10)
Create a directory called "jni
" under the project to keep all the C/C++ codes, by right-click on the project ⇒ New ⇒ Folder ⇒ In "Folder name", enter "jni
".
Create a "makefile
" under the "jni
" directory, by right-click on the "jni
" folder ⇒ new ⇒ File ⇒ In "File name", enter "makefile
" ⇒ Enter the following codes. Take note that you need to use tab (instead of spaces) for the indent.
# Define a variable for classpath
CLASS_PATH = ../bin
# Define a virtual path for .class in the bin directory
vpath %.class $(CLASS_PATH)
# $* matches the target filename without the extension
# Pre JDK-10. JDK 10 removes the javah utility, need to use javac -h instead [TO CHECK]
HelloJNI.h : HelloJNI.class
javah -classpath $(CLASS_PATH) $*
This makefile create a target "HelloJNI.h
", which has a dependency "HelloJNI.class
", and invokes the javah
utility on HelloJNI.class
(under -classpath
) to build the target header file.
Right-click on the makefile ⇒ Make Targets ⇒ Create ⇒ In "Target Name", enter "HelloJNI.h
".
Run the makefile for the target "HelloJNI.h
", by right-click on the makefile ⇒ Make Targets ⇒ Build ⇒ Select the target "HelloJNI.h
" ⇒ Build. The header file "HelloJNI.h
" shall be generated in the "jni
" directory. Refresh (F5) if necessary. The outputs are:
make HelloJNI.h javah -classpath ../bin HelloJNI
Read "GCC and Make" for details about makefile.
Alternatively, you could also use the CMD shell to run the make file:
// change directory to the directory containing makefile
> make HelloJNI.h
You can even use the CMD shell to run the javah
(Pre JDK-10):
> javah -classpath ../bin HelloJNI
Step 4: C Implementation - HelloJNI.c
Create a C program called "HelloJNI.c
", by right-click on the "jni
" folder ⇒ New ⇒ Source file ⇒ In "Source file", enter "HelloJNI.c
". Enter the following codes:
#include <jni.h> #include <stdio.h> #include "HelloJNI.h" JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) { printf("Hello World!\n"); return; }
Modify the "makefile
" as follows to generate the shared library "hello.dll
". (Again, use tab to indent the lines.)
# Define a variable for classpath
CLASS_PATH = ../bin
# Define a virtual path for .class in the bin directory
vpath %.class $(CLASS_PATH)
all : hello.dll
# [email protected] matches the target, $< matches the first dependency
hello.dll : HelloJNI.o
gcc -Wl,--add-stdcall-alias -shared -o [email protected] $<
# [email protected] matches the target, $< matches the first dependency HelloJNI.o : HelloJNI.c HelloJNI.h gcc -I"D:\bin\jdk1.7\include" -I"D:\bin\jdk1.7\include\win32" -c $< -o [email protected] # $* matches the target filename without the extension HelloJNI.h : HelloJNI.class javah -classpath $(CLASS_PATH) $* clean : rm HelloJNI.h HelloJNI.o hello.dll
Right-click on the "makefile
" ⇒ Make Targets ⇒ Create ⇒ In "Target Name", enter "all
". Repeat to create a target "clean
".
Run the makefile for the target "all
", by right-click on the makefile ⇒ Make Targets ⇒ Build ⇒ Select the target "all
" ⇒ Build. The outputs are:
make all javah -classpath ../bin HelloJNI gcc -I"D:\bin\jdk1.7\include" -I"D:\bin\jdk1.7\include\win32" -c HelloJNI.c -o HelloJNI.o gcc -Wl,--add-stdcall-alias -shared -o hello.dll HelloJNI.o
The shared library "hello.dll
" shall have been created in "jni
" directory.
Step 5: Run the Java JNI Program
You can run the Java JNI program HelloJNI
. However, you need to provide the library path to the "hello.dll
". This can be done via VM argument -Djava.library.path
. Right-click on the project ⇒ Run As ⇒ Run Configurations ⇒ Select "Java Application" ⇒ In "Main" tab, enter the main class "HelloJNI
" ⇒ In "Arguments", "VM Arguments", enter "-Djava.library.path=jni
" ⇒ Run.
You shall see the output "Hello World!" displayed on the console.
2.7 JNI in NetBeans
[TODO]
3. JNI Basics
JNI defines the following JNI types in the native system that correspond to Java types:
- Java Primitives:
jint
,jbyte
,jshort
,jlong
,jfloat
,jdouble
,jchar
,jboolean
for Java Primitive ofint
,byte
,short
,long
,float
,double
,char
andboolean
, respectively. - Java Reference Types:
jobject
forjava.lang.Object
. It also defines the following sub-types:jclass
forjava.lang.Class
.jstring
forjava.lang.String
.jthrowable
forjava.lang.Throwable
.jarray
for Java array. Java array is a reference type with eight primitive array and oneObject
array. Hence, there are eight array of primitivesjintArray
,jbyteArray
,jshortArray
,jlongArray
,jfloatArray
,jdoubleArray
,jcharArray
andjbooleanArray
; and one object arrayjobjectArray
.
The native functions receives argument in the above JNI types and returns a value in the JNI type (such as jstring
, jintArray
). However, native functions operate on their own native types (such as C-string, C's int[]
). Hence, there is a need to convert (or transform) between JNI types and the native types.
The native programs:
- Receive the arguments in JNI type (passed over by the Java program).
- For reference JNI type, convert or copy the arguments to local native types, e.g.,
jstring
to a C-string,jintArray
to C'sint[]
, and so on. Primitive JNI types such asjint
andjdouble
do not need conversion and can be operated directly. - Perform its operations, in local native type.
- Create the returned object in JNI type, and copy the result into the returned object.
- Return.
The most confusing and challenging task in JNI programming is the conversion (or transformation) between JNI reference types (such as jstring
, jobject
, jintArray
, jobjectArray
) and native types (C-string
, int[]
). The JNI Environment interface provides many functions to do the conversion.
JNI is a C interface, which is not object-oriented. It does not really pass the objects.
[C++ object-oriented interface?!]
4. Passing Arguments and Result between Java & Native Programs
4.1 Passing Primitives
Passing Java primitives is straight forward. A jxxx
type is defined in the native system, i.e,. jint
, jbyte
, jshort
, jlong
, jfloat
, jdouble
, jchar
and jboolean
for each of the Java's primitives int
, byte
, short
, long
, float
, double
, char
and boolean
, respectively.
Java JNI Program: TestJNIPrimitive.java
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class TestJNIPrimitive {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Declare a native method average() that receives two ints and return a double containing the average
private native double average(int n1, int n2);
// Test Driver
public static void main(String args[]) {
System.out.println("In Java, the average is " + new TestJNIPrimitive().average(3, 2));
}
} |
This JNI program loads a shared library myjni.dll
(Windows) or libmyjni.so
(Unixes). It declares a native
method average()
that receives two int
's and returns a
double
containing the average value of the two int
's. The main()
method invoke the average()
.
Compile the Java program into "TestJNIPrimitive.class
" and generate the C/C++ header file "TestJNIPrimitive.h
":
javac -h . TestJNIPrimitive.java
C Implementation - TestJNIPrimitive.c
The header file TestJNIPrimitive.h
contains a function declaration Java_TestJNIPrimitive_average()
which takes a JNIEnv*
(for accessing JNI environment interface), a jobject
(for referencing this object
), two jint
's (Java native method's two arguments) and returns a jdouble
(Java native method's return-type).
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint);
The JNI types jint
and jdouble
correspond to Java's type int
and double
, respectively.
The "jni.h
" and "win32\jni_mh.h
" (which is platform dependent) contains these typedef
statements for the eight JNI primitives and an additional jsize
.
It is interesting to note that jint
is mapped to C's long
(which is at least 32 bits), instead of of C's int
(which could be 16 bits). Hence, it is important to use jint
in the C program, instead of simply using int
. Cygwin does not support __int64
.
// In "win\jni_mh.h" - machine header which is machine dependent
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
// In "jni.h"
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;
The implementation TestJNIPrimitive.c
is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitive.h"
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
(JNIEnv *env, jobject thisObj, jint n1, jint n2) {
jdouble result;
printf("In C, the numbers are %d and %d\n", n1, n2);
result = ((jdouble)n1 + n2) / 2.0;
// jint is mapped to int, jdouble is mapped to double
return result;
} |
Compile the C program into shared library (jni.dll
).
gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o myjni.dll TestJNIPrimitive.c
Now, run the Java Program:
java -Djava.library.path=. TestJNIPrimitive
C++ Implementation - TestJNIPrimitive.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <jni.h>
#include <iostream>
#include "TestJNIPrimitive.h"
using namespace std;
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average
(JNIEnv *env, jobject obj, jint n1, jint n2) {
jdouble result;
cout << "In C++, the numbers are " << n1 << " and " << n2 << endl;
result = ((jdouble)n1 + n2) / 2.0;
// jint is mapped to int, jdouble is mapped to double
return result;
} |
Compile the C++ program:
g++ -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o myjni.dll TestJNIPrimitive.cpp
4.2 Passing Strings
Java JNI Program: TestJNIString.java
1 2 3 4 5 6 7 8 9 10 11 12 |
public class TestJNIString {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Native method that receives a Java String and return a Java String
private native String sayHello(String msg);
public static void main(String args[]) {
String result = new TestJNIString().sayHello("Hello from Java");
System.out.println("In Java, the returned string is: " + result);
}
} |
This JNI program declares a native
method sayHello()
that receives a Java String
and returns a Java String
. The main()
method invokes the sayHello()
.
Compile the Java program and generate the C/C++ header file "TestJNIString.h
":
javac -h . TestJNIString.java
C Implementation - TestJNIString.c
The header file TestJNIString.h
contains this function declaration:
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *, jobject, jstring);
JNI defined a jstring
type to represent the Java String
. The last argument (of JNI type jstring
) is the Java String
passed into the C program. The return-type is also jstring
.
Passing strings is more complicated than passing primitives, as Java's String
is an object (reference type), while C-string is a NULL-terminated char
array. You need to convert between Java String
(represented as JNI jstring
) and C-string (char*
).
The JNI Environment (accessed via the argument JNIEnv*
) provides functions for the conversion:
- To get a C-string (
char*
) from JNI string (jstring
), invoke methodconst char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)
. - To get a JNI string (
jstring
) from a C-string (char*
), invoke methodjstring NewStringUTF(JNIEnv*, char*)
.
The C implementation TestJNIString.c
is as follows.
- It receives the JNI string (
jstring
), convert into a C-string (char*
), viaGetStringUTFChars()
. - It then performs its intended operations - displays the string received and prompts user for another string to be returned.
- It converts the returned C-string (
char*
) to JNI string (jstring
), viaNewStringUTF()
, and return thejstring
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <jni.h>
#include <stdio.h>
#include "TestJNIString.h"
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
// Step 1: Convert the JNI String (jstring) into C-String (char*)
const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL);
if (NULL == inCStr) return NULL;
// Step 2: Perform its intended operations
printf("In C, the received string is: %s\n", inCStr);
(*env)->ReleaseStringUTFChars(env, inJNIStr, inCStr); // release resources
// Prompt user for a C-string
char outCStr[128];
printf("Enter a String: ");
scanf("%s", outCStr); // not more than 127 characters
// Step 3: Convert the C-string (char*) into JNI String (jstring) and return return (*env)->NewStringUTF(env, outCStr); } |
Compile the C program into shared library.
gcc -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o myjni.dll TestJNIString.c
Now, run the Java Program:
java -Djava.library.path=. TestJNIString In C, the received string is: Hello from Java Enter a String: test In Java, the returned string is: test
JNI Native String Functions
JNI supports conversion for Unicode (16-bit characters) and UTF-8 (encoded in 1-3 bytes) strings. UTF-8 strings act like null-terminated C-strings (char
array), which should be used in C/C++ programs.
The JNI string (jstring
) functions are:
// UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)
// Can be mapped to null-terminated char-array C-string
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
// Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding.
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
// Informs the VM that the native code no longer needs access to utf.
jstring NewStringUTF(JNIEnv *env, const char *bytes);
// Constructs a new java.lang.String object from an array of characters in modified UTF-8 encoding.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
// Returns the length in bytes of the modified UTF-8 representation of a string.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
// Translates len number of Unicode characters beginning at offset start into modified UTF-8 encoding // and place the result in the given buffer buf. // Unicode Strings (16-bit character) const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy); // Returns a pointer to the array of Unicode characters void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars); // Informs the VM that the native code no longer needs access to chars. jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length); // Constructs a new java.lang.String object from an array of Unicode characters. jsize GetStringLength(JNIEnv *env, jstring string); // Returns the length (the count of Unicode characters) of a Java string. void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf); // Copies len number of Unicode characters beginning at offset start to the given buffer buf
UTF-8 strings or C-strings
The GetStringUTFChars()
function can be used to create a new C-string (char*
) from the given Java's jstring
. The function returns NULL
if the memory cannot be allocated. It is always a good practice to check against NULL
.
The 3rd parameter isCopy
(of jboolean*
), which is an "in-out" parameter, will be set to JNI_TRUE
if the returned string is a copy of the original java.lang.String
instance. It will be set to JNI_FALSE
if the returned string is a direct pointer to the original String
instance - in this case, the native code shall not modify the contents of the returned string. The JNI runtime will try to return a direct pointer, if possible; otherwise, it returns a copy. Nonetheless, we seldom interested in modifying the underlying string, and often pass a NULL
pointer.
Always invoke ReleaseStringUTFChars()
whenever you do not need the returned string of GetStringUTFChars()
to release the memory and the reference so that it can be garbage-collected.
The NewStringUTF()
function create a new JNI string (jstring
), with the given C-string.
JDK 1.2 introduces the GetStringUTFRegion()
, which copies the jstring
(or a portion from start
of length
) into the "pre-allocated" C's char
array. They can be used in place of GetStringUTFChars()
. The isCopy
is not needed as the C's array is pre-allocated.
JDK 1.2 also introduces the Get/ReleaseStringCritical()
functions. Similar to GetStringUTFChars()
, it returns a direct pointer if possible; otherwise, it returns a copy. The native method shall not block (for IO or others) between a pair a GetStringCritical()
and ReleaseStringCritical()
call.
For detailed description, always refer to "Java Native Interface Specification" @ http://docs.oracle.com/javase/7/docs/technotes/guides/jni/index.html.
Unicode String
Instead of char*
, it uses a jchar*
to store the Unicode characters.
C++ Implementation - TestJNIString.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <jni.h>
#include <iostream>
#include <string>
#include "TestJNIString.h"
using namespace std;
JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
// Step 1: Convert the JNI String (jstring) into C-String (char*)
const char *inCStr = env->GetStringUTFChars(inJNIStr, NULL);
if (NULL == inCStr) return NULL;
// Step 2: Perform its intended operations
cout << "In C++, the received string is: " << inCStr << endl;
env->ReleaseStringUTFChars(inJNIStr, inCStr); // release resources
// Prompt user for a C++ string
string outCppStr;
cout << "Enter a String: ";
cin >> outCppStr;
// Step 3: Convert the C++ string to C-string, then to JNI String (jstring) and return
return env->NewStringUTF(outCppStr.c_str());
} |
Use g++
to compile the C++ program:
g++ -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o myjni.dll TestJNIString.cpp
Take note that C++ native string functions have different syntax from C. In C++, we could us "env->
", instead of "(env*)->
". Furthermore, there is no need for the JNIEnv*
argument in the C++ functions.
Also take note that C++ support a string
class (under the header <string>
which is more user-friendly, as well as the legacy C-string (char array).
[TODO] Is C++ string
class supported directly?
4.3 Passing Array of Primitives
JNI Program - TestJNIPrimitiveArray.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class TestJNIPrimitiveArray {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Declare a native method sumAndAverage() that receives an int[] and
// return a double[2] array with [0] as sum and [1] as average
private native double[] sumAndAverage(int[] numbers);
// Test Driver
public static void main(String args[]) {
int[] numbers = {22, 33, 33};
double[] results = new TestJNIPrimitiveArray().sumAndAverage(numbers);
System.out.println("In Java, the sum is " + results[0]);
System.out.println("In Java, the average is " + results[1]);
}
} |
C Implementation - TestJNIPrimitiveArray.c
The header "TestJNIPrimitiveArray.h
" contains the following function declaration:
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_average (JNIEnv *, jobject, jintArray);
In Java, array is a reference type, similar to a class. There are 9 types of Java arrays, one each of the eight primitives and an array of java.lang.Object
. JNI defines a type for each of the eight Java primitive arrays, i.e, jintArray
, jbyteArray
, jshortArray
, jlongArray
, jfloatArray
, jdoubleArray
, jcharArray
, jbooleanArray
for Java's primitive array of int
, byte
, short
, long
, float
, double
, char
and boolean
, respectively. It also define a jobjectArray
for Java's array of Object
(to be discussed later).
Again, you need to convert between JNI array and native array, e.g., between jintArray
and C's jint[]
, or jdoubleArray
and C's jdouble[]
. The JNI Environment interface provides a set of functions for the conversion:
- To get a C native
jint[]
from a JNIjintArray
, invokejint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)
. - To get a JNI
jintArray
from C nativejint[]
, first, invokejintArray NewIntArray(JNIEnv *env, jsize len)
to allocate, then usevoid SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)
to copy from thejint[]
tojintArray
.
There are 8 sets of the above functions, one for each of the eight Java primitives.
The native program is required to:
- Receive the incoming JNI array (e.g.,
jintArray
), convert to C's native array (e.g.,jint[]
). - Perform its intended operations.
- Convert the return C's native array (e.g.,
jdouble[]
) to JNI array (e.g.,jdoubleArray
), and return the JNI array.
The C implementation "TestJNIPrimitiveArray.c
" is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <jni.h>
#include <stdio.h>
#include "TestJNIPrimitiveArray.h"
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage
(JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
// Step 1: Convert the incoming JNI jintarray to C's jint[]
jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL);
if (NULL == inCArray) return NULL;
jsize length = (*env)->GetArrayLength(env, inJNIArray);
// Step 2: Perform its intended operations
jint sum = 0;
int i;
for (i = 0; i < length; i++) {
sum += inCArray[i];
}
jdouble average = (jdouble)sum / length;
(*env)->ReleaseIntArrayElements(env, inJNIArray, inCArray, 0); // release resources
jdouble outCArray[] = {sum, average};
// Step 3: Convert the C's Native jdouble[] to JNI jdoublearray, and return
jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2); // allocate
if (NULL == outJNIArray) return NULL;
(*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2, outCArray); // copy return outJNIArray; } |
JNI Primitive Array Functions
The JNI primitive array (jintArray
, jbyteArray
, jshortArray
, jlongArray
, jfloatArray
, jdoubleArray
, jcharArray
andjbooleanArray
) functions are:
// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean
NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
The GET|Release<PrimitiveType>ArrayElements()
can be used to create a new C's native array jxxx[]
from the given Java jxxxArray
. GET|Set<PrimitiveType>ArrayRegion()
can be used to copy a jxxxArray
(or a portion from start
of length
) to and from a pre-allocated C native array jxxx[]
.
The New<PrimitiveType>Array()
can be used to allocate a new jxxxArray
of a given size. You can then use the Set<PrimitiveType>ArrayRegion()
function to fill its contents from a native array jxxx[]
.
The Get|ReleasePrimitiveArrayCritical()
functions does not allow blocking calls in between the get and release.
5. Accessing Object's Variables and Calling Back Methods
5.1 Accessing Object's Instance Variables
JNI Program - TestJNIInstanceVariable.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class TestJNIInstanceVariable {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Instance variables
private int number = 88;
private String message = "Hello from Java";
// Declare a native method that modifies the instance variables
private native
|