One of the main hurdles I encountered while working to create Tellas was getting the Java Native Interface to work and link everything correctly. In this post I’ll go about explaining exactly how I went about this process.
The first step, preferably, is to have a working C++ program to interface with. This is what you’ll be running with the JNI, and you’ll want to have the build process setup with CMake. The program you’re using, eg. if using OpenGL, may already have dependencies that are linked using CMake. If so, it integrates into this system nicely. Otherwise, it isn’t too tricky to make your own. Firstly you’ll need to actually import JNI. This shouldn’t be too tricky, with just the given lines:
find_package(JNI REQUIRED)
include_directories(${JNI_INCLUDE_DIRS})
Then all you need to do for JNI is specify the files to use. A lot of the time, overlapping #includes for these can cause problems, so you need to be careful only to add those not included by any. This might look something like this:
set(SOURCE_FILES
main.cpp
src/glad.c)
add_library(OpenGLProject SHARED ${SOURCE_FILES})
This will make a library out of your C++ code, that the Java will use. Through the process of generating JNI, there is a bit of recompiling, so expect to build it often. The build is remarkably simply, and done simply by two commands:
cd build
# This is simply moving to a build directory
cmake ../
make PROJECT_NAME
# PROJECT_NAME is just whatever you called the project
# in the CMake file
Now you have a basic C++ side setup, the next step is the Java!
The process is relatively simple on this side, but somewhat tricky to complete without error. First the Java needs to load the library, which it does using this command
// Loads the C++
System.load(
FileSystems.getDefault
.getPath("build/libPROJECT_NAME.so")
.normalize.toAbsolutePath.toString);
Of course, you’ll need to replace the path with that appropriate. Depending on the structure, you may need to experiment to ensure it have the correct relative path. It should give an error whenever it fails, so you should know when this step has succeeded. When it does, you can start looking at the Java JNI calls. It’s a straightforward process, and can be done simply by the native keyword:
public static native void method(int input);
Or in Scala:
@native def method(input: Int): Unit
Now this file has a native method. To create the C++ side of this method, you need to use the javah command. This generates the header file that you reference. You reference it by the name relative to the package – eg. for a file
package src;
class JNITest{
public static native void method(int input);
}
Would use the command:
javah src.JNITest
Be aware this works on .class files, so you’ll need to compile the project first before using javah on it.
This will generate a complex header file, and contain the method in something like this:
JNIEXPORT void JNICALL Java_src_JNITest_method
(JNIEnv *, jobject, jint);
Now you’re almost there. This header file is recognized by the JVM, but you still need to define it! Simply make a file of the same name but with the cpp extension instead. Then you define by just copying the declaration:
JNIEXPORT void JNICALL Java_src_JNITest_method
(JNIEnv *, jobject, jint i)
{
std::cout << "Received number: " << i << std::endl;
}
Now you’ll need to be careful to ensure everything is correctly linked. You should #include the header file in the C++ file you just made, and that C++ file should be either in the CMake list of files or included by one. Remember to rebuild after any C++ changes.
Now everything should all be setup. Run the Java program! It should use the library you built and recognize the C++ program. Some common errors it might give if you did something wrong:
java.lang.UnsatisfiedLinkError: Can’t load library
– This means you’re directory in the System.load call is wrong – try different starting directories and make sure it’s the right file.java.lang.UnsatisfiedLinkError: jni.GLWrapper.method()
– This means it can’t find the JNI file for that method in the library – make sure it’s loaded in the CMake and you’ve rebuilt the library.
If you did everything right, you should be able to call JNI from Java! Moving forward, you should find it easier, and my workflow suggestion is a build script for C++, a JNI script to generate the JNI for all files, and a Java compiler/runner to run the code. One of the big problems for me was finding the right directories for everything, but IntelliJ helped a lot by compiling the Java to a separate folder.
If you’ve struggled at all with any steps, and reading over doesn’t seem to help, feel free to comment a question, or analyse my working example here. You can also feel free to copy the scripts, as they were a pain to get working and would rather you not have to spend time getting them up. I hope this be useful to you getting JNI working.