JNI: Calling C++ Code from Java
Explore the step-by-step process of bridging the gap between Java and C++ using JNI
Table of contents
While exploring the New Architecture of React Native, I found the JSI(JavaScript Interface) and how it communicates from JS to C++ and vice-versa.
So I started gathering information about how this works, upon doing some quick internet searches, I found JNI(Java Native Interface) and how we can call our C++ code from Java.
To understand it, I did some research and managed to compile my first code, however, it was just a hello world program, but it gave me an idea of how it works.
Prerequisites
Before Starting to code, make sure you have Installed
Java and g++/gcc on your machine
JAVA_HOME environment variable set
You can verify them by running the below commands
java --version
g++ --version
echo $JAVA_HOME
Your output should look like this
If you don't see something like this, please install and setup the proper environment
So, Once you are ready Let's start coding.
Setup
Make a new directory and change to it
mkdir jni-demo
cd jni-demo
Now open your project with your code editor,
Make a new file named HelloWorld.java
and write the below code
public class HelloWorld {
static {
System.load("/home/vivek/jni-demo/libnative.so");
}
public static void main(String[] args) {
new HelloWorld().greetMe();
}
//native method with no body
public native void greetMe();
}
Let's break down this code into chunks
Inside the class, there is a static block of code defined by static { ... }
. This block is executed when the class is loaded by the Java Virtual Machine (JVM).
In this case, the block calls System.load()
to load a shared library file named "libnative.so" located at "/home/vivek/jni-demo/". The shared library contains the implementation of the native method that will be called later.(We will be generating this in the next steps)
The static block is used to load the native library because it ensures the library is loaded when the class is first loaded.
In the main() method, we are creating a new instance of HelloWorld and call the hello() method.
The hello() method is declared as native, which means it is implemented in native code (like C/C++).
When hello() is called, it will execute the native implementation from the loaded library, allowing calling native code from Java.
Ok, Now run the below command
javac -h . HelloWorld.java
The above command will compile the HelloWorld.java
file and generate a header file called HelloWorld.h
and HelloWorld.class
file. The HelloWorld.h
file contains the prototype for the hello()
method.
You should see the file generated like this.
Now It's Time to implement our native method which we defined earlier in our Java code
Make a new file called HelloWorld.cpp and write the below code
#include <iostream>
#include <jni.h>
#include "HelloWorld.h"
void hello()
{
std::cout << "HORRY I AM RUNNING FORM C++ ha" << std::endl;
}
JNIEXPORT void JNICALL Java_HelloWorld_hello(JNIEnv *env, jobject thisObject)
{
hello();
}
The JNIEXPORT
keyword indicates that the hello()
method is exported to Java.
This means that the hello()
method can be called from Java.
The Java_HelloWorld_hello()
function is the Java entry point for the hello()
method. This function takes two parameters: the JNIEnv
pointer and the jobject
object. The JNIEnv
pointer is used to interact with the Java Virtual Machine (JVM). The jobject
object is a reference to the HelloWorld
object in Java.
The hello()
method simply calls the hello()
function. The hello()
function prints the message to the console.
Now run the below command
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorld.cpp -o HelloWorld.o
The above command is used to compile the C++ file HelloWorld.cpp
and generate an object file called HelloWorld.o
.
The -c
option tells the compiler to compile the file without linking it. This means that the object file will not be able to run by itself.
The -fPIC
option tells the compiler to generate position-independent code. This means that the code can be relocated to any address in memory.
The -I
option tells the compiler to include the specified directories when searching for header files. In this case, the compiler is told to include the JAVA_HOME/include
and JAVA_HOME/include/linux
directories. These directories contain the header files for the Java Native Interface (JNI).
g++ -shared -fPIC -o libnative.so HelloWorld.o -lc
-shared
: This option tells the compiler to create a shared library instead of an executable binary. Shared libraries are dynamically linked at runtime.-fPIC
: This option stands for "Position Independent Code" and is necessary for creating a shared library. It ensures that the generated code can be loaded and executed at any memory address.-o
libnative.so
: This specifies the output file name. The resulting shared library will be named "libnative.so".HelloWorld.o
: This is the object file that contains the compiled code for the native implementation of thegreetMe()
method. The object file is linked with other necessary libraries to create the final shared library.-lc
: This option links the C standard library (libc
) with the shared library. It ensures that any standard C functions used in the native implementation are resolved correctly.
At this point, we are ready to execute our code
make sure you have files like this
Now run
java HelloWorld
And you should see the output like this
And if you see carefully, that's the point we added in our C++ code.
So, at this point, you called a C++ method from your Java code.
But wait, now every time we change the Java file and C++ file, We need to run all four commands, it's annoying, so let's add them to a shell script
Create a new file script.sh
and paste the below code
#!/bin/bash
# Compile Java file ,generate header file
javac -h . HelloWorld.java
# C++ source file -> object file
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorld.cpp -o HelloWorld.o
# Create shared library
g++ -shared -fPIC -o libnative.so HelloWorld.o -lc
# Run java
java HelloWorld
After this, make sure you give permission by running chmod +x script.sh
Now our script is ready, run it by ./script.sh
in the terminal
And you should see the output like this
Now, this is just a Hello World Program, Now let's see how we can pass the function arguments and user input.
In the Next Example, We are going to see How we can take input from the user and pass those input to our native method and we will calculate the answer in C++ code.
We are going to use the Same HelloWorld.java
file, so open it and paste the below code in it
import java.util.Scanner;
public class HelloWorld {
static {
System.load("/home/vivek/jni-demo/libnative.so");
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter the first number: ");
int number1 = scanner.nextInt();
System.out.print("Enter the second number: ");
int number2 = scanner.nextInt();
HelloWorld p1 = new HelloWorld();
long answer = p1.addToNumber(number1, number2);
System.out.println(answer);
}
public native long addToNumber(int a, int b);
}
Here, we are simply taking two user inputs by Scanner class and passing them to our native method addToNumber(int a,int b)
.
Now Open our HelloWorld.cpp
and paste the below code.
#include <iostream>
#include <jni.h>
#include "HelloWorld.h"
long calculateSum(int a, int b)
{
return (long)a + (long)b;
}
JNIEXPORT jlong JNICALL Java_HelloWorld_addToNumber(JNIEnv *, jobject thisObject, jint int1, jint int2)
{
std::cout << "Received Numbers are first:" << int1 << " "
<< "Second " << int2 << std::endl;
return calculateSum(int1, int2);
}
Here,JNIEXPORT jlong JNICALL Java_HelloWorld_addToNumber(JNIEnv *, jobject thisObject, jint int1, jint int2)
is a function declaration in C/C++ for a native method addToNumber()
, from HelloWorld Class.
Now execute ./script.sh
and you should see the output look like this
So At this point, You Just passed inputs and the sum is calculated in C++ code.
Now, Let's Pass the object and update values of it in C++
Again in HelloWorld.java
Paste below code
public class HelloWorld {
private String name;
private int age;
static {
System.load("/home/vivek/jni-demo/libnative.so");
}
public static void main(String[] args) {
Student stu = new Student("Vivek", 24);
new HelloWorld().print(stu);
System.out.println("Updated Values in C++");
System.out.println("New Name is "+stu.getName());
System.out.println("New Age is "+stu.getAge());
}
public native void print(Student s1);
}
class Student {
private String name;
public String getName() {
return name;
}
private int age;
public int getAge() {
return age;
}
Student(String name, int age) {
this.age = age;
this.name = name;
}
}
Now Open HelloWorld.cpp
and paste below code
#include <iostream>
#include <jni.h>
#include "HelloWorld.h"
JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jobject thisObject, jobject hw)
{
jclass hwClass = env->GetObjectClass(hw);
jfieldID nameFieldID = env->GetFieldID(hwClass, "name", "Ljava/lang/String;");
jfieldID ageFieldID = env->GetFieldID(hwClass, "age", "I");
jstring nameObj = (jstring)env->GetObjectField(hw, nameFieldID);
const char *name = env->GetStringUTFChars(nameObj, nullptr);
jint age = env->GetIntField(hw, ageFieldID);
// Print the Student Details
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "C++: Updating Student Values";
// Updatinng Name
int number;
std::string newName;
std::cout << "\nEnter name";
std::cin >> newName;
// Updating Age
std::cout << "\nEnter age ";
std::cin >> number;
jint newAge = number;
jstring newUpdateNameObj = env->NewStringUTF(newName.c_str());
env->SetObjectField(hw, nameFieldID, newUpdateNameObj);
env->SetIntField(hw, ageFieldID, newAge);
}
jclass hwClass = env->GetObjectClass(hw);
: This line retrieves thejclass
object corresponding to the class of thehw
object. TheGetObjectClass()
function is called on theJNIEnv
pointerenv
to get the class object associated withhw
.jfieldID nameFieldID = env->GetFieldID(hwClass, "name", "Ljava/lang/String;");
: This line obtains thejfieldID
of thename
field in theHelloWorld
class. TheGetFieldID()
function is called onenv
withhwClass
, the name of the field ("name"), and the signature of the field ("Ljava/lang/String;") as parameters.jfieldID ageFieldID = env->GetFieldID(hwClass, "age", "I");
: This line retrieves thejfieldID
of theage
field in theHelloWorld
class, similar to the previous line, but for theage
field.jstring nameObj = (jstring)env->GetObjectField(hw, nameFieldID);
: This line retrieves the value of thename
field from thehw
object using theGetObjectField()
function. The result is assigned to ajstring
variablenameObj
.const char *name = env->GetStringUTFChars(nameObj, nullptr);
: This line converts thejstring
nameObj
to a C-style string (const char*
). TheGetStringUTFChars()
function is called onenv
withnameObj
andnullptr
as parameters.jint age = env->GetIntField(hw, ageFieldID);
: This line retrieves the value of theage
field from thehw
object using theGetIntField()
function. The result is assigned to ajint
variableage
.
Updating object values in C++;
env->SetObjectField(hw, nameFieldID, newUpdateNameObj);
: This line sets the value of thename
field in theHelloWorld
object (hw
). TheSetObjectField()
function is called onenv
with three parameters: thehw
object, thenameFieldID
obtained earlier, andnewUpdateNameObj
, which is ajstring
object representing the updated name.env->SetIntField(hw, ageFieldID, newAge);
: This line sets the value of theage
field in theHelloWorld
object (hw
). TheSetIntField()
function is called onenv
with three parameters: thehw
object, theageFieldID
obtained earlier, andnewAge
, which is anint
representing the updated age.
Now Run ./script.sh
and you should see out put like this
So If you made till here, Give yourself a treat and dive more in deep to learn much about JNI, It's amazing stuff.
Until Next Time,
Keep Coding, Keep Debugging
Reach out to me