Recently, I encountered significant trouble finding a good article that completely explains the process of calling a C++ native function from Java. I conducted some experiments and finally succeeded. I will be sharing the instructions step-by-step.
In this article, I will present a very basic example of taking an array of numbers as input in Java, computing their sum using a native C++ code, and then printing the output back to the console using Java. I will keep the article concise and encourage you to explore the official documentation if you encounter any issues.
I will be using Windows and GCC compilers for this demonstration. A 32-bit C++ compiler is compatible with a 32-bit Java compiler, and a 64-bit C++ compiler is compatible with a 64-bit Java compiler. MinGW does not publish x64 versions of GCC compilers for Windows (it is only available for Linux). Therefore, if you are using Windows, ensure that you install the 32-bit Java version. I am also assuming that you are using JDK 8 or a later version.
All the mentioned commands must be run from the same directory unless specified otherwise.
Let's begin.
How To?
1. Create a C++ shared library
We will create a shared library written in C++, along with a header file that will contain only the signature of the native method. Remember, by convention, shared libraries have a .dll extension on Windows and a .so extension on Linux.
To compile:-
> g++ -shared -fPIC NativeSumCalculator.cpp -o NativeSumCalculator.dll
> g++ -shared -fPIC NativeSumCalculator.cpp -o NativeSumCalculator.dll
The -shared flag indicates the generation of a shared library that is dynamically loaded during runtime. (If this library is supposed to be in a separate directory, then add the directory to the LD_LIBRARY_PATH environment variable.) The -fPIC flag generates position-independent code. On Windows, use the .dll extension for shared libraries. On Linux, use the .so extension. By default, Java searches for .dll files on Windows and .so files on Linux when System.loadLibrary() is used. We will be using System.load() instead of System.loadLibrary(), which takes an absolute path to avoid confusion.
Once compiled, you may delete the cpp file (but not the header file). By doing this, we will be in a situation where we have only the shared library that calculates the sum. Generally, this is always the case. The native code exists in the form of a shared library with available method signatures in a header file, and we are supposed to write a bridge and a wrapper to call such methods.
> java -XshowSettings:properties -version
2. Write the Java code and generate the JNI headers
Our Java code has a single class, SumCalculator, with a native method that, when called, will be resolved to the C++ method. Compile the Java code.
> javac SumCalculator.java
You will obtain a class file. Use the javah tool (available with the JDK) to generate the JNI header file. This JNI header file is a C++ header file and will be implemented in C++.
Note: From JDK 8 and above, the compilation and generation of the JNI header file have been combined into a single command. The -h option specifies the directory to place the generated header file. We will be placing it in the current working directory.
> javac SumCalculator.java -h .
3. Implement the header file in C++
A SumCalculator.h header file would be generated after the above step. The contents of the file would be as follows:The above header file will be implemented by the following C++ file. This C++ code will convert Java types to C++ types, call the native library function, and then return the result back in the form of Java types. You may refer to the official documentation for details regarding the conversion of Java and C++ types.
Compiling this C++ file is a bit tricky.
> g++ -shared -fPIC NativeBridge.cpp -o NativeBridge.dll -L. -l:NativeSumCalculator.dll -I "C:\Program Files\Java\jdk-12.0.1\include" -I "C:\Program Files\Java\jdk-12.0.1\include\win32"
The -L flag specifies the directory containing the shared library. (If the shared library is not located in the current working directory, you must specify the directory.) The -l flag (lowercase 'l') specifies the base name of the shared library file. The -I flag (uppercase 'I') is used to include directories that contain header files, such as jni.h and jni_md.h. These header files contain information about Java types. Replace the default C++ include folders with the actual folder containing your header files.
4. Load the library and run the Java program
We will modify the Java program to load the library by adding a static line. Please note that System.load() takes an absolute path to the library. The dependent shared library (NativeSumCalculator.dll) will be automatically loaded as it is located in the Java library location. Once loaded, recompile the program and run it. The output will be displayed.
> javac SumCalculator.java
> java SumCalculator
> java SumCalculator
Conclusion
That's it! Now, you are able to run native C++ code from Java. Such calls can be highly effective in large computations where JVMs generally prove to be slower than running a native version of the code. You might encounter the very famous UnsatisfiedLinkError for sure. In that case, please ensure that you have followed the article properly. You may mention your problem in the comments, and I will try my best to resolve them.
Comments
Post a Comment