Developing Android Native Applications using NDK+JNI (Part 2) – Building the first native application

On 10 de dezembro de 2012 by Vicente Amorim

As we’ve seen on our previous post (http://www2.decom.ufop.br/imobilis/?p=1781), the installation process of NDK + ADT plugin is quite simple. After do that, it is now time to develop our first application using the NDK framework.

During this post we’ll proceed in two different steps:

1) Create the Android project which will makes use of native code;

2) Create an example application to validate the execution flow.

1 – Creating the Android project

1.1 – Open the Eclipse+ADT IDE.

1.2 – Right click on “Package Explorer’s” empty area (according with the bellow image).

1.3 – Select “New” -> “Android Application Project“.

1.4 – Configure the details of new Android application and then, click on “Next” button (Here we’ll be using the details listed by bellow picture).

1.5 – Configure the path where the application will be stored, as well as the options related to “launcher icon” and if the “main activity” will be automatically created. After that, click on “Next” button.

1.6 – If you have checked “Create custom launcher icon” option on previous screen, then you have now to customize the Android application’s launcher icon. After that, click on “Next” button.

1.7 – If you have checked the “Create activity” option on two screens ago, you have now to select the desired type of activity that will be automatically created. After that, click on “Next” button.

1.8 –  Next step is try to execute your just created application:

1.8.1 – Connect your Android device through the USB cable (if you have one);

1.8.2 – Click on “Run NDKExample” button located at the top bar.

1.8.3 – Select your Android physical device in the list or create a new Android Virtual Device which runs the emulator.

1.8.4 – Check the result on your selected physical or virtual device.

1.9 – Once we have created the Android project, it is now time to add the support to native code. To do that:

1.9.1 – Right click over your project name folder on “Package Explorer” (in our case, “NDKExample”).

1.9.2 – Select “Android Tools” -> “Add Native Support…

1.9.3 – On the next screen, insert the name your native library will have. Note that the prefix “lib” and the suffix “.so” are automatically added.

1.9.4 – After that, check if the “jni” folder was created on your project according to the bellow picture.

2 – Creating an example application using NDK

Until now we have only configured the environment will be using for the next applications. Lets now build an example application which will makes use of native code.

2.1 – The first step is to open you main activity file. Into our project it resides inside the “MainActivity.java“. At that file, we’ll insert a code portion to load the native library at runtime. The parameter this call receives is the name of our library without the prefix (“lib“) and suffix (“.so“).

[sourcecode language=”java”]
static {

System.loadLibrary("NativeFunctionality");

}
[/sourcecode]

2.2 – After that, we’ll create two different native functions: HelloWorldNDK() and GetAnswer(). Both functions will be used to illustrate the way NDK works. To do that, add the following piece of code to your main activity.

[sourcecode language=”java”]
public native void HelloWorldNDK();
public native String GetAnswer();
[/sourcecode]

2.3 – Save your project and rebuild everything in order to generate the binaries for new changes.

2.4 – Now, it’s time to generate the stubs for native code. This task can be automatically accomplished using the javah tool. To do that, using your console (or command prompt in windows) issue the following commands:

[sourcecode language=”bash”]
cd <project_directory>\bin
javah -jni <path_to_the_main_activity_w_packet>
mv <generated_header_file>.h <desired_header_name>
[/sourcecode]

Example (considering our workspace):

[sourcecode language=”bash”]
cd C:\Documents and Settings\iMobilis\workspace\NDKExample\bin
javah -jni com.test.NDKExample.MainActivity
mv com_test_NDKExample_MainActivity.h NativeFunctionality.h
[/sourcecode]

                      At the end, copy the NativeFunctionality.h (or your generated file) to the “jni” folder of your project.

2.5 – At this point, the generated stub can be used to fill the implementation of our desired native functions. The bellow code is the content of our NativeFunctionality.h.

[sourcecode language=”c”]
/* DO NOT EDIT THIS FILE – it is machine generated */

#include <jni.h>

/* Header for class com_test_NDKExample_MainActivity */

#ifndef _Included_com_test_NDKExample_MainActivity

#define _Included_com_test_NDKExample_MainActivity

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class:     com_test_NDKExample_MainActivity

* Method:    HelloWorldNDK

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_com_test_NDKExample_MainActivity_HelloWorldNDK

(JNIEnv *, jobject);

/*

* Class:     com_test_NDKExample_MainActivity

* Method:    GetAnswer

* Signature: ()Ljava/lang/String;

*/

JNIEXPORT jstring JNICALL Java_com_test_NDKExample_MainActivity_GetAnswer

(JNIEnv *, jobject);

#ifdef __cplusplus

}

#endif

#endif
[/sourcecode]

2.6 – After adding the stubs at your project, windows users maybe have some errors on project build:

In order to overcome this, you need to open the project properties and add the path to the headers files which weren’t found. Like in the bellow picture (only changing the path to where the NDK is installed at your disc):

2.7 – Now, it’s time to add some code to our native functions. Initially we’ll just implement the HelloWorldNDK() function. When called, the function will print the following message on log: “Native function called!“. To do that, add the following piece of code at your implementation file (.cpp – In our case: NativeFunctionality.cpp):

[sourcecode language=”c”]
#include "NativeFunctionality.h"

#include <android/log.h>

#include <pthread.h>

#include <unistd.h>

#define LOG_TAG "NDKTest"

JNIEXPORT void JNICALL Java_com_test_NDKExample_MainActivity_HelloWorldNDK (JNIEnv *env, jobject obj)

{

__android_log_print(ANDROID_LOG_INFO,LOG_TAG,"Native function called!");

return;

}

JNIEXPORT jstring JNICALL Java_com_test_NDKExample_MainActivity_GetAnswer (JNIEnv *env, jobject obj)

{

}
[/sourcecode]

2.8 – In order to support the “__android_log_print()” call, you need to modify the Android.mkmakefile adding the following line:

[sourcecode language=”bash”]
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
[/sourcecode]

At the end, your Android.mk file will be like this:

[sourcecode language=”bash”]
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog

LOCAL_MODULE    := NativeFunctionality

LOCAL_SRC_FILES := NativeFunctionality.cpp

include $(BUILD_SHARED_LIBRARY)
[/sourcecode]

2.9 – Now, your native side is ready to be called from Android framework space. It can be done adding the following piece of code to your main activity:

[sourcecode language=”java”]
@Override

protected void onStart() {

super.onStart();

HelloWorldNDK();

}
[/sourcecode]

2.10 – Save your project. Rebuild everything and run the project again. Note that the message “Native function called!” will be displayed on logcat as soon as the Android application starts. It means that your native code was properly called.

2.11 – Now, it’s time to implement the “GetAnswer()” native function. Let’s go back to our implementation file (.cpp) and fill the stub related to GetAnswer() function. Our main intent here is to get back some information from the native environment. Add the following piece of code on your proper stub inside the implementation file:

[sourcecode language=”c”]
JNIEXPORT jstring JNICALL Java_com_test_NDKExample_MainActivity_GetAnswer (JNIEnv *env, jobject obj)

{

return (env->NewStringUTF("String returned from native environment!"));

}
[/sourcecode]

Note that there’s a function (NewStringUTF), that makes the conversion from a pointer of char type to a jstring object (which is the representation of Java’s String object into the native environment).

2.12 – Once again, our native side implementation is ok. We just need now to call this function properly from the Android’s framework side. In order to see the returned message (“String returned from native environment!”) on the screen we’ll put it directly to a TextView component (which was automatically created by the ADT when creating the project). The final code of our MainActivity.java is:

[sourcecode language=”java”]
public class MainActivity extends Activity {

static {

System.loadLibrary("NativeFunctionality");

}

TextView ndkTextView;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

// Inflate the menu; this adds items to the action bar if it is present.

getMenuInflater().inflate(R.menu.activity_main, menu);

return true;

}

@Override

protected void onStart() {

super.onStart();

HelloWorldNDK();

ndkTextView = (TextView)findViewById(R.id.ndkText);

String ndkString = GetAnswer();

ndkTextView.setText(ndkString);

}

public native void HelloWorldNDK();

public native String GetAnswer();

}
[/sourcecode]

2.13 – Now, save your project, rebuild everything and run it again. As soon as the application is started, the string returned by the GetAnswer() function will be displayed on screen (because we’ve used the TextView to show it).

This is the end of 2nd part of our posts related to the integration of Android framework with native environment. In the next one we’ll see more about asynchronous calls from Android space to native space.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *