JNI: Calling C++ Code from Java

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 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

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);
}
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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++;

  1. 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.

  2. 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

LinkedIn

Twitter

Github

Did you find this article valuable?

Support Vivek Suthar by becoming a sponsor. Any amount is appreciated!