Android JNI Application With NDK

Filed Under: Android

In this tutorial, we’ll be discussing JNI and develop a basic Android Application using NDK tools. A background in c/c++ would be useful, though not a prerequisite.

Android NDK is a companion tool of Android SDK that allows us to use native code C/C++ in our development.

To use NDK tools, ensure that the following two highlighted tools are downloaded from the SDK Manager:

Android Jni Ndk Setup

Android Jni Ndk Setup

What is JNI?

JNI stands for Java Native Interface. It acts as a bridge through which the Java/Kotlin code can call native code and vice-versa.
Moreover, JNI is needed typically in games since most of the engines are built using c/c++.

In order to call a native method in Java, you need to first define it in the Java code using the native keyword.

Example:
public native String stringFromJNI()
The native keyword transforms the method into an abstract method which we need to implement in our shared library in c/c++.

To define the method in c/c++ we need to use the following signature:


extern "C" JNIEXPORT jstring JNICALL
Java_com_journaldev_androidjnibasics_MainActivity_stringFromJNI(JNIEnv* env,jobject)

We prepend the package name with Java_.
And the package name is followed by the Activity name without the extension and the native method name.

JNIEXPORT– marks the function into the shared lib as exportable so it will be included in the function table, and thus JNI can find it
JNICALL – combined with JNIEXPORT, it ensures that our methods are available for the JNI framework.

The JNIEnv is the most important component of the JNI space. It acts as a bridge to access the Java objects.

JNI types and there equivalent types in Java are given below:

  • boolean : jboolean
  • byte : jbyte
  • char : jchar
  • double : jdouble
  • float : jfloat
  • int : jint
  • long : jlong

To load the native library on startup add the following block in your code:


    static {
        System.loadLibrary("native-lib");
    }

“native-lib” is the name of the native file of c/c++.

In the next section, we’ll be creating our JNI application in Android.
We’ll be covering:

  • How to get/send Strings for Java to native code.
  • View logs from native code in Android Studio Log console.
  • Get an array of strings from c++ in Java.

Getting Started

First, create a new project and select C++ type template to integrate JNI in the application.

Android Jni Project Setup 1

Android Jni Project Setup 1

Next, select the C++ compiler from the next screen.

Android Jni Project Setup 2

Android Jni Project Setup 2

Let’s see how our project structure looks in the next section.

Project Structure

Android Jni Project Structure

Android Jni Project Structure

Android Studio can use CMake to compile C and C++ code into a native library that the IDE then packages into your APK.

Code

In the default setup, we have a native function that allows us to get a string from the native code in our Java code using JNI.

Let’s improve upon the layout with two new buttons for more JNI functions that we’ll call.

The code for the activity_main.xml layout is given below:


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnJni"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="8dp"
        android:text="PASS YOUR NAME TO JNI"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/sample_text" />

    <Button
        android:id="@+id/btnJniStringArray"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="8dp"
        android:text="RETURN STRING ARRAY"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnJni" />


</android.support.constraint.ConstraintLayout>

The code for the MainActivity.java class is given below:


package com.journaldev.androidjnibasics;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    Button btnJNI, btnJNIStringArray;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

        btnJNI = findViewById(R.id.btnJni);
        btnJNIStringArray = findViewById(R.id.btnJniStringArray);
        btnJNI.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String result = sendYourName("Anupam", "Chugh");
                Toast.makeText(getApplicationContext(), "Result from JNI is " + result, Toast.LENGTH_LONG).show();
            }
        });

        btnJNIStringArray.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String[] strings = stringArrayFromJNI();

                Toast.makeText(getApplicationContext(), "First element is "+strings[0], Toast.LENGTH_LONG).show();

            }
        });

    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
    public native String sendYourName(String firstName, String lastName);
    public native String[] stringArrayFromJNI();

    
}

We’ve defined three native methods – string, computing strings in native code and returning, returning string array.

Let’s dig into the native-lib.cpp file which contains the functions defined in C++:


#include 
#include 
#include 
#include 

extern "C" JNIEXPORT jstring JNICALL
Java_com_journaldev_androidjnibasics_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";

    __android_log_write(ANDROID_LOG_DEBUG, "API123", "Debug Log");

    return env->NewStringUTF(hello.c_str());
}


extern "C" JNIEXPORT jstring JNICALL
Java_com_journaldev_androidjnibasics_MainActivity_sendYourName(
        JNIEnv* env,
        jobject, jstring firstName, jstring lastName) {
    char returnString[20];
    const char *fN = env->GetStringUTFChars(firstName, NULL);
    const char *lN = env->GetStringUTFChars(lastName, NULL);

    strcpy(returnString,fN); // copy string one into the result.
    strcat(returnString,lN); // append string two to the result.

    env->ReleaseStringUTFChars(firstName, fN);
    env->ReleaseStringUTFChars(lastName, lN);

    __android_log_write(ANDROID_LOG_DEBUG, "API123", returnString);

    return env->NewStringUTF(returnString);
}

extern "C"
JNIEXPORT jobjectArray JNICALL Java_com_journaldev_androidjnibasics_MainActivity_stringArrayFromJNI(JNIEnv *env, jobject jobj)
{

    char *days[]={"Java",
                  "Android",
                  "Django",
                  "SQL",
                  "Swift",
                  "Kotlin",
                  "Springs"};

    jstring str;
    jobjectArray day = 0;
    jsize len = 7;
    int i;

    day = env->NewObjectArray(len,env->FindClass("java/lang/String"),0);

    for(i=0;i<7;i++)
    {
        str = env->NewStringUTF(days[i]);
        env->SetObjectArrayElement(day,i,str);
    }

    return day;
}


To convert from JNI strings to a native char array, you can use the GetStringUTFChars method of the env. But it needs to be released after use.

android_log_write is used to log the statements in the Android console.

The output when the above application was run is given below:

Android Jni Output

Android Jni Output

This brings an end to this tutorial. You can download the project from the link below or view the full source code in our Github Repository.

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages