Sunday, April 2, 2017

Android NDK Tutorial

Android Native Development Kit (NDK)

                      Android apps are typically written in Java, with its elegant object-oriented design. However, at times, you need to overcome the limitations of Java, such as memory management and performance, by programming directly into Android native interface. Android provides Native Development Kit (NDK) to support native development in C/C++, besides the Android Software Development Kit (Android SDK) which supports Java.

Passing value from java to C

In Java

private native short passIntReturnInt (int p);

static {

In C

#include <jni.h>
#include <android/log.h>
JNIEXPORT jint JNICALL Java_cookbook_chapter2_PassingPrimitiveActivity_passIntReturnInt(JNIEnv *pEnv, jobject pObj, jint pIntP) {
__android_log_print(ANDROID_LOG_INFO, "native", "%d in %d bytes", pIntP, sizeof(jint));
return pIntP + 1;


LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := PassingPrimitive
LOCAL_SRC_FILES := primitive.c


Manipulating strings in JNI

                        Strings are somewhat complicated in JNI, mainly because Java strings and C strings are internally different. Understanding the basics of encoding is essential to comprehend the differences between Java string and C string. The Unicode Standard is a character coding system designed to support the worldwide interchange, processing, and display of the written texts of the diverse languages and technical disciplines of the modern world. Unicode assigns a unique number for each character it defines, called code point. There are mainly two categories of encoding methods that support the entire Unicode character set, or a subset of it.

                   The first one is the Unicode Transformation Format (UTF), which encodes a Unicode code point into a variable number of code values. UTF-8, UTF-16, UTF-32, and a few others belong to this category. The numbers 8, 16, and 32 refer to the number of bits in one code value. The second category is the Universal Character Set (UCS) encodings, which encodes a Unicode code point into a single code value. UCS2 and UCS4 belong to this category. The numbers 2 and 4 refer to the number of bytes in one code value. 

JNIEXPORT jstring JNICALL Java_cookbook_chapter2_StringManipulationActivity_passStringReturnString(JNIEnv *pEnv, jobject pObj, jstring pStringP){

__android_log_print(ANDROID_LOG_INFO, "native", "print jstring: %s", pStringP);
const jbyte *str;
jboolean *isCopy;
str = (*pEnv)->GetStringUTFChars(pEnv, pStringP, isCopy);
__android_log_print(ANDROID_LOG_INFO, "native", "print UTF-8 string: %s, %d", str, isCopy);

jsize length = (*pEnv)->GetStringUTFLength(pEnv, pStringP);
__android_log_print(ANDROID_LOG_INFO, "native", "UTF-8 string length (number of bytes): %d == %d", length, strlen(str));
__android_log_print(ANDROID_LOG_INFO, "native", "UTF-8 string ends with: %d %d", str[length], str[length+1]);
(*pEnv)->ReleaseStringUTFChars(pEnv, pStringP, str);
char nativeStr[100];
(*pEnv)->GetStringUTFRegion(pEnv, pStringP, 0, length, nativeStr);
__android_log_print(ANDROID_LOG_INFO, "native", "jstring converted to UTF-8 string and copied to native buffer: %s", nativeStr);

const char* newStr = "hello 安卓";
jstring ret = (*pEnv)->NewStringUTF(pEnv, newStr);
jsize newStrLen = (*pEnv)->GetStringUTFLength(pEnv, ret);
__android_log_print(ANDROID_LOG_INFO, "native", "UTF-8 string with Chinese characters: %s, string length (number of bytes) %d=%d", newStr, newStrLen, strlen(newStr));
return ret;


Ex : JNIEXPORT void JNICALL Java_cookbook_chapter2_ManagingReferenceActivity_weakReference(JNIEnv *pEnv, jobject pObj, jstring pStringP, jboolean pDelete){
static jstring stStr;
const jbyte *str;
jboolean *isCopy;
if (NULL == stStr) {
stStr = (*pEnv)->NewWeakGlobalRef(pEnv, pStringP);
str = (*pEnv)->GetStringUTFChars(pEnv, stStr, isCopy);
if (pDelete) {
(*pEnv)->DeleteWeakGlobalRef(pEnv, stStr);
stStr = NULL;

Manipulating classes in JNI

Class descriptor: A class descriptor refers to the name of a class or an interface. It can be derived by replacing the "." character in Java with "/" in JNI programming. For example, the descriptor for class java.lang.String is java/lang/String.

FindClass and class loader: The JNI function FindClass has the following prototype:

jclass FindClass(JNIEnv *env, const char *name);

GetSuperclass: The JNI function GetSuperclass has the following prototype:

jclass GetSuperclass(JNIEnv *env, jclass clazz);

Manipulating objects in JNI

Create instance objects in the native code: Four JNI functions can be used to create instance objects of a Java class in the native code, namely AllocObject, NewObject,NewObjectA, and NewObjectV.

The AllocObject function creates an uninitialized object, while the other three methods take a constructor as an input parameter to create the object. The prototypes for the four functions are as follows:

jobject AllocObject(JNIEnv *env, jclass clazz);

jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...);

jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, jvalue *args);

jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args);

GetObjectClass: This JNI function has the following prototype:

jclass GetObjectClass(JNIEnv *env, jobject obj);

It returns a local reference to the class of the instance object obj. The obj argument must not be NULL, otherwise it will cause the VM to crash.

IsInstanceOf: This JNI function call has the following prototype:
jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);

Manipulating arrays in JNI

Arrays are represented by jarray or its subtypes such as jobjectArray andjbooleanArray.

Create new arrays: JNI provides NewObjectArray and New<Type>Array functions to create arrays for objects and primitive types. Their function prototypes are as follows:

jarray NewObjectArray(JNIEnv *env, jsize length, jclass elementType, jobject initialElement);

<ArrayType> New<Type>Array(JNIEnv *env, jsize length);

GetArrayLength: This native function has the following prototype:

jsize GetArrayLength(JNIEnv *env, jarray array);

Access object arrays: JNI provides two functions to access object arrays, namelyGetObjectArrayElement and SetObjectArrayElement.

The two functions have the following prototype:

jobject GetObjectArrayElement(JNIEnv *env,jobjectArray array, jsize index);

void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);

Access arrays of primitive types: void GetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint* buf);
void SetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint* buf);

Secondly, if we want to access a large array, then GetIntArrayElements andReleaseIntArrayElements are the JNI functions for us. They have the following prototype: jint *GetIntArrayElements(JNIEnv *env, jintArray array, jboolean *isCopy);
void ReleaseIntArrayElements(JNIEnv *env, jintArray array, jint *elems, jint mode);

Accessing Java static and instance fields in the native code the access of fields (both static and instance fields) in Java from native code:

jfieldID data type: jfieldID is a regular C pointer pointing to a data structure with details hidden from developers.

Accessing static fields: JNI provides three functions to access static fields of a Java class. They have the following prototypes:

jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
<NativeType> GetStatic<Type>Field(JNIEnv *env,jclass clazz, jfieldID fieldID);

void SetStatic<Type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID,<NativeType> value);

Accessing instance field: Accessing instance fields is similar to accessing static fields. JNI also provides the following three functions for us:

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

<NativeType> Get<Type>Field(JNIEnv *env,jobject obj, jfieldID fieldID);

void Set<Type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, <NativeType> value);
Calling static and instance methods from the native code

jmethodID data type: Similar to jfieldID, jmethodID is a regular C pointer pointing to a data structure with details hidden from the developers.
Method descriptor: This is a modified UTF-8 string used to represent the input (input arguments) data types and output (return type) data type of the method.

Calling static methods: JNI provides four sets of functions for native code to call Java methods. Their prototypes are as follows:

jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

<NativeType> CallStatic<Type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);

<NativeType> CallStatic<Type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);

<NativeType> CallStatic<Type>MethodV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args);

Calling instance methods: Calling instance methods from the native code is similar to calling static methods. JNI also provides four sets of functions as follows:

jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

<NativeType> Call<Type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);

<NativeType> Call<Type>MethodA(JNIEnv *env,jobject obj, jmethodID methodID, jvalue *args);

<NativeType> Call<Type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
Caching jfieldID, jmethodID, and referencing data to improve performance

The first approach caches at the class initializer. In Java, we can have something similar to the following: private native static void InitIDs();
static {
System.loadLibrary(<native lib>);

The second approach caches the IDs at the point of usage. We store the field or method ID in a static variable, so that the ID is valid the next time the native method is invoked.
Checking errors and handling exceptions in JNI

JNI defines two functions to check for exceptions, as follows: jboolean ExceptionCheck(JNIEnv *env);
jthrowable ExceptionOccurred(JNIEnv *env);

When the second function is used, an additional JNI function can be called to examine the details of the exception: void ExceptionDescribe(JNIEnv *env);

There are generally two ways to handle an exception. The first approach is to free the resources allocated at JNI and return. This will leave the responsibility of handling the exception to the caller of the native method.

The second practice is to clear the exception and continue executing. This is done through the following JNI function call: void ExceptionClear(JNIEnv *env);

Throw exceptions in the native code: JNI provides two functions to throw an exception from native code. They have the following prototypes:

jint Throw(JNIEnv *env, jthrowable obj);

jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);

Fatal error: A special type of error is the fatal error, which is not recoverable. JNI defines a function FatalError, as follows, to raise a fatal error:

void FatalError(JNIEnv *env, const char *msg);

Integrating assembly code in JNI

Android NDK allows you to write assembly code at JNI programming. Assembly code is sometimes used to optimize the critical portion of code to achieve the best performance.

Compile assembly $ $ANDROID_NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc -S tmp.c -o AssemblyMultiplyDemo.s --sysroot=$ANDROID_NDK/platforms/android-14/arch-arm/

The usage of the assembly code to implement a native method:

Inline assembly at C code: We can write inline assembly code for Android NDK development.

Generating a separate assembly code: One approach to write assembly code is to write the code in C or C++, and use a compiler to compile the code into assembly code. $ $ANDROID_NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc -S <c_file_name>.c -o <output_file_name>.s --sysroot=$ANDROID_NDK/platforms/android-<level>/arch-<arch>/

Compile the assembly code: Compiling assembly code is just like compiling C/C++ source code. As shown in the file, we simply list the assembly file as a source file as follows:

LOCAL_SRC_FILES := AssemblyMultiplyDemo.s assemblyinjni.c

Building Android NDK applications for different CPU features

Android NDK contains a library named cpufeatures, which can be used to detect the CPU family and optional features at runtime.

Add it in the static library list in as follows:


At the end of the file, import the cpufeatures module:

$(call import-module,cpufeatures)

In the code, include the header file <cpu-features.h>

Get the CPU family. The function prototype is as follows:

AndroidCpuFamily android_getCpuFamily();

It returns an enum. The supported CPU families are listed in the section to follow. ANDROID_CPU_FAMILY_MIPS

For the ARM CPU family, the supported CPU feature detections are as follows:
ANDROID_CPU_ARM_FEATURE_ARMv7: It means that the ARMv7-a instruction is supported.
ANDROID_CPU_ARM_FEATURE_VFPv3: It means that the VFPv3 hardware FPU instruction set extension is supported. Note that this refers to VFPv3-D16, which provides 16 hardware FP registers.
ANDROID_CPU_ARM_FEATURE_NEON: It means that he ARM Advanced SIMD (also known as NEON) vector instruction set extension is supported. Note that such CPUs also support VFPv3-D32, which provides 32 hardware FP registers.

Debugging an Android NDK application with logging messages

Android logging system provides a method for collecting logs from various applications into a series of circular buffers. The logcat command is used to view the logs.

mylog.h #include <android/log.h>

#define LOG_LEVEL 9
#define LOG_TAG "NDKLoggingDemo"

#define LOGU(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_UNKNOWN, LOG_TAG, __VA_ARGS__);}
#define LOGD(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_DEFAULT, LOG_TAG, __VA_ARGS__);}
#define LOGV(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__);}
#define LOGDE(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__);}
#define LOGI(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__);}
#define LOGW(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__);}
#define LOGE(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__);}
#define LOGF(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__);}
#define LOGS(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_SILENT, LOG_TAG, __VA_ARGS__);}


void outputLogs() {
LOGU(9, "unknown log message");
LOGD(8, "default log message");
LOGV(7, "verbose log message");
LOGDE(6, "debug log message");
LOGI(5, "information log message");
LOGW(4, "warning log message");
LOGE(3, "error log message");
LOGF(2, "fatal error log message");
LOGS(1, "silent log message");

Debugging an Android NDK application with NDK GDB Android NDK introduces a shell script named ndk-gdb to help one to launch a debugging session to debug the native code.

The application is built with the ndk-build command.
AndroidManifest.xml has the android:debuggable attribute of the<application> element set to true. This indicates that the application is debuggable even when it is running on a device in the user mode. Make sure that the debuggable attribute in AndroidManifest.xml is set to true.

Build the native library with the command "ndk-build NDK_DEBUG=1".

Run the application on an Android device. Then, start a terminal and enter the following command: $ ndk-gdb

Android NDK Multithreading

At Android NDK, POSIX Threads(pthreads) is bundled in Android's Bionic C library to support multithreading. This chapter mainly discusses the API functions defined in the pthread.h and semaphore.h header files,

Thread creation

Syntax : int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
void pthread_exit(void *value_ptr);
int pthread_join(pthread_t thread, void **value_ptr);

Ex: void jni_start_threads() {
pthread_t th1, th2;
int threadNum1 = 1, threadNum2 = 2;
int ret;
ret = pthread_create(&th1, NULL, run_by_thread, (void*)&threadNum1); // run_by_thread is function name
ret = pthread_create(&th2, NULL, run_by_thread, (void*)&threadNum2);
void *status;
ret = pthread_join(th1, &status);
int* st = (int*)status;
LOGI(1, "thread 1 end %d %d", ret, *st);
ret = pthread_join(th2, &status);
st = (int*)status;
LOGI(1, "thread 2 end %d %d", ret, *st);

Why we need Synchronizing
As per operating system terminology, mutex and semaphore are kernel resources that provide synchronization services (also called as synchronization primitives).

The producer-consumer problem:

Note that the content is generalized explanation. Practical details vary with implementation.

Consider the standard producer-consumer problem. Assume, we have a buffer of 4096 byte length. A producer thread collects the data and writes it to the buffer. A both the threads should not run at the same time.consumer thread processes the collected data from the buffer. Objective is,Using Mutex:

A mutex provides mutual exclusion, either producer or consumer can have the key (mutex) and proceed with their work. As long as the buffer is filled by producer, the consumer needs to wait, and vice versa.

At any point of time, only one thread can work with the entire buffer. The concept can be generalized using semaphore.

Using Semaphore:

A semaphore is a generalized mutex. In lieu of single buffer, we can split the 4 KB buffer into four 1 KB buffers (identical resources). A semaphore can be associated buffers at the same time.with these four buffers. The consumer and producer can work on different Strictly speaking, a mutex is locking mechanism used to synchronize access to a resource. Only one task (can be a thread or process based on OS abstraction) can acquire the mutex. It means there is ownership associated with mutex, and only the owner can release the lock (mutex).

Semaphore is signaling mechanism (“I am done, you can carry on” kind of signal). For example, if you are listening songs (assume it as one task) on your mobile and at the same time your friend calls you, an interrupt is triggered upon which an interrupt service routine (ISR) signals the call processing task to wakeup.


Is a key to a toilet. One person can have the key - occupy the toilet - at the time. When finished, the person gives (frees) the key to the next person in the queue.

Officially: "Mutexes are typically used to serialise access to a section of re-entrant code that cannot be executed concurrently by more than one thread. A mutex object only allows one thread into a controlled section, forcing other threads which attempt to gain access to that section to wait until the first thread has exited from that section." Ref: Symbian Developer Library

(A mutex is really a semaphore with value 1.)


Is the number of free identical toilet keys. Example, say we have four toilets with identical locks and keys. The semaphore count - the count of keys - is set to 4 at beginning (all four toilets are free), then the count value is decremented as people are coming in. If all toilets are full, ie. there are no free keys left, the semaphore count is 0. Now, when eq. one person leaves the toilet, semaphore is increased to 1 (one free key), and given to the next person in the queue.

Officially: "A semaphore restricts the number of simultaneous users of a shared resource up to a maximum number. Threads can request access to the resource (decrementing the semaphore), and can signal that they have finished using the resource (incrementing the semaphore)." Ref: Symbian Developer Library

Synchronizing native threads with mutex at Android NDK

A mutex can be initialized with the pthread_mutex_init function, which has the following prototype: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);


The following four functions are available to lock and unlock a mutex: int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_lock_timeout_np(pthread_mutex_t *mutex, unsigned msecs);

int cnt = 0;
int THR = 10;
void *run_by_thread1(void *arg) {
int* threadNum = (int*)arg;
while (cnt < THR) {
while ( pthread_mutex_trylock(&mux2) ) {
pthread_mutex_unlock(&mux1); //avoid deadlock
usleep(50000); //if failed to get mux2, release mux1 first
LOGI(1, "thread %d: cnt = %d", *threadNum, cnt);

Synchronizing native threads with conditional variables at Android NDK pthread_mutex_t mux;
pthread_cond_t cond;
void jni_start_threads() {
pthread_t th1, th2;
int threadNum1 = 1, threadNum2 = 2;
int ret;
pthread_mutex_init(&mux, NULL);
pthread_cond_init(&cond, NULL);
ret = pthread_create(&th1, NULL, run_by_thread1,
LOGI(1, "thread 1 started");
ret = pthread_create(&th2, NULL, run_by_thread2,
LOGI(1, "thread 2 started");
ret = pthread_join(th1, NULL);
LOGI(1, "thread 1 end %d", ret);
ret = pthread_join(th2, NULL);
LOGI(1, "thread 2 end %d", ret);

int cnt = 0;
int THR = 10, THR2 = 5;
void *run_by_thread1(void *arg) {
int* threadNum = (int*)arg;
while (cnt != THR2) {
LOGI(1, "thread %d: about to wait", *threadNum);
pthread_cond_wait(&cond, &mux);
LOGI(1, "thread %d: cnt = %d", *threadNum, cnt);

Programming with the dynamic linker library in Android NDK

Dynamic loading is a technique to load a library into memory at runtime, and execute functions or access variables defined in the library. It allows the app to start without these libraries. void naDLDemo(JNIEnv* pEnv, jclass clazz) {
void *handle;
double (*sqrt)(double);
const char *error;
handle = dlopen("", RTLD_LAZY);
if (!handle) {
LOGI(1, "%s\n", dlerror());
dlerror(); /* Clear any existing error */
*(void **) (&sqrt) = dlsym(handle, "sqrt");
if ((error = dlerror()) != NULL) {
LOGI(1, "%s\n", error);
LOGI(1, "%f\n", (*sqrt)(2.0));

Add an file under the jni folder with the following content:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := DynamicLinker
LOCAL_SRC_FILES := DynamicLinker.cpp
LOCAL_LDLIBS := -llog -ldl

he following functions are defined in the dlfcn.h header file by the Android dynamic linking library: void* dlopen(const char* filename, int flag);
int dlclose(void* handle);
const char* dlerror(void);
void* dlsym(void* handle, const char* symbol);
int dladdr(const void* addr, Dl_info *info);

Understand the files

Android NDK provides an easy-to-use build system, which frees us from writing makefiles. However, we still need to provide some basic inputs to the system through and

The file is a GNU makefile fragment that describes the sources to the Android build system. The sources are grouped into modules. Each module is a static or shared library. The Android NDK provides a few predefined variables and macros.

CLEAR_VARS: This variable points to a script, which undefines nearly all module description variables except LOCAL_PATH include $(CLEAR_VARS)

BUILD_SHARED_LIBRARY: This variable points to a build script, which determines how to build a shared library from the sources listed, based on the module description. We must have LOCAL_MODULE and LOCAL_SRC_FILES defined when including this variable, as follows:


my-dir: The my-dir macro returns the path of the last included makefile, which is usually the directory containing the current file. It is typically used to define the LOCAL_PATH, as follows:

LOCAL_PATH := $(call my-dir)

all-subdir-makefiles: This macro returns a list of files located in all subdirectories of the current my-dir path. include $(call all-subdir-makefiles)

LOCAL_PATH: This is a module description variable, which is used to locate the path to the sources. It is usually used with the my-dir macro, as follows:

LOCAL_PATH := $(call my-dir)

LOCAL_MODULE: This is a module description variable, which defines the name of our module.

LOCAL_SRC_FILES: This is a module description variable, which lists out the sources used to build the module.

LOCAL_C_INCLUDES: This is an optional module description variable, which provides a list of the paths that will be appended to the include search path at compilation. LOCAL_C_INCLUDES := $(LOCAL_PATH)/../libbmp/

LOCAL_SHARED_LIBRARIES: This is an optional module description variable, which provides a list of the shared libraries the current module depends on. LOCAL_SHARED_LIBRARIES := libbmp

LOCAL_LDLIBS: This is an optional module description variable, which provides a list of linker flags. It is useful to pass the system libraries with the -l prefix. LOCAL_LDLIBS := -llog

Using a library as a prebuilt library

The content of this file is as follows:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libbmp-prebuilt
LOCAL_SRC_FILES := libbmp-0.1.3/lib/libbmp.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libbmp-0.1.3/include/
include $(CLEAR_VARS)
LOCAL_MODULE := PortingWithBuildSystem
LOCAL_SRC_FILES := PortingWithBuildSystem.c
LOCAL_STATIC_LIBRARIES := libbmp-prebuilt



Post a Comment

Popular Posts

Recent Posts


Unordered List


Powered by Blogger.


Recent Posts


Contact Form


Email *

Message *