Java and C/C++: JNI Guide

While writing Android application or other generic java application you sometimes need to write code in C/C++. Java is a dynamic language with a lot of security checks for example on every array access, every type cast , on function call and return and more. those checks affect performance and if you want to manipulate an image you better write it in C.

JNI – Java Native Interface

JNI is an extension to java for interfacing native C/C++ code. You should use it for the following situations:

  1. Running algorithms – with better performance (no security checks and no dynamic features)
  2. Working with pointers – for example when you need to access hardware
  3. Calling functions not available in Java – for example some system calls like ioctl and more

Same Address Space – Different heaps

Java is working with automatic memory management – Garbage collector. C/C++ use manual memory management (malloc-free / new-delete)

The garbage collector manages the memory, moves objects to minimize fragmentation and deallocate unneeded memory regions. This is why the system creates 2 different heaps in the process address space  – one for java and one for native.Sending array from C code to C code is always done by address, Sending Array from Java code to Java code is always done by reference. On a trivial JNI implementation sending array from java to C is done by value means if we have a java array (in the java heap) and we want to send it to a native function, we first need to copy it to the native heap and after the function returns copy it again. On a smarter implementation, the garbage collector pin the array until the function returns and send only address.

Another problem we have is in strings manipulation , C string is a simple array of characters ending with null terminated char (‘\0’). In Java it is an object with properties and ,methods so while sending a string from java to C you need to add the null terminated char and sometimes you need to copy the string to the native heap.

In my examples I’m using Android Studio to write android application with native code. In Android you need to install NDK to use JNI.

Simple Example:

First we need to add native support to the Gradle scripts:

    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }

we are using cmake for our build system, all other declarations are in the CMakeLists.txt file

To add native support we first need to load the library (shared object or DLL):

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

This code loads the file libnative-lib.so (Or native-lib.dll on windows application).

Now We need to declare the native function in the java class – You need to call this function to invoke the C function:

public native int add(int a,int b);

The symbol name in C must be Java_[package]_[class]_function so if we declare the native function in MainActivity class in package com.devarea.jnitest you have to write the C code exactly this way:

extern "C"
JNIEXPORT jint JNICALL Java_com_devarea_jnitest_MainActivity_add
         (JNIEnv *env, jobject instance, jint a, jint b) 
{
    return a+b;
}

The extern “C” is required if the file extension is cpp so the symbol name will be the name of the function only.

The function gets 4 parameters:

  • JNIEnv – pointer to JNI interface – a class with many function pointers
  • jobject – the java object
  • jint a ,jint b – typedefs to java integers

Note that if we declare the function as static we will get jclass as second parameter represents the Java class object

We can use jobject and jclass to access java properties and methods from C code – see below

Primitive types

Java primitives (byte, short, int, long, float, double , char, boolean) are passed by value so its easy to work with them – just use the correct typedef in your C code (starting with j)

Strings

String are different in Java and C so you need special handling:

Sending Java string to C:

Java code:

public native int getStrLen(String s);

C code:

extern "C"
JNIEXPORT jint JNICALL
Java_com_devarea_jnitest_MainActivity_getStrLen(JNIEnv *env, jobject instance, jstring s_) {
    const char *s = env->GetStringUTFChars(s_, 0);

    jint len=strlen(s);

    env->ReleaseStringUTFChars(s_, s);
    return len;
}

First we copy the java string to the native heap, then calculate length and release the native string

Note that it is sometimes possible to pin the string in the java heap and send only pointer (ReleaseStringUTFChars is required for unpinning)

If you need to know if the VM really copied the string you can send jboolean variable by address to the second argument:

    jboolean b;
    const char *s = env->GetStringUTFChars(s_, &b);

    if(b)
        puts("we have a copy");

Returning a string from C to Java:

Java code:

public native String stringFromJNI();

C code:

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

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

Arrays

If we want to send a java array to C code:

Java code

public static native int addArray(int []arr);

C code:

extern "C"
JNIEXPORT jint JNICALL
Java_com_devarea_MainActivity_addArray
        (JNIEnv *env, jclass type, jintArray jarr) {

    jint *arr = env->GetIntArrayElements(jarr, NULL);
    int res=0;
    int size = env->GetArrayLength(jarr);
    for(int i=0;i<size;i++)
        res+=arr[i];

    env->ReleaseIntArrayElements(jarr, arr, 0);
    return res;
}

We first copy the Java Array to C using GetIntArrayElements , then ask the array size , manipulate the c array and release it

It is similar to string handling

Return Java Array from C:

Java code:

public static native int[] getArray();

C code:

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_mabel_devarea_MainActivity_getArray
        (JNIEnv *env, jclass type) {

    int arr[10]={1,2,3,4,5,6,7,8,9,0};
    jintArray ret = env->NewIntArray(10);

    env->SetIntArrayRegion(ret,0,10,arr);

    return ret;

}

Global and Local references:

It is very important to understand the difference between local and global reference. When you pass and object to the native code , the VM creates a local reference , the object will not move or freed until the function returns

What if we want to save the reference in a global variable in the native code and use it during another call for example:

Java code:

public static native void saveArray(int []arr);
public static native int addArray();

On saveArray we just save the reference and on addArray we use it:

C code:

jintArray gl_arr;

extern "C"
JNIEXPORT void JNICALL
Java_com_devarea_jnitest_MainActivity_saveArray(JNIEnv *env, jclass type, jintArray arr_) {

    gl_arr = arr_;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_devarea_MainActivity_addArray(JNIEnv *env, jclass type) {
    jint *arr = env->GetIntArrayElements(gl_arr, NULL);
    int res=0;
    int size = env->GetArrayLength(gl_arr);
    for(int i=0;i<size;i++)
        res+=arr[i];

    env->ReleaseIntArrayElements(gl_arr, arr, 0);
    return res;
}

This is a Wrong Code!!!

We need to tell the VM that we are saving a reference to the object so it won’t move it or free it between the calls. For that we need to declare a global reference:

The correct C code:

jintArray gl_arr;

extern "C"
JNIEXPORT void JNICALL
Java_com_devarea_jnitest_MainActivity_saveArray
        (JNIEnv *env, jclass type, jintArray arr_) {

    gl_arr = (jintArray )env->NewGlobalRef((jobject) arr_);
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_devarea_jnitest_MainActivity_addArray
        (JNIEnv *env, jclass type) {

    jint *arr = env->GetIntArrayElements(gl_arr, NULL);
    int res=0;
    int size = env->GetArrayLength(gl_arr);
    for(int i=0;i<size;i++)
        res+=arr[i];

    env->ReleaseIntArrayElements(gl_arr, arr, 0);
    env->DeleteGlobalRef(gl_arr);
    return res;
}

 

Direct Buffer Access

Sometimes we need to work with a large amount of data and be sure that the VM won’t copy it. We can use ByteBuffer for this:

Java code:

ByteBuffer buf = ByteBuffer.allocateDirect(1000);
processData(buf);

.....


public static native void processData(ByteBuffer buf);

C code:

extern "C"
JNIEXPORT void JNICALL
Java_com_devarea_jnitest_MainActivity_processData(JNIEnv *env, jclass type, jobject buf) {

    char *data=(char *)env->GetDirectBufferAddress(buf);

    int len = env->GetDirectBufferCapacity(buf);

    for(int i=0;i<len;i++)
    {
        data[i]='X';
    }
}

It can be very useful if you are using the same buffer in more than one function. You can also create the buffer inside the native code (using malloc) and return it to the java code using env->NewDirectByteBuffer

Reflection

The only supported type in JNI are strings and arrays, Other objects passed as jobject and if you want to access their data , properties and methods you need to use reflection:

Example – java code:

package com.example.testjni;

public class Nlib {
	public native int func(int a,int b);
	
	private int getNum(int a)
	{
		return a+100;
	}
	static 
	{
		System.loadLibrary("testJNI");
	}
}

“func” is a member function, you need to create a Nlib object and use it to call func:

Nlib obj=new Nlib()
obj.func()

obj (this) is passed as jobject

C code:

JNIEXPORT jint JNICALL Java_com_example_testjni_Nlib_func
  (JNIEnv *env, jobject obj, jint a, jint b)
{
	jclass cl=env->GetObjectClass(obj);

	jmethodID m=env->GetMethodID(cl,"getNum","(I)I");

	jint num=env->CallIntMethod(obj,m,a+b);

	return num;

}

In this example we retrieve the Class object for obj , then we use reflection to get the method object of a function getNum which get an integer as a parameter and returns integer. The last thing we do is call that function using object obj and send a+b as parameter

Using this method we can call to any function but it is complex to write (note that we can also call to a private function like this example)

 

JNI_OnLoad

You can write a function to initialize the native library , one use of this function is to load a function pointers table to eliminate the long functions name for example:

Java code:

public class MyLib {
    static {
        System.loadLibrary("mylib");
    }

    public native static long f1(long n);
    public native static long f2(long n);
    public native static long f3(long n);

}

C++ code:

namespace com_example_cpptest {

    static jlong myf1(JNIEnv *env, jclass clazz, jlong n) {
        // implement;
    }
    static jlong myf2(JNIEnv *env, jclass clazz, jlong n) { 
       // implement; 
    }
    static jlong myf3(JNIEnv *env, jclass clazz, jlong n) { 
       // implement; 
    }  


    static JNINativeMethod method_table[] = {
            { "f1", "(J)J", (void *) myf1 },
            { "f2", "(J)J", (void *) myf2 },
            { "f3", "(J)J", (void *) myf3 }
    };
}

using namespace com_example_cpptest;

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    javaVM = vm;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    } else {
        jclass clazz = env->FindClass("com/devarea/jnitest/MyLib");
        if (clazz) {
            jint ret = env->RegisterNatives(clazz, method_table, sizeof(method_table) / sizeof(method_table[0]));
            env->DeleteLocalRef(clazz);
            return ret == 0 ? JNI_VERSION_1_6 : JNI_ERR;
        } else {
            return JNI_ERR;
        }
    }
}

As you can see from the code , in this way we don’t need to declare the functions with the long symbol names because we are using a functions pointers.

Working with Native threads

Sometimes you want to create a thread in the native code , for example to make the task asynchronous , you can just use the native system call or function to do:

static jlong fibNR(JNIEnv *env, jclass clazz, jlong n) {
        pthread_t t1;
        pthread_create(&t1,NULL,threadfn,(void*)n);
        return 0;
}

Now simply write the function threadfn. But if you want to access the JNI interface its not trivial – the parameter env is created for each thread and if you want to use it on a custom thread you need to create one. This is done using AttachCurrentThread:

    static void *threadfn(void *p)
    {
        jlong res = longlastfn((long)p);
        JNIEnv *env;
        int n=javaVM->AttachCurrentThread(&env,NULL);

        // use env to access any function

        javaVM->DetachCurrentThread();
        return (void*)res;
    }


    static jlong fibNR(JNIEnv *env, jclass clazz, jlong n) {
        pthread_t t1;
        pthread_create(&t1,NULL,threadfn,(void*)n);
        return 0;
    }

You can access javaVM by saving it as a global variable in JNI_OnLoad or retrieving it from env on the function that creates the thread:

JavaVM *vm;
env->GetJavaVM(&vm);

Exception Handling

You can use exceptions in 2 cases:

Calling a native code that generate java exception:

java code:

public native void getException() throws IOException;

C code:

extern "C"
JNIEXPORT void JNICALL
Java_com_mabel_developer_jnitest_MainActivity_getException(JNIEnv *env, jclass type) {

    // Do something and if you want to throw exception:

    jclass cl = env->FindClass("java/io/IOException");
    env->ThrowNew(cl,"error from JNI");

    return;

}

The second case is calling a java code from native and check if it throws exception:

env->CallIntMethod(obj, method, 10, 20);
if(env->ExceptionCheck())
{
     // handle exception
     env->ExceptionClear();
}

 

 

 

 

Tagged ,

6 thoughts on “Java and C/C++: JNI Guide

  1. Hi there. You are very wrong about java and array-bounds-checks. They are optimized out of the inner loop. There are not checks performed on every typecast. Please read up on the subject… 😉

    1. Optimization is subject of JVM implementation. You can’t count on this and about the casting , in java it is always dynamic cast, you can’t make static cast like c++, this is required for security reasons

  2. Thank you very much for giving me all the information I’m looking for.

  3. return int array: Code does not work

    int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
    jintArray ret = env->NewIntArray(10);

    env->SetIntArrayRegion(ret, 0, 10, arr);

    it should have been jint (j missing in example)
    jint arr[10] = { 1,2,3,4,5,6,7,8,9,0 };

  4. Thanks for this detailed summery!
    When I get my data in my C native code from Java – I would like to call a C++ function from the native code with the data I got from the Java call. However I have no clue how to do this. Do you have a hint for me? Thanks in advanced.

  5. Thank you.

Comments are closed.