Download JNI

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts
no text concepts found
Transcript
Foreign Function Interface: JNI
Justin Catterson
Abstract
Most languages contain a foreign function interface. The purpose of this is to reuse libraries that
other languages have already established, access legacy code written in other languages, and to
give programmers the ability to use multiple languages for a program to achieve better
performance. Java is a preferred programming language because it is simple to write and has a
large library, but performance issues and code that have proven the test of time has prevented
Java from becoming the language of choice. The Java Native Interface (JNI) was developed to
resolve these problems. My paper will discuss when the JNI should be used, some examples that
explain how it works, problems with the JNI, typical coding errors that may occur while
programming, and possible applications.
Introduction
Early in the development stages of Java, it was realized that Java would need a way to interface
with native languages. The first release of Java’s Development Kit (JDK 1.0) included a way to
communicate with C and C++. Libraries such as “Java.lang”, “Java.io”, and “Java.net” all used
the native interface to communicate with the host operating system. The original foreign
function interface did not take into account how different Java Virtual Machines (JVM) handle
memory and different garbage collection schemes. The Java Native Interface (JNI) was
developed in 1997 with the goal of being compatible across all JVM’s. Other concerns while
developing the JNI was to be as efficient as possible, to allow the support of time-critical
systems, and allow Java Virtual Machine’s to be embedded into native languages. The JNI has
become the standard for communicating with native interfaces in Java, and all libraries that
communicate with native languages have been re-written to follow the JNI standard. Figure 1
shows where the JNI fits into the Java Virtual Machine, if Java calls a native functions, JNI calls
the dynamically linked library that contains the native method. If the native application is calling
a library in Java, JNI will allow the native application to create a Java Virtual Machine.
2
Figure 1: How JNI communicates with both native apps and the JVM
Determining to use the JNI
Before deciding you are going to use the JNI, you should first examine your other options.
Using the JNI will add complexity to your program; it will make your program more difficult to
debug, you will also have to consider memory leaks, you will be more vulnerable to security
issues, and it can be expensive to make the call through JNI. If you decide to use JNI, you will
lose one of Java’s advantages, being portable; all code that is implemented in a native language
will not be portable and will have to be re-written to support other operating systems. For all
these reasons, it is obvious you should first review your other options before investing the effort
into developing a solution through the JNI. One way to get around using the JNI is through data
communication, such as a TCP/IP connection. In this case, the programmer will need to set-up a
client-server type system, and the server will need to determine how to interoperate the data.
Using a TCP/IP connection does come with its own risks, but this is out of scope for this paper.
Another possible solution is to use a JDBC (Java Database Connectivity) this solution simply
connects Java to a database. The native application would need also to connect to the database,
and the two programs would communicate information through the database. One risk you run
with using a database is cost, if the database is not designed properly, maintaining the system
may become expensive. Another alternative to using the JNI is by using distributed object
technology such as the Java IDL API. An IDL (Interface Definition Language) can be used to
find objects, call methods, and get returned data while on a remote system, using a TCP/IP
connection.
Although there are added complexities in using the Java Native Interface, there are some
acceptable reasons for deciding to use it. All of the alternatives expect that the native part of the
program and the Java part of the program can exist on separate processes, and sometimes on a
different computer entirely. All of the cases in which it is acceptable to use the JNI, the native
code and the Java code exist on the same process. Separating the program into processes may
cause a “memory footprint” which may be problematic. It takes less memory to load a native
library than to start a new process. Sometimes an application will need to use a feature that is not
supported in the language; if such a case rises, the JNI may be the only solution. It is also
accepted to use the JNI when you want to access an existing library from a native language. In
the field of software engineering, you want to try to use already existing code; there is no sense
in re-inventing the wheel. Code that has been rigorously tested should be used rather than
risking creating new code that has not been tested on-site or in the field in which the code will be
3
used. Java’s design concentrated on reliability, for example, Java ensures that the indices of an
array are within range. Java’s concern of reliability did come with a tradeoff, the cost of
execution time. Typically, when dealing with real-time embedded systems we have to deal with
time-critical sections. Because Java typically cannot execute as fast as some native languages
such as C, Java does not get considered as a possible solution for embedded systems or in any
situations where time is the primary concern. With the JNI, Java could be used as a possible
solution for embedded systems; we will examine this possibility later in this paper. Using the
Java Native Interface will enable programmers to enjoy some of the advantages that other
languages have to offer such as execution time.
Java to C++ Tutorial
To fully understand how the JNI works and how it could possibly be used, we must run through
some tutorials. In this section, I will instruct you how to use the Java Native Interface to call a
native function through Java. My tutorial was executed using the Windows 7 operating system,
Java Development Kit (JDK) version 1.6_20 and a copy of Microsoft Visual Studio 2010.
Although my tutorial was executed with Windows OS, JDK 1.6_20, and Visual Studio 2010, you
do not need to do the same. You will need at least JDK 1.2 and some way to compile the C++
code into a DLL. This program will read in a file from myfile.txt that I do not provide, just make
one. “myfile.txt” should include a list of numbers less than 10 digits with each number separated
by one new line character feed. Comment out the call to “divideByZero”, this was used to
demonstrate a crash.
1. Add to your system variable path the location of your JDK bin folder. In my case,
C:\Program Files (x86)\Java\jdk1.6.0_20\bin
2. Write the Java class “jniBubbleSort” seen on the right side figure 1.1, save the file as
“jniBubbleSort.java”
3. Compile the Java code with javac jniBubbleSort.java
4. Create the header file for C++ with javah jniBubbleSort
5. Write the native C++ code seen in figure 1.1, save the file as jniBubbleSort.cpp
6. Execute the vcvarsall.bat in the vc directory of Visual Studio
7. Compile the native file into a dll using using cl -Iinclude -Iinclude\win32 -MD –LD
jniBubbleSort.cpp -FejnijniBubbleSort.dll. The –Iinclude and –Iinclude\win32 is where
the linker for jni.h and jni_md.h files exists respectively. The –MD links the
jniBubbleSort.dll with the Win32 multithreaded C library.
8. Now you should be able to execute the program using java jniBubbleSort
4
Figure 2: Java to C++ using the JNI
From the C++ code, you might be wondering what the extra parameters, JNIEnv * and the
jobject are for. The JNIEnv pointer points to the list of JNI functions that are available for the
C++ code to use. The JNIEnv can be used to access Java classes, methods, and fields that are a
part of the Java Virtual Machine, but it is not used in this tutorial. In a later tutorial I will be
using the JNIEnv to access a field in a Java class. For a list of JNI functions please visit
http://download.oracle.com/javase/1.5.0/docs/guide/jni/spec/functions.html. The jobject field is
a reference to the Java object in which the native method belongs to. You can think of the
jobject field passed as the reserved word ‘this’ in C++. If any parameters were intended to be
passed, they would appear after the two parameters that are generated from the JNI.
Earlier in this paper, “Determining to use the JNI”, it was stated that it is acceptable to use the
JNI to use libraries previously written. In order to use previously used libraries, you will need to
make a wrapper class due to the code generated by the JNI. To use a library previously written,
create a Java class that will contain all the functions in the legacy library. For each native
method created, you will then be able to call the native function you desire and return any data
desired.
C++ to Java: Embedding a JVM Tutorial
As shown in figure 1.0, the JNI may also be used to embed a Java Virtual Machine within a
native application. In order to embed a JVM into a native application, you must add to the
5
system path environment variable to the file location of where the JVM.dll exists. In my case I
found the jvm.dll file in the folder location “C:\Program Files
(x86)\Java\jdk1.6.0_20\jre\bin\client”. Without the jvm.dll folder location in the environment
variable path, you will be unable to initialize an instance of the Java Virtual Machine. To
compile this program as an executable use cl -Iinclude -Iinclude\win32 -MD Sample2.cpp FeRunMain.exe -link "C: \Program Files (x86)\Java\jdk1.6.0_20\lib\jvm.lib". In this tutorial
created by IBM developerWorks, we will create a JVM and call a Java method to square the
number 5. We will first create the JVM, and then find the class that contains the method to
square our number, sample2.java. To find the Java class, we will use FindClass(“sample2”).
After finding the Java class we will find the Method ID of the static method intMethod. To find
the static method’s Method ID we use getStaticMethodID passing three parameters.
Figure 3: C++ to Java using the JNI
1. the Java class that was found using FindClass
2. The name of the method we are trying to find, intMethod
3. Lastly a string to represent the signature of the parameter that will be passed into the
method and the return type
For “intMethod”, there is only one parameter, an integer, the method also returns an integer, so
the third parameter of “getStaticMethodID” looks like, “(I)I”. For a full list of signature types
please visit http://download.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html.
In this example, most of the code is for initializing the Java Virtual Machine. My tutorial has the C code
and the Java code in separate directories, so I must set an option to point to the Sample2.class directory, Djava.class.path=”c:/Users/Justin/Desktop/SeniorSem/cToJava/JavaCode”.
Mapping Types
6
Because Java and native languages such as C treat types differently, we must investigate how the
Java Native Interface maps types. For example, Java treats arrays as objects and C treats arrays
as pointers to memory locations. In the JNI, it treats types as either a primitive type or a
reference type. Primitive types are defined as a specific number of bits and are either signed or
unsigned. An example of a primitive type is a char. A character in Java is an unsigned 16-bit
Unicode character. The Java character is mapped to an unsigned short in C called a jchar. In
most cases, you can treat the jchar as a char in C, but note that the character set in Java can be
twice as large as the ASCII table supported in C (8 bits). Reference types include everything that
does not have a pre-defined bit size. The JNI splits reference types into a few different classes,
each which inherit from jObject see figure 4. When using Java reference types, you cannot use
the same syntax as you would if the type was native to C. For example, you cannot use
myClass.doIt() to call the objects myClass’s doIt method. If you would like to call myClass’s
doIt method you must find the class of the object and the method id to call the method using the
JNI functions. In a later tutorial I will be showing this. There is also added syntax for accessing
arrays. To access elements of a Java array, you must either copy over the elements of the array
into a C array or retrieve a pointer to which the Java array exists. If the array size is small or you
only want a small section of an array, it is typical to copy the elements into a C array with
Get<Type>ArrayRegion, but if the array size is large it may get expensive to make a C copy of
the array.
Within the different classes of references, they categorize these objects by how they can be
removed from the virtual machine. There are local references, global references, and weak
global references. Local references are objects that have been new’d by a native method and
only exist during the execution of the native method. Local references are common because
there is nothing that needs to be done to specify that the object is a local reference. If you want
to make an object C static within a method, meaning there is only one reference that exists to the
object within memory, you should not use a local reference. If a static object is declared local, it
will not exist after the execution of the native method, and later calls to the native method could
cause memory corruption or could cause the program to crash. To allow the C program to
declare objects static, the programmer must use newGlobalRef or newWeakGlobalRef see figure
5 (code taken from #1) on the declarations. Global references exist until the programmer
specifies to delete the object using DeleteGlobalRef. Weak references do not need to be
specified to be deleted from memory, they can be garbage collected, but they may also be deleted
by the programmer using deleteWeakGlobalRef.
7
Figure 4: JNI objects
Figure 5: Code using a global reference
JNI Issues
As stated in “Determining to use the JNI”, there are many issues that are related to using the JNI.
In this section we will investigate the security problem, memory management issues, and check
out the overhead cost of using the Java Native Interface. By using the JNI, it becomes very easy
to read/write to data is Java you should not be able to. Because of the JNIEnv pointer, you have
access to all classes and fields. So long as the native language knows about a class, it will be
able to read/write to private fields and overwrite the values of constants. See figure 1.4 for code
I wrote that exposes this issue and many other issues. This is not the only problem related to
native languages accessing memory in Java’s heap. Typically for accessing large arrays that are
a part of the Java’s heap because it can be expensive to make a copy of the array in the native
language, you get a pointer to the array instead. With a pointer to the array, it is easy to go
outside the bounds of the array and accidently read/overwrite data in Java’s heap not part of the
8
array. When using the JNI, you will be responsible for memory management on the native
language side. Although in C, this is nothing new, you will need to mange memory for the Java
Virtual Machine. In my example, figure 6, I am not only accessing a private field, I have a
memory leak. The memory leak exists because my call getStringUTFChars does not have a
corresponding releaseStringUTFChars. Be very conscious about all JNI calls you make and
ensure that you do not need to make a corresponding call to release memory. This example is
also missing error checks. For every call from the native language to Java, you will need to
check that errors have not occurred because the native language will not throw the exception.
Figures 3 and 5 are using error checking for finding the classes and finding the method id’s. In
figure 6, it probably should check that square has a value. Returning reference types from native
languages can also cause problems. Let’s say you wanted to return an UnsafePerson seen in
example 1.4 in a native function. The return type for this native function would be a jObject, so
it would be really easy to return a different jObject than what the Java code was expecting.
Figure 6: JNI coding issue example
In order to determine whether or not making use of the JNI for time critical situations, we
investigate how much time it takes to make a call through the Java Native Interface. Demetruis
L. Davis wrote benchmark tests for two algorithms, heap sort and Discrete Fast Fourier
transform. The tests he ran were not optimized so these programs are not testing how good the
optimization is for the compilers. In his research he runs his tests with different array sizes to
check the impact of array sizes on Java and on C++. He also runs a number of iterations to
determine how expensive it is to make the call through the JNI. The results are shown in three
graphs on figure 7 separated by array size. This graph demonstrates, for this case it is worth
making the call through the JNI to execute the C++ algorithm rather than executing the
equivalent Java implementation. The far right graph on figure 7 shows a large performance hit
for the JNI implementation, but this is not caused from the expense of the JNI. The author states
that this graph most likely is due to memory limitations on the computer used for testing. For
more statistical data test visit [4]. These metrics will be better at determining how much time it
9
will cost for different JNI operations and on different Just-In-Time compilers. For most JNI
operations, it takes a couple hundred nanoseconds, so typically it is fairly negligible.
Figure 7: JNI execution time vs. pure Java execution time
JNI Applications
Now that we have investigated when we shouldl use the JNI and understand how some of the
problems that can occur, we can begin to look into where it would be applicable to use the Java
Native Interface in industry. C and assembly languages dominate the market for small
embedded systems because of its efficiency and its ease to communicate with other languages.
As embedded projects become larger and more complex, it becomes more difficult to maintain
and to develop. Java has become more attractive for developing large embedded projects
because there are a large number of developers that are familiar with the language and Java’s
large library makes it easier to develop than C/C++. With the help of the JNI, Java could be used
to write the “upper stream” [7] and have the C code control the devices. With Java being
independent from the host platform, the Java software could be reused on different systems,
reducing the cost of distributing the software on different machines.
The Android phones follow this exact development scheme. Applications such as contacts, email, browser settings, and all applications that come with the Android package are written in
Java. Other programs that manage resources, such as voice applications are mostly written in
Java. The Android does use C/C++ for CPU tasks and to drive other peripherals. Even Androids
Virtual Machine, Dalvik Virtual Machine, is written in C.
10
Design Conclusions
This paper has shown you when it is acceptable to use the Java Native Interface. Once it has
been decided to use the JNI, you must try to avoid some of the issues related. Designing a good
system will also help prevent some of the issues that may happen such as memory leaks and keep
the overhead of the JNI minimal. Try to keep the control flow simple, code that goes back and
forth between the JVM and native code, is probably designed incorrectly. Every time you make
calls back and forth you are wasting time. Keep the native code to a minimum, by doing this you
will prevent memory leaks that could occur, and your code will be less complex. Separate your
Java code from your native code. Native code is platform dependent, by separating the Java
code from the native code; you will have a porting layer. A possible application for the JNI is
for large embedded projects. Keep the microcontrollers written in C and have the “upper
stream” such as applications within the embedded system written in Java. If you are interested in
learning more about embedded development using the JNI, I would suggest starting with
Android development.
References
[1] Sheng Liang (June 1999). The Java Native Interface Programmer’s Guide and Specification.
Retrieved from http://java.sun.com/docs/books/jni/download/jni.pdf
[2] Gang Tan; Andrew W. Appel; Srimat Chakradhar; Anand Raghunathan; Srivaths Ravi;
Daniel Wang (2006). Safe Java Native Interface.
Retrieved from http://www.cs.princeton.edu/~appel/papers/safejni.pdf
[3] Scott Stricker(March 2002). Java Programming with JNI.
Retrieved from http://www.ibm.com/developerworks/java/tutorials/j-jni/
[4] Dawid Kurzyniec; Vaidy Sunderam. Efficient Cooperation between Java and Native
Codes - JNI Performance Benchmark.
Retrieved from http://janet-project.sourceforge.net/papers/jnibench.pdf
[5] Demetrius L. Davis. To JNI or not to JNI?
Retrieved from http://www.ewp.rpi.edu/hartford/~rhb/cs_seminar_2004/SessionC3/davis.pdf
[6] Preetham Chandrian (August 2011). Efficient Java Native Interface for Android based
Mobile Devices.
Retrieved from http://repository.asu.edu/items/9315
[7] Nguyen Thi Thu Trang; Tran Canh Toan; Nguyen Manh Tuan; Takenobu Aoshima
(October 31, 2007). An experience in developing embedded software using JNI.
Retrieved from js.vnu.edu.vn/tn_2_08/b7.pdf