The Android NDK works a bit like JNI (Java Native Interface) which allows to connect Java programs to C/C++. With NDK, you can compile dynamic and static libraries using the provided cross compilation chain (i.e. a way to create program on a foreign platform or processor).
Although not all Android APIs can be accessed yet from the NDK, the most important one is provided: OpenGL ES, the famous 3D API for handheld devices! In today’s article, I am going to show you how to get a basic NDK project up and running with OpenGL ES 2 and STL support. I am going to explain how to prepare the development environment and how the project files can be personalized.
Go to Android SDK & NDK Part 1: Setting-up your environment
Chapter 1: The Exodus: from Java to C++
Chapter 2: STL & Boost on Android
Chapter 3: Compiling C/C++ libraries with the NDK
Chapter 4: Android talks C++
Chapter 5: Time to run!
References
Special notice:
Please also note that Android currently supports Open GL ES 1.0, 1.1 partially & 2.0 since the Eclair release. I however don’t know if all devices will be compatible with GLES 2.0 eventually. Sadly, this also the case for the Android emulator which works only in GLES 1.x mode currently… So if no development phone is available, then consider using GL ES 1.1 only.
The Exodus: from Java to C++
Native libraries can be loaded dynamically when the Java application is launched with a static constructor in the main Java file:
public class VirtualXperiments extends Activity { static { // Loads the libcubic.so library. System.loadLibrary("stlport"); System.loadLibrary("virtualxp"); } }
Dynamic libraries are a piece of executable loaded on demand. They are the only one saved into the final application archive (i.e. the APK file) and loaded from an Android program. Dynamic libraries can be linked together so that a library A can makes use of a library B. And if a third library requires B, then it detects that the library is already loaded and use it (this is why they are also called shared libraries). On the other hand, static libraries are embedded in a dynamic library during compilation: the binary code (is copied into the final library, without regards to duplication. They make dynamic libraries bigger but “all-included”, without dependencies (you may remember the “DLL not found” syndrome on Windows).
And now, to use a native library, well guess what? You need native functions :
public class VirtualXperiments extends Activity { ... public static native void virtualXpInit (int width, int height); public static native void virtualXpStep (); }
Then just call these functions like classic Java methods! In VirtualXperiments, these methods will handle the Open GL ES calls to render 3D objects (setting the mesh, textures, sending them to the device, etc.). But the screen initialization (e.g. allocating a GL context) and display (e.g. swapping display buffers) has to be performed in Java.
Android provides a Java component named GLSurfaceView to allow easy integration of Open GL ES in Android apps. As our app is going to use Open GL 2 only, we need to use a specialized version of GLSurfaceView which selects a compatible rendering context. Hopefully, Googles’ engineers have included some examples in the NDK (see the hello-gl2 example). I made some trivial modifications to create a GL2SurfaceView component which takes a rendering context (i.e. the application) and a renderer (the rendering logic) as parameters. The corresponding file can be found in the sample project attached to this article and needs to be copied in the com.codexperiments.virtual package.
To integrate this new component, VirtualXperiments.java needs to be updated:
public class VirtualXperiments extends Activity { public void onCreate(Bundle savedInstanceState) { ... // We remove setContentView(R.layout.main); which loads the GUI defined // in the resource files. Instead, we ask the application to use our own // Open GL component as GUI. setContentView(new GL2SurfaceView(new VirtualXpRenderer(), getApplication())); } // The GL component uses a renderer to define the rendering logic (what is // rendered, where, etc.). It can of course define the application logic too // but that is a matter of design. // The present renderer delegates computation to the native libraries, which // performs itself the application (almost nothing) and rendering operations. private static class VirtualXpRenderer implements GLSurfaceView.Renderer { public void onDrawFrame(GL10 gl) { virtualXpStep(); } public void onSurfaceChanged(GL10 gl, int width, int height) { virtualXpInit(width, height); } public void onSurfaceCreated(GL10 gl, EGLConfig config) { // Do nothing. } } }
The Java side is now ready. Let’s see how to create a native library.
STL & Boost on Android
C++ is a language and not a framework, whereas as Java comes with a full-featured framework (for collections, GUI, etc.). This is why the C++ Standard Template Library has been created. The STL provides lots of useful piece of code such as containers, strings, algorithms, etc… It is now the standard in almost all C/C++ program but several implementation exists with compatibility issues. More particularly, Android NDK causes some trouble with STL because it does not handle exceptions. As stated by Google’s engineer David Turner in the NDK Google Group, exceptions (and RTTI) were deliberately discarded from the official implementation mainly because of the poorly generated code. I do not know if that still stands nowadays, but it has an immediate effect on existing code reusability. Hence the “minimal C++ support” stated by the NDK documentation.
That is why you may find some custom NDK with exceptions ability on the Web. I myself prefer to use the official NDK to avoid any dependency on third party modifications. But if compatibility or code reuse matters to you, then definitely give it a try. Hopefully for me, a few C++ coders patched the popular open source STL implementation named STLport to handle the Android platform (many thanks to Emmanuel in the NDK Google groups). STLport is quite portable (and more lightweight compared to the GNU implementation) and thus provides an option to deactivate exceptions. The patch to fully support Android is currently not included in the official release, so the code must be taken from the repository (you need GIT versioning system client installed on your system):
> git clone git://stlport.git.sourceforge.net/gitroot/stlport/stlport stlport
Put the downloaded directory in the folder “[VIRTUALXP_HOME]/cots”. The COTS directory, which means Component On The Shelf (a popular denomination), contains all external libraries. Although this can overload a bit the versioning system, I prefer having my COTS in my project to save and integrate new versions more easily, avoid version conflicts and have a ready to work project after Checkout. That is a matter of taste!
Anyway, another very popular framework for C++ projects is Boost (can be downloaded from here). Boost is a really huge library, in terms of size as well as functionalities! Hopefully most of it is only templates, which means that only include files (and no C++ implementation files) are provided for most of it. So no need to compile a library here: just include what you need and use it. Boost is particularly famous for its smart pointer implementation, a low-cost C++ wrappers to standard pointers offering additional capabilities like automatic garbage collection. This helps a lot in the prevention of the most common C++ diseases: memory leaks and destroyed object access. See theofficial documentation for more information on Boost features.
However, I warn you that there are some annoying incompatibilities between StlPort and Boost. One small example: when disabling exception support, boost stops throwing exceptions (that is supported) and passes an exception bad_alloc to the function boost::throw_exception which has to be redefined by users (I didn’t try this yet). Sadly, STLPort does not declare bad_alloc when exception mode is disabled. This looks more like a STLPort bug to me. Anyway, to take care of that, STLPort needs to be modified. Look for _new.h in STLPort (if you are in Eclipse, use Ctrl+Shift+R to quickly find a file by its name. When I was telling you that Eclipse is great!) and create a new optional directive to force bad_alloc declaration by replacing:
... #if defined (_STLP_USE_EXCEPTIONS) && defined (_STLP_NEW_DONT_THROW_BAD_ALLOC) # ifndef _STLP_INTERNAL_EXCEPTION # include <stl/_exception.h> # endif ...
with the following
... #if (defined (_STLP_USE_EXCEPTIONS) || defined(_STLP_DEFINE_BAD_ALLOC)) && defined (_STLP_NEW_DONT_THROW_BAD_ALLOC) # ifndef _STLP_INTERNAL_EXCEPTION # include <stl/_exception.h> # endif ...
Don’t forget to redefine throw_exception or you may get an undefined compilation error (especially when using boost smart pointer):
namespace boost { void throw_exception(std::exception const & e) { Your exception handling here... } }
Now, when compiling STLPort for Android, just define _STLP_DEFINE_BAD_ALLOC directive (-D_STLP_DEFINE_BAD_ALLOC in VIRTUALXP_CFLAGS, see next chapter) or add it (#define _STLP_DEFINE_BAD_ALLOC 1) to _android.h in STLPort config directory. You should also define BOOST_EXCEPTION_DISABLE and BOOST_NO_EXCEPTIONS for Boost. Note that as the sample project at the end of the article is very simple and doesn’t need Boost, these are not defined in the provided files.
Compiling C/C++ libraries with the NDK
The building system has changed with the new NDK R4. Prior to that version, a project descriptor Application.mk located in the “[ANDROID_NDK]/app” directory was required. That file, which was built using make, had to point to your home project containing an Android.mk file which was the real makefile. And to build native libraries, being in the NDK directory when launching the make command was a necessary condition.
The NDK R4 building system is now a bit simpler: the only thing required is to have an Android.mk file under a “jni” directory within your own project (e.g. in [VIRTUALXP_HOME]/jni). The new ndk-build command, located in [ANDROID_NDK], can then be launched from your project directory (or using the -C parameter to specifiy your project directory).
Because I like to personalize my project directories, I am still going to use an Android.mk file. This file is a prerequisite for people who do not want to use the “jni” directory. In my case, I like to name it “cpp”. Let’s create a [VIRTUALXP_HOME]/Application.mk file which defines the C/C++ projects to compile and the main options:
# Defines where your project folder is. It can be an absolute path to your Java # project or a path relative to the current dir (where ndk-build is launched) # defined by my-dir (NDK project file location, e.g. [NDK_HOME]/app/cubic/project). APP_PROJECT_PATH := $(call my-dir) # Defines the name and path of the makefile APP_BUILD_SCRIPT := $(APP_PROJECT_PATH)/Android.mk $(info $(APP_BUILD_SCRIPT)) # Defines the native projects that must be compiled. These names point to the libs # defined in the Android.mk file. APP_MODULES := stlport virtualxp # Can be set to release or debug. For optimization purpose. APP_OPTIM := debug
Edit 11/06/2010: I just discovered that in the NDK R4, APP_MODULES is not required any more. If it’s not specified, by default, all modules will be compiled. So it’s fine not to define it.
Then, we need to create in [VIRTUALXP_HOME] the Android.mk file, which is the C/C++ makefile handling cross compilation. Its location is defined by the APP_BUILD_SCRIPT variable defined in the Application.mk. As previously said, by convention, it should be located in [VIRTUALXP_HOME]/jni. But why doing simple when we can do pretty :
# Stores the project directory. Useful when you compile several libraries to restore # the build location. VIRTUALXP_HOME := $(call my-dir) # Defines common options (android release, here the 5th which is bundled with # Android Java API 5, 6 and 7). Also defines some preprocessor variables for # Open GL ES (for extensions beware that it may not be compatible with all # devices), STLport, ... VIRTUALXP_CFLAGS := \ -isystem $(SYSROOT)/usr/include --sysroot=build/platforms/android-5/arch-arm \ -mandroid -Wno-psabi \ -DGL_GLEXT_PROTOTYPES -D_STLP_NO_CWCHAR -D__ANDROID__ -DDEBUG # This is a C++ optimization, nothing specific to Android. It avoids (when applicable) # copying objects initialized inside a function when they are returned. They get # initialized directly at their final location. VIRTUALXP_CPPFLAGS := -felide-constructors # STLport compilation # Clear options from previous compiled projects if appliable. include $(CLEAR_VARS) # Name of the project, used in the Application.mk project file. LOCAL_MODULE := stlport # C Compiler options. "-I" adds C/C++ include files from the specified directory. LOCAL_CFLAGS := $(VIRTUALXP_CFLAGS) -I$(VIRTUAL_HOME)/cots/stlport/stlport # C++ compiler options LOCAL_CPPFLAGS := $(VIRTUALXP_CPPFLAGS) # Project source directory. LOCAL_PATH := $(VIRTUALXP_HOME)/cots/stlport/src # I only compile the files I need to make compilation faster and more important # the final stl library smaller (40Kb instead of 750Kb). #LOCAL_SRC_FILES := strstream.cpp messages.cpp iostream.cpp ctype.cpp dll_main.cpp complex_trig.cpp locale_catalog.cpp complex_io.cpp allocators.cpp string.cpp ostream.cpp locale_impl.cpp libstlport.so locale.cpp istream.cpp time_facets.cpp fstream.cpp bitset.cpp codecvt.cpp stdio_streambuf.cpp complex.cpp ios.cpp sstream.cpp num_put.cpp num_put_float.cpp cxa.c facets_byname.cpp numpunct.cpp monetary.cpp num_get_float.cpp num_get.cpp c_locale.c collate.cpp LOCAL_SRC_FILES := allocators.cpp dll_main.cpp include $(BUILD_SHARED_LIBRARY) # VirtualXperiments compilation include $(CLEAR_VARS) LOCAL_MODULE := virtualxp # The source code will be located in a cpp folder LOCAL_PATH := $(VIRTUALXP_HOME)/cpp LOCAL_CFLAGS := $(VIRTUALXP_CFLAGS) LOCAL_CPPFLAGS := $(VIRTUALXP_CPPFLAGS) -I$(VIRTUALXP_HOME)/cots/stlport/stlport LOCAL_SRC_FILES := VirtualXpJNI.cpp LOCAL_SHARED_LIBRARIES := stlport # Do not forget to indicates the required dynamic libraries. They will not be # embedded in the present library but rather loaded at the same time. # Library names must appear undecorated (GLESv2 instead of libGLESv2.so). # If you want to use static libraries, then the LOCAL_STATIC_LIBRARIES # variable has to be used instead. LOCAL_LDLIBS := -llog -lGLESv2 include $(BUILD_SHARED_LIBRARY)
Edit 12/03/2010: After digging a bit (thank you G.D.) -DTARGET_OS=android, -DANDROID and -D__SGI_STL_INTERNAL_PAIR_H were reminiscences of some past code and are not necessary any more. -Dandroid was used by my own engine (so not required here). On the other hand__ANDROID__ is used by stlport. But although it looks like it can compile without it, I’d rather set it! -D__NEW__ is used to bypass new included in arch-arm and avoid a conflict with operators provided by STLPort. However it’s already defined in STLPort _android.h config file and therefore is not essential. The Android.mk above is now updated with these modifications.
Moreover, I don’t know if some of you got the problem, but when deploying an application with STLPort on a recent device, I kept running into an unexpected crash until I realized there was already a “stlport” system library. I don’t know if that was specific to this device or if it is systematically deployed on Android 2.2. Anyway, a simple solution to that problem is to rename LOCAL_MODULE stlport into something different (like mystlport) and any references in LOCAL_SHARED_LIBRARIES option.
The NDK project is now configured. We only miss one minor thing now: the source code!
Android talks C++
I have created two source files: VirtualXpJNI.cpp and VirtualXp.cpp. No need for includes files in this very simple project. Below is VirtualXpJNI.cpp. This file is the entry point for the native library. It defines and implements the native methods found earlier in the Java part. These JNI methods are just calling an internal function. This indirection avoids coupling between JNI interface and C/C++ code and thus improves reusability. And finally, notice the keyword extern “C” which forbids name mangling when C++ compiler is used (to ensure functions names are unique). Indeed, Java expects the C convention when looking for native methods.
#include <jni.h> #include "VirtualXp.cpp" extern "C" { JNIEXPORT void JNICALL Java_com_codexperiments_virtual_VirtualXperiments_virtualXpInit(JNIEnv * env, jobject obj, jint width, jint height); JNIEXPORT void JNICALL Java_com_codexperiments_virtual_VirtualXperiments_virtualXpStep(JNIEnv * env, jobject obj); }; JNIEXPORT void JNICALL Java_com_codexperiments_virtual_VirtualXperiments_virtualXpInit(JNIEnv * env, jobject obj, jint width, jint height) { setupGraphics(width, height); } JNIEXPORT void JNICALL Java_com_codexperiments_virtual_VirtualXperiments_virtualXpStep(JNIEnv * env, jobject obj) { renderFrame(); }
Native methods names are constituted of:
- a concatenation of the “Java_” prefix, the Java package path, the class name and finally the native method name. Underscores are used as separators.
- a return type surrounded by the JNIEXPORT and JNICALL macros which respectively publishes the method (to make it available to the outside world) and describes the calling convention (parameter order, stack or registered based, etc.).
- a JNIEnv parameter, which is a handle to JVM functions (more information on Chapter 4 of the official Sun JNI documentation, see references), like creating Java arrays, calling java method (in a way similar to the Reflection API), etc… JNIEnv is specific to each thread and thus, can not be shared.
- effective parameters. Basic types are redefined by JNI to be compatible with the JVM: jint for integer, jstring for strings, jobject for Java objects, etc.
The second file is VirtualXp.cpp. This is the file which contains the effective OpenGL code which is going to render a simple green triangle. This code is the one that can be found in the Android NDK example hello-gl2. I am not going to explain it here as this is practically pure Open GL code: renderFrame() is called by Java at each iteration through the JNI interface.
#include <android/log.h> ... #define LOG_TAG "libclubic" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) ... LOGI("after %s() glError (0x%x)\n", op, error); ... void renderFrame() { ... glClearColor(grey, grey, grey, 1.0f); ... } ...
The only point I want to notice is the Android specific code which logs message the way a classic printf would. Although many of the Java features are not provided (in which case it should probably be implemented in Java anyway), it is always good idea to look for what is available (e.g. zlib) in your “[Android_NDK]/build/platforms/[your platform]/arch-arm/usr/include” folder.
Time to run!
We are almost done! To compile the project, go to the $ANDROID_NDK directory and run the following command:
[Android_NDK]/ndk-build -C [VirtualXperiments] NDK_APPLICATION_MK=[VirtualXperiments]/Application.mk
And if you want to erase what has been previously compiled, just add the clean parameter
[Android_NDK]/ndk-build -C [VirtualXperiments] NDK_APPLICATION_MK=[VirtualXperiments]/Application.mk clean
Just check that the libraries have been generated and copied into the “[VIRTUALXP_HOME]/libs” directory. And voilà! Your first hybrid app is compiled! Just a little trick to compile automatically with eclipse:
- Right click on your Project and click on the “Properties” item,
- Select the Builders tab
- Create a new builder of type Program
- Click on the first “Variables…” button.
- Select “Edit Variables…” and add a new ANDROID_NDK environment variable pointing to your own [ANDROID_NDK] directory. It is always a good idea to make use of Eclipse variables to keep a environment independent project configuration.
- Fill-in the form like in the screenshot below. If you use a different project name, then just replace VirtualXperiments by your own project name.
- Now, when you click on the build button, ndk-build will be automatically run and native libraries built. Much easier!
Automatically launch the NDK build command using Eclipse
Now run it using Eclipse (click on the Run or Debug button) after making sure your phone is connected. I remind you that Open GL ES 2 does not work on the Android emulator, so no need to try to run VirtualXperiments on it. The final project can be downloaded here.
The final result
I will try to explain in a future post how to set-up a Open GL ES environment on your Linux system with the PowerVR SDK. This is a really practical way to debug applications directly on your computer and to analyse them with tools such as Valgrind (for memory leaks, etc.).
References
- Java JNI Documentation
- NDK Documentation: Android JNI Tips
- Google Groups: Can C++ STL and other C++ components be used in NDK?
- Google Groups: STL on Android: STLport compiling & testing