Mixing Java and C/C++ Code

Calling a C/C++ function from Java is actually quite easy but requires several steps:

  1. The native method must be declared in your Java code.
  2. The Java Native Interface (JNI) glue layer needs to be implemented.
  3. Android makefiles have to be created.
  4. The native method must be implemented in C/C++.
  5. The native library must be compiled.
  6. The native library must be loaded.

It really is easy in its own twisted way. We will go through each one of these steps, and by the end of this section, you will know the basics of mixing Java and C/C++. We will discuss the more intricate details of the Android makefiles, which allow you to optimize your code even more, in later sections. Since the Android NDK exists for Linux, MacOS X, and Windows (with Cygwin, or without when using NDK revision 7), the specific steps may vary slightly although the overall operations will remain the same. The following steps assume an Android project is already created and you now want to add native code to it.

Declaring the Native Method

The first step is shown in Listing 2-1 and is rather trivial.

Listing 2-1.Declaration of the Native Method in Fibonacci.java

public class Fibonacci {
    public static native long recursiveNative (int n); // note the ‘native' keyword
}

The native method is simply declared with the native keyword, and no implementation is provided in Java. The method shown above is public, but native methods can be public, protected, private, or package-private, just like any other Java method. Similarly, native methods don't have to be static methods, and don't have to use primitive types only. From the caller's point of view, a native method is just like any other method. Once it is declared, you can start adding calls to this method in your Java code, and everything will compile just fine. However, if your application runs and calls Fibonacci.recursiveNative, it will crash with an UnsatisfiedLinkError exception. This is expected because you really haven't done much so far other than declare a function, and the actual implementation of the function does not exist yet.

Once your native method is declared, you can start writing the JNI glue layer.

Implementing the JNI Glue Layer

Java uses the JNI framework to call methods from libraries written in C/C++. The Java Development Kit (JDK) on your development platform can help you with building the JNI glue layer. First, you need a header file that defines the function you are going to implement. You don't have to write this header file yourself as you can (and should) use the JDK's javah tool for that.

In a terminal, simply change directories to your application directory, and call javah to create the header file you need. You create this header file in your application's jni directory. Since the jni directory does not exist initially, you have to create it explicitly before you create the header file. Assuming your project is saved in ~/workspace/MyFibonacciApp, the commands to execute are:

cd ~/workspace/MyFibonacciApp
mkdir jni
javah –classpath bin –jni –d jni com.apress.proandroid.Fibonacci

NOTE: You have to provide the fully qualified name of the class. If javah returns a “Class com.apress.proandroid.Fibonacci not found” error, make sure you specified the right directory with –classpath, and the fully qualified name is correct. The –d option is to specify where the header file should be created. Since javah will need to use Fibonacci.class, make sure your Java application has been compiled before you execute the command.

You should now have a header file only a mother could love called com_apress_proandroid_Fibonacci.h in ~/workspace/MyFibonacciApp/jni, as shown in Listing 2-2. You shouldn't have to modify this file directly. If you need a new version of the file (for example, if you decide to rename the native method in your Java file or add a new one), you can use javah to create it.

Listing 2-2. JNI Header File

/* DO NOT EDIT THIS FILE – it is machine generated */
#include <jni.h>
/* Header for class com_apress_proandroid_Fibonacci */

#ifndef _Included_com_apress_proandroid_Fibonacci
#define _Included_com_apress_proandroid_Fibonacci
#ifdef __cplusplus
extern “C” {
#endif
/*
 * Class:       com_apress_proandroid_Fibonacci
 * Method:    recursiveNative
 * Signature: (I)J
 */
JNIEXPORT jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative
  (JNIEnv *, jclass, jint);

#ifdef __cplusplus
}
#endif
#enddif

A C header file alone won't do you any good though. You now need the implementation of the Java_com_apress_proandroid_Fibonacci_recursiveNative function in a file you will create, com_apress_proandroid_Fibonacci.c, as shown in Listing 2-3.

Listing 2-3. JNI C Source File

#include “com_apress_proandroid_Fibonacci.h”

/*
 * Class:       com_apress_proandroid_Fibonacci
 * Method:    recursiveNative
 * Signature: (I)J
 */
jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative
  (JNIEnv *env, jclass clazz, jint n)
{
    return 0; // just a stub for now, let's return 0
}

All functions in the JNI layer have something in common: their first argument is always of type JNIEnv* (pointer to a JNIEnv object). The JNIEnv object is the JNI environment itself that you use to interact with the virtual machine (should you need to). The second argument is of type jclass when the method is declared as static, or jobject when it is not.

TIP: Try javah with the –stubs option to generate the C file (javah –classpath bin –stubs com_apress_proandroid_Fibonacci –d jni). It may work if you are using an old JDK, although it is likely you'll get this error message: “Error: JNI does not require stubs, please refer to the JNI documentation”.

Creating the Makefiles

At that point, you most certainly could compile this C++ file into a library using the NDK's GCC compiler, but the NDK provides a tool, ndk-build, that can do that for you. To know what to do, the ndk-build tool uses two files that you create:

  • Application.mk (optional)
  • Android.mk

You should create both files in the application's jni directory (where the JNI header and source files are already located). As a source of inspiration when creating these two files, simply refer to existing projects that already define these files. The NDK contains examples of applications using native code in the samples directory, hello-jni being the simplest one. Since Application.mk is an optional file, you won't find it in every single sample. You should start by using very simple Application.mk and Android.mk files to build your application as fast as possible without worrying about performance for now. Even though Application.mk is optional and you can do without it, a very basic version of the file is shown in Listing 2–4.

Listing 2–4. Basic Application.mk File Specifying One ABI

APP_ABI := armeabi-v7a

This Application.mk specifies only one version of the library should be built, and this version should target the Cortex family of processors. If no Application.mk is provided, a single library targeting the armeabi ABI (ARMv5) will be built, which would be equivalent to defining an Application.mk file, as shown in Listing 2–5.

Listing 2–5. Application.mk File Specifying armeabi As Only ABI

APP_ABI := armeabi

Android.mk in its simplest form is a tad more verbose as its syntax is dictated, in part, by the tools that will be used to eventually compile the library. Listing 2–6 shows a basic version of Android.mk.

Listing 2–6. Basic Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := fibonacci

LOCAL_SRC_FILES := com_apress_proandroid_Fibonacci.c

include $(BUILD_SHARED_LIBRARY)

The file must start with the definition of the local path, where Android.mk is located. The Android NDK provides several macros you can use in your makefiles, and here we use the my-dir macro, which returns the path of the last included makefile. In our case, the last included makefile is simply Android.mk in ~/workspace/MyFibonacciApp/jni, and therefore LOCAL_PATH will be set to ~/workspace/MyFibonacciApp.

The second line is simply to clear all the LOCAL_XXX variables, except LOCAL_PATH. If you forget that line, the variables could be defined incorrectly. You want your build to start in a predictable state, and therefore you should never forget to include that line in Android.mk before you define a module.

LOCAL_MODULE simply defines the name of a module, which will be used to generate the name of the library. For example, if LOCAL_MODULE is set to fibonacci, then the shared library will be libfibonacci.so. LOCAL_SRC_FILES then lists all the files to be compiled, in this case only com_apress_proandroid_Fibonacci.c (the JNI glue layer) as we haven't implemented the actual Fibonacci function yet. Whenever you add a new file, always remember to add it to LOCAL_SRC_FILES or it won't be compiled into the library.

Finally, when all the variables are defined, you need to include the file that contains the rule to actually build the library. In this case, we want to build a shared library, and therefore we include $(BUILD_SHARED_LIBRARY).

While this may seem convoluted, at first you will only need to worry about defining LOCAL_MODULE and LOCAL_SRC_FILES as the rest of the file is pretty much boilerplate.

For more information about these makefiles, refer to the Application.mk and Android.mk sections of this chapter.

Implementing the Native Function

Now that the makefiles are defined, we need to complete the C implementation by creating fibonacci.c, as shown in Listing 2–7, and calling the newly implemented function from the glue layer, as shown in Listing 2–8. Because the function implemented in fibonacci.c needs to be declared before it can be called, a new header file is also created, as shown in Listing 2–9. You will also need to add fibonacci.c to the list of files to compile in Android.mk.

Listing 2–7. Implementation of the New Function in fibonacci.c

#include “fibonacci.h”

uint64_t recursive (unsigned int n)
{
    if (n > 1) return recursive(n-2) + recursive(n-1);
    return n;
}

Listing 2–8. Calling the Function From the Glue Layer

#include “com_apress_proandroid_Fibonacci.h”
#include “fibonacci.h”

/*
 * Class:       com_apress_proandroid_Fibonacci
 * Method:    recursiveNative
 * Signature: (I)J
 */
jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative
  (JNIEnv *env, jclass clazz, jint n)
{
return recursive(n);
}

Listing 2–9. Header File fibonacci.h

#ifndef _FIBONACCI_H_
#define _FIBONACCI_H_

#include <stdint.h>

extern uint64_t recursive (unsigned int n);

#endif

NOTE: Make sure you use the right types in your C/C++ code as jlong is 64-bit. Use well-defined types such as uint64_t or int32_t when size matters.

Some may argue that using multiple files creates unnecessary complexity, and everything could be implemented in the glue layer, that is, in a single file instead of three or four (fibonacci.h, fibonacci.c, com_apress_proandroid_Fibonacci.c, and possibly even com_apress_proandroid_Fibonacci.h). While this is technically feasible, as shown in Listing 2–10, it is not recommended. Doing this would tightly couple the glue layer with the implementation of the native function, making it harder to reuse the code in a non-Java application. For example, you may want to reuse the same header and C/C++ files in an iOS application. Keep the glue layer JNI-specific, and JNI-specific only.

While you may also be tempted to remove the inclusion of the JNI header file, keeping it as it guarantees your functions are consistent with what is defined in the Java layer (assuming you remember to recreate the header file with the javah tool whenever there is a relevant change in Java).

Listing 2–10. All Three Files Combined Into One

#include “com_apress_proandroid_Fibonacci.h”
#include <stdint.h>

static uint64_t recursive (unsigned int n)
{
    if (n > 1) return recursive(n-2) + recursive(n-1);
    return n;
}

/*
 * Class:       com_apress_proandroid_Fibonacci
 * Method:    recursiveNative
 * Signature: (I)J
 */
jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative
  (JNIEnv *env, jclass clazz, jint n)
{
return recursive(n);
}

Compiling the Native Library

Now that the C implementation is complete, we can finally build the shared library by calling ndk-build from the application's jni directory.

TIP: Modify your PATH environment variable to include the NDK directory so you can call ndk-build and other scripts easily without having to specify the command's full path.

The result is a shared library called libfibonacci.so in the lib/armeabi directory. You may have to refresh the project in Eclipse to show the newly created libraries. If you compile and run the application, and the application calls Fibonacci.recursiveNative, it will again crash with an UnsatisfiedLinkError exception. This is a typical error as many developers simply forget to explicitly load the shared library from the Java code: the virtual machine is not clairvoyant yet and needs to be told what library to load. This is achieved by a call to System.loadLibrary(), as shown in Listing 2–11.

Listing 2–11. Loading the Library in Static Initialization Block

public class Fibonacci {
    static {
        System.loadLibrary(“fibonacci”); // to load libfibonacci.so
    }

    public static native long recursiveNative (int n);
}

Loading the Native Library

Calling System.loadLibrary from within a static initialization block is the easiest way to load a library. The code in such a block is executed when the virtual machine loads the class and before any method is called. A potential, albeit quite uncommon, performance issue is if you have several methods in your class, and not all of them require everything to be initialized (for example, shared libraries loaded). In other words, the static initialization block can add significant overhead that you would want to avoid for certain functions, as shown in Listing 2–12.

Listing 2–12. Loading the Library in the Static Initialization Block

public class Fibonacci {
    static {
        System.loadLibrary(“fibonacci”); // to load libfibonacci.so
        // do more time-consuming things here, which would delay the execution of
superFast
    }
    public static native long recursiveNative (int n);

    public long superFast (int n) {
        return 42;
    }
}

NOTE: The time it takes to load a library also depends on the library itself (its size and number of methods, for example).

So far, we have seen the basics of mixing Java and C/C++. While native code can improve performance, the difference is in part due to how the C/C++ code is compiled. In fact, many compilation options exist, and the result may vary greatly depending on which options are used.

The following two sections tell you more about the options you can define in the Application.mk and Android.mk makefiles, which until now were very basic.

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

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