In this article, routines written in C/C++ are called native functions. The JNI API allows programming in Java without requiring the existing native, possibly legacy, libraries to be entirely rebuilt, including native system libraries. JNI API can mainly be used in two ways:
- Creating a library that can be called from a Java application
- Embedding JVM into a C/C++ application and executing Java code with the JNI Invocation API.
Programming with the JNI is generally reserved for experienced programmers who need to take advantage of platform-specific function outside of JVM. Programming with JNI API on AIX requires expert knowledge of:
- C and/or C++
- Java
- JNI API
- AIX OS
- AIX C/C++ compilers
The power and flexibility that come with JNI come at the cost of portability. The WORA principle certainly does not apply when JNI is involved. JNI specifications are defined by Sun Microsystems Inc. Only JNI specification 1.0 native methods were specific to Sun's JVM. JNI specifications starting with 1.1 define JVM-neutral native method interface, variable access, and method invocation JNI API. However, compiler variants and options to create shared libraries for use through JNI API are different on different platforms. More importantly, the JNI specification does not dictate how JNI is to be implemented, so JNI implementation varies from one JVM vendor to another. Sun JCK and JNI specification assert conformance to the specification, but not to an implementation. Problems might arise with code that was developed with the assumption of methods implementation, instead of strictly complying with specification.
General JNI programming considerations
This section discusses general JNI programming issues, such as performance, exception handling, and memory management.
Writing portions of the application, especially those that are heavily used, in native code and linking it with Java is usually intended to improve performance. However, communication between JVM and native code is generally slow, and too many JNI calls can degrade performance. Handling exceptions within JNI native code itself can also degrade performance.
Handling exceptions natively can be essential when integrating native code with Java. JNI allows native functions to catch exceptions themselves, or to throw exceptions back to JVM. Handling exceptions within native code can degrade performance, but is necessary if JNI function returns error instead of success status. Handling exceptions within JNI code itself usually requires usage of the following JNI functions: ExceptionOccurred(), IsInstanceOf(), ExceptionCheck(), and ExceptionClear() to clear the exception. These functions are computationally expensive, ExceptionCheck() being less expensive than ExceptionOccurred(), as the latter has to create an object to be referred to as well as a local reference.
When an exception has been thrown, no further JNI function calls should be made after the exception, except those calls that deal with exception, until flag has been cleared. Further calls may lead to unpredictable results, including JVM abnormal termination. Programmers should check for any exceptions that can be fixed natively, clear the exception, then fix it. If exception is not checked, the next JNI code invocation will detect pending exception and throw it. Debugging exceptions thrown from a different point in the code than the point where the exception was actually created will definitely be more complicated.
Using native code increases the chance of memory leaks. JVM garbage collector (GC) no longer shields programmers from the necessity of deallocating used native resources. Some JNI interface functions help in releasing previously allocated resources: ReleaseStringUTFChars(), ReleaseStringChars(), DeleteGlobalRef(), DeleteLocalRef(), and so on.
Local references
References from native code to Java objects on the Java heap might not be in a form traceable by GC. JNI automatically creates a local reference to any object that is referenced from native code on the Java heap, which is essentially a pointer to an address on the Java heap created in the respective thread stack.
Local references are valid for the duration of a native function call. They are freed automatically when out of scope after the native function returns, but passed to all the functions on the stack. The local reference is passed to all the functions called within the function that created local reference originally.
With local references there are two common issues to consider:
- Losing local reference
- When the method in which local reference was created exits, that local reference goes out of scope. An object may still exist on the Java heap, but a pointer to it - from the native function stack local reference area that GC can see - is lost. This object is eligible for collection as unreachable, as far as GC is concerned. GC cannot trace any native references from any native thread stack that may exist. At this point, use of native references that might exist on the native stack can be fatal, as nothing guarantees existence of the object on the Java heap during the next GC cycle.
- Local reference capacity
- Each local reference costs some amount of JVM resource. Although local references are automatically freed after the native method returns to Java, excessive allocation of local references may cause JVM to run out of memory during the execution of a native method. Function
EnsureLocalCapacity (JNIEnv *env, jint capacity)ensures that at least a given number of local references can be created in the current thread. It returns 0 on success; otherwise, it returns a negative number and throws anOutOfMemoryError. Before it enters a native method, JVM automatically ensures that at least 16 local references can be created as dictated by JNI specification. For backward compatibility, JVM allocates local references beyond the ensured capacity. For debugging support, JVM may give a warning that too many local references are being created:***ALERT: JNI local ref creation exceeded capacityIn the Java 2 SDK, programmers can supply the
-verbose:jnicommand line option to turn on these messages. The JNI specification does not dictate local reference capacity of a JVM, nor does it require use of this message. This message might or might not appear, and at different times, in JVMs from different vendors.
Copied versus pinned data
Converting arrays from Java to native code is complex because, as opposed to native languages, elements in Java arrays are not always placed together in contiguous memory. When a native function requires access to an array created in Java, JNI must organize the array correctly, then must ensure that the array is returned back to JVM when native function completes.
There is a substantial difference between creating object arrays and primitive type arrays through JNI. Function NewObjectArray() takes as arguments array size (jint), the array type (jclass), and initial value for each array element (usually NULL). Creating arrays of primitives is similar, though New<primitive_type>Array() takes only one argument, which is the size of the array. Each of these functions builds a Java array. However, the elements of that array may not be contiguous in memory. To obtain access to individual elements of a primitive array within native code requires a call to Get<primitive_type>ArrayElements, with the j<primitive_type>Array and jboolean as arguments. The jboolean is passed by reference, and is set by the function to either JNI_TRUE or JNI_FALSE. If the array is laid out in contiguous memory, the resulting jboolean is JNI_FALSE, meaning that native code has a direct access pointer to the array, and that data is pinned. Otherwise, JNI_TRUE means that a copy of the array was created, and it can be moved in a GC compaction cycle or collected.
Whether jboolean is JNI_TRUE or JNI_FALSE or not, the contract that the JNI specification makes is that a reference returned by Get<primitive_type>ArrayElements is valid until the corresponding Release<type>ArrayElements() is executed. JNI specification states "It is not possible to predict whether any given JVM will copy or pin data on any particular JNI call." If jboolean is JNI_FALSE, the array will be pinned, preventing JVM from garbage collecting it or moving it during the GC compaction phase. The JNI specification states clearly that one must Release<primitive_type>ArrayElements() when one has finished using an array reference. It is not conditional on the reference being a copy; it does not matter if the jboolean variable is JNI_TRUE or JNI_FALSE, Release<primitive_type>ArrayElements() must be called to avoid memory leak, and update the array in JVM.
IBM JVM generally uses the pinning implementation. A common programming error is to free only copied data, so that the heap gradually becomes more and more fragmented and filled with chunks of pinned data until eventually failure occurs. This is another example of problems that might arise if an application was written relying on a particular JVM implementation instead of strictly adhering to the JNI specification. Freeing only copied data would probably work correctly on a JVM that prefers copy method. If the application is migrated to a pinning JVM or a JVM changes its implementation, the code will fail if not written to the specification.
As a general rule, Release<type> () should always be called after any function that uses a jboolean flag that shows whether returned data is a copy or pinned.
Release<primitive_type>ArrayElements() must be used, requiring the arguments: JNIEnv interface pointer, the array itself, a pointer to the array, and the mode in which it is to be released. If the data was pinned, any changes made to it were copied directly into the Java heap, so mode parameter is ignored. If the jboolean flag indicates return data as a copy, the mode flag should be used to make changes made to the data. Generally, to avoid implementation-specific details of JVMs from different vendors, the most generic way is to pass NULL (0) for jboolean flag and always pass 0 for the mode flag.
Unlike an array of primitive types, there is no handle to the object array in JVM in a format that C/C++ can use. There is no way to convert a JVM object into a C++ object; native code must access it in JVM memory space through JNI. There is no need to release JVM's object arrays since they are never copied from the JVM's memory space, so JVM's GC will take care of that.
This section covers JNI on AIX samples, API versions, shared libraries, multithreading, and AIX compilers.
Programmers testing their skills with JNI API on AIX for the first time are strongly encouraged to install the sample fileset for AIX JDK, and become familiar with JNI code examples and information. For AIX JDK 1.4.1 filesets are Java14.samples for 32 bit version, and Java14_64.samples for 64-bit version. It is also recommended you read the JDK User Guide, especially the sections pertinent to JNI compatibility.
In JDK 1.4.1 it is no longer possible to use the JNI 1.1 interface. You can't call JNI_CreateJavaVM() and pass it a version of JNI_VERSION_1_1(0x00010001). The versions that can be passed are JNI_VERSION_1_2(0x00010002) and JNI_VERSION_1_4(0x00010004).
The most common programming errors when creating and using JNI shared libraries are:
- Not including both libjava.a (../jre/bin) and libjvm.a (../jre/bin/classic) in the LIBPATH for programs that embed and start JVM through JNI Invocation API.
- Setting SETUID on the binary executable that starts JVM through JNI Invocation API.
- Not including, or not having correct permissions on, directories holding JNI shared objects.
- Trying to load 32-bit shared objects into a 64-bit process, or vice versa.
- Not creating a thread-safe reentrant shared object for multi-threaded applications.
The environment variable LIBPATH tells AIX applications, such as the JVM, where to find shared libraries. This use of LIBPATH is equivalent to the use of LD_LIBRARY_PATH in other Unix-based systems. The shared libraries for the JVM are in the ./jre/bin and ./jre/bin/classic subdirectories of the Java installation. For compatibility with scripts designed for other Unix-based systems, the Java launcher programs prepend LD_LIBRARY_PATH, if it is set, to the beginning of LIBPATH. However, if your application needs to search specific directories when looking for shared libraries, the preferred variable to set is LIBPATH. If the application starts JVM from its code using Invocation API, you must include both of the above-mentioned directories to load both libjava.a and libjvm.a.
If the program binary executable has SETUID bit on, the LIBPATH variable is automatically cleared when the program starts due to security reasons. For JNI Invocation API application that loads libraries dynamically (using dlopen()) can prevent calls that need LIBPATH to search for shared objects, such as the JNI_CreateJVM() call, from working since the LIBPATH is no longer set up properly. Either the SETUID bit needs to be removed, or the application needs to be run as root user. When SETUID permission bit needs to be preserved, another alternative that might work, depending on the actual environment would be to set LIBPATH explicitly in the code with the setenv() statement. For example,
setenv("LIBPATH","/usr/java14/jre/bin:/usr/java14/jre/bin/classic", 1); |
When the JNI application throws java.lang.UnsatifiedLinkError, it usually indicates a problem with the way the application was written, compiled, or configured. Most common programming errors are: not having a correct library path for System.loadLibrary() to search, permissions on directories in the path, or calling native methods before loading the library. Generally, the library should be loaded in a static block, ensuring that the library is loaded before any native methods are called.
Trying to load a 64-bit shared library into a 32-bit JVM process address space, or vice versa, will also most likely result in java.lang.UnsatisfiedLinkError. There is no way to load a 64-bit object into a 32-bit process space, or vice versa. The linker appropriately selects objects from the library based on the type of linking that is requested (32-bit or 64-bit), and creates an object or application of that type.
JNI shared libraries to be loaded into JVM must be created as reentrant thread-safe objects, with appropriate compiler variants, as discussed in more detail in AIX compilers.
The IBM JVM for AIX uses the AIX POSIX pthreads package for threading. Java threads created by the JVM use the POSIX pthreads model supported on AIX. Currently, this is on a 1-to-1 mapping with the kernel threads. When developing a JNI program, you must run with a 1-to-1-thread model and system contention scope if creating pthreads in your own program. This can be controlled using the following environment setting:
export AIXTHREAD_SCOPE=S |
Another option is to preset in the code the thread's scope attribute to PTHREAD_SCOPE_SYSTEM using the AIX pthread_attr_setscope() function when the thread is created.
By default, the AIXTHREAD_MUTEX_DEBUG, AIXTHREAD_RWLOCK_DEBUG and AIXTHREAD_COND_DEBUG environment variables are set to OFF. These three variables disable the lists of mutexes, read-write locks, and condition variables that are used by the debugger, thus reducing the overhead of maintaining the lists. They need to be set to OFF for JNI Invocation API programs.
If your JNI application is multithreaded, special attention must be paid to POSIX specifications implemented on AIX. Some POSIX specifications are left undefined and therefore platform-specific. Native code implementing multithreading might need to be changed when porting JNI code from one OS to another. The threads library provides some advanced features to be used by trained programmers. Implementation of the advanced features and attributes is optional, and depends on POSIX options. It might require special attention when porting the application from a non-AIX platform to AIX, or between different AIX releases.
For example, you might need to set pthread stack size. The pthread stacksize attribute is defined in AIX. It depends on the stack size POSIX option, which might not be implemented on other systems. The stacksize attribute specifies the minimum stack size that will be allocated for a thread. The pthread_attr_getstacksize() subroutine returns the value of the attribute, and the pthread_attr_setstacksize() subroutine sets the value. If the value of stacksize is less than 96KB, a stack of 96KB is to be allocated by default in current AIX implementation of the threads library. The allocation is always a multiple of 4KB.
The stack size of the main thread is controlled by the user-based ulimit parameter. The stack size of the JVM-created threads, such signal handler thread, finalizer thread, and so on, is controlled by the native thread stack size -Xss parameter. It defaults to:
- 256K for JDK 122 and 130
- 512K for JDK 131 and JDK 1.4.1 32-bit
- 1M for JDK 1.4.1 64-bit
The values are subject to change. If a thread is created without specifying the stack size with pthread_attr_setstacksize(), thread stack size defaults to AIX base value. Joining the thread created without specifying the stack size (created with JNI_CreateJavaVM() through AttachCurrentThread()) can result in stack overflow and SIGSEGV. Small default AIX pthread stack size might not be enough for computational complexity requirements usually presented to JVM threads. See the following code excerpt that summarizes issues mentioned in this section:
pthread_attr_t attr; pthread_t tid; pthread_attr_init(&attr); pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); /* sets system contention scope for thread */ pthread_attr_setstacksize(&attr, 512*1024); /* sets stack size to 512K */ default for 32-bit JVM 1.4.1 and 1.3.1 threads */ pthread_create(&tid, &attr, launchThread, NULL); pthread_join(tid,&status); |
It is very important to have the latest fixes applied to the IBM C/C++ compiler you use, and the latest fixes for the C++ RTE (xlC.* filesets) installed. Base levels and updates for the xlC environment are available at the time of this writing from ftp://aix.software.ibm.com/aix/products/ccpp/.
You must use thread-safe reentrant compiler variants of VAC and VACPP, such as xlC_r or xlc_r/cc_r, to compile C++ or C code, respectively, into thread-safe reentrant shared objects that are to be linked into a shared library. Use ld for C code, and makeC++SharedLib_r for C++ code, for later loading into JVM process space through JNI. Alternatively, you would need to manually pass to non-reentrant compiler variant macros and add libraries that the above-mentioned reentrant thread-safe compiler variants use by default.
AIX – Advanced Interactive Executive Operating System
API – Application Programming Interface
GC – Garbage Collector
JCK – Sun Java Compatibility Kit
JDK – Java Development Kit
JNI – Java Native Interface
JRE – Java Runtime Environment
JVM – Java Virtual Machine
OS – Operating System
RTE – Run Time Environment
VAC – IBM Visual Age C Compiler
VACPP – IBM Visual Age C++ Compiler
WORA – Write Once Run Anywhere
- Read the book
Java Native Interface: Programmer's Guide and Specification
, which is a definitive resource and a comprehensive guide to working with the JNI.
-
Trail: Java Native Interface
- See JNI Enhancements
Introduced in version 1.2 of the JavaTM 2 SDK for information on new functions and enhancements.
-
JNI Enhancements
Introduced in Version 1.4 of the JavaTM 2 SDK has a wealth of information.
-
IBM developer kits - diagnosis documentation provides diagnosis information that relates to IBM's 1.3.1 SDKs and 1.4.1 SDKs.
Comments (Undergoing maintenance)





