Solved tasks: lab-3-solved.zip
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.
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/
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 (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.
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());
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.
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).
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).
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.