Calling a C/C++ function from Java is actually quite easy but requires several steps:
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.
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.
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”.
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:
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.
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);
}
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);
}
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.
3.145.36.221