Chapter 2. Compiling Native Code in Android

In this chapter, we will explore the basics of a simple native program, also known as static binary, to be run in the device. You will also learn how to build a shared library (dynamic binary), and finally how to call that shared library within a simple Android project. Let's get started.

Your First Native Android App

You will need an Android project to host your native application, but before you create that, you must create an Android Virtual Device (AVD).

Creating an AVD

With version 1.5 of the SDK, Google introduced the concept of virtual devices (AVDs). An AVD is simply a set of configuration attributes applied to an emulator image that allows the developer to target a specific version of the SDK. The following are the basic versions of the SDK targeted by AVDs:

  • Android 1.1: This target maps to the Android SDK version 1.1 R2.

  • Android 1.5: This target maps to the Android SDK version 1.5 R2.

  • Android 1.6: This target maps to the Android SDK versions 1.6 and 2.0.

  • Google APIs - 1.5 or 1.6: This target must be used if you are building an application that uses Google APIs such as Maps and Search.

Warning

In Android 1.5 and later, an AVD does not include the Google Maps API. Keep this in mind if you are building a maps-enabled app. Your map application will not start in an Android 1.5 AVD.

Let's create an AVD to target an SDK 1.5 for the sample application.

  1. In Eclipse Galileo, click the black phone icon on the Android toolbar (see Figure 2-1). This opens the Android Virtual Devices Manager dialog box.

    Android toolbar within Eclipse

    Figure 2-1. Android toolbar within Eclipse

  2. In the dialog box, enter a name for the AVD (droid1.5 in this example).

  3. Select a target SDK (Android 1.5 in this example).

  4. Optionally, enter an SD card absolute path (if you already have one), such as /home/user/sdcard.iso, or a size, such as 128MB. Figure 2-2 shows the Android Virtual Devices Manager dialog box for this example.

  5. Click Create AVD.

Tip

You should create an SD card for the AVD, and use it for the examples in later chapters to store large game files (for Doom and Wolfenstein 3D). Game files should not be saved in the device main file system, unless you want to run out of space after installing one game.

Android Virtual Device Manager dialog box settings for SDK 1.5

Figure 2-2. Android Virtual Device Manager dialog box settings for SDK 1.5

Note that in Android 1.6 and later, the GUI has changed a little, as shown in Figure 2-3. To create an AVD, click New to open the Create New AVD dialog box, enter a name, select a firmware target plus SD card size, and then click Create AVD.

Creating a new AVD in Android SDK 1.6

Figure 2-3. Creating a new AVD in Android SDK 1.6

Creating the Android Project

With the AVD in place, the next task is to create the project for this chapter, as follows:

  1. Click the New Android Project icon on the main toolbar (see Figure 2-1). This opens the New Android Project dialog box.

  2. In the dialog box, enter a project name (ch02.Project in this example).

  3. Enter an application name (Chapter2 in this example).

  4. Enter a package name (ch02.project in this example).

  5. Enter an activity name (MainActivity in this example).

  6. Specify a minimum SDK version (3 in this example). Figure 2-4 shows the completed New Android Project dialog box for this example.

  7. Click Finish.

New Android Project dialog box for this chapter's example

Figure 2-4. New Android Project dialog box for this chapter's example

Application Architecture

Let's consider what we wish to accomplish with this application:

  • We want to create the basic Android application. When run in the emulator, the app will create a default view with the text "Hello Chapter2!".

  • Within the project, we will create a native folder with files to accomplish the following:

    • Create a native library with a main subroutine that will be called from the Android main activity using JNI.

    • The main library subroutine will invoke a Java method (using JNI) within the Android project, sending a text message back to the Android Java layer.

  • The library will be loaded at runtime within Java using a System.load(path).

Figure 2-5 shows the file system layout of the project.

Tip

It would be helpful to import the project source (ch02.Project) into your workspace to go along with this chapter.

The following Java files describe the project:

  • ch02.project.MainActivity.java: This file is created by the wizard and should already exist in the project.

  • jni.Natives.java: This is a new file that contains native methods to be invoked within the native library and callbacks that the C library will perform within Java.

Project layout

Figure 2-5. Project layout

You must create the following files within the native folder (see Figure 2-5):

  • lib.c: This is the main library code. It contains all the necessary JNI system calls to cascade information back and forth between Android and C.

  • testlib.c: This is a test program for the library. Its main function is to make sure there are no symbols missing from the native library.

  • main.c: This is a static program used to demonstrate how to compile a static native program that can be run in the device.

  • Makefile: This is the project's native build file. It has targets for the following:

    • Build the native library

    • Build the library test program

    • Generate JNI header files required to cascade information back and forth between Java and C

    • Build the static test program

    • Deploy the files to the device for testing

Let's look at the files in more detail to understand what they do. We'll start with the Java layer.

Main Activity

The 8.70ch02.project.MainActivity.java file is created by the wizard, and it is the entry point to the phone application. Listing 2-1 shows the code for this file. There are some remarkable things to note about this file.

As you should know, when the application starts, the method onCreate(Bundle savedInstanceState) will be invoked by Android. This method performs three critical steps:

  • It installs (simply copies) the native library (libch02.so) from the assets folder of the application package to the project's files folder (located in /data/data/PACKAGE_NAME/files; the name of the package is ch02.project). This step is required before the library can be loaded by JNI.

  • It loads the native library using System.load(path).

  • Finally, it runs the main library sub by invoking the native method Natives.LibMain(String[] argv).

Native libraries cannot be loaded within the application package; thus, they must be copied somewhere in the file system before invoking System.load(). The obvious place would be the application's file folder (located in /data/data/Package_Name/files), but the library can be saved anywhere where permissions allow it.

Warning

Shared libraries should not be saved on the SD card, as Android does not allow you to run executable code from the SD card. Developers that have large shared libraries might be tempted to do so to save some space. Save yourself some trouble and don't do it.

Note the way the library is installed:

writeToStream(
getAssets().open(LIB),
openFileOutput(LIB, 0)
);

The getAssets().open() method will open a stream from the package assets folder (where the library will be stored). The writeToStream method will then write the stream using openFileOutput(LIB, 0), which will open an output stream to the default application folder (/data/data/ch02.project/files). getAssets and openFileOutput are Android system calls, and LIB is the name of the shared library (libch02.so). The second argument to openFileOutput (zero) tells Android to assign private permissions to the file (that means it can be read by only the application itself).

Example 2-1. Main Activity for This Chapter's Example

package ch02.project;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import jni.Natives;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
    private static final String LIB = "libch02.so";
    private static final String LIB_PATH =
        "/data/data/ch02.project/files/" + LIB;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        try {
            // Install lib
            System.out.println("Installing LIB: " + LIB);

            // Copy lib from assests folder to files folder:
            // /data/data/PKG_NAME/files
            writeToStream(getAssets().open(LIB), openFileOutput(LIB, 0));

            // Load Lib
            System.load(LIB_PATH);

            // Run it
            String[] argv = { "MyLib", "arg1", "arg2" };

            Natives.LibMain(argv);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Write to a stream
     *
* @param in
     * @param out
     * @throws IOException
     */
    public static void writeToStream(InputStream in, OutputStream out)
            throws IOException {
        byte[] bytes = new byte[2048];

        for (int c = in.read(bytes); c != −1; c = in.read(bytes)) {
            out.write(bytes, 0, c);
        }
        in.close();
        out.close();
    }
}

In Android 1.5 and later, native libraries can be stored within the project under a folder named libs/armeabi, then loaded using the system call System.loadLibrary(LIB_NAME). Thus, Listing 2-1 can be simplified by removing the library installation step:

public class MainActivity extends Activity
{
    private static final String LIB = "ch02";

    // Load library: Note that the prefix (lib) and the extension (.so) must be stripped.
    static {
        System.loadLibrary(LIB);
    }

    public void onCreate(Bundle savedInstanceState) {
        // ...
        Natives.LibMain(argv);
    }
}

Native Interface

The native interface is defined in the Java class jni.Natives.java. It has two important methods that deal with the C library (see Listing 2-2):

  • static native int LibMain(String[] argv): This is the native library main subroutine. It will be called within the Android main activity. The method also takes a list of argument strings to be passed along. Notice the keyword native, which tells the Java compiler it is implemented natively.

  • private static void OnMessage(String text, int level): This method is meant to be called from the C library, with a string message and integer value (level). This method will simply print the message to the console.

Note

As you should know, with JNI, you can invoke subroutines both ways: from Java to C (using the native keyword) or from C to Java, as you'll see once we get to the native stuff.

Example 2-2. Native Interface Class

package jni;

public class Natives
{

    /**
     * Native Main Loop
     *
     * @param argv
     * @return
     */
    public static native int LibMain(String[] argv);

    /**
     * This fires on messages from the C layer
     *
     * @param text
     */
    @SuppressWarnings("unused")
    private static void OnMessage(String text, int level) {
        System.out.println("OnMessage text:" + text + " level=" + level);
    }

}

Natives.LibMain requires a native implementation. On the other hand, OnMessage (which is invoked from C) simply prints the message to standard output. With this in mind, let's take a look at the native code.

Native Library

Here 11.330is where all the work should take place. We start with the implementation of the actual library lib.c (see Listing 2-3). This file lives in the native folder within the project folder.

Note

Native libraries in Linux (also known as shared objects) are the equivalents of dynamic link libraries (DLLs) in Windows. By convention, shared objects are named as lib<NAME><VERSION>.so.

Example 2-3. Native Library Implementation

#include <stdio.h>
#include <stdlib.h>

/* JNI Includes */
#include <jni.h>

#include "include/jni_Natives.h"

#define CB_CLASS "jni/Natives"

/**
 * OnMessage callback
 */
#define CB_CLASS_MSG_CB  "OnMessage"
#define CB_CLASS_MSG_SIG  "(Ljava/lang/String;I)V"

// prototypes

// Lib main Sub
int lib_main(int argc, char **argv) ;

// Used to get the len of a Java Array
const int getArrayLen(JNIEnv * env, jobjectArray jarray);

// printf str messages back to java
void jni_printf(char *format, ...);

// Global env ref (for callbacks)
static JavaVM *g_VM;

// Global Reference to the native Java class jni.Natives.java
static jclass jNativesCls;

/*
 * Class:     jni_Natives
 * Method:    LibMain
 * Signature: ([Ljava/lang/String;)V
 */
JNIEXPORT jint JNICALL Java_jni_Natives_LibMain
  (JNIEnv * env, jclass class, jobjectArray jargv)
{
    // Obtain a global ref to the caller jclass
    (*env)->GetJavaVM(env, &g_VM);

    // Extract char ** args from Java array
    jsize clen =  getArrayLen(env, jargv);

    char * args[(int)clen];
int i;
    jstring jrow;
    for (i = 0; i < clen; i++)
    {
        // Get C string from Java String[i]
        jrow = (jstring)(*env)->GetObjectArrayElement(env, jargv, i);
        const char *row  = (*env)->GetStringUTFChars(env, jrow, 0);

        args[i] = malloc( strlen(row) + 1);
        strcpy (args[i], row);

        // Print args
        jni_printf("Main argv[%d]=%s", i, args[i]);

        // Free Java string jrow
        (*env)->ReleaseStringUTFChars(env, jrow, row);
    }

    /*
     * Load the jni.Natives class
     */
    jNativesCls = (*env)->FindClass(env, CB_CLASS);

    if ( jNativesCls == 0 ) {
        jni_printf("Unable to find class: %s", CB_CLASS);
        return −1;
    }

    // Invoke the Lib main sub. This will loop forever
    // Program args come from Java
    lib_main (clen, args);
    return 0;
}


/**
 * Send a string back to Java
 */
jmethodID mSendStr;

static void jni_send_str( const char * text, int level) {
    JNIEnv *env;

    if ( !g_VM) {
        printf("I_JNI-NOVM: %s
", text);
        return;
    }

    (*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL);

    // Load jni.Natives if missing
if ( !jNativesCls ) {
        jNativesCls = (*env)->FindClass(env, CB_CLASS);

        if ( jNativesCls == 0 ) {
                printf("Unable to find class: %s", CB_CLASS);
                return;
        }
    }

    // Call jni.Natives.OnMessage(String, int)
    if (! mSendStr ) {
        // Get  aref to the static method: jni.Natives.OnMessage
        mSendStr = (*env)->GetStaticMethodID(env, jNativesCls
            , CB_CLASS_MSG_CB
            , CB_CLASS_MSG_SIG);
    }
    if (mSendStr) {
        // Call method
        (*env)->CallStaticVoidMethod(env, jNativesCls
                , mSendStr
                , (*env)->NewStringUTF(env, text)
                , (jint) level );
    }
    else {
        printf("Unable to find method: %s, signature: %s
"
                , CB_CLASS_MSG_CB, CB_CLASS_MSG_SIG );
    }
}

/**
 * Printf into the Java layer
 * does a varargs printf into a temp buffer
 * and calls jni_sebd_str
 */
void jni_printf(char *format, ...)
{
    va_list         argptr;
    static char             string[1024];

    va_start (argptr, format);
    vsprintf (string, format,argptr);
    va_end (argptr);

    jni_send_str (string, 0);
}

/**
 * Get Java array length
 */
const int getArrayLen(JNIEnv * env, jobjectArray jarray)
{
return (*env)->GetArrayLength(env, jarray);
}

/**
 * Library main sub
 */
int lib_main(int argc, char **argv)
{
    int i;

    jni_printf("Entering LIB MAIN");

    for ( i = 0 ; i < argc ; i++ ) {
        jni_printf("Lib Main argv[%d]=%s", i, argv[i]);
    }
    return 0;
}

Let's dissect this file to understand what it does. Any C/C++ program that plans to do JNI calls must include the header file:

#include <jni.h>

This header file has the prototypes for all the JNI system calls to be used by your library. It can be found in your system's Java home under JAVA_HOME/include, with extra Linux dependencies under JAVA_HOME/include/linux. At compile time, these paths must be included using -I$JAVA_HOME/include and -I$JAVA_HOME/include/linux in the Makefile (The agcc script you created in Chapter 1 will take care of all this).

Next, it includes the jni_Natives header file:

#include "include/jni_Natives.h"

This file contains the user-defined JNI prototypes for all native methods defined in the jni.Natives class. It is machine-generated and must not be edited by the user. The actual generation will be set up in the Makefile. To generate this file manually, the following command can be used:

javah -cp ../bin -d include jni.Natives

Here, javah is the Java Virtual Machine (JVM) command to generate native header files from Java classes, -cp defines the class path search path, -d include tells javah to save the file in the include folder (creating it if required), and jni.Natives is the Java class name from which you wish to extract the headers.

Next, the following constants are defined:

#define CB_CLASS "jni/Natives"
#define CB_CLASS_MSG_CB  "OnMessage"
#define CB_CLASS_MSG_SIG  "(Ljava/lang/String;I)V"

CB_CLASS is the name of the Java class that will be invoked within C (note that the period separating path names is replaced by /). CB_CLASS_MSG_CB is the name of the Java method (OnMessage) that will be invoked (see Listing 2-2). CB_CLASS_MSG_SIG is a critical constant that defines the Java signature of the OnMessage Java method. Let's take a closer look at this signature:

(Ljava/lang/String;I)V

A Java method signature has the format (ARGUMENTS)RETURN_TYPE, where the arguments can be encoded as follows:

  • I = Integer

  • B = Byte

  • S = Short

  • C = Char

  • LJava_Class; = For Java classes enclosed by : L and ;

In our case, the first argument is a Java string (Ljava/lang/String;), and the second is an integer (I). Note that all arguments are defined by a single character (except for classes that are enclosed by L;), and there are no separators between them. Finally, V is the return type defined as void.

Warning

Method signatures are a major pain when coding in JNI. Any mistake in this string, and the library will not be able to find the method at runtime.

Next, the file defines the prototypes for the functions within the library:

  • int lib_main(int argc, char **argv): This is the entry point to the library. It receives the number of arguments (argc) and a list of arguments (argv), similar to the standard C main() function.

  • int getArrayLen(JNIEnv * env, jobjectArray jarray): This function is used to get the length of a Java array, which will be translated into a C array for use by the library.

  • void jni_printf(char *format, ...): This function is used by the library to send a text message back to Java. Note that ... indicates that the function will receive a vector of arguments.

Finally, we need two global references:

static JavaVM *g_VM;
static jclass jNativesCls;

g_VM is a reference to the JVM, and it will be used make JNI system calls. jNativesCls is a reference to the jni.Natives Java class used to invoke the Java method OnMessage. Note that the static keyword tells the compiler that these variables should be visible only within code in lib.c.

Converting a Java Array to a C Array

Converting a Java string array to a C char array is a very useful tool to send arguments to a native library. As you can see from Listing 2-4, this can be a tricky situation. The following are the key steps:

  • Get the size of the Java array, and for each element of the array:

    • Get the Java String[i] element using GetObjectArrayElement(JNIEnv * env, jobjectArray jarray, int pos).

    • Convert the retrieved element into a C string (char *) using GetStringUTFChars(JNIEnv * env, jstring jrow, 0).

  • Allocate space for the C array using malloc(length of string + 1). Note that an extra space is allocated for the terminator character.

  • Copy the characters using strcpy (char ** target, char * source).

  • Release Java String[i] using ReleaseStringUTFChars(JNIEnv * env, jstring jrow, char * row).

Example 2-4. Converting a Java String Array into a C Char Array

// Extract char ** args from Java array
jsize clen =  getArrayLen(env, jargv);

char * args[(int)clen];

int i;
jstring jrow;

// Loop thru Java array
for (i = 0; i < clen; i++)
{
    // Get String[i]
    jrow = (jstring)(*env)->GetObjectArrayElement(env, jargv, i);

    // Convert String[i] to char *
    const char *row  = (*env)->GetStringUTFChars(env, jrow, 0);

    // Allocate space
    args[i] = malloc( strlen(row) + 1);

    // Copy
    strcpy (args[i], row);

    // Free java string jrow
    (*env)->ReleaseStringUTFChars(env, jrow, row);
}

Getting the Size of a Java Array

To get the size of a Java array, use the JNI function (*env)->GetArrayLength(env, jobjectArray jarray), where env is a pointer to the JNI environment, and jarray is a reference to the Java array. For example, to get the size of the array jargs using environment env, use the following:

(*env)->GetArrayLength(env, jargs)

Invoking a Java Static Void Method

To invoke the static void jni.Natives.OnMessage method. you must perform the following steps:

  1. Load the jni.Natives class with the following:

    (*env)->FindClass(env, "jni/Natives")
  2. Get the ID of the method to be invoked using a reference to the jni.Natives class, the name of the method (OnMessage), and the signature (Ljava/lang/String;I)V.

    jmethodID mSendStr = (*env)->GetStaticMethodID(env
                   , jNativesCls
                   , "OnMessage"
                   , "(Ljava/lang/String;I)V");
  3. Call the static void method, passing the class, method ID, and the method arguments: a Java string and integer in this case.

    (*env)->CallStaticVoidMethod(env, jNativesCls
                , mSendStr
                , (*env)->NewStringUTF(env, text)
                , (jint) level );

Defining a Variable-Arguments Function in C

The final piece of the puzzle is a function to perform the actual invocation of the Java method described in the previous section, as shown in Listing 2-5. This function is meant to be called anywhere within the library after jni.Natives.LibMain() is invoked. It is called jni_printf and works pretty much as printf does, using the very useful variable-arguments technique.

Example 2-5. Sending a String to Java Using Variable Arguments

void jni_printf(char *format, ...)
{
    va_list         argptr;
    static char    string[1024];

    va_start (argptr, format);
    vsprintf (string, format, argptr);
    va_end (argptr);
jni_send_str (string, 0);
}

va_list, va_start, and va_end are used to build an output string using a C string format and a sequence of arguments. This allows the developer to mimic a printf-style function for a specific need. Thus, for example, to print an arbitrary message to the Java console within the library, use the following command:

jni_printf("This is a message %d, %p, %x, %s", 10, somePointer, 0xFF, "Hello Java")

"This is a message %d, %p, %x, %s" is called a character format. The rest are variable arguments sent to the function. Also note that you should add the header #include <stdarg.h> to use variable arguments.

Compiling and Testing the Shared Library

We can now proceed to compile the native library. Listing 2-6 shows the Makefile for this purpose. This file defines the following targets:

  • default (all): This target will build both the library and the test static binary a.out.

  • lib: This target will build only the shared library.

  • testlib: This target will build a simple program to test the library and make sure there are no missing symbols.

  • jni: This target will generate the C header for the native Java class jni.Natives. The output will be stored in the include folder under the current directory.

  • pushbin: This target will push the static test binary (a.out) to the device /data folder using the adb push command.

  • pushlib: This target will push the library and the test program to the device /data folder.

Warning

Commands within a target of a Makefile must use the tab separator, which has been replaced by spaces in Listing 2-6. Do not copy and paste this listing into a Makefile, as it will not run properly. Instead, refer to the source code for the chapter.

Example 2-6. Makefile for This Chapter's Example

#####################################
# Makefile
#####################################
# Compiler and loader
CC = agcc
LD = ald

# Flags
CFLAGS  = -Werror
MACROS  =
INCLUDES =

# Static objects
OBJ = main.o

# Shared library ob
LIBOBJ = lib.o

# Test shared lib
TESTLIBOBJ = testlib.o

# Default make target
all: testlib $(OBJ)
    @echo
    @echo "Linking..."
    $(LD) -static -o a.out $(OBJ)

# Build lib
lib: $(LIBOBJ)
    @echo
    @echo "Linking Shared library..."
    $(LD) -shared -o libch02.so $(LIBOBJ)
    @echo
    @echo "Copying Shared library to assets folder"
    cp libch02.so ../assets

# Build test program for lib
testlib: lib $(TESTLIBOBJ)
    @echo
    @echo "Linking Test for Shared library"
    $(LD) -o testlib $(TESTLIBOBJ) -L. -lch02

# Build JNI Headers (for lib)
jni:
    @echo "Creating JNI C headers..."
    javah -jni -classpath ../bin -d include jni.Natives

# Compile
.c.o:
    @echo
    @echo "Compiling $<..."
    $(CC) -c $(FLAGS) $(MACROS) $(INCLUDES)  $<
# Cleanup
clean:
    rm *.o

#
# Push binary into device
#
pushbin:
    adb push a.out /data

# Push lib & test program to the device
pushlib:
    adb push testlib /data
    adb push libch02.so /data

Type make lib to compile the library (see Listing 2-7). Note that the Makefile uses the agcc and ald scripts created in the previous chapter. Other interesting aspects of the process include the following:

  • CFLAGS=-Werror: This tells the compiler to abort whenever there is a warning in the compilation process. This can be a powerful debugging tool to find programming errors within your code.

  • javah -jni -classpath ../bin -d include jni.Natives: This creates the C header file for the native methods in the jni.Natives class. Output is saved in the include folder.

  • adb push FILENAME DEVICE_DEST: This is the Android SDK command to upload a file to a specific location in the device.

Example 2-7. Makefile Output

user@ubuntu:~/ ch02.Project/native$ make lib
Compiling lib.c...
agcc -c     lib.c

Linking Shared library...
ald -shared -o libch02.so lib.o
arm-none-linux-gnueabi-ld: warning: library search path "/usr/lib/jvm/java-6-
sun/jre/lib/i386" is unsafe for cross-compilation

Copying Shared library to assets folder
cp libch02.so ../assets

Compiling testlib.c...
agcc -c     testlib.c

Linking Test for Shared library
ald -o testlib testlib.o -L. -lch02
arm-none-linux-gnueabi-ld: warning: library search path "/usr/lib/jvm/java-6-
sun/jre/lib/i386" is unsafe for cross-compilation
arm-none-linux-gnueabi-ld: warning: cannot find entry symbol _start; defaulting to 00008340

Notice the warnings in Listing 2-7:

Library search path "/usr/lib/jvm/java-6-sun/jre/lib/i386" is unsafe for cross-compilation.

Here, the compiler is letting you know that the local system Java JNI libraries in this folder are not safe (which makes sense, since you are compiling for a different architecture). However, you do need to link against JNI, thus you have no choice.

Cannot find entry symbol start; defaulting to 00008340

This indicates that the loader cannot find an entry point for the library test program. By default, it will call the first function in the file—main(int argc, char **argv) in this case.

Note

Google does not provide support for JNI, or native development for that matter, as it wishes to push development in Java. I think this is a mistake, as there is a lot of native code that can be ported into the device (such as games). Sadly, native development has been neglected, although new tools such as the Native Development Kit (NDK), described in Chapter 1, are trying to fix the problem.

Troubleshooting Missing Symbols

Before you run the project, you should make sure there are no missing symbols in the native library. This can be a tricky process, as the successful compilation of a shared library will not tell you about missing symbols. Even worse, if symbols are missing, and you try to run the program, the library will fail to load at runtime, leaving you with the headache of figuring out what went wrong.

An easy solution is to write a simple C program that will invoke the library (see Listing 2-8). Then, when linked, the compiler will tell you about missing symbols, which can then be fixed. This program can be also useful in testing the library from the command line before testing from the application itself.

Example 2-8. Simple Test Program for the Library

#include <stdio.h>

extern int lib_main(int argc, char **argv);

//void _start(int argc, char **argv)
int main(int argc, char **argv)
{

    int i;
    printf("Argc=%d Argv=%p
", argc, argv);
for ( i = 0 ; i < argc ; i++ ) {
        printf("Main argv[%d]=%s
", i, argv[i]);
    }

    printf("Starting Lib main sub
");
    lib_main(argc, argv) ;

    exit (0);
}

Compile the program with make testlib, or manually using the helper scripts, as follows:

agcc -c     testlib.c
ald -o testlib testlib.o -L. -lch02

Note that you must link with -L. -lch02, which tells the compiler to link against libch02.so and search for the library in the current folder.

Now, start your emulator and let's test the library.

Testing the Dynamic Library on the Device

To test the library, upload the files to the device using make pushlib, which expands to the following commands:

adb push libcg02.so /data
adb push testlib /data

Then log in to the device, change to the /data folder, and execute the test program:

$ adb shell
# cd /data
# chmod 777 lib * test*
# ./testlib
bionic/linker/linker.c:1581| ERROR:   833 could not load 'libch02.so'
bionic/linker/linker.c:1641| ERROR: failed to link ./testlib
bionic/linker/linker.c:1741| ERROR: CANNOT LINK EXECUTABLE './testlib'

Here, you have run the program in the version 1.5 of the emulator. As you see, the library fails to load. The next section provides tips to figure out what is going on. If you try this process in the SDK version 1.0 R2, it will work.

Note that this doesn't mean the library will fail to load from the Java app. It simply tells you there is a problem with the native linker /system/bin/linker.

Debugging with strace

For some reason, the native test program for the library runs in version 1.0 R2 of the SDK, but fails to load in 1.5 R2. The output gives a clue: the file bionic/linker/linker.c:1581 fails to load the library. There is a simple Linux tool called strace that can help in this situation.

The strace tool runs the specified command until it exits. It intercepts and records the system calls that are called by a process and the signals that are received by a process. The name of each system call, its arguments, and its return value are printed. This is a useful diagnostic and debugging tool for solving problems with programs for which the source is not readily available. You will find that a great deal can be learned about a system and its system calls by tracing even ordinary programs.

The book source provides an Android version of strace. (strace is now a built into versions 1.5 and later of the Android SDK.) Let's push it and see what is going on, as shown in Listing 2-9.

Example 2-9. strace Tool Output

$ adb push strace /data
$ adb shell
# cd /data

# ./strace ./testlib
execve("./testlib", ["./testlib"], [/* 10 vars */]) = 0
getpid()                                = 835
gettid()                                = 835
sigaction(SIGILL, {0xb0001a99, [], SA_RESTART}, {SIG_DFL}, 0) = 0
sigaction(SIGABRT, {0xb0001a99, [], SA_RESTART}, {SIG_DFL}, 0) = 0
sigaction(SIGBUS, {0xb0001a99, [], SA_RESTART}, {SIG_DFL}, 0) = 0
stat64("/system/lib/libch02.so", 0xbef9ea58) = −1 ENOENT (No such file or directory)
stat64("/lib/libch02.so", 0xbef9ea58)   = −1 ENOENT (No such file or directory)
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, −1, 0) = 0x40000000
mprotect(0x40000000, 4096, PROT_READ)   = 0
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 1), ...}) = 0
brk(0)                                  = 0x11000
brk(0x11000)                            = 0x11000
brk(0x12000)                            = 0x12000
mprotect(0x40000000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x40000000, 4096, PROT_READ)   = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(1, "bionic/linker/linker.c:1581| ERR"..., 70bionic/linker/linker.c:1581| ERROR:   835 could not load 'libch02.so'
) = 70
write(1, "bionic/linker/linker.c:1641| ERR"..., 61bionic/linker/linker.c:1641| ERROR: failed to link ./testlib
) = 61
write(1, "bionic/linker/linker.c:1741| ERR"..., 71bionic/linker/linker.c:1741| ERROR: CANNOT LINK EXECUTABLE './testlib'
) = 71
exit_group(−1)                          = ?
Process 835 detached

The following lines give a clue to the source of the problem:

stat64("/system/lib/libch02.so", 0xbef9ea58) = −1 ENOENT (No such file or directory)
stat64("/lib/libch02.so", 0xbef9ea58)   = −1 ENOENT (No such file or directory)

The linker first tries to open the library from the device /system/lib folder. This is a read-only file system, and user-defined libraries cannot be saved there. Next, the linker searches the /lib folder, which doesn't exist, thus the link fails. The linker is not searching in the current directory—that is the problem!

The good news is that this will not prevent the library from loading within the Java application, as long as there are no missing symbols.

If you run the same sequence in a version 1.0 R2 of the SDK, you will see that the second line becomes the following:

stat64("./libch02.so", 0xbef9ea58)   = 0 OK

Thus, the program runs successfully.

Note

It is hard to say what has changed from version 1.0 to 1.5 in the file bionic/linker/linker.c, as Google provides no support in this matter. I can only speculate, but my guess is that either the developers forgot to search in the current folder or some new compilation option can be used to tell the linker where to search.

Compiling Statically

Finally, if you wish to write a command-line tool to run in the device, you must do so statically. Consider the simple program in Listing 2-10 to print the command-line arguments to stdout.

Example 2-10. Simple Command-Line Program

#include <stdio.h>

int main(int argc, char **argv)
{
    int i;

    for ( i = 0 ; i < argc ; i++ ) {
        printf("Main argv[%d]=%s
", i, argv[i]);
    }

    printf("Hello World
");
    exit( 0);
}

If you compile the program with -static using the helper scripts, you get the following error:

agcc -c     main.c
ald -static -o a.out main.o
arm-none-linux-gnueabi-ld: cannot find -lc

The linker cannot find the C runtime libc.so, even though the path to the library is correct. Remember that ald uses -nostdlib to bypass linking against standard libraries.

When compiling statically (using -static), you must remove -nostdlib from the loader. Thus, the correct linking command should be as follows:

user@ubuntu:~/ch02.Project/native$ arm-none-linux-gnueabi-ld
 --dynamic-linker=/system/bin/linker
 -rpath /system/lib
 -rpath /home/user/tmp/android/system/lib
 -L/home/user/tmp/android/system/lib
 -static -o a.out main.o -lc -lm
/home/user/mydroid/prebuilt/darwin-x86/toolchain/
 arm-eabi-4.2.1/lib/gcc/arm-eabi/4.2.1/libgcc.a
 arm-none-linux-gnueabi-ld: warning: cannot find entry symbol _start;
 defaulting to 000080e0

Now you can test a.out in the device:

$ make pushbin
$ adb shell
# ./a.out test
Main[0]=test
Hello World

Testing the Native Application

With the compiled library, you can test the phone application and see if the library is loaded by JNI. From the command line, run the make command:

$ make lib

It will compile the library and also copy it to the assets folder in the Java project, so it can be installed when the application runs. Don't forget to refresh the project folder (press F5 on the main folder), so the IDE will pack it before running it.

Now let's create a run configuration and start the application. Here is how:

  1. Select Run Run Configurations from the main menu.

  2. In the Run Configurations dialog box, right-click Android Application in the left tree and choose New.

  3. Enter a configuration name (ch02) and select a project (ch02.Project), as shown in Figure 2-6. Then click Run.

Run Configurations dialog box for the project

Figure 2-6. Run Configurations dialog box for the project

The application will run in the emulator and display the text "Hello Chapter2!". There is nothing out of the ordinary here. We must look at the logcat view to see the messages from the native layer. Figure 2-7 shows the output of the device log.

logcat output for the project

Figure 2-7. logcat output for the project

In the output, notice the following lines:

Trying to load lib /data/data/ch02.project/files/libch02.so ...
Added shared lib /data/data/c69+h02.project/files/libch02.so ...

These are JNI messages that tell us the library loaded successfully and the native methods can now be invoked within Java. The lines in green represent the callbacks performed by the native library calling the jni.Natives.OnMessage() method. Success! You have taken the first and most difficult step in building hybrid games for Android.

What's Next?

In this chapter, you have taken the first steps for building a hybrid game using JNI by learning how to create the main Java activity and loading a native library within it. Next, you learned about Java native methods using the native keyword plus the C header file required to implement them. You also learned some useful C tricks, such as converting Java arrays to C arrays, getting the size of a Java array, and invoking Java methods within C.

You then learned how to troubleshoot common mistakes and test the native library in the emulator. Things are getting more exciting by the minute.

This and the previous chapter provide the basic foundation if you are planning to port a game that has significant Linux C code to the Android platform. In the next chapter, you will learn how easy to use and powerful Android can be if you plan to build a Java-only game.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.211.165