Imagine you are a developer who needs to use a C++ DLL within Java; JNI makes this possible. I'm creating this documentation to simplify the process since I had some difficulties understanding it myself. I hope to help you understand. We will cover the following topics:
- What is JNI
- How to use JNI step by step
We will use IntelliJ and Visual Studio for this.
First, to get started with everything we need, download the Java Development Kit (JDK) for Java and configure your machine accordingly. Reminder: If your application is 64-bit, download the x64 version; if it's 32-bit, you'll need an older version since Java stopped creating 32-bit versions after Java 8. Access the JDK 11 download link, download it, and it will automatically be configured in your environment variables. If not, follow these steps: Open the terminal and type:
C:\Users>setx JAVA_HOME "C:\Program Files\Java\jdk-xx.x.x"
C:\Users>setx PATH "%JAVA_HOME%\bin"
Now it's configured, and you should receive this message:
C:\Users>java --version
java 11.0.16.1 2022-08-18 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.16.1+1-LTS-1)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.16.1+1-LTS-1, mixed mode)
Now, let's talk about JNI (Java Native Interface). To put it simply, it's a way that Oracle created for Java to communicate with C++. There's also JNA, maintained by a GitHub community (but we won't discuss it here). Below is an example image:
Here, you can see that JNI acts as an intermediary between the JVM and the C++ .dll where it runs the Java code, which in turn calls C++ functions. The .dll is created with a mix of Java and C++ in a JNI header.
First, download the IntelliJ platform. After that, create a project; it can be IntelliJ, Gradle, or Maven, it doesn't matter. Here's an example Java code snippet:
public class CallCppFromJava {
public static native String sayWrapperHello();
public static native String sayCppDllHello(String str);
public static native int sumWrapper(int a, int b);
public static native int sumCppDll(int a, int b);
public static void main(String[] args) {
}
}
Here, we've created four methods: two of them will use our JNI DLL, which we'll call the "wrapper" from now on, and the other two will be returned by the native C++ DLL. Don't forget to generate the .h header (typed in the terminal); it's a key component for us.
Create a native C++ DLL using Visual Studio:
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
extern "C" __declspec (dllexport) const char* sayCppHello(const char* str)
{
return str;
}
extern "C" __declspec (dllexport) int sumCppDll(int a, int b)
{
return a + b;
}
Now, let's create the wrapper. Create another native C++ DLL, but this time add the .h generated from Java:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#include <iostream>
#include <Windows.h>
/* Header for class CallCppFromJava */
#ifndef _Included_CallCppFromJava
#define _Included_CallCppFromJava
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: CallCppFromJava
* Method: sayWrapperHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_CallCppFromJava_sayWrapperHello
(JNIEnv *, jclass);
/*
* Class: CallCppFromJava
* Method: sayCppDllHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_CallCppFromJava_sayCppDllHello
(JNIEnv *, jclass, jstring);
/*
* Class: CallCppFromJava
* Method: sumWrapper
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_CallCppFromJava_sumWrapper
(JNIEnv *, jclass, jint, jint);
/*
* Class: CallCppFromJava
* Method: sumCppDll
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_CallCppFromJava_sumCppDll
(JNIEnv *, jclass, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
Properties > C/C++ > Additional Include Directories and add "C:\Program Files\Java\jdk-11.0.16.1\include" and "C:\Program Files\Java\jdk-11.0.16.1\include\win32", disable precompiled header.
and click Apply. Your .cc file should look like this:
#include "CallCppFromJava.h"
extern "C"
{
JNIEXPORT jstring JNICALL Java_CallCppFromJava_sayWrapperHello(JNIEnv* env, jclass cls)
{
return env->NewStringUTF("Hello From Wrapper");
}
JNIEXPORT jstring JNICALL Java_CallCppFromJava_sayCppDllHello(JNIEnv* env, jclass cls, jstring str)
{
const char* cppStr = env->GetStringUTFChars(str, JNI_FALSE);
HMODULE hModule = LoadLibraryA("D:\\DEV\\JNI\\Dll2\\x64\\Release\\Dll2.dll");
typedef const char* (*sayCppHello)(const char*);
sayCppHello hello = (sayCppHello)GetProcAddress(hModule, "sayCppHello");
return env->NewStringUTF(hello(cppStr));
}
JNIEXPORT jint JNICALL Java_CallCppFromJava_sumWrapper(JNIEnv* env, jclass cls, jint a, jint b)
{
return a + b;
}
JNIEXPORT jint JNICALL Java_CallCppFromJava_sumCppDll(JNIEnv* env, jclass cls, jint a, jint b)
{
HMODULE hModule = LoadLibraryA("D:\\DEV\\JNI\\Dll2\\x64\\Release\\Dll2.dll");
typedef int (*sumCppDll)(int, int);
sumCppDll sum = (sumCppDll)GetProcAddress(hModule, "sumCppDll");
return sum(a,b);
}
}
Returning to Java, let's simply make calls to the functions:
public class CallCppFromJava {
static{
System.load("D:\\DEV\\JNI\\Dll3\\x64\\Release\\Dll3.dll");
}
public static native String sayWrapperHello();
public static native String sayCppDllHello(String str);
public static native int sumWrapper(int a, int b);
public static native int sumCppDll(int a, int b);
public static void main(String[] args) {
System.out.println("sayWrapperHello: " + sayWrapperHello());
System.out.println("sayCppDllHello: " + sayCppDllHello("Hello"));
System.out.println("sumWrapper: " + sumWrapper(1, 2));
System.out.println("sumCppDll: " + sumCppDll(1, 2));
}
}
output:
sayWrapperHello: Hello From Wrapper
sayCppDllHello: Hello
sumWrapper: 3
sumCppDll: 3