2011年5月17日火曜日

AndroidStudyMemo=Calling Native Code from Java

There are three main steps necessary to add a call from Java to native code, as follows:

1. Add a declaration for the new function in your Java class file as a native type.

2. Add a static initializer for the library that the native function will be compiled into.

3. Add the function of the appropriate name, following a very specific naming scheme

to the native source file.

 

This isn't as complex as it sounds, but we go through each step now.The SimpleNDK

project has a class called NativeBasicsActivity.java. Let's start there by adding the

declaration for the native function.The following declaration must be added to the class:

private native void basicNativeCall();

 

Now, make sure that the native library with this function is loaded.This doesn't need

to happen for every call, just for each library. In the Android.mk file,we identified

the library as simplendk, so we load that library. Add this static initializer to the

NativeBasicsActivity class:

              static {

              System.loadLibrary("simplendk");

              }

 

Finally, the function needs to be added to one of the C files in the native library that's

being compiled. Each function must follow a very specific naming convention. Instead of

dots, each part of the function name is separated using an underscore, as follows:

Java_package_name_ClassName_functionName

              (JNIEnv *env, jobject this, your vars...);

For the example, that means our function looks like this:

              void Java_com_androidbook_simplendk_NativeBasicsActivity_basicNativeCall

                            (JNIEnv *env, jobject this)

              {

                            // do something interesting here

              }

 

That's a lengthy function name, but you get errors if you name it incorrectly.This

function is now called whenever the Java method declared as basicNativeCall() is

invoked. But how will you know? Add the following line to the function and then make

sure to include "android/log.h" in the C file:

              _ _android_log_print(ANDROID_LOG_VERBOSE, DEBUG_TAG, "Basic call");

 

And there you have it! Your first call from Android Java to C native. If you're familiar

with JNI, you might realize that it's mostly the same.The main difference is which

libraries are available. If you're familiar with JNI, you should find using the NDK fairly

straightforward.

 

Handling Parameters and Return Values

Now that you can make a basic native call, let's pass some parameters in to C and then return

something.We make a simple little C function that takes a format string that works

with the stdio sprintf() call and two numbers to add.The numbers are added, placed

in the format string, and a new string is returned.Although simplistic, this demonstrates

the handling of Java objects and reminds us that we need to manage memory properly in

native C code.

              jstring Java_com_androidbook_simplendk_NativeBasicsActivity_formattedAddition

                            (JNIEnv *env, jobject this, jint number1,

                            jint number2, jstring formatString)

              {

                            // get a C string from a Java string object

                            jboolean fCopy;

                            const char * szFormat =

                                          (*env)->GetStringUTFChars(env, formatString, &fCopy);

                            char * szResult;

                            // add the two values

                            jlong nSum = number1+number2;

                            // make sure there's ample room for nSum

                            szResult = malloc(sizeof(szFormat)+30);

                            // make the call

                            sprintf(szResult, szFormat, nSum);

                            // get a Java string object

                            jstring result = (*env)->NewStringUTF(env, szResult);

                            // free the C strings

                            free(szResult);

                            (*env)->ReleaseStringUTFChars(env, formatString, szFormat);

                            // return the Java string object

                            return(result);

              }

 

The JNI environment object is used for interacting with Java objects. Regular C functions

are used for regular C memory management.

 

Using Exceptions with Native Code

Native code can throw exceptions that the Java side can catch as well as check for exceptions

when making calls to Java code.This makes heavy use of the JNIEnv object and

might be familiar to those with JNI experience.The following native function throws an

exception if the input number parameter isn't a certain value:

 

              void Java_com_androidbook_simplendk_NativeBasicsActivity_throwsException

                            (JNIEnv * env, jobject this, jint number)

              {

                            if (number < NUM_RANGE_MIN || number > NUM_RANGE_MAX) {

                                          // throw an exception

                                          jclass illegalArgumentException =

                                          (*env)->FindClass(env, "java/lang/IllegalArgumentException");

                                          if (illegalArgumentException == NULL) {

                                                        return;

                                          }

                                          (*env)->ThrowNew(env, illegalArgumentException,

                                          "What an exceptional number.");

                            } else {

                                          _ _android_log_print(ANDROID_LOG_VERBOSE, DEBUG_TAG,

                                          "Nothing exceptional here");

                            }

              }

 

The Java declaration for this, as you might expect, needs a throws clause.

private native void throwsException(int num)

throws IllegalArgumentException;

 

Basically, the exception class is found through reflection.Then, the ThrowNew() method

of the JNIEnv object is used to do the actual throwing of the exception.

To show how to check for an exception in native C code,we need to also show how

to call a Java method from C.The following block of code does just that:

 

              void Java_com_androidbook_simplendk_NativeBasicsActivity_checksException

                            (JNIEnv * env, jobject this, jint number)

              {

                            jthrowable exception;

                            jclass class = (*env)->GetObjectClass(env, this);

                            jmethodID fnJavaThrowsException =

                                          (*env)->GetMethodID(env, class, "javaThrowsException", "(I)V");

                            if (fnJavaThrowsException != NULL) {

                                          (*env)->CallVoidMethod(env, this, fnJavaThrowsException, number);

                                          exception = (*env)->ExceptionOccurred(env);

                                          if (exception) {

                                                        (*env)->ExceptionDescribe(env);

                                                        (*env)->ExceptionClear(env);

                                                        _ _android_log_print(ANDROID_LOG_ERROR,

                                                        DEBUG_TAG, "Exception occurred. Check LogCat.");

                                          }

                            } else {

                                          _ _android_log_print(ANDROID_LOG_ERROR,

                                          DEBUG_TAG, "No method found");

                            }

              }

 

The call to the GetMethodID() function is best looked up in your favorite JNI reference

or online. It's basically a reflective way of getting a reference to the method, but the

fourth parameter must be supplied correctly. In this case, it takes a single integer and

returns a void.

 

Because the method returns a void, use the CallVoidMethod() function to actually

call it and then use the ExceptionOccurred() function to check to see if the method

threw an exception. If it did, the ExceptionDescribe() function actually writes the exception

out to LogCat, but it looks slightly different from a normal exception output.

Then the exception is cleared so it doesn't go any further.

The Java method being called, javaThrowsException(), is defined as follows:

              @SuppressWarnings("unused") // is called from native

              private void javaThrowsException(int num)

              throws IllegalArgumentException {

                            if (num == 42) {

                                          throw new IllegalArgumentException("Anything but that number!");

                            } else {

                                          Log.v(DEBUG_TAG, "Good choice in numbers.");

                            }

              }

 

The use of the @SuppressWarnings option is due to the fact that the method is never

called directly from Java, only from native code.You can use this process of calling Java

methods for Android SDK methods, as well. However, remember that the idea of using

NDK is generally to increase performance. If you find yourself doing many Android calls,

the performance might be improved by simply staying on the Java side of things and leaving

algorithmically heavy functionality on the native side.

 

 

0 件のコメント:

コメントを投稿