03. Android Internals

  • Description: In-depth view of the Android lower components: Linux kernel, Dalvik, Binder, Android framework, Managers
  • Practical part: Android NDK installation, tools and building simple Apps

Lecture

Practical

Solved tasks: lab-3-solved.zip

Task 0 - Set NDK Path

In Eclipse you must set the NDK path if it is not set already. In the top menu select Window > Preferences, then go to Android > NDK. NDK Location should be set to the NDK path. If you are using Beacon Mountain on Linux the default the path is /opt/intel/beaconmountain/NDK.

Task 1 - Create a Native Project

Create a new project. Name it native1 and make sure the package is com.example.native1. Then right click on the project in the Project Explorer window and select Android Tools > Add Native Support from the menu. set the library to native1 as well. This creates a jni folder in your project which contains two files: native1.cpp and Android.mk.

Edit native1.cpp and add the following function:

extern "C" {
	jstring Java_com_example_native1_MainActivity_getString(JNIEnv *env, jobject thiz);
}
 
jstring Java_com_example_native1_MainActivity_getString(JNIEnv *env, jobject thiz)
{
	return env->NewStringUTF("Hello world from JNI!");
}

C++ adds decorators to functions so the Dalvik VM would not be able to locate them if not declared as extern “C”.

The equivalent C code is:

jstring Java_com_example_native1_MainActivity_getString(JNIEnv *env, jobject thiz)
{
	return (*env)->NewStringUTF(env, "Hello world from JNI!");
}

As you can see the function name is a bit special: it starts with Java and contains the full class name (including package) and ends with the name of the function. The first two parameters are always a pointer to a JNIEnv class (or structure in C) which is an interface to the virtual machine and a jobject which is a reference to the object this method is called from (or a jclass for static methods).

To call the function from Java you must do two things: load the library and declare the method as a function. Add the following lines to your MainActivity.

static {
	System.loadLibrary("native1");
}
 
private native String getString();

Then, make sure your activity has a TextView (with an id to reference it), and at onCreate, set the TextView to the String returned by getString(). Test by running the application in the ARM emulator, since this is what it will compile to by default.

There is a more practical way of generating function prototypes: using javah. Declare all your native functions in the Java files first and then call javah to generate the headers.

For example:

javah -classpath android_workspace/native1/bin/classes/ com.example.native1.MainActivity

will generate com_example_native1_MainActivity.h which contains

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_native1_MainActivity */
 
#ifndef _Included_com_example_native1_MainActivity
#define _Included_com_example_native1_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_native1_MainActivity
 * Method:    getString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_native1_MainActivity_getString
  (JNIEnv *, jobject);
 
#ifdef __cplusplus
}
#endif
#endif

You can compile the native part of the project outside the IDE by using the ndk-build tool. You can run it in the folder directly, or from another folder by using the -C parameter.

ndk-build -C /path/to/jni/folder/

Task 2 - Makefiles

The NDK uses a build-ndk command to build the libraries. The build system contains multiple smaller makefiles called fragments which are called at build time depending on the declarations in two files: Android.mk and Application.mk. Application.mk contains global declarations such as: what modules are used (APP_MODULES), optimization (APP_OPTIM), compilation flags (APP_CFLAGS and APP_CPPFLAGS), the architecture (APP_ABI).

First, let's make the application compile on x86 architectures. To do this add Application.mk to the jni folder in your project and add

APP_ABI = x86

The valid APP_ABI targets for now are: x86, armeabi, armeabi-v7a, mips and all. You can add more than one architecture at a time or use all if you want to target all available architectures.

APP_OPTIM can be set to debug or release. Set it to debug.

APP_OPTIM = debug

Now let's take a look at Android.mk. This is what Eclipse generates when adding native support:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := native1
LOCAL_SRC_FILES := native1.cpp

include $(BUILD_SHARED_LIBRARY)

LOCAL_PATH is the path to the files, in this case it's the jni/ directory.

include $(CLEAR_VARS) clears all the LOCAL_ variables except LOCAL_PATH.

LOCAL_MODULE is the name of the library: in this case it will generate **libnative1.so**.
LOCAL_SRC_FILES are the files that make up the module, native1.cpp in this case.

Finally, include $(BUILD_SHARED_LIBRARY) will include the fragments necessary to build the library.

To add another library you can simply add another sets of declarations like these, for example:

include $(CLEAR_VARS)

LOCAL_MODULE    := native2
LOCAL_SRC_FILES := native2.cpp

include $(BUILD_SHARED_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE    := native1
LOCAL_SRC_FILES := native1.cpp

include $(BUILD_SHARED_LIBRARY)

You added native2 declaration at the top because we will have to build it first for the next tasks, but they can be in any order otherwise.

Add native2.cpp and see if it generate libnative2.so. Add the following content:

int getRandInt() {
	return 4;
}
 
char * getRandString() {
	int i = getRandInt();
	switch(i) {
		case 0:
			return "NULL";
		case 1:
			return "NOT NULL";
		case 2:
			return "OFF BY ONE ERROR";
		default:
			return "COUNTING IS DIFFICULT";
	}
}

Select from the top menu Build > Build all and then check the Console tab in the lower part of the screen or the libs/ folder in the Project Explorer.

Task 3 - Shared library

Add native2.h file

#ifndef NATIVE2_H_
#define NATIVE2_H_
 
extern int getRandInt();
extern char * getRandString();
 
 
#endif /* NATIVE2_H_ */

and include it in both native1.cpp and native2.cpp.

You want to call native2 functions from native1. To do this, you must tell the linker to link the two files together. Add

LOCAL_SHARED_LIBRARIES := native2

to Android.mk in the section corresponding to native1 module.

Also add

System.loadLibrary("native2");

before the loading native1, otherwise native2 will not get loaded.

Replace the return in the getString function with

return env->NewStringUTF(getRandString());

Task 4 - Static libraries

There is no point in dynamically loading the native2 library, so you can change it to a static library. To do this, you need to change two lines. First, change

include $(BUILD_SHARED_LIBRARY)

to

include $(BUILD_STATIC_LIBRARY)

for the native2 module and

LOCAL_SHARED_LIBRARIES := native2

to

LOCAL_STATIC_LIBRARIES := native2

for the native module.

Remove

System.loadLibrary("native2");

from your activity.

Task 5 - Standalone binary

To compile a binary you can use Android.mk, but include BUILD_EXECUTABLE instead.

include $(CLEAR_VARS)

LOCAL_MODULE    := native3
LOCAL_SRC_FILES := native3.cpp

LOCAL_STATIC_LIBRARIES := native2

include $(BUILD_EXECUTABLE)

Run build from the top menu (Project > Build all). This will generate a native 3 executable in the libs/ folder, which will not get included in the APK.

Use this code for native3.cpp:

#include <stdio.h>
#include "native2.h"
 
int main(int argc, char **argv)
{
	printf("%s\n", getRandString());
}

Running is the executable is only possible if you have root and are able to set the execution bit in a folder which allows it (/sdcard is probably locked, but /data or /system should allow this).

Task 6 - Logging from Native Code

To use logging from native code you will have to include <android/log.h>. Add this include to native2.cpp. You will have to also link this module against liblog. To do this add to the native2 module:

LOCAL_LDLIBS := -llog

Then, to print something, you need to use this macro:

__android_log_print(log level, string tag, printf format, variables according to format);

For example

__android_log_print(ANDROID_LOG_VERBOSE, "TAG1", "variable 1 = %ld", var1);

Instrument the two functions in native2 library to tell you they have been called.

You might have to add -llog to your other modules (since they include native2).

Task 7 - Making getRandInt() work

Make getRandInt return a random integer between 0 and and 5 and log the results.

Hint: stdlib and time

Add a button to your Activity and call getRandString each time you push the button and update the text to the new string.

ndk/courses/03.txt · Last modified: 2014/03/23 21:06 by petre.eftime
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0