Introduction
This post is about using Android NDK (C/C++) for your first Hello World application. We'll be using the sample application in NDK distribution to kickstart. We'll also be looking at the essential changes to get C/C++ code to compile and run from Eclipse IDE easily.
While developing pure C/C++ applications are possible on Android, it is good to get a feel of Java based development in the first place. Either that or if you are interested in developing using Java alone, you may head to one of these posts first:
Why an IDE for C/C++? Why not command-line only?
Speed
Being a C programmer with more than a decade experience in embedded systems, I got used to command line tools and editors. Indeed those skills are very useful. While we'll explore coding for Android SDK+NDK from command line later, do give Eclipse IDE a couple of hours for native development. Am a convert at least when it comes to Android native (C/C++) development; IDE accelerates development exponentially. Trust me.You'll see and appreciate the advantages of indexing and auto-completion feature in following sections.
Mix
Other than that, we've to admit that not all Android APIs are officially exposed through NDK. Unofficial hacks may work on specific firmware and aren't future proof. While we can create pure native apps, from time to time, we will need to make calls to Java world to get things done.
For instance, game apps could remain 99.99% native (C/C++) but the last 0.01% might involve and Java (+JNI) ... say to check license with Google Play servers, to launch an intent to open a webpage, to get pre-authenticated tokens (without password entry), showing customized version of google maps etc. So, other than accelerating C/C++ coding with the NDK, the IDE will also help in mix cases involving a bit of Java.
Compared to the initial NDK releases we've come a long way. The first version didn't even support pure native apps. Gradually [ very very slowly :( ] things are improving in the NDK; we now have partial support for audio, video etc. But it still makes a lot of sense to code in NDK (pure or mixed) where the app involves heavy computation (eg. game physics) or when porting from other platforms (eg. iOS).
Interactive App / Game / No GC!
Any decent/serious 3D (or even 2D) game should consider avoiding Java to the maximum extent possible. One could spend most of their dev time ensuring that there are no allocations in Java. But still there will be places where the system will kick in the garbage collector (GC) and your game will see a jank, i.e., few frames taking twice/thrice the usual time to refresh and the input/touch takes twice as long to effect. These GC calls can't be explicitly timed by the developer and the game experience is terrible.Number crunching
Even non-interactive applications involving heavy number crunching benefit from native code as it has less overhead, has better performance and consumes less power for the same task.When to avoid?
However the bridge between Java and native (C/C++) world is a bottle neck. Passing data across infrequently and processing for a long time in native code is a good use case. But frequently exchanging large chunks of data will involve so much overhead smuggling across the boundary that you'll lose the benefits of going native.Ready?!
Download and Install the NDK
Follow instructions on http://developer.android.com/tools/sdk/ndk/index.html to download and install the NDK.It is typically just a zip or compressed tar-ball that you extract on your PC/Mac to your favorite folder. Simple.
Eclipse CDT
Assuming you've already run Hello World using Eclipse as suggested in the introduction, you already have a setup of Eclipse IDE suitable for development using NDK as well.If eclipse was installed using Android ADT Bundle, it would almost certainly have support for C/C++.
Verify that using menu Eclipse > Preferences. You'd see a C/C++ section as shown in the screen shot.
Tip: If you don't find it, don't fret, you may still upgrade existing eclipse to support C/C++. All you need to do is install CDT plugin as suggested in http://www.eclipse.org/cdt/downloads.php
This usually involves these steps:
From menu go to Help > Install new software > Add and key in the latest repository URL you found on the CDT download page (something like http://download.eclipse.org/tools/cdt/releases/juno)
Wait for it it refresh, check most of the components, agree to licenses, choose to install and wait for it to finish.
Running the Sample
Here are the steps to run the sample application.From menu File > New > Project, choose "Android Project from Existing Code".
In the resulting screen, browse to the path of the sample application, hello-jni, in the ndk folder that you just downloaded and extracted.
JNI stands for Java Native Interface. This is the magic glue that lets Java and Native code interact.
Open the AndroidManifest.xml file to make a few minor changes and ensure that it looks so:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.hellojni" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="17" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".HelloJni" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
- We've changed minSdkVersion to 9. Which will be relevant eventually when we wish to do pure native applications (no Java component).
- We've added a targetSdkVersion. Make sure, it is the same as the one you are compiling against (Project > Properties > Android > Project Build Target > API level). Latest was 17 in mid 2013.
- We've pointed the app to use an icon. If the icons are missing in res/drawable* folders, copy an icon for each drawable level (drawable-hdpi, drawable-xhdpi, ...) from some existing project say "Hello World" we generated using wizard in the SDK example.
- We've also removed debuggable tag and allowed backup.
Hope these changes aren't required in future NDK releases. The samples should already have these in place.
Trying to run the application at this stage will fail unless C/C++ build is configured. The error would look something like:
W/dalvikvm( 6622): Exception Ljava/lang/UnsatisfiedLinkError; thrown while initializing Lcom/example/hellojni/HelloJni;
W/dalvikvm( 6622): Class init failed in newInstance call (Lcom/example/hellojni/HelloJni;)
D/AndroidRuntime( 6622): Shutting down VM
W/dalvikvm( 6622): threadid=1: thread exiting with uncaught exception (group=0x40c491f8)
E/android.os.Debug( 191): !@Dumpstate > dumpstate -k -t -n -z -d -o /data/log/dumpstate_app_error
E/AndroidRuntime( 6622): FATAL EXCEPTION: main
E/AndroidRuntime( 6622): java.lang.ExceptionInInitializerError
E/AndroidRuntime( 6622): at java.lang.Class.newInstanceImpl(Native Method)
E/AndroidRuntime( 6622): at java.lang.Class.newInstance(Class.java:1319)
E/AndroidRuntime( 6622): at android.app.Instrumentation.newActivity(Instrumentation.java:1026)
E/AndroidRuntime( 6622): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1883)
E/AndroidRuntime( 6622): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1993)
E/AndroidRuntime( 6622): at android.app.ActivityThread.access$600(ActivityThread.java:127)
E/AndroidRuntime( 6622): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1159)
E/AndroidRuntime( 6622): at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime( 6622): at android.os.Looper.loop(Looper.java:137)
E/AndroidRuntime( 6622): at android.app.ActivityThread.main(ActivityThread.java:4507)
E/AndroidRuntime( 6622): at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime( 6622): at java.lang.reflect.Method.invoke(Method.java:511)
E/AndroidRuntime( 6622): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:978)
E/AndroidRuntime( 6622): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:745)
E/AndroidRuntime( 6622): at dalvik.system.NativeStart.main(Native Method)
E/AndroidRuntime( 6622): Caused by: java.lang.UnsatisfiedLinkError: Couldn't load hello-jni: findLibrary returned null
E/AndroidRuntime( 6622): at java.lang.Runtime.loadLibrary(Runtime.java:365)
E/AndroidRuntime( 6622): at java.lang.System.loadLibrary(System.java:535)
E/AndroidRuntime( 6622): at com.example.hellojni.HelloJni.<clinit>(HelloJni.java:64)
E/AndroidRuntime( 6622): ... 15 more
W/ActivityManager( 191): Force finishing activity r.intent.getComponent().flattenToShortString()
That had to be expected if C/C++ code isn't compiled. The VM is looking for our C/C++ module's library (hello-jni.so) which is missing.
Configuring Eclipse for NDK (C/C++)
First of all, point Eclipse > Preferences > Android > NDK to the install folder.
The project is still a Java project as far as Eclipse is concerned.
Android tools have a way to convert project to NDK projects but the support is incomplete. So we'll not take that route.
To inform it about the C/C++ nature, right click on the project's top folder and choose New > Other.
In the resulting screen choose "Convert to C/C++ project"
Choose "Makefile project" in the next screen. If the option to choose toolchain shows Android GCC, go for it. Otherwise just leave it at "Other toolchain" for the time being and fix the toolchains later.
Right click on the project's top folder and see Properties > C/C++ build. Does it show ndk-build as the default build command?
If it doesn't, untick "Use default build command" and enter custom command pointing to ndk-build in your ndk folder. Also untick "Generate make files automatically" if it was ticked.
Cross fingers and build/run the app.
If it still complains of ndk-build not being found in path, go to Eclipse > Preferences > C/C++ > Build > Environment and "Add" PATH pointing to your ndk install folder. That's where ndk-build command resides.
Observe that we've changed Preferences instead of Project properties. This change would reflect on all projects in this workspace.
(Of course if it still fails, go back to our method of mentioning ndk-build along with path explicitly.
On running the app you'd see a "Hello from JNI" message.
Eclipse Indexer Workarounds for Android NDK
Often Android ADT plugin for eclipse breaks how NDK components work. The only way out is to avoid Android toolchain and manual configuration of build, paths and symbols.
- Choose your project, go to Project > Properties > C/C++ Build > Tool Chain editor
- toggle "Display compatible toolchains only" checkbox
- that would show you toolchains other than Android's own
- choose Linux GCC or something that can run on your dev machine
- leave the Current builder at "GNU Make builder"
- apply and then, go to Project > Properties > C/C++ Build
- instead of default build command, provide full path to ndk-build in the ndk directory
- ensure "Generate makefiles automatically" is not ticked
- in project properties or eclipse preferences (if adding to all configuations)
- set indexer to use active build configuration
- under C/C++ General > Paths and symbols > Symbols, add
- __ANDROID__ with a dummy true value like 1 for all configurations and languages
- add following build variables and environment variables
- CFLAGS =-DANDROID -D__ANDROID__ -D__arm__=1
- CXXFLAGS =-DANDROID -D__ANDROID__ -D__arm__=1
- CPPFLAGS =-DANDROID -D__ANDROID__ -D__arm__=1
- under C/C++ General > paths and symbols > Includes, add
- these for all configurations (assembler, GNU C and GNU C++)
- (see Update below for a longer list)
- /Users/rajesh/ndk/platforms/android-9/arch-arm/usr/include
- /Users/rajesh/ndk/sources/android/native_app_glue
- /Users/rajesh/ndk/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.4.3/include
- additionally for c++ in specific, if using STL ask for a version of STL include one of the stl headers too headers in path
- /Users/rajesh/ndk/sources/cxx-stl/stlport/stlport
- (Ensure those paths are valid and contain some headers)
Update: ... with the added support for renderscript, ndk_helper, ... the set of paths I include now have changed to the following
- /Users/rajesh/ndk/platforms/android-19/arch-arm/usr/include/rs/cpp
- /Users/rajesh/ndk/platforms/android-19/arch-arm/usr/include/rs
- /Users/rajesh/ndk/sources/cxx-stl/stlport/stlport
- /Users/rajesh/ndk/sources/cxx-stl/gabi++/include
- /Users/rajesh/ndk/platforms/android-19/arch-arm/usr/include
- /Users/rajesh/ndk/platforms/android-19/arch-x86/usr/include/rs/cpp
- jni
- /Users/rajesh/ndk/platforms/android-19/arch-x86/usr/include/rs
- /Users/rajesh/ndk/platforms/android-19/arch-x86/usr/include
- /Users/rajesh/ndk/platforms/android-19/arch-mips/usr/include/rs/cpp
- /Users/rajesh/ndk/platforms/android-19/arch-mips/usr/include/rs
- /Users/rajesh/ndk/toolchains/arm-linux-androideabi-4.6/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.6/include
- /Users/rajesh/ndk/toolchains/arm-linux-androideabi-4.6/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.6/include-fixed
- /Users/rajesh/ndk/platforms/android-19/arch-mips/usr/include
- /Users/rajesh/ndk/platforms/android-19/arch-arm/usr/include/machine
- /Users/rajesh/ndk/platforms/android-19/arch-mips/usr/include/asm
- /Users/rajesh/ndk/sources/android/native_app_glue
- /Users/rajesh/ndk/sources/android/ndk_helper
Important: Only with the builder and indexer working together, will you realize the true capabilities of native code development on Eclipse. Hope Android tool developers fix the issues and hope it works out of the box. But till then, do take the extra step the day you run the first NDK app and you won't regret. Given details in this page, and may be some help from comments here or stackoverflow.com you'll get it working within a few minutes.
Error checking apart, it helps with auto complete feature and will at least double your coding speed.Ctrl+space and Ctrl+dot can do wonders in Eclipse. Auto completion apart, it can assist with arguments to a function and navigation. Ctrl+click, say on jobject, will take us straight to jni.h where it is declared. Further, where sources are available, we can navigate to the respective definition with the same Ctrl+click on the function declaration. Return with Ctrl+{ or go forward again with Ctrl+}. Of course all these key mappings can be customized in Eclipse>Preferences.
Explore the code
HelloJni.java
In src>...>HelloJni.java, you'll find a line that loads our native library using
static { System.loadLibrary("hello-jni"); }
This happens whenever the library this Java class is loaded even before the constructors are called as the scope is static.
Find declaration of a native function in this class
public native String stringFromJNI();
On a call to this function, from java, if it is implemented in native code (that is now loaded and readily available from hello-jni.so), the respective native function will be called.
hello-jni.c
In jni>hello-jni.c, you'll find the function with a weird long name
jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env, jobject thiz ) {
return (*env)->NewStringUTF(env, "Hello from JNI !");
}
Observe that the name is Java followed by the package name, the Java class name and the function name itself separated by underscores.
If this looks ugly to you, you can use a function called RegisterNatives to register many functions with the VM with custom names in a special function called JNI_onLoad(). We'll see that in subsequent projects.
While that will help avoid the weird name, we'll still have to deal with JNIEnv and jobject. Observe how the env is helping us return a string suitable for consumption by the Java end.
Android.mk
Finally we must explore Android.mk. This is the equivalent of make file for android native modules. Each native module (typically organized in sub-folders in jni), will contain such a file.
- LOCAL_PATH := $(call my-dir)
- stores the current directory location in LOCAL_PATH
- include $(CLEAR_VARS)
- clears all variables that might be set from a previous module build (observe that LOCAL_PATH is stored before a call to this)
- LOCAL_MODULE := hello-jni
- the name of the resulting library will be libhello-jni.so (if shared) or libhello-jni.a (if static)
- LOCAL_SRC_FILES := hello-jni.c
- the source files (C/C++) that need to be build. Headers are not provided here.
- include $(BUILD_SHARED_LIBRARY)
- this ensures that make file code to build shared library becomes part of this make.
There are many options eg. you could build static instead of shared library with one word change (BUILD_STATIC_LIBRARY), see ndk/docs directory for documentation on Android.mk and many other aspects of the NDK. Read introduction parts of each page there right now but use that as reference than a book to read entirely right away.
Also go through JNI Tips on official android site along with code.
While this was perhaps, the simplest example possible, explore each of the sample apps in the NDK (perhaps in the order shown here)
- hello-jni: Just display a hello string from Java fetched from native C code
- test-libstdc++: This example demonstrates how to useAndroid's implementation of libstdc++. Basically shows what to do in Application.mk and Android.mk files to use the library.
- two-libs: Shows how to implement multiple libraries and make calls across these libraries. The call is just to add two numbers.
- native-activity: This is the place to start if you wish to entirely avoid Java to the extent possible and code purely in C/C++. Requires minimum SDK version to be 9. The example creates a OpenGL ES 1.x surface and fills the screen with color depending up on the point on screen touched by the user. This example avoids java src folder entirely!
- bitmap-plasma: This example does processing of a plasma gradient in native code but the bitmap is displayed using Java. This also shows how a specific feature of NDK helps access bitmap data from native code easily. In absence of AndroidBitmap extensions to the NDK, passing around the bitmap and associated details was a bit of a hassle.
- native-plasma: This example tries to do the same as the bitmap plasma example but through pure native activity. Both processing and display are done in native code. The most fascinating aspect of this example is the way it accesses the display. It draws directly to the native window without intermediate layers; ie., no OpenGL ES etc. Just a simple color data buffer dump. Such raw access is useful for the platform developers (not app developers).
- hello-gl2: This example shows basic use of OpenGL ES 2.0 based graphics capabilities from native code. Just displays a green triangle in flickering (dimming and brightening) gray background.
- gles3-jni: Similar to hello-gl2, this showcases OpenGL ES 3.x instead.
- hello-neon: New ARM architecture based chips optionally support a faster instruction set codenamed NEON along with the older instruction. This example shows how to use this specific sub-architecture. Implements an FIR filter and times its performance.
- module-exports: This example demonstrates the use of module exports feature (LOCAL_EXPORT_CFLAGS and other such variables). Three modules are defined:
- foo exports its include directory and a linker flag and is a static library
- bar simply uses foo, as a static library and builds itself as a shared library
- zoo uses bar, also resulting in a shared library
- native-audio: Shows how to play audio using OpenSL ES from native code. Till 2013, very few hardware supported this feature properly.
- native-media: Shows how to play video using OpenMAX AL form native code. Till 2013, very few hardware supported this feature properly.
- san-angeles: This example is an Android NDK port of a very popular demo app that showcases graphics capabilities using pretty tiny code for such output.
- HelloComputeNDK: Shows a simple example of using renderscript (RS) from NDK. Turns a bitmap to monochrome using a RS call from NDK which is in-turn invoked from Java.
- Teapot, MoreTeapots: These two examples show the generic OpenGL teapot example. What is special about this is the inclusion of NDKHelper. NDKHelper's sources are part of NDK download (ndk/sources/android/ndk_helper) and contain bunch of (currently undocumented) files to ease development of native_activity apps and OpenGL shader based apps. This can improve to a game engine in the future.
Enjoy!