An introduction to native backed objects with the Android NDK

As many Android developers know the Android NDK is used to cross-compile native (C/C++) code to run in Android programs. Unfortunately, because it uses JNI we’re limited to a C-style call interface. Tools like SWIG can be used to automatically generate wrappers for existing code including C++ classes. In this post I’ll provide an introduction to native backed objects in Java, which are objects whose implementation/resources are primarily implemented in a native language. I won’t be using SWIG since it might muddle everything a bit because of what it takes to get it to wrap standard library classes on the NDK.

What are we trying to accomplish? Our goal here is to have the ability to write Java code that operates on normal Java objects, but underneath all of the “magic” is happening in C/C++ land. There are two main use cases for this: speed-ups from using faster native code for memory intensive/processor intensive operations, and for leveraging existing code bases into a new Android project. Especially in the second case, native backed objects give you the ability to write UI code for a new Android project that uses your existing C++ classes as the main base of functionality; we simply create “wrapper” classes that can be used in Java land the same way you’ve been using them in C++ land, but now have the added benefit of being directly inter-operable with the Android framework.

In this small example, we’ll create a wrapper for std::string that implements the CharSequence interface. CharSequence is a scaled-down version of String. While String is immutable and offers many helper functions, CharSequence boils this down to the simple functionality requirement of giving access to, well, a sequence of characters. Many Android framework functions take CharSequences instead of Strings to add flexibility to how exactly the strings are stored and represented in memory. Here is a basic game plan for constructing your own native backed object classes:

  • Write allocator functions for any constructors you may wish to call when newing objects.
  • Write a deallocator function.
  • For each member you wish to call, write a function that takes a pointer parameter as well as all parameters needed to call that function.

Here is the source code for a simple wrapper for std::string:

#include 
#include 

#define JNI(X) JNIEXPORT Java_std_string_##X
#define CAST(X) reinterpret_cast(X)

extern "C" 
{

jlong JNI(alloc)(JNIEnv*,jclass*)
{
	return reinterpret_cast(new std::string());
}

void JNI(delete)(JNIEnv*, jclass*, jlong ptr)
{
	delete CAST(ptr);
}

jchar JNI(charAtImpl)(JNIEnv*, jclass*, jlong ptr, jint index)
{
	return CAST(ptr)->at(index);
}

jint JNI(lengthImpl)(JNIEnv*, jclass*, jlong ptr)
{
	return CAST(ptr)->size();
}

jint JNI(subSequenceImpl)(JNIEnv*, jclass*, jlong ptr, jint start, jint end)
{
	return reinterpret_cast(new std::string(CAST(ptr)->substr(start,end)));
}

jstring JNI(toStringImpl)(JNIEnv* env, jclass*, jlong ptr)
{
	return env->NewStringUTF(CAST(ptr)->c_str());
}

}

What we’ve done here is essentially unroll the implicit “this” pointer inside C++ function calls. This pointer (passed as a jlong) is then cast to our target type and the appropriate member function called. The matching Java class looks like this:

package std;

public class string implements CharSequence {

	private native static long alloc();
	private native static void delete(long ptr);
	private native static char charAtImpl(long ptr, int index);
	private native static int lengthImpl(long ptr);
	private native static long subSequenceImpl(long ptr, int start, int end);
	private native static String toStringImpl(long ptr);

	private final long ptr;
	private final boolean alloced;

	public string() {
		ptr = alloc();
		alloced = true;
	}

	public string(long _ptr, boolean _alloced) {
		ptr = _ptr;
		alloced = _alloced;
	}

	@Override
	public void finalize() {
		if(alloced)
			delete(ptr);
	}

	@Override
	public char charAt(int index) {
		return charAtImpl(ptr, index);
	}

	@Override
	public int length() {
		return lengthImpl(ptr);
	}

	@Override
	public CharSequence subSequence(int start, int end) {
		return new string(subSequenceImpl(ptr, start, end), true);
	}

	@Override
	public String toString() {
		return toStringImpl(ptr);
	}

	public long getPtr() {
		return ptr;
	}

}

The crux of the native backed object is the ptr member of the class. This holds the actual address (stored simply as a number; it’s cast in C++ land) to the instance of the object inside the C++ heap. For existing objects, this can be obtained by returning address-of as a long from a JNI function.

An important distinction needs to be made when dealing with your already existed code: you must differentiate between objects that were allocated and are being managed by your underlying code that you are “spying” on, and “new” objects that were created inside Java land. This distinction is required if you leverage the finalize() method to automatically free the C++ memory you created in Java land. In our above code this is solved by having two constructors: one that does a new and marks that we should delete the underlying memory when finalized, and a parameterized version that can explicitly be told if the the memory should be freed. If you are using this class to access an automatic (“stack”) member of an existing instance of a C++ class that will eventually go out of scope/be deleted by your normal C++ code, you must use the second constructor (with the second parameter set to false) to avoid a double delete.

Example std::string native backed object project source

The above example source contains the wrapper class and C++ code, as well as a tiny test Activity that will perform some equivilent sprintf/String.format work. If the C++ button is clicked, we do this:

	
char buff[32];
std::string* obj = CAST(ptr);
sprintf(buff,"%d",rand());
*obj = buff;
if(obj->size() > 2)
    *obj = obj->substr(1);
*obj += "!";

While when in pure Java mode this code is called:

for(int i = 0 ; i < data.length ; ++i) { 
    final String s = String.format("%d", rand.nextInt()); 
    if(s.length() > 2)
        data[i] = "!" + s.substring(1);
}

In either case, a CharSequence implementation can be assigned directly to a TextView, resulting in such prettiness:

If you’re looking to port over a large C++ project, definitely check out SWIG. I don’t personally use it much because a lot of the code I work on requires some massaging before it’s Android ready, but it’s certainly an execute tool that will save you lots of repetitive writing.

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.


Posted

in

by

Tags:

Comments

Leave a Reply

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