Java Programming: JNA vs. JNI on Windows

Some developers used JNA to call native C/C++ interfaces of Dynamsoft Barcode Reader in Java program. The app ran slowly and sometimes crashed. Based on the use case, I created a simple project for building JNA and JNI on Windows. The sample does not only show how to invoke native code but also share how to check the Java thread stack size and solve the stack overflow issue on Windows.

Download and Installation

Accessing Native Code via JNA and JNI

Open Eclipse to create a new project. The structures of JNA and JNI folders are almost the same.

eclipse project

Java code

Create the Java function decodefile() for JNA and JNI:

JNA JNI Java

C/C++ code

For JNA, you just need to create a standard function and export it. For JNI, you have to comply with the JNI naming convention.

JNA JNI native code

Building shared libraries with CMake

Copy shared libraries to **/jna(jni)/platforms/win**:

  • DBRx64.lib
  • DBRx86.lib
  • DynamsoftBarcodeReaderx64.dll
  • DynamsoftBarcodeReaderx86.dll

Create CMakeLists.txt:

cmake_minimum_required (VERSION 2.6)
project (dbr)
MESSAGE( STATUS "PROJECT_NAME: " ${PROJECT_NAME} )

# Check platforms
if (CMAKE_HOST_WIN32)
    set(WINDOWS 1)
    set(JAVA_HOME "E:/Program Files (x86)/Java/jdk1.8.0_191")
    set(JAVA_INCLUDE "${JAVA_HOME}/include")
    set(JAVA_INCLUDE_OS "${JAVA_HOME}/include/win32")
endif()

MESSAGE( STATUS "JAVA_INCLUDE: " ${JAVA_INCLUDE})

# Configure a header file to pass some of the CMake settings
# to the source code
configure_file (
    "${PROJECT_SOURCE_DIR}/BarcodeReaderConfig.h.in"
    "${PROJECT_BINARY_DIR}/BarcodeReaderConfig.h"
)

# Add search path for include and lib files
if(WINDOWS)
    link_directories("${PROJECT_SOURCE_DIR}/platforms/win" "${JAVA_HOME}/lib") 
    include_directories("${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/include" "${PROJECT_SOURCE_DIR}" "${JAVA_INCLUDE}" "${JAVA_INCLUDE_OS}")
endif()

# Add the executable
add_library(jnadbr SHARED NativeBarcodeReader.cxx)
if(WINDOWS)
    if(CMAKE_CL_64)
        target_link_libraries (jnadbr "DBRx64")
    else()
        target_link_libraries (jnadbr "DBRx86")
    endif()
endif()

# Set installation directory
set(CMAKE_INSTALL_PREFIX "${PROJECT_SOURCE_DIR}/../")
if(WINDOWS)
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/platforms/win/" DESTINATION "${CMAKE_INSTALL_PREFIX}" FILES_MATCHING PATTERN "*.dll")
    install (TARGETS jnadbr DESTINATION "${CMAKE_INSTALL_PREFIX}")
endif()

To build the shared library for JNI, replace jnadbr' with jnidbr’.

Build the project for 64-bit Java runtime:

cd jna
mkdir build
cd build
cmake -G"Visual Studio 15 2017 Win64" .. 
cmake --build . --config Release --target install

Create Demo.java for test:

public static void main(String[] args) {
		int count = 1;
		
		JNABarcode jnaReader = new JNABarcode();
		
		System.out.println("JNA start.");
		long start = System.currentTimeMillis();
		for (int i = 0; i < count; i++) {
			jnaReader.decodefile();
		}
		long end = System.currentTimeMillis();
		System.out.println("JNA end. Time cost: " + (end - start) + "ms");
		
		JNIBarcode jniReader = new JNIBarcode();
		System.out.println("JNI start.");
		start = System.currentTimeMillis();
		for (int i = 0; i < count; i++) {
			jniReader.decodefile();
		}
		end = System.currentTimeMillis();
		System.out.println("JNI end. Time cost: " + (end - start) + "ms");
	}

Run the app:

javac -cp libs\jna-3.0.9.jar src\com\dynamsoft\*.java
java -cp libs\jna-3.0.9.jar;src\ com.dynamsoft.Demo

JNA init time

Why is running JNA much slower than running JNI?

The native code is the same, why the performance is different? The only possible reason is when calling the native method the first time it takes a long time to initialize the JNA environment.

jna

We can call the native method to initialize the JNA environment before measuring the performance.

JNA initialization

Build and rerun the app. The JNA performance of invoking native method looks no different to JNI.

JNA JNI performance

Now we get the conclusion that using JNA takes extra loading time.

Java VM Stack Overflow Exception

The stack overflow exception showed up when runtime environment switched from 64-bit to 32-bit.

JVM stack overflow

According to https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/crashes001.html, the issue is caused by thread stack size. To solve the issue, we’d better know the default thread stack size. Let’s try the following command:

Linux

java -XX:+PrintFlagsFinal -version | grep ThreadStackSize

Linux thread stack size

Windows

java -XX:+PrintFlagsFinal -version | findstr ThreadStackSize

Windows thread stack size

The value of stack size is 0 on Windows. Maybe the answer is in the source code.

Download and install TortoiseHg.

Get JVM source code:

hg clone http://hg.openjdk.java.net/jdk8/jdk8/hotspot/

Search for ‘stack size’ to find the following informative paragraph.

stack size on Windows

To verify whether the default stack size is 320K on Windows, I tried the code snippet located in os_windows.cpp:

typedef u_char* address;
#include <windows.h>

address current_stack_base() {
  MEMORY_BASIC_INFORMATION minfo;
  address stack_bottom;
  size_t stack_size;

  VirtualQuery(&minfo, &minfo, sizeof(minfo));
  stack_bottom =  (address)minfo.AllocationBase;
  stack_size = minfo.RegionSize;
  
  while( 1 )
  {
    VirtualQuery(stack_bottom+stack_size, &minfo, sizeof(minfo));
    if ( stack_bottom == (address)minfo.AllocationBase )
      stack_size += minfo.RegionSize;
    else
      break;
  }

#ifdef _M_IA64
#endif
  
  return stack_bottom + stack_size;
}

size_t sz;
  MEMORY_BASIC_INFORMATION minfo;
  VirtualQuery(&minfo, &minfo, sizeof(minfo));
  sz = (size_t)current_stack_base() - (size_t)minfo.AllocationBase;
  printf("The default Java thread stack size for this system is %u bytes(%u KB).\n", sz, sz / 1024);

To avoid stack overflow exception, we should allocate big chunks of memory on the heap instead of the stack.

memory allocation

Re-compile the native code in x86 mode:

cd jna
mkdir build
cd build
cmake .. 
cmake --build . --config Release --target install

Compile and run the Java code with 32-bit JDK:

"E:\Program Files (x86)\Java\jdk1.8.0_191\bin\javac.exe" -cp libs\jna-3.0.9.jar src\com\dynamsoft\*.java
"E:\Program Files (x86)\Java\jdk1.8.0_191\bin\java.exe" -cp libs\jna-3.0.9.jar;src\ com.dynamsoft.Demo

default stack size

We can see the default stack size is 320K by default on Windows. To enlarge the stack size, append -Xss option when running Java program:

"E:\Program Files (x86)\Java\jdk1.8.0_191\bin\java.exe" -Xss1024 -cp libs\jna-3.0.9.jar;src\ com.dynamsoft.Demo

References

Source Code

https://github.com/yushulx/jna-jni-windows