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.
You will need an Android project to host your native application, but before you create that, you must create an Android Virtual Device (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.
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.
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.
In the dialog box, enter a name for the AVD (droid1.5
in this example).
Select a target SDK (Android 1.5 in this example).
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.
Click Create AVD.
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.
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.
With the AVD in place, the next task is to create the project for this chapter, as follows:
Click the New Android Project icon on the main toolbar (see Figure 2-1). This opens the New Android Project dialog box.
In the dialog box, enter a project name (ch02.Project
in this example).
Enter an application name (Chapter2 in this example).
Enter a package name (ch02.project
in this example).
Enter an activity name (MainActivity
in this example).
Specify a minimum SDK version (3 in this example). Figure 2-4 shows the completed New Android Project dialog box for this example.
Click Finish.
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.
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:
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:
Let's look at the files in more detail to understand what they do. We'll start with the Java layer.
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.
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); } }
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.
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.
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.
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.
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 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); }
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)
To invoke the static void jni.Natives.OnMessage
method. you must perform the following steps:
Load the jni.Natives
class with the following:
(*env)->FindClass(env, "jni/Natives")
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");
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 );
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.
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.
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.
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.
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.
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.
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
.
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.
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.
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.
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
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:
Select Run Run Configurations from the main menu.
In the Run Configurations dialog box, right-click Android Application in the left tree and choose New.
Enter a configuration name (ch02
) and select a project (ch02.Project
), as shown in Figure 2-6. Then click Run.
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.
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.
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.
3.129.211.165