Android Programming: Pushing the Limits (2014)
Part III. Pushing the Limits
Chapter 14. Native Code and JNI
Although the Java-based APIs available for Android are usually sufficient for most applications, a time will
come when you need to go native. You’ll find several reasons for having to use native code in your application.
Perhaps you have an existing proprietary native library that you need to integrate with an application on
Android and re-implementing it in Java would take too much time. Another common reason is performance.
Most performance-critical code in Android is already running in native, not within the Dalvik VM. When you
need to use a performance-critical library, it’s often better to use the native version than trying to find, or port,
a Java version.
I begin this chapter by explaining how to write pure native applications for Android. Because games are the most
common reason for writing pure native applications, I also show how to use OpenGL ES 2.0 for Android in C. Next,
I show how you can mix Java and native code to get the best from both worlds. Then I introduce the basics of the
Java Native Interface (JNI) and how you can use it in your code. Finally, I take you through an example of how to
integrate an existing native C library in your Java application using the stand-alone toolchain .
For this chapter, I assume that you have at least a basic knowledge of the C programming language.
However, even if you don’t, this chapter can help you understand how to work with native code for
your Android application.
A Note on CPU Architecture
When you build a piece of native code with the Android NDK, it will match a specific application binary
interface (ABI). These ABIs represent the different types of CPU architectures that Android supports. At the
time of this writing, the vast majority of Android devices run on ARM CPUs, or more specifically, ARMv7a. This
is an extension of the standard ARM architecture, but it supports a number of more advanced features such as
hardware floating-point computation. The ABIs supported by the Android NDK are armeabi (basic 32-bit ARM
support), armeabi-v7a (ARMv7), x86 (32-bit Intel x86 or IA-32), and mips (MIPS32 revision 1).
When you’re building your native code, consider which CPU architecture (ABI) your application needs to
support. If you compile your code only for ARMv7, your application won’t be visible on the Google Play Store for
devices with the other CPU architectures. In this way you can use native libraries as a sort of filter for the Google
Play Store as well.
You’re now probably asking why you shouldn’t build your native code for every possible platform because
you just need to specify which ABIs you target in the build script (see “Android NDK Build Scripts,” later in this
chapter). The reason for not choosing to support all ABIs is that the native code you’re compiling may not
support some platforms. For instance, if your native code uses a specific CPU feature, porting your code to other
architectures may require a bit of extra work.
Writing Android Applications in C
The C programming language is one of the oldest programming languages still in use. Dennis Ritchie originally
developed it between 1969 and 1973 at AT&T Bell Labs. The language was standardized as ANSI C or C89 in
1989 and has since received several updates, named according to the year of release—for example, C90, C99,
and C11. The latest version, C11, was approved in December 2011.
In this chapter, I’ve intentionally focused on writing native code in C and excluded instructions and
examples for C++. Although a lot of the code here will work for C++, in some situations, C++ requires
different handling, and mixing the instructions for both of these languages can be confusing. Refer to
the official Android NDK documentation on how to use C++ to write native code for your applications.
Android has support for writing all or parts of your application in native C. To get started, you first need to
download the Android Native Development Kit (NDK) from the Android Developers site, which comes as a ZIP
file that you extract in your home directory.
You can find the full instructions on downloading and installing the Android NDK at http://
developer.android.com/tools/sdk/ndk.
Android NDK Build Scripts
After you download and extract the Android NDK, you can start developing native-only applications. However,
you still need the standard Android SDK to package your code into a working APK. You need to place all the
native code that your application will run in the jni directory of the main sources in your project.
In order for Android to understand how to compile your native code, you need to provide the NDK with the
proper build scripts. These files use the same syntax as the normal GNU makefiles, but their names differ from
what you find in source code you compile for Linux desktops or similar UNIX-based operating systems.
The only file required is Android.mk. Here is a simple example of such a file:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := native-activity
LOCAL_SRC_FILES := main.c
LOCAL_LDLIBS := -llog -landroid -lEGL -lGLESv1_CM
LOCAL_STATIC_LIBRARIES := android_native_app_glue
include $(BUILD_SHARED_LIBRARY)
$(call import-module,android/native_app_glue)
This example is taken from the native-activity sample application found in the Android NDK (<ndk root>/
samples/native-activity/). I’m using this sample code as a foundation for explaining how a native-
only Android application works. Notice the line starting with LOCAL_STATIC_LIBRARIES that tells the build
system to include a local static library when building this code, a detail I get back to in the section “Porting a
Native Library to Android.”
The second file is Application.mk, which describes which native modules are needed by your application.
Here you also can define additional build parameters, as shown in this example:
APP_ABI := all
APP_PLATFORM := android-10
Note in the preceding example that the contents of Application.mk. APP_ABI are used to tell which ABIs
this application supports—in this case, all the ABIs supported by the current NDK. APP_PLATFORM is similar to
the uses-sdk element in the manifest and specifies which Android API level is supported.
Native Activities
After you define the build files, you can move on to the code for your application. The example in this section is
still based on the native-activity sample found in the Android NDK.
<activity
android:name=”android.app.NativeActivity”
android:label=”@string/app_name”
android:configChanges=”orientation|keyboardHidden”>
<meta-data
android:name=”android.app.lib_name”
android:value=”native-activity”/>
<intent-filter>
<action android:name=”android.intent.action.MAIN”/>
<category android:name=”android.intent.category.LAUNCHER”/>
</intent-filter>
</activity>
Note: How you define the main Activity in your manifest differs from Java-based applications. A native
Activity will always use the name android.app.NativeActivity.
Next, you specify which native library should be loaded through the meta-data element with the name
android.app.func_name. This library must be the same as what you specified for LOCAL_MODULE in the
Android.mk file.
When writing a native-only application using the NativeActivity, you have a number of choices. The
native-activity sample found in the Android NDK shows the method that most developers are likely to be
interested in. It uses the API provided by android_native_app_glue.h, which makes it easier to handle
the threading and also shows how to set up sensor and event input.
#include <jni.h>
#include <errno.h>
#include <EGL/egl.h>
#include <GLES/gl.h>
#include <android/sensor.h>
#include <android/log.h>
#include <android_native_app_glue.h>
In the preceding code, notice the included headers from the file main.c in the native-activity sample. By
including android_native_app_glue.h in your native code and also including the local static library
android_native_app_glue in Android.mk, you get a lot of boilerplate code for free. As a result, you can
focus on the actual work of your application, instead of having to set up everything yourself and manage all the
threading.
The following function is the entry point for your native Activity when using the android_native_app_
glue library. Because of the amount of code involved, I didn’t include the actual code from the native-activity
sample, but you can refer to the sample in the Android NDK for the specific details.
void android_main(struct android_app* state) {
// Application initialization and setup goes here...
state->onInputEvent = engine_handle_input;
while(1) {
// Application thread loops here...
}
}
The parameter state has the type of an android_app pointer, which points to a struct where you can
access application data and set callback functions. In the preceding code, you see how to give a pointer to
a previously declared function that implements the input event handling. Here is an example of how such a
function is declared:
static int32_t engine_handle_input(struct android_app* app, AInputEvent*
event) {
// Handle input events here...
}
The examples in this section provide a brief introduction for implementing your native-only Android
application. Refer to the native-activity sample in the Android NDK for further details and additional examples
of how to use OpenGL ES on the native side for your application.
Working with JNI
When writing an application with both native C and managed Java code, you need to use the Java Native
Interface (JNI) for bridging. This is a software layer and API that allows native code to call methods on Java
objects and Java methods to call native functions.
On the Java side, you only need to declare the methods that connect to a native function with the prefix
native. Doing so causes the VM to look up a native function, as I show in the next section. In order for native
code to invoke a method on a Java object, more work is involved, which I show in the section “Calling a Java
Method from Native,” later in this chapter.
Calling Native Functions from Java
To call a native function from a Java method, you need to declare a special method in your class with the keyword
native that will work as a bridge to the native side. This declaration causes the JVM to look up a native function
with the name formed by the package, class, and method name whenever that native method is invoked.
public class NativeSorting {
static {
System.loadLibrary(“sorting_jni”);
}
public NativeSorting() {
}
public void sortIntegers(int[] ints) {
nativeSort(ints);
}
private native void nativeSort(int[] ints);
}
The preceding is a simplified example of a class that provides a method for sorting an array of int. There are
two methods, besides the constructor, in this class. The first, sortIntegers(), is the one that you call from
your other Java classes, and it’s a regular Java method. The second, nativeSort(), is the method that points
to a function in your native code. Although you can have native methods declared as public, it’s good practice
to keep them private and wrap their call in a Java method that can manage error handling.
Although you can write the native code from scratch, a tool called javah found in the Java SDK (not the
Android SDK) can help you with this. This command-line tool generates a C header file with the function
declaration that your native methods map to. Run the following command in the src/main directory of your
project. Make sure you compile the Java code for your application first.
$ $ javah -classpath ../../build/classes/release/ -d jni/ com.aptl.
jnidemo.NativeSorting
The following command shows how to generate a header file for the NativeSorting class shown in the
previous code example. The -classpath points to the compiled class files (usually under the <project
path>/build directory), not the DEX file, and the -d parameter points to the output directory for the
generated headers (usually your jni directory in your project). This command generates a file named com_
aptl_jnidemo_NativeSorting.h in the jni directory that contains the declaration of the native function.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_aptl_jnidemo_NativeSorting */
#ifndef _Included_com_aptl_jnidemo_NativeSorting
#define _Included_com_aptl_jnidemo_NativeSorting
#ifdef __cplusplus
extern “C” {
#endif
/*
* Class: com_aptl_jnidemo_NativeSorting
* Method: nativeSort
* Signature: ([F)V
*/
JNIEXPORT void JNICALL Java_com_aptl_jnidemo_NativeSorting_nativeSort
(JNIEnv *, jobject, jintArray);
#ifdef __cplusplus
}
#endif
#endif
This code shows the generated files. As the comment on the first line says, you shouldn’t edit this file. You only
copy the method declaration to the .c files that implement the function.
The following code shows the implementation of the JNI function declared in the header file com_aptl_
jnidemo_NativeSorting.h. This example doesn’t do much in the JNI_OnLoad function, other than
return a constant indicating that this code follows JNI version 1.6, which is the one supported by the Dalvik VM.
#include <jni.h>
#include <android/log.h>
#include “com_aptl_jnidemo_NativeSorting.h”
void quicksort(int *arr, int start, int end);
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL Java_com_aptl_jnidemo_NativeSorting_nativeSort
(JNIEnv *env, jobject obj, jintArray data) {
jint* array = (*env)->GetIntArrayElements(env, data, 0);
jint length = (*env)->GetArrayLength(env, data);
quicksort(array, 0, length);
(*env)->ReleaseIntArrayElements(env, data, array, 0);
}
void quicksort(int *arr, int start, int end)
{
// Left out as an exercise for your upcoming interview at Google...
}
In this example, the calls to GetIntArrayElements, GetArrayLength, and ReleaseIntArray
Elements are the JNI-specific code. The first function call retrieves a pointer to the internal data of the array
that can be passed to regular C function. The second one gives you the size of the array. Finally, the third one
tells the JVM that the native side is done with the array and that it can copy back its content. These function calls
are needed because the complex data types (arrays and objects) that are passed from Java to native through JNI
must be accessed through the JNIEnv object.
Note: The call to GetIntArrayElements returns a jint pointer, which points to the data within the
jintArray received in the function, and you can treat the jint pointer as a normal int pointer.
The preceding JNI example is for demonstration purposes only; you should always perform sorting of
this kind using Arrays.sort() or Collections.sort(). Normally, you won’t need to perform
sorting on the native side because the Java implementation is fast enough.
Calling a Java Method from Native
Calling a Java method from the native side is a bit more complicated than calling a native function from Java.
Because Java is object-oriented, every method call must be made toward a Java object (or toward a Java class
for static methods). Thus you need to get a reference not only to the method but also to the object you want
to call the method on. This is where the JNIenv, JavaVM, and jobject parameters shown in the earlier
examples become important.
Consider the example of native sorting in the previous section. Everything in that example is happening on the
same thread, and the native function call will block until the sorting is completed. You can modify this so that
the sorting will happen asynchronously. A callback object is passed to the native method, which will then be
called by the native code using JNI functions, as shown here:
public void sortIntegersWithCallback(int[] ints, Callback callback) {
nativeSortWithCallback(ints, callback);
}
private native void nativeSortWithCallback(int[] ints, Callback callback);
public interface Callback {
void onSorted(int[]sorted);
}
The preceding methods show the Java side of the modified native sorting. With the following, you generate the
header file as before using the javah tool.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_aptl_jnidemo_NativeSorting */
#ifndef _Included_com_aptl_jnidemo_NativeSorting
#define _Included_com_aptl_jnidemo_NativeSorting
#ifdef __cplusplus
extern “C” {
#endif
/*
* Class: com_aptl_jnidemo_NativeSorting
* Method: nativeSortWithCallback
* Signature: ([ILcom/aptl/jnidemo/NativeSorting/Callback;)V
*/
JNIEXPORT void JNICALL Java_com_aptl_jnidemo_NativeSorting_
nativeSortWithCallback
(JNIEnv *, jobject, jintArray, jobject);
#ifdef __cplusplus
}
#endif
#endif
Note: The function declaration contains a jobject parameter. This is a JNI reference to the callback object
you’ll use for returning the result.
The following is the new implementation of the native code for your sorting, divided into multiple parts to
make things clearer. The changes from the previous example are shown in bold:
#include <jni.h>
#include <android/log.h>
#include <pthread.h>
#include “com_aptl_jnidemo_NativeSorting.h”
JavaVM *g_vm;
struct thread_args {
int* data;
int data_size;
jobject callback;
};
void quicksort(int *arr, int start, int end);
void background_sorting(void* args);
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
g_vm = vm;
return JNI_VERSION_1_6;
}
As you can see, first you include the pthread library that provides the native API for threading. Next, you declare
two global variables, a reference to the JavaVM and a struct that will be used to pass arguments to the
new thread. The JavaVM variable is assigned in the JNI_OnLoad function. Also note the declaration of the
function background_sorting. This function is passed to the new thread.
JNIEXPORT void JNICALL
Java_com_aptl_jnidemo_NativeSorting_nativeSortWithCallback
(JNIEnv *env, jobject obj, jintArray data, jobject callback) {
jint* array;
jint length;
jmethodID callbackMethodId;
jclass callbackClass;
pthread_t thread;
struct thread_args* myThreadData = malloc(sizeof(struct thread_args));
array = (*env)->GetIntArrayElements(env, data, 0);
length = (*env)->GetArrayLength(env, data);
myThreadData->data = array;
myThreadData->data_size = length;
myThreadData->callback = (*env)->NewGlobalRef(env, callback);
(*env)->ReleaseIntArrayElements(env, data, array, JNI_COMMIT);
pthread_create(&thread, NULL, (void*)background_sorting,
(void*) myThreadData);
}
In the preceding example the new JNI function mapping to your native method first allocates a new struct
thread_args that will contain the parameters for the new thread. Next, you create a global reference to
the callback object through the call to NewGlobalRef(), which you must do because all references in a JNI
function are valid only on the current thread. This is followed by a call to ReleaseIntArrayElements,
which notifies the VM that you’re done with the jintArray. However, the difference between this example and
the one shown in “Calling Native Functions from Java” is that you pass JNI_COMMIT as the last parameter—to
prevent the VM from freeing the array variable that contains the data. Finally, you call pthread_create() to
start the new thread. The third argument to pthread_create() is a pointer to the function that will run on
the new thread, and the fourth argument is the argument passed to that function.
The following code shows the function that will execute on the background native thread:
void background_sorting(void* arg) {
struct thread_args *data = (struct thread_args *) arg;
JNIEnv* env = NULL;
jclass callbackClass;
jmethodID callbackMethodId;
jintArray result;
quicksort(data->data, 0, data->data_size);
(*g_vm)->AttachCurrentThread(g_vm, &env, NULL);
result = (*env)->NewIntArray(env, data->data_size);
(*env)->SetIntArrayRegion(env, result, 0, data->data_size, data->data);
callbackClass = (*env)->GetObjectClass(env, data->callback);
callbackMethodId = (*env)->GetMethodID(env, callbackClass,
“onSorted”, “([I)V”);
(*env)->CallVoidMethod(env, data->callback, callbackMethodId, result);
free(data->data);
free(data);
(*env)->DeleteGlobalRef(env, data->callback);
(*g_vm)->DetachCurrentThread(g_vm);
}
First, you cast the argument back to a pointer of the expected type. You can now call quicksort() using
these arguments to sort the array. Next, you need to connect to the Java VM, create a Java array for the result,
find the callback method and invoke it, and finally free all the resources.
The call to AttachCurrentThread() tells the Java VM that the current thread should be connected and
the JNIEnv argument will be assigned to a valid instance. Now you can make calls on the JNIEnv object
and create a new Java int array. To find the callback method, you need to look up the class by a call to
GetObjectClass() with the global jobject reference as an argument. Next, you retrieve a jmethodID
by calling GetMethodID() with the jclass, the name of the method, and its native signature as arguments.
With a jmethodID and a jobject, you can call CallVoidMethod() to invoke the Java method.
The signature of a Java method is a string that identifies the arguments and the return value. You can
read more about type signatures at http://docs.oracle.com/javase/6/docs/technotes/
guides/jni/spec/types.html#wp16432.
Lastly, you free the data allocated in the struct, delete the global reference to the callback object, and detach
the VM from this thread.
The example in this section doesn’t include error handling and is used only to illustrate how to pass Java objects
between two threads and how to invoke methods from a native pthread. The important thing to remember is
to free all resources properly to avoid the most common cause of errors when using JNI.
Android Native APIs
Whether you’re writing an application entirely in native C or by combining native code with Java using JNI, you
will most likely need to use some of the native APIs for Android. In this section, I cover some of these APIs and
how you can include them in your code.
Using a native API requires you to do two things: Add a reference in the Android.mk file so that your shared
library will link to it and include the correct header file in your C code.
To see the full documentation for the supported native APIs, refer to the NDK documentation (<ndk root>/
documentation.html).
The C Library
The C programming language has a standard library named C library, or libc. The Android implementation of
this library is called Bionic and is slightly different from the standard libc implementation found on regular Linux
distributions.
The Android NDK build system always links to the C library, so all you need to do when using this library is
include the correct header file (stdio.h, stdlib.h, and so on). Also, the pthread and math libraries are
automatically linked in the same way.
Native Android Logging
The logging functionality used in Android through the Java class Log is also available in native C through the
log library. To include support for this in your native code, add the following to your Android.mk file:
LOCAL_LDLIBS := -llog
In your C code, you need to include the header file as follows:
#include <android/log.h>
To simplify your code, wrap the functions declared in the Android log library in a macro. Use the following code
in your C code to define log macros to be used throughout your native code:
#define TAG “native-log-tag”
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, TAG,
__VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, TAG,
__VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, TAG,
__VA_ARGS__))
#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, TAG,
__VA_ARGS__))
Native OpenGL ES 2.0
If you’ll be writing a pure native application, you will most likely implement the user interface for your
Activity using OpenGL ES 2.0, as shown in an earlier example in the section “Native Activities.” To use this
API in your native code, add -lGLESv2 to LOCAL_LDLIBS in your Android.mk file and include the headers
GLES2/gl2.h and GLES2/gl2ext.h in your C code.
It’s also a good idea to include the EGL library in order to set up a GL context. Use -lEGL in the Android.mk
file and include EGL/egl.h and EGL/eglext.h in your code.
Native Audio with OpenSL ES
The native audio API in Android is based on the OpenSL ES 1.0.1 from Khronos Group. This API provides a way
to perform native audio operations that is generally faster than the Java alternatives. If your application requires
very low-latency audio, use this API. To include the API, use the following flag in your Android.mk file:
LOCAL_LDLIBS += -lOpenSLES
Use the following include statements in your code to utilize both the standard OpenSL ES API and the
Android-specific extensions:
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Platform.h>
#include <SLES/OpenSLES_Android.h>
#include <SLES/OpenSLES_AndroidConfiguration.h>
Porting a Native Library to Android
Often you will have the source code for an existing, usually open-source, library that you want to integrate into
your application. However, doing so can be difficult because these libraries are often built around the traditional
build system (usually a GNU makefile generated by the tools Autoconf and Automake) for Linux or other
UNIX operating systems. You could copy all the required source files and write a new Android.mk file from
scratch that configures the build and includes the right headers. However, this approach can prove very time-
consuming and error-prone, but luckily there’s a way around this issue.
The Android NDK provides the capability to generate a stand-alone toolchain that works in the same way as
the standard toolchain on your computer. By referring to this toolchain when building the library, you can
use the build system the source code is configured for and build a binary library that you can include in your
application. This process makes it easy to port an existing native library written in C/C++ and reduces the
complexity of your builds as well.
To build the stand-alone toolchain, run the following command:
$NDK/build/tools/make-standalone-toolchain.sh --platform=android-5
--install-dir=<install path>
This command creates a new stand-alone toolchain for the ARM ABI and Android API level 5. If you don’t
provide the install-dir parameter, a compressed archive with the toolchain is placed in your system’s
temp directory.
Building a Native Library
The library for the example in this section is the audio codec library called Opus. This is a free, open-source audio
codec that provides an excellent result for interactive speech as well as streaming music over the Internet. This
library uses technology from both the SILK codec from Skype and the CELT codec from Xiph.org. I don’t go into
the details of how Opus works; instead, I focus on the fact that you can encode and decode between a large
audio frame recorded in PCM format and a much smaller Opus packet.
You can download the source code and read more about the Opus codec at http://opus-
codec.org.
Even though this example uses the Opus audio codec, the same method applies to most other native libraries
that support Linux. Simply download your library and perform the same operations shown in this section.
After you download and extract the library, you need to configure your environment before building.
$ export PATH=$TOOLCHAINS/arm-linux-androideabi-4.6/bin/:${PATH}
$ export CC=arm-linux-androideabi-gcc
$ export CXX=arm-linux-androideabi-g++
These commands prefix your path so that the binaries in your toolchain have precedence over the default ones
for your operating system. It will also set two environment variables used by the build systems to determine
which compiler architecture to use.
Install all your stand-alone toolchains in a common directory and assign an environment variable. This
way, you can easily refer to a specific toolchain.
This command generates the file Makefile used for compiling and installing the library at the specified
position (note that <project path> requires an absolute path).
opus-1.0.2 $ ./configure --prefix=<project path>/jni/libopus --host=
arm-eabi
The preceding command needs to be executed in the directory where you extracted the Opus source code and
in the same terminal where you configured your environment. It sets up the build parameters for your sources.
The host parameter tells the configure script which CPU is to be targeted. The prefix parameter decides where
the compiled binaries and header files are installed. Use the directory of your Android application as a prefix if
that is where you wish the compiled library to be placed. Note that if the jni and libopus directories are
missing, they will be created.
Before you can continue and build the library, you probably need to manually edit the generated makefile. In a
normal UNIX environment, you can have several versions of the same library installed, which are identified by an
internal version number as well as their filename (for instance, libXYZ.1.2.3.so). The Android NDK doesn’t
support this version information, so before you build the library, you need to tell the build system to avoid
applying any version information. You do so by editing the file Makefile that was generated by the configure
script. Open the file in a text editor and find the line that appears as follows:
libopus_la_LDFLAGS = -no-undefined -version-info 3:0:3
Now remove the –version-info argument and replace it with the argument –avoid-version, which
tells the build system not to add this information in the compiled result. The result will appear as follows:
libopus_la_LDFLAGS = -no-undefined –avoid-version
Now you’re ready to compile and install your library in your project’s jni directory:
opus-1.0.2 $ make ; make install
The preceding line is actually two commands, make and make install. The first command compiles the
code according to the configuration set up by the configure script. make install copies all the resulting
binaries and other files to the installation directory. You have now successfully installed the compiled Opus as
an ARM binary in the jni directory of your project. You’re ready to configure the NDK makefiles and implement
the JNI bridge to call the library.
Static Library Module
The library you just built consists of the actual library binary named libopus.so as well as a number of C
header files. The header files are required in order to call the functions inside the library itself. When building
your native code with the NDK, be sure to define a separate module for the native library. This is why the output
path was <project path>jni/libopus and not simply <project path>/jni. The Android.mk file
you place in the libopus directory looks like this:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libopus
LOCAL_SRC_FILES := libopus.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
The makefile configuration described in the preceding examples defines the module as a shared library and
exports the include files found in the include directory. Because no sources need to be compiled for this
module (you already did that in the previous section), you specify only the libopus.so file as the source files
to be included.
JNI Module for Native Library
It’s time to create a new directory named opus_jni next to the libopus directory. This is where your JNI
wrapper module for the native library is implemented. Start with the Android.mk file that will import your
module.
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := opus_jni
LOCAL_SRC_FILES := opus_jni.c
LOCAL_LDLIBS := -llog
LOCAL_SHARED_LIBRARIES := libopus
include $(BUILD_SHARED_LIBRARY)
$(call import-module,libopus)
This Android.mk file tells the NDK to include the static local library libopus in your build, just as was done
in the native-activity sample shown in “Android NDK Build Scripts,” earlier in this chapter. The next step is to
define the Java class that will provide the native methods.
The following class loads the library named opus_jni, and for each new instance, a new encoder and a new
decoder are initialized. You then have two public methods, encodePcm() and decodeOpus(), that do the
actual codec work. Finally, there is a destroy() method that frees up the memory used by the codec. You
now use the javah tool, as shown earlier, to generate the header file for these functions. After you have the
header file, you can start implementing the JNI code.
public class OpusCodec {
{
System.loadLibrary(“opus_jni”);
}
public OpusCodec(int sampleRate, int channels) {
initOpusEncoder(sampleRate, channels);
initOpusDecoder(sampleRate, channels);
}
private native void initOpusDecoder(int sampleRate, int channels);
public native int decodeOpus(byte[] encodedData,
int encodedLength,
short[] pcmOutput,
int pcmLength);
private native void initOpusEncoder(int sampleRate, int channels);
public native int encodePcm(short[] pcmData,
int pcmLength,
byte[] encodedData,
int encodedLength);
public native void destroy();
}
Here is the entire JNI code, piece by piece, starting with the header files and definitions:
#include <opus/opus.h>
#include <opus/opus_defines.h>
#include <opus/opus_types.h>
#include <opus/opus_multistream.h>
#include <android/log.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include “com_aptl_opus_OpusCodecTest_OpusCodec.h”
#define LOG_TAG “OpusTest”
#define LOGD(LOG_TAG, ...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG,
__VA_ARGS__)
#define LOGV(LOG_TAG, ...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_
TAG,
__VA_ARGS__)
#define LOGE(LOG_TAG, ...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
__VA_ARGS__)
OpusDecoder* gOpusDecoder;
OpusEncoder* gOpusEncoder;
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
return JNI_VERSION_1_6;
}
As shown here, the locally generated header files for the JNI functions and the headers for the Opus codec
that are located in the libopus module are included. The two global variables, OpusDecoder and
OpusEncoder, are the references to the decoder and encoder objects in the Opus library.
You also define the shorter log functions as well as the default JNI_OnLoad function to tell the system that it
supports the correct JNI version.
/*
* Class: com_aptl_opus_OpusCodecTest_OpusCodec
* Method: initOpusDecoder
* Signature: (II)I
*/
JNIEXPORT void JNICALL Java_com_aptl_opus_OpusCodecTest_OpusCodec_
initOpusDecoder
(JNIEnv *env, jobject obj, jint sampleRate, jint channels) {
int error;
gOpusDecoder = opus_decoder_create(sampleRate, channels, &error);
LOGD(LOG_TAG, “Decoder initialized: %d”, error);
}
/*
* Class: com_aptl_opus_OpusCodecTest_OpusCodec
* Method: initOpusEncoder
* Signature: (II)I
*/
JNIEXPORT void JNICALL Java_com_aptl_opus_OpusCodecTest_OpusCodec_
initOpusEncoder
(JNIEnv *env, jobject obj, jint sampleRate, jint channels) {
int error;
gOpusEncoder = opus_encoder_create(sampleRate, channels,
OPUS_APPLICATION_VOIP, &error);
}
The two init functions call the respective create function from the Opus library. In this example, you
provide the encoder with a parameter telling it to optimize its processing for VoIP applications.
/*
* Class: com_aptl_opus_OpusCodecTest_OpusCodec
* Method: decodeOpus
* Signature: ([BI[BIZ)I
*/
JNIEXPORT jint JNICALL Java_com_aptl_opus_OpusCodecTest_OpusCodec_
decodeOpus
(JNIEnv *env, jobject obj, jbyteArray in, jint inLength, jshortArray
out,
jint outLength) {
opus_int32 encodedBytes;
jbyte* jniIn;
jshort* jniOut;
if(in != NULL) {
jniIn = (*env)->GetByteArrayElements(env, in, NULL);
jniOut = (*env)->GetShortArrayElements(env, out, NULL);
encodedBytes = opus_decode(gOpusDecoder,
jniIn,
inLength,
jniOut,
outLength, 0);
(*env)->ReleaseByteArrayElements(env, in, jniIn, JNI_ABORT);
(*env)->ReleaseShortArrayElements(env, out, jniOut, 0);
} else {
jniOut = (*env)->GetShortArrayElements(env, out, NULL);
encodedBytes = opus_decode(gOpusDecoder,
NULL,
0,
jniOut,
(*env)->GetArrayLength(env, out), 1);
}
return encodedBytes;
}
The decode function allows the input to the opus_decode function to be null, which handles situations
where you’re missing an incoming packet of Opus data because of network latency and want the decoder to
handle the situation. The Opus library is designed to handle such cases and will generate a PCM package to fill
out missing audio.
/*
* Class: com_aptl_opus_OpusCodecTest_OpusCodec
* Method: encodePcm
* Signature: ([BI[BI)I
*/
JNIEXPORT jint JNICALL Java_com_aptl_opus_OpusCodecTest_OpusCodec_
encodePcm
(JNIEnv *env, jobject obj, jshortArray in, jint inLength, jbyteArray
out,
jint outLength) {
opus_int32 encodedBytes;
jshort* jniIn;
jbyte* jniOut;
jniIn = (*env)->GetShortArrayElements(env, in, NULL);
jniOut = (*env)->GetByteArrayElements(env, out, NULL);
encodedBytes = opus_encode(gOpusEncoder,
jniIn,
inLength,
jniOut,
outLength);
if(encodedBytes == OPUS_BAD_ARG) {
LOGE(LOG_TAG, “OPUS_BAD_ARG”);
} else if(encodedBytes == OPUS_BUFFER_TOO_SMALL) {
LOGE(LOG_TAG, “OPUS_BUFFER_TOO_SMALL”);
} else if(encodedBytes == OPUS_INTERNAL_ERROR) {
LOGE(LOG_TAG, “OPUS_INTERNAL_ERROR”);
} else if(encodedBytes == OPUS_INVALID_PACKET) {
LOGE(LOG_TAG, “OPUS_INVALID_PACKET”);
} else if(encodedBytes == OPUS_UNIMPLEMENTED) {
LOGE(LOG_TAG, “OPUS_UNIMPLEMENTED”);
} else if(encodedBytes == OPUS_INVALID_STATE) {
LOGE(LOG_TAG, “OPUS_INVALID_STATE”);
} else if(encodedBytes == OPUS_ALLOC_FAIL) {
LOGE(LOG_TAG, “OPUS_ALLOC_FAIL”);
}
(*env)->ReleaseShortArrayElements(env, in, jniIn, JNI_ABORT);
(*env)->ReleaseByteArrayElements(env, out, jniOut, 0);
return encodedBytes;
}
The encode function takes PCM data represented as an array of short and encodes. The result ends up in the
byte array that the Java side can work with.
The destroy function is the last part in this file. It’s important to call this native method from your Java code as
follows once you’re done with the OpusCodec instance; otherwise, you’ll leak memory.
/*
* Class: com_aptl_opus_OpusCodecTest_OpusCodec
* Method: destroy
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_aptl_opus_OpusCodecTest_OpusCodec_destroy
(JNIEnv *env, jobject obj) {
opus_decoder_destroy(gOpusDecoder);
opus_encoder_destroy(gOpusEncoder);
}
You now have a fully functioning JNI wrapper for the Opus codec optimized for VoIP applications. With just
some small modifications, you can craft this implementation to your own needs and build your own VoIP
application or adapt it for streaming music.
Summary
In this chapter, I introduced you to the Android NDK and how you can use it to write and build native code for
your Android applications. You can choose to write your entire application in native, which is many times the
case when it comes to 3D games where there’s an existing 3D engine available, or you can combine it with Java
code and write native C code where further optimization is needed. Also, thousands of open-source libraries
are on the Internet that you may find useful, and with the techniques shown in this chapter, you can easily port
them to Android and create a simple JNI wrapper to access them from Java.
This chapter certainly isn’t a comprehensive discussion of all things regarding the NDK and JNI. However, for
further details, please refer to the official documentation in the Android NDK as well as the JNI specifications
from Oracle, and check out “Further Resources,” below.
Further Resources Android Developers Site
“JNI Tips” specific to Android development: http://developer.android.com/training/articles/perf-jni.html
Oracle Websites
Oracle. “Java SE Documentation.” Here you can find information on JNI Documentation: http://docs.oracle.com/javase/7/docs/technotes/guides/jni/index.html
Oracle. “Java Native Interface Documentation.” Here you can find specification for JNI: http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html