Technical Library
Native Code
Finally a look at what options are available to us when developing in C/C++ or assembly language.
C/C++ development of Android apps is done using the Native Development Kit, or NDK. This post assumes that you have the NDK downloaded and installed as per these instructions, with the "ndk-build" command available on your PATH (or know how to translate the instructions below to specify an absolute path). The example has been developed using NDK version 5c, released in June 2011. Building the example also requires the Android SDK and the Eclipse ADT plug-in to be installed.
User interface
When developing natively for Android, there are two possible ways to do this:
- Using the Dalvik runtime environment to present a UI to the user and capturing events, but performing parts (or all) of the program logic in native code.
- Creating a Native Activity, and rendering your own UI using OpenGL ES.
Because the amount of OpenGL ES boilerplate required for using exclusively native code would have somewhat overshadowed the example code, a mixed Dalvik/Native solution was used instead.
The Native Activity approach is certainly very useful for developers of cross-platform applications, as it greatly reduces the thickness of the required Android-specific software layer. If this sounds interesting, do look at Google's documentation of the Native Activity class, which contains quite comprehensive example code.
Your C environment
Native code in an Android app integrates with the Dalvik VM (even if it is a native activity - it is just more hidden from you then) using the Java Native Interface (JNI). This defines how Java and Native code can call each other. This includes the naming standard (similar to C++ name mangling) used to connect your native code to a specific Java class. For the attached example, the command line javah -jni workspace/ndkmc/bin/com.arm.android.example.ndkmcpkg.ndkmcact
is used to generate an include (.h) file with the correct function prototypes from the Java class. The javah
command extracts this from the specified .class
file, which takes it from whatever functions have been declared as native in the corresponding .java
file.
Bionic C library
The Bionic C library implements a version of the Pthreads API. It does explicitly exclude the pthread_cancel()
function, but apart from that most of the pthread_*
and sem_*
functions are implemented. What is not implemented however is SysV IPC - Google dedicate an entire text file in the NDK documentation to discussing the rationale for this:
docs/system/libc/SYSV-IPC.html
. So in short, if it is declared in pthread.h
or semaphore.h
, it will mostly work as expected.
Also included in the NDK documentation is an overview of the Bionic library:
docs/system/libc/OVERVIEW.html
. There is a very scary statement in there:
At the moment, Bionic does not provide or use read/write memory barriers.
This means that using it on certain multi-core systems might not be supported, depending on its exact CPU architecture.
However, all of the common Pthread APIs make use of kernel helper functions for things like atomic updates, which means that mutexes and semaphores will still correctly use memory barriers where required.
Debug printout
If you want some debug output from your program, printf is not your ally here. Android applications log output through the logcat facility, which provides a printf-style logging facility not entirely unlike UNIX syslog. The ADT also has a "logcat" output tab which can be enabled through Window->Show View->Other...->Android->LogCat.
Shared library usage in native code
The NDK includes a minimal set of common libraries guaranteed to be available on all devices supporting this version of the NDK. This does however mean that many useful libraries that are available in your system cannot trivially be linked against by your native code (one example is libjpeg). You can find which libraries are supported by looking into your NDK installation directory - under platforms/android-<API_LEVEL>/arch-arm/usr/lib
.
The example
ndkmc.zip 19.6 KB
In this example I wanted to include enough processing to show any difference in performance between a single and multiple threads, without making the code itself very complex. I also wanted to show that the basic Pthread calls work as expected, and give a small hint of what non-obvious requirements a JNI interface imposes on C code. What better way to do this than by breaking up generating a pretty picture of the Mandelbrot set into one thread per core available in the system, passing the buffer between Dalvik and Native?
Example overview
This example comes with a fairly simple RelativeLayout set up with two buttons, one TextView and one ImageView. One button initiates parallel processing of the set, the other initiates serial processing. The example contains two source files:
src/com.arm.android.example.ndkmcpkg/ndkmcact.java
, providing the user interface and time measurementjni/nativecode.c
, providing the data processing portion
Startup
On startup the app begins executing the Dalvik part of the app, which:
- loads the native library "libnativecode.so" (with the "lib" and ".so" parts implicit)
- checks how many cores are available to it, notes this number for later use, and writes it into the TextView
- sets up a global semaphore for the app to prevent multiple simukltaneous button presses from trying to update the same resources simultaneously
- registers inline onClivk handler functions for the buttons - each simply calling the (still in Dalvik)
render()
function with the number of threads to split the processing into
Processing
When one of the buttons is pressed, the render()
function is called. It acquires the sanitizer
semaphore to ensure only one call is processed at a time, and then reads the dimensions of the ImageView (doing this every time means it works also when the tablet is rotated). Based on these dimensions it allocates an int array able to hold a 32-bit Bitmap (RGB + Alpha) for this space. It then performs the native call to getNextFrame()
, measuring wall clock time before and after to give a count of elapsed time later.
At the Native level, we enter at the (name mangled for JNI) getNextFrame()
function, allocate space for our thread data structures. We then inform the Dalvik VM that we will be directly referencing into the pixel array and that it cannot be moved (or reclaimed) until we say so. Arguably, it might have been better design to call NewIntArray()
to allocate the space within the native code and eliminate some potential copying for this specific case. The code then determines how many slices the task should be split into, and how many rows of pixels should be handled by each slice ("one" and "all of them" being the case for a single thread). We then spawn a thread, using pthread_create()
, for each slice and the main thread then blocks (see comments near the end of this post) waiting for all of them to complete by calling pthread_join()
as many times as it created threads. We then free the allocated thread data structures, release our hold of the pixel array, and return.
Back in the Dalvik code, upon return we create a Bitmap object from the array, set that as the content of our ImageView, print out the elapsed wall clock time in our TextView and release the sanitizer semaphore to permit further button presses to be processed.
Importing, building and running the project
Importing
- File->Import...
- General->Existing Projects into Workspace (click 'Next')
- Select archive file (click 'Browse' and select the downloaded ndkmc.zip file)
- Projects: ndkmc (should be ticked automatically)
- Click 'Finish'
Building
Before you try to build the example, you need to manually build the native library. There is unfortunately no automatic integration between the Android SDK and NDK in the ADT. However, this is not a very complex operation:
- simply open up the command line interface as appropriate to your operating system
- navigate to the Eclipse workspace you just imported the project into and continue down into the
ndkmc/jni
subdirectory - execute the
ndk-build
command.
This builds libraries for all configured target platforms (see below in "Notes about the NDK/SDK") and copies them into the correct subdirectory under libs/
. Due to the lack of integration, you might need to force Eclipse to refresh the project in order for the native library to be visible to the ADT and so included into the resulting package. Simply right-click on the project and select "Refresh".
Once this is done, right-click on the project and select "Build Project". If your Eclipse environment is set to build automatically, you might first need to clean the project (Project->Clean) to ensure a complete rebuild.
Running
Right-click on the project (or go to the Run menu with the "ndkmc" project selected) and select Run as ...->Android Application. If you do not yet have a default target configured, you will likely be prompted to create a virtual device or select a connected hardware platform.
Notes about the SDK/NDK
The Android Virtual Devices provided by the SDK all emulate devices based on the (quite old - was in end-user products released 2005) ARMv5TE architecture. The overwhelming majority of new Android devices are based on Cortex-A processors, which implement the ARMv7 architecture Application profile (ARMv7-A). Building the native library for the older architecture excludes many useful instructions for use by the compiler - including any support for hardware floating point handling. However, unless an alternative target is explicitly declared in jni/Application.mk
, this is exactly what the NDK does.
Some, but not all, of the examples provided with the NDK do explicitly set the APP_ABI
build variable in order to improve performance on modern targets. I have done this in this example, but note that since the emulator is still ARMv5TE, the variable must be set to armeabi armeabi-v7a
and not just armeabi-v7a
in order to be able to run on the emulator, or older platforms. The highest supported available version will then be automatically loaded by the System.loadLibrary()
call at runtime. By configuring the native library to build for this target — with significant performance improvement.
Also, the emulator emulates a single-core device, so unless you have a dual-core hardware platform available the parallelized examples are expected to be slower than the serial ones.
UI responsiveness rears its ugly head again
One thing to consider is that although you may be performing the actual work on threads in the native environment, any JNI calls from the Dalvik environment will be completely synchronous. Hence, you either need to implement your interfaces as asynchronous calls (initiate processing, check progress, ...) or you will still need to implement at least an AsyncTask inside the Dalvik environment in order to prevent the UI from freezing and the Android popping up the dreaded ANR message in front of your users.
The attentive reader will notice that my example does not do this, and so serves as a warning example of how frustrating an unresponsive UI can be. Note how the buttons remain "pressed" throughout the entire processing. Correcting this is left as an exercise for the truly interested readers : ).
References
https://java.sun.com/docs/books/jni/html/design.html - JNI Design description
https://java.sun.com/developer/onlineTraining/Programming/JDCBook/jnistring.html - Passing strings and arrays between Java and native code
https://groups.google.com/group/android-platform/browse_thread/thread/0aad393da2da65b1 - discussion on Bionic Pthread support