How to Package JNI Shared Library into Jar File

The article is not about how to create Java native methods invoking C/C++ APIs. It aims to help developers to build a jar package containing JNI shared library, made with Dynamsoft Barcode Reader, for Windows, Linux, and macOS from scratch. I will demonstrate how to create JNI shared libraries with CMake, as well as how to package class files and shared libraries into jar files with Eclipse and Maven.

How to Use Maven with Dynamsoft Barcode Reader

Dynamsoft has provided a full Java development package. You can use it with Maven as follows:

<repositories>
    <repository>
      <id>dbr</id>
      <url>https://download.dynamsoft.com/maven/dbr/jar</url>
    </repository>
  </repositories>
<dependencies>
    <dependency>
      <groupId>com.dynamsoft</groupId>
      <artifactId>dbr</artifactId>
      <version>6.2</version>
    </dependency>
  </dependencies>

If you are not interested in how to build such a jar package step by step, you can ignore the following paragraphs.

How to Define Native Methods in Java

Create a new Maven project in Eclipse.

Eclipse maven project

Create a Java class NativeBarcodeReader.java:

public class NativeBarcodeReader {
	
	private long nativePtr = 0;

	static {
		if (System.getProperty("java.vm.vendor").contains("Android")) {
			System.loadLibrary("dbr");
		} else {
			try {
				if (NativeLoader.load()) {
					System.out.println("Successfully loaded Dynamsoft Barcode Reader.");
				}
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	public NativeBarcodeReader() {
		nativePtr = nativeCreateInstance();
	}
	
	public void destroyInstance() {
		if (nativePtr != 0)
			nativeDestroyInstance(nativePtr);
	}
	
	public void setLicense(String license) {
		nativeInitLicense(nativePtr, license);
	}
	
	public void decodeFile(String fileName) {
		nativeDecodeFile(nativePtr, fileName);
	}

	private native int nativeInitLicense(long nativePtr, String license);
	
	private native long nativeCreateInstance();
	
	private native void nativeDestroyInstance(long nativePtr);
	
	private native void nativeDecodeFile(long nativePtr, String fileName);
}

Note: If use System.load() method, you have to load all dependencies before loading the JNI shared library.

Eclipse will automatically compile the source code to a class file. Use javah to generates a C header NativeBarcodeReader.h from the class in jni folder.

mkdir jni
cd jni
javah -cp ..\target\classes -o NativeBarcodeReader.h com.dynamsoft.barcode.NativeBarcodeReader

Create the corresponding NativeBarcodeReader.cxx file:

#include "NativeBarcodeReader.h"
#include "DynamsoftBarcodeReader.h"

#ifdef __cplusplus
extern "C"
{
#endif

    /*
    * Class:     com_dynamsoft_barcode_NativeBarcodeReader
    * Method:    nativeInitLicense
    * Signature: (JLjava/lang/String;)I
    */
    JNIEXPORT jint JNICALL Java_com_dynamsoft_barcode_NativeBarcodeReader_nativeInitLicense(JNIEnv *env, jobject, jlong hBarcode, jstring license)
    {
        const char *pszLicense = env->GetStringUTFChars(license, NULL);

        if (hBarcode)
        {
            DBR_InitLicense((void *)hBarcode, pszLicense);
        }

        env->ReleaseStringUTFChars(license, pszLicense);
        return 0;
    }

    /*
    * Class:     com_dynamsoft_barcode_NativeBarcodeReader
    * Method:    nativeCreateInstance
    * Signature: ()J
    */
    JNIEXPORT jlong JNICALL Java_com_dynamsoft_barcode_NativeBarcodeReader_nativeCreateInstance(JNIEnv *, jobject)
    {
        return (jlong)DBR_CreateInstance();
    }

    /*
    * Class:     com_dynamsoft_barcode_NativeBarcodeReader
    * Method:    nativeDestroyInstance
    * Signature: (J)V
    */
    JNIEXPORT void JNICALL Java_com_dynamsoft_barcode_NativeBarcodeReader_nativeDestroyInstance(JNIEnv *, jobject, jlong hBarcode)
    {
        if (hBarcode)
        {
            DBR_DestroyInstance((void *)hBarcode);
        }
    }

    /*
    * Class:     com_dynamsoft_barcode_NativeBarcodeReader
    * Method:    nativeDecodeFile
    * Signature: (JLjava/lang/String;)V
    */
    JNIEXPORT void JNICALL Java_com_dynamsoft_barcode_NativeBarcodeReader_nativeDecodeFile(JNIEnv *env, jobject, jlong ptr, jstring fileName)
    {
        if (ptr)
        {
            void *hBarcode = (void *)ptr;
            const char *pszFileName = env->GetStringUTFChars(fileName, NULL);

            DBR_DecodeFile(hBarcode, pszFileName, "");

            STextResultArray *paryResult = NULL;
            DBR_GetAllTextResults(hBarcode, &paryResult);

            int count = paryResult->nResultsCount;
            int i = 0;
            for (; i < count; i++)
            {
                printf("Index: %d, Type: %s, Value: %s\n", i, paryResult->ppResults[i]->pszBarcodeFormatString, paryResult->ppResults[i]->pszBarcodeText); // Add results to list
            }

            // Release memory
            DBR_FreeTextResults(&paryResult);

            env->ReleaseStringUTFChars(fileName, pszFileName);
        }
    }

#ifdef __cplusplus
}
#endif

The code here is pretty simple. You can add more implementations if you like.

SDK

Download Dynamsoft Barcode Reader for Windows, Linux and macOS.

Copy OS-dependent shared libraries to jni/platforms folder.

  • jni/platforms/win
    • DBRx64.lib
    • DynamicPdfx64.dll
    • DynamsoftBarcodeReaderx64.dll
    • vcomp110.dll
  • jni/platforms/linux
    • libDynamicPdf.so
    • libDynamsoftBarcodeReader.so
  • jni/platforms/macos
    • libDynamsoftBarcodeReader.dylib

How to Use CMake to Build JNI Shared Library

Create a CMakeLists.txt file.

Define Java include and lib directories:

if (CMAKE_HOST_WIN32)
    set(WINDOWS 1)
    set(JAVA_INCLUDE "C:/Program Files/Java/jdk1.8.0_181/include")
    set(JAVA_INCLUDE_OS "C:/Program Files/Java/jdk1.8.0_181/include/win32")
elseif(CMAKE_HOST_APPLE)
    set(MACOS 1)
    set(JAVA_INCLUDE "/System/Library/Frameworks/JavaVM.framework/Headers")
    set(JAVA_INCLUDE_OS "")
elseif(CMAKE_HOST_UNIX)
    set(LINUX 1)
    set(JAVA_INCLUDE "/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/")
    set(JAVA_INCLUDE_OS "/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux")
endif()

if(WINDOWS)
    link_directories("${PROJECT_SOURCE_DIR}/platforms/win" "C:/Program Files/Java/jdk1.8.0_181/lib") 
    include_directories("${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/include" "${PROJECT_SOURCE_DIR}" "${JAVA_INCLUDE}" "${JAVA_INCLUDE_OS}")
elseif(LINUX)
    link_directories("${PROJECT_SOURCE_DIR}/platforms/linux") 
    include_directories("${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/include" "${PROJECT_SOURCE_DIR}" "${JAVA_INCLUDE}" "${JAVA_INCLUDE_OS}")
elseif(MACOS)
    link_directories("${PROJECT_SOURCE_DIR}/platforms/macos") 
    include_directories("${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/include" "${PROJECT_SOURCE_DIR}" "${JAVA_INCLUDE}")
endif()

Link Dynamsoft Barcode Reader to generate JNI shared libraries for different platforms. Rename the file suffix from dylib to jnilib for macOS:

add_library(dbr SHARED NativeBarcodeReader.cxx)
if(MACOS)
    set_target_properties(dbr PROPERTIES SUFFIX ".jnilib")
endif()
if(WINDOWS)
    if(CMAKE_CL_64)
        target_link_libraries (dbr "DBRx64")
    else()
        target_link_libraries (dbr "DBRx86")
    endif()
else()
    target_link_libraries (dbr "DynamsoftBarcodeReader")
endif()

Install libraries:

set(CMAKE_INSTALL_PREFIX "${PROJECT_SOURCE_DIR}/../src/main/")
set(ECLIPSE_PATH "java/com/dynamsoft/barcode/native")
set(MAVEN_PATH "resources/com/dynamsoft/barcode/native")
if(WINDOWS)
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/platforms/win" DESTINATION "${CMAKE_INSTALL_PREFIX}${ECLIPSE_PATH}")
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/platforms/win" DESTINATION "${CMAKE_INSTALL_PREFIX}${MAVEN_PATH}")
    install (TARGETS dbr DESTINATION "${CMAKE_INSTALL_PREFIX}${ECLIPSE_PATH}/win")
    install (TARGETS dbr DESTINATION "${CMAKE_INSTALL_PREFIX}${MAVEN_PATH}/win")
elseif(LINUX)
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/platforms/linux" DESTINATION "${CMAKE_INSTALL_PREFIX}${ECLIPSE_PATH}")
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/platforms/linux" DESTINATION "${CMAKE_INSTALL_PREFIX}${MAVEN_PATH}")
    install (TARGETS dbr DESTINATION "${CMAKE_INSTALL_PREFIX}${ECLIPSE_PATH}/linux")
    install (TARGETS dbr DESTINATION "${CMAKE_INSTALL_PREFIX}${MAVEN_PATH}/linux")
elseif(MACOS)
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/platforms/macos" DESTINATION "${CMAKE_INSTALL_PREFIX}${ECLIPSE_PATH}")
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/platforms/macos" DESTINATION "${CMAKE_INSTALL_PREFIX}${MAVEN_PATH}")
    install (TARGETS dbr DESTINATION "${CMAKE_INSTALL_PREFIX}${ECLIPSE_PATH}/macos")
    install (TARGETS dbr DESTINATION "${CMAKE_INSTALL_PREFIX}${MAVEN_PATH}/macos")
endif()

The ECLIPSE_PATH  is used to export jar files by Eclipse, whereas the  MAVEN_PATH is used to export jar files by Maven.

Build the JNI shared library.

Windows

E.g., Visual Studio 2017

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

Linux & macOS

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

How to Package Native Libraries into Jar File

Edit the pom.xml file to add the directory of shared libraries:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.dynamsoft</groupId>
    <artifactId>barcode</artifactId>
    <version>1.0.0</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <excludes>
                    <exclude>**/*.md</exclude>
                    <exclude>**/*.h</exclude>
                    <exclude>**/*.lib</exclude>
                </excludes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Generate the jar file in the target folder:

mvn package

Maven JNI shared library

Alternatively, you can use Eclipse to export the jar file.

Eclipse export JNI shared library

Here is the generated jar file.

java jni jar

How to Load JNI Shared Libraries from a Jar File

List all dependent library files:

String[] filenames = null;
		if (Utils.isWindows()) {
			filenames = new String[] {"vcomp110.dll", "DynamicPdfx64.dll", "DynamsoftBarcodeReaderx64.dll", "dbr.dll"};
		}
		else if (Utils.isLinux()) {
			filenames = new String[] {"libDynamicPdf.so", "libDynamsoftBarcodeReader.so", "libdbr.so"};
		}
		else if (Utils.isMac()) {
			filenames = new String[] {"libDynamsoftBarcodeReader.dylib", "libdbr.jnilib"};
		}
		
		boolean ret = true;
		
		for (String file : filenames) {
			ret &= extractAndLoadLibraryFile(dbrNativeLibraryPath, file, tempFolder);
		}

Extract the shared libraries to the temporary folder:

private static boolean extractAndLoadLibraryFile(String libFolderForCurrentOS, String libraryFileName,
			String targetFolder) {
		String nativeLibraryFilePath = libFolderForCurrentOS + "/" + libraryFileName;

		String extractedLibFileName = libraryFileName;
		File extractedLibFile = new File(targetFolder, extractedLibFileName);

		try {
			if (extractedLibFile.exists()) {
				// test md5sum value
				String md5sum1 = md5sum(NativeBarcodeReader.class.getResourceAsStream(nativeLibraryFilePath));
				String md5sum2 = md5sum(new FileInputStream(extractedLibFile));

				if (md5sum1.equals(md5sum2)) {
					return loadNativeLibrary(targetFolder, extractedLibFileName);
				} else {
					// remove old native library file
					boolean deletionSucceeded = extractedLibFile.delete();
					if (!deletionSucceeded) {
						throw new IOException(
								"failed to remove existing native library file: " + extractedLibFile.getAbsolutePath());
					}
				}
			}

			// Extract file into the current directory
			InputStream reader = NativeBarcodeReader.class.getResourceAsStream(nativeLibraryFilePath);
			FileOutputStream writer = new FileOutputStream(extractedLibFile);
			byte[] buffer = new byte[1024];
			int bytesRead = 0;
			while ((bytesRead = reader.read(buffer)) != -1) {
				writer.write(buffer, 0, bytesRead);
			}

			writer.close();
			reader.close();

			if (!System.getProperty("os.name").contains("Windows")) {
				try {
					Runtime.getRuntime().exec(new String[] { "chmod", "755", extractedLibFile.getAbsolutePath() })
							.waitFor();
				} catch (Throwable e) {
				}
			}

			return loadNativeLibrary(targetFolder, extractedLibFileName);
		} catch (IOException e) {
			System.err.println(e.getMessage());
			return false;
		}

	}

Load libraries with absolute path:

private static synchronized boolean loadNativeLibrary(String path, String name) {
		File libPath = new File(path, name);
		if (libPath.exists()) {
			try {
				System.load(new File(path, name).getAbsolutePath());
				return true;
			} catch (UnsatisfiedLinkError e) {
				System.err.println(e);
				return false;
			}

		} else
			return false;
	}

Run the test:

java -cp ./target/barcode-1.0.0.jar com.dynamsoft.barcode.Test

References

Source Code

https://github.com/dynamsoft-dbr/java-jni-barcode