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.
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
1java --version 2g++ --version 3echo $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.
Make a new directory and change to it
1mkdir jni-demo 2cd jni-demo
Now open your project with your code editor,
Make a new file named HelloWorld.java
and write the below code
1public class HelloWorld { 2 static { 3 System.load("/home/vivek/jni-demo/libnative.so"); 4 } 5 6 public static void main(String[] args) { 7 new HelloWorld().greetMe(); 8 } 9 10 //native method with no body 11 public native void greetMe(); 12}
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
1javac -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
1#include <iostream> 2#include <jni.h> 3#include "HelloWorld.h" 4void hello() 5{ 6 std::cout << "HORRY I AM RUNNING FORM C++ ha" << std::endl; 7} 8JNIEXPORT void JNICALL Java_HelloWorld_hello(JNIEnv *env, jobject thisObject) 9{ 10 hello(); 11}
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
1 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).
1g++ -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 the greetMe()
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
1java 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
1#!/bin/bash 2 3# Compile Java file ,generate header file 4javac -h . HelloWorld.java 5 6# C++ source file -> object file 7g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorld.cpp -o HelloWorld.o 8 9# Create shared library 10g++ -shared -fPIC -o libnative.so HelloWorld.o -lc 11 12# Run java 13java 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
1import java.util.Scanner; 2 3public class HelloWorld { 4 static { 5 System.load("/home/vivek/jni-demo/libnative.so"); 6 } 7 8 public static void main(String[] args) { 9 Scanner scanner = new Scanner(System.in); 10 11 System.out.print("Enter the first number: "); 12 int number1 = scanner.nextInt(); 13 14 System.out.print("Enter the second number: "); 15 int number2 = scanner.nextInt(); 16 HelloWorld p1 = new HelloWorld(); 17 18 long answer = p1.addToNumber(number1, number2); 19 20 System.out.println(answer); 21 } 22 23 public native long addToNumber(int a, int b); 24}
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.
1#include <iostream> 2#include <jni.h> 3#include "HelloWorld.h" 4 5long calculateSum(int a, int b) 6{ 7 return (long)a + (long)b; 8} 9JNIEXPORT jlong JNICALL Java_HelloWorld_addToNumber(JNIEnv *, jobject thisObject, jint int1, jint int2) 10{ 11 12 std::cout << "Received Numbers are first:" << int1 << " " 13 << "Second " << int2 << std::endl; 14 return calculateSum(int1, int2); 15}
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
1public class HelloWorld { 2 3 private String name; 4 private int age; 5 6 static { 7 System.load("/home/vivek/jni-demo/libnative.so"); 8 } 9 10 public static void main(String[] args) { 11 Student stu = new Student("Vivek", 24); 12 new HelloWorld().print(stu); 13 System.out.println("Updated Values in C++"); 14 System.out.println("New Name is "+stu.getName()); 15 System.out.println("New Age is "+stu.getAge()); 16 } 17 18 public native void print(Student s1); 19} 20 21class Student { 22 23 private String name; 24 25 public String getName() { 26 return name; 27 } 28 29 private int age; 30 31 public int getAge() { 32 return age; 33 } 34 35 Student(String name, int age) { 36 this.age = age; 37 this.name = name; 38 } 39}
Now Open HelloWorld.cpp
and paste below code
1#include <iostream> 2#include <jni.h> 3#include "HelloWorld.h" 4 5JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jobject thisObject, jobject hw) 6{ 7 jclass hwClass = env->GetObjectClass(hw); 8 jfieldID nameFieldID = env->GetFieldID(hwClass, "name", "Ljava/lang/String;"); 9 jfieldID ageFieldID = env->GetFieldID(hwClass, "age", "I"); 10 jstring nameObj = (jstring)env->GetObjectField(hw, nameFieldID); 11 const char *name = env->GetStringUTFChars(nameObj, nullptr); 12 jint age = env->GetIntField(hw, ageFieldID); 13 14 // Print the Student Details 15 std::cout << "Name: " << name << std::endl; 16 std::cout << "Age: " << age << std::endl; 17 std::cout << "C++: Updating Student Values"; 18 // Updatinng Name 19 int number; 20 std::string newName; 21 std::cout << "\nEnter name"; 22 std::cin >> newName; 23 // Updating Age 24 std::cout << "\nEnter age "; 25 std::cin >> number; 26 jint newAge = number; 27 jstring newUpdateNameObj = env->NewStringUTF(newName.c_str()); 28 29 env->SetObjectField(hw, nameFieldID, newUpdateNameObj); 30 env->SetIntField(hw, ageFieldID, newAge); 31}
jclass hwClass = env->GetObjectClass(hw);
: This line retrieves the jclass
object corresponding to the class of the hw
object. The GetObjectClass()
function is called on the JNIEnv
pointer env
to get the class object associated with hw
.
jfieldID nameFieldID = env->GetFieldID(hwClass, "name", "Ljava/lang/String;");
: This line obtains the jfieldID
of the name
field in the HelloWorld
class. The GetFieldID()
function is called on env
with hwClass
, 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 the jfieldID
of the age
field in the HelloWorld
class, similar to the previous line, but for the age
field.
jstring nameObj = (jstring)env->GetObjectField(hw, nameFieldID);
: This line retrieves the value of the name
field from the hw
object using the GetObjectField()
function. The result is assigned to a jstring
variable nameObj
.
const char *name = env->GetStringUTFChars(nameObj, nullptr);
: This line converts the jstring
nameObj
to a C-style string (const char*
). The GetStringUTFChars()
function is called on env
with nameObj
and nullptr
as parameters.
jint age = env->GetIntField(hw, ageFieldID);
: This line retrieves the value of the age
field from the hw
object using the GetIntField()
function. The result is assigned to a jint
variable age
.
Updating object values in C++;
env->SetObjectField(hw, nameFieldID, newUpdateNameObj);
: This line sets the value of the name
field in the HelloWorld
object (hw
). The SetObjectField()
function is called on env
with three parameters: the hw
object, the nameFieldID
obtained earlier, and newUpdateNameObj
, which is a jstring
object representing the updated name.
env->SetIntField(hw, ageFieldID, newAge);
: This line sets the value of the age
field in the HelloWorld
object (hw
). The SetIntField()
function is called on env
with three parameters: the hw
object, the ageFieldID
obtained earlier, and newAge
, which is an int
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