9

Getting to Java: The JNI

The previous chapter detailed the process of creating a HAL, an abstract interface between the Android framework and a novel bit of hardware.

That is only halfway there. Although a HAL is the canonical way of plumbing hardware into Android, it is not sufficient to make the device useful from Android Java programs. To do that, we need the Java’s Native Interface, the JNI.

The goal for this chapter is to cross the boundary into Android’s implementation language, Java. We’ll cross that boundary by coding a Java language application that connects to the proximity sensor using its HAL and that logs its status once every minute or so. We’ll achieve that goal in three steps:

  1. We’ll create a simple native application that talks directly to the proximity sensor. This application is a simple extension of the one discussed at the end of Chapter 6.

  2. We’ll refactor that application to use the HAL we built in the last chapter. A native application that uses the HAL is a useful artifact: The only significant distinction between a native application and the corresponding Java application is, exactly, the implementation language. The choice of environments depends entirely on the preferences of the team that will build and support it.

  3. We’ll implement an application in Java using Java’s Native Interface (the JNI) that uses the native HAL from code running inside the Android’s bytecode interpreter.

Note

It was our intention to run the last example, the Java application, also as a daemon. Unfortunately, we were unable to construct SE rules that allowed it to run in ART under init. Instead, the Java application shown here must be run as a system service started by Zygote (as described in Chapter 7).

Code Structure

The applications described in this chapter are complete and freestanding. Therefore, each goes into its own subdirectory of “app” directory in the Acme One source structure. Figure 9.1 shows the structure (with the code from the previous chapters elided).

Images

Figure 9.1 Application Code Layout

Although the code for each of the applications is nested inside the “app” folder in the Acme One device file structure, there is no need for all the code to be a fixed part of the Acme One source code repository. As usual, creating a new git repo for each separate application and using repo to add them at checkout time to the workspace makes sense.

It is possible to do even better, though. Notice that, as described, each of the applications has exactly the same function: periodically logging proximity data to the console. There is no reason ever to build all three for a single given device. All three implementations might exist—perhaps as legacy implementations or specialized versions required for some specific device—but having all three in the workspace at once would be useless, at best.

The repo tool not only supports this scenario—three different versions of the same application—but a nifty feature called groups (first described in Chapter 2) makes it very convenient. Listing 9.1 shows the additions to the manifest.

Listing 9.1 Manifest Additions for the Proximity Applications

<!-- Acme Applications -->
  <project path="app/simple_daemon"
           name="simple_daemon" remote="acme"
           groups="nodefault" />
  <project path=app/native_daemon
           name="native_daemon" remote="acme"
           groups="nodefault" />
  <project path=app/java_daemon
           name="java_daemon" remote="acme"
           groups="nodefault" />

Listing 9.1 demonstrates the use of this new workspace customization feature, the groups attribute. Annotating a project in the manifest with the group “nodefault” indicates to the repo tool that the annotated repository should not be downloaded as it normally would be when the workspace is synched. To pull the code for one (or more) of the applications into the workspace, use the repo tool -g flag to specify a group that includes the desired implementation.

One simple way to select a particular project is by its name. Every project belongs to a group whose name is “name:” followed by the value of the project’s name attribute. For example, when used with the manifest shown in part in Listing 9.1, the following command will create a workspace that contains the simple-daemon application:

repo init -g name:simple_daemon ...

Using the Device

The code for the first version of the application is almost trivial. It simply opens the device directly (no HAL), polls at a fixed interval, and logs the result. This is the trivial extension of the daemon from Chapter 6. Listing 9.2 shows the code for it.

Listing 9.2 Simple Native Proximity Application

#include <unistd.h>
#include <stdio.h>
#include <android/log.h>
#include "dev/proximity_sensor.h"

#define DELAY_SECS 60
#define ALOG(msg) __android_log_write(ANDROID_LOG_DEBUG, "PROXIMITY", msg)

int main(int argc, char *argv[]) {
    struct proximity_params_t config;
    char message[128];

    int fd = open_sensor(config);
    if (fd < 0)
        return -1;

    int n = 0;
    int precision;
    while (true) {
        sleep(DELAY_SECS);

        n++;
        if (n < 10) {
            precision = 40;
        } else {
            n = 0;
            precision = 80;
        }

        int proximity = poll_sensor(fd, precision);

        if ((proximity < config.proximity_min)) {
            close_sensor(fd);
            return 0;
        }

        snprintf(message,
                 sizeof(message),
                 "proximity @%2d: %4.2f",
                  precision,
                 (100.0 * (proximity - config.proximity_min))
                          / config.proximity_range);

        ALOG(message);
    }
}

This code polls the sensor once every minute with a precision of 40 and once every 10 minutes with a precision of 80. It logs the result to the console.

Listing 9.3 shows the blueprint file used to build the application. It appeared previously in Chapter 6 as Listing 6.11.

Listing 9.3 Building the Simple Native Application

cc_binary {
    name: "acmesimpledaemon",
    relative_install_path: "hw",
    init_rc: ["vendor.acmesimpledaemon.acme.one.rc"],
    header_libs: [
        "libacmeproximityshim_headers",
        "liblog_headers",
    ],
    srcs: [
        "acme-simple-daemon.cpp"
    ],
    shared_libs: [
        "liblog",
        "libcutils",
    ],
    static_libs: [
        "libacmeproximityshim",
    ],
    vendor: true,
    proprietary: true,
}

Using the HAL

The second version of the application is only slightly different from the first. The functional part of the code, the loop that logs proximity readings, is identical. The only differences between this code and that of the preceding application are that, instead of opening the device directly, it requests the device HAL by name from the OS and then uses the returned reference to invoke sensor methods through the HAL.

Listing 9.4 shows the code for the second version of the application (located at device/acme/one/app/native_daemon/acme-native-daemon.cpp).

Listing 9.4 HAL Native Proximity Application

#include <unistd.h>
#include <stdio.h>
#include <android/log.h>
#include <hardware/hardware.h>

#include "dev/proximity_hal.h"

#define DELAY_SECS 60
#define ALOG(msg) __android_log_write(ANDROID_LOG_DEBUG, "PROXIMITYD", msg)

int main(int argc, char *argv[]) {
    const hw_module_t* module
    if (hw_get_module(ACME_PROXIMITY_SENSOR_MODULE, &module) {
        ALOG("Failed to load Acme proximity HAL module");
        return -1;
    }
        
    proximity_sensor_device_t* device;
    if (module->methods->open(
        module,
        nullptr,
        reinterpret_cast<struct hw_device_t**>(& device))) {
        ALOG("Failed to open Acme proximity HAL");
    	return -1;
    }

    proximity_params_t config = device->params;
    char message[128];

    int n = 0;
    int precision;
    while (true) {
        sleep(DELAY_SECS);

        n++;
        if (n < 10) {
            precision = 40;
        } else {
            n = 0;
            precision = 80;
        }

        int proximity = device->poll_sensor(device, precision);

        if ((proximity < config.proximity.min)) {
            device->common.close(reinterpret_cast<hw_device_t *>(device));
            return 0;
        }

        snprintf(message, sizeof(message), "proximity @%2d: %4.2f", precision,
            (100.0 * (proximity - config.proximity.min)) / config.proximity.range);

        ALOG(message);
    } 
}

The only thing worthy of particular notice, here, is that the second argument to the open method is null. It could have been used by the HAL to do runtime specialization. The simple Acme Proximity HAL described in the last chapter, however, ignores the parameter completely.

The build script for this second application is also nearly identical to that for the first native application. The acmenativedaemon must have the same SE label as that used for the simple version. That label allows it access to USB serial devices and to be started by init. The label is applied in file_contexts within the SE policy folder. Because these things are so similar, they are not included here. The only differences are the application name and the libraries it uses, as shown in Listing 9.5.

Listing 9.5 HAL Native Proximity Application Build File

cc_binary {
    name: "acmenativedaemon",
    relative_install_path: "hw",
    init_rc: ["vendor.acmenativedaemon.acme.one.rc"],
    header_libs: [
        "libacmeproximityshim_headers",
        "liblog_headers",
        "libhardware_headers",
    ],
    srcs: [
        "acme-native-daemon.cpp"
    ],
    shared_libs: [
        "liblog",
        "libcutils",
        "libhardware",
    ],
    vendor: true,
    proprietary: true,
}

Using the Java Native Interface

Most Android applications are written in interpreted languages. The source code for these applications—probably Kotlin or Java—is compiled to bytecodes. Bytecodes are not instructions that can be executed by any actual hardware. Instead, as discussed in Chapter 7, they are native instructions for a virtual machine. The virtual machine is an application that runs on the target device, interprets each of the bytecodes in the compiled app, and executes a set of native instructions necessary to perform the action described by the bytecode.

Most Android code, then, is executed as interpreted bytecodes. Clearly, the execution of those bytecodes can do only things that the virtual machine that interprets those bytecodes was built to do. In particular, because no virtual machine has compiled into it the ability to talk to the Acme proximity sensor, there is no way that interpreted code can use the sensor.

Fortunately, interpreted virtual machine instructions are not a running program’s only interface to native instructions and the operating system. Since its creation, the Java language has defined a mechanism that allows an application to execute arbitrary native code uninterpreted and outside the virtual machine. The mechanism is as old as Java itself and is called the Java Native Interface (JNI). The Android virtual machines implement this mechanism.

Note

Although the JNI allows the execution of machine instructions that are not part of the virtual machine, JNI code is executed as part of the same process that is running the virtual machine. JNI code executes “outside the virtual machine” only in the sense that it is not executing instructions that virtual machine designers provided. It is still virtual machine methods that are below the JNI code in the call stack and to which control will return when the execution of the JNI code completes.

Executing Native Code

Figure 9.2 illustrates the ways in which native code can be used in an Android application.

Application code, as discussed earlier, is typically written in an interpreted language. It is represented in Figure 9.2 by the largest box at the top of the figure.

Android application code depends on a library of standard functions and classes in the java.* and android.* packages. The java packages have an API that is very similar to the Java 8 JRE. The android library defines the Android runtime environment. Both of these libraries are implemented largely in Java and, therefore, most of the code in each compiles into bytecodes that are executed by the virtual machine.

Beneath the interpreted code and shown in the center of Figure 9.2 is the virtual machine. It is, of course, written in a language (probably C and C++) that is compiled into machine instructions that are native to the target device.

To the left and right of the virtual machine in Figure 9.2 are two more pieces of code that are compiled directly (again, usually from C or C++) into instructions native to the target device. Although run as part of the application, these pieces of code are not compiled to bytecodes and are not interpreted by the virtual machine.

Images

Figure 9.2 Interpreted and Native Code

Nearly all Android programs make use of the native code represented by the block on the bottom right of the figure when they use the runtime libraries. Although, as mentioned earlier, much of the runtime environment code is implemented in Java, the implementations of certain functions that do specialized things—like interfacing with the kernel, file and network I/O or performing highly optimized functions like encryption and decryption—are all native (non-interpreted) code called from the Java.

The Java Native Interface (JNI) is the mechanism by which the interpreted runtime library code calls the device native code. It is also a well-defined, public API; is implemented by the Android virtual machine; and can be used directly by application code, just as it is used by the Java and Android runtime libraries. Any application can use the JNI to execute native code: While it is running, it can load an arbitrary native library and execute the code in it.

Note

A previous Java-based smartphone standard, J2ME, enforced security and controlled application access to hardware by preventing the use of native code. Applications could only execute instructions that were compiled into the virtual machine; access to instructions deemed “dangerous” was carefully controlled.

Android has a much different security model and no such restriction. Applications can and do execute their own native code.

JNI: The Java Side

One more time, let’s interrupt our strict up-the-stack journey through the Android landscape and look at the JNI starting from above, in code written in Java.

The Java side of the JNI is straightforward and quite simple. It consists of the keyword “native” and the system method, System.loadLibrary.

A method with the keyword native in its declaration—a native function—is similar to the declaration of a function in a Java interface: It declares the function prototype but not its implementation. Methods declared in an interface must be defined in the classes that implement the interface. Methods declared “native,” to the contrary, are not defined in Java at all. Instead, the virtual machine expects to find their definitions as canonically named symbols in a library loaded with the load (or loadLibrary) method.

Note

Kotlin uses the keyword “external” to accomplish the same thing: declaring a function whose definition is elsewhere.

Listing 9.6 shows the declaration of the three methods that Java code will need to interact with the proximity device.

Listing 9.6 Native Method Declarations

package com.acme.device.proximity;
// ...

public class AcmeProximitySensor {
   // ...
    private static native long open();
    private static native int poll(long hdl, int precision);
    private static native void close(long hdl);
}

JNI: The Native Side

These three methods must now be defined in a linkable library. In this example, the library will be written in C.

The Android virtual machine will translate calls to these native methods into calls to canonically named functions in a native library. The definitions for the corresponding functions must have exactly the names that the runtime expects them to have. Fortunately, there is tool that is part of the Java Development Kit that will generate the C prototypes for the native definitions automatically: javah.

Note

javah has been deprecated as of Java 9. Even in Java 8, its functionality can be duplicated with the -h flag for the java compiler, javac.

Javah need not be part of the build process. Running it is necessary only when new native methods are introduced or when one of the signatures of an existing native methods changes: when the headers it generates will be different from the headers it generated last time it was run. Some shops decide to create the headers once and then check them in to source control like any other source file.

Because the process of generating the native prototypes can be automated, some shops do prefer to make it part of the build. When there are a lot of native methods and they are changing frequently, this is a very reasonable approach. Note, though, that javah only generates the header files and the function prototypes! If the corresponding function definitions (presumably in a .c file) do not match, the build will fail with native compiler errors.

Running javah by hand is quite simple. It takes as arguments:

  • The classpath identifying the directory (or jar) that is the top of the package tree containing the compiled Java .class files. The classpath is specified using the -cp command line option.

  • Either a -d or -o argument indicating the directory or file (respectively) into which the tool should put the generated output.

  • The fully qualified name of the class containing native declarations for which headers are to be generated.

For instance, run from the root of the Java application package (device/acme/one/app/java_daemon), the following command will create a .h file from the binary generated by compiling the code in Listing 9.6:

javah -cp java 
 -o cpp/com_acme_device_proximity_AcmeProximitySensor.h 
  com.acme.device.proximity.AcmeProximitySensor

Javah will search the directory “java” for the class file containing the class com.acme.device.proximity.AcmeProximitySensor, (probably java/com/acme/device/proximity/AndroidProximitySensor.class) and create C prototypes for any native methods it finds there. The prototypes will be written to the file cpp/com_acme_device_proximity_AcmeProximitySensor.h. Listing 9.7 shows the generated file.

Listing 9.7 Proximity HAL JNI Function Prototypes

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

#ifndef _Included_com_acme_device_proximity_AcmeProximitySensor
#define _Included_com_acme_device_proximity_AcmeProximitySensor
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_acme_device_proximity_AcmeProximitySensor
 * Method:    open
 * Signature: ()J
 */
JNIEXPORT jlong
JNICALL Java_com_acme_device_proximity_AcmeProximitySensor_open
  (JNIEnv *, jclass);

/*
 * Class:     com_acme_device_proximity_AcmeProximitySensor
 * Method:    poll
 * Signature: (JI)I
 */
JNIEXPORT jint
JNICALL Java_com_acme_device_proximity_AcmeProximitySensor_poll
  (JNIEnv *, jclass, jlong, jint);

/*
 * Class:     com_acme_device_proximity_AcmeProximitySensor
 * Method:    close
 * Signature: (J)V
 */
JNIEXPORT void
JNICALL Java_com_acme_device_proximity_AcmeProximitySensor_close
  (JNIEnv *, jclass, jlong);

#ifdef __cplusplus
}
#endif
#endif

Note, especially, the extern "C" { ... } directive. It is essential! It prevents a C++ compiler from mangling the names of the functions and making their definitions unrecognizable as the definitions for the corresponding Java native declaration.

Note

Canonically named methods are not the only way to link the native implementation of a method to its Java declaration. The RegisterNatives JNI function takes, as an argument, an array of JNINativeMethod, each of which identifies a Java method (by fully qualified signature) and includes a pointer to the native implementation. Used in the JNI_OnLoad method (called by the VM when it loads a native library), JNINativeMethod provides an alternative way of connecting Java and native methods.

Note also that these native definitions depend on the header file jni.h. The jni.h header file contains the definitions for the native type abstractions for Java’s base types—int, long, [] (array), and so on—some macros (JNIEXPORT, JNICALL, and so on), but most importantly the definition of the JNI environment, a structure of opaque pointers to standard JNI functions. These functions allow native code to work with Java objects.

This chapter will conclude with a slightly deeper discussion of the JNI native environment. For the moment, though, let’s just assume (as it is usually safe to do), that a jint is an int, a jlong is a long, and so on.

The next step is implementing the functions in native code. Stealing code from Listing 9.4 makes this a trivial task. Listing 9.8 shows the result.

Listing 9.8 Proximity HAL JNI Implementation

#include <jni.h>
#include <string>
#include <hardware/hardware.h>

#include "dev/proximity_hal.h"

JNIEXPORT jlong JNICALL Java_com_acme_device_proximity_AcmeProximitySensor_open
  (JNIEnv * env, jclass clazz) {
    const hw_module_t *module;

    if (hw_get_module(ACME_PROXIMITY_SENSOR_MODULE, &module))
        return -1;
    long device;
    if (module->methods->open(
            module,
            nullptr,
            reinterpret_cast<struct hw_device_t **>(&device)))
        return -1;
    return (jlong) device;
}

JNIEXPORT jint JNICALL Java_com_acme_device_proximity_AcmeProximitySensor_poll
  (JNIEnv *env, jclass clazz, jlong handle, jint precision);
    auto *device = reinterpret_cast<proximity_sensor_device_t *>(handle);
    return device->poll_sensor(device, precision);
}

JNIEXPORT jint JNICALL Java_com_acme_device_proximity_AcmeProximitySensor_close
  (JNIEnv * env, jclass clazz, jlong handle);
    auto device = reinterpret_cast<proximity_sensor_device_t *>(handle);
    return device->common.close(reinterpret_cast<hw_device_t *>(device));
}

This is a calculated and extremely simple example. Once again, though, the reader is cautioned! The JNI is extensive, complex, and easy to break. Entire books exist about this topic alone.

The alert reader will notice that the reference to the sensor, returned by the HAL, is cast as a jlong in both the open and the close methods. This is the introduction to a common and powerful JNI technique. Its purpose will become obvious when the corresponding methods are implemented in Java.

A Java Proximity Application

Having plumbed a path from Java code through the HAL and into the Acme Proximity Sensor, we can now implement an analog for the applications shown in Listings 9.2 and 9.4 that is written in Java.

The Native Shim

The first step will be to complete the AcmeProximitySensor class, shown in part in Listing 9.6. It is the shim that connects the Java environment to the native environment.

It is very much best practice that the shim code abstract away even the faintest whiff of native-ness. The API for the shim should follow all the best practices standard for any Java API. In particular, declaring the API in a mockable Java interface is a great way to make it possible to test client code without requiring access to any specific hardware.

Listing 9.9 shows a complete implementation of the AcmeProximitySensor class.

Listing 9.9 Proximity HAL Java Implementation

public class AcmeProximitySensor implements AutoCloseable {
    static { System.loadLibrary(“acmeproximityjni”); }

    private long peer;

    public void init() throws IOException {
        synchronized (this) {
            if (peer != 0L) { return; }
            peer = open();
            if (peer == 0L) {
                throw new IOException(“Failed to open proximity sensor”);
            }
        }
    }
    
    public int poll(int precision) throws IOException {
        synchronized (this) {
            if (peer == 0L) { throw new IOException(“Device not open”); }
            return poll(peer, precision);
        }
    }

    @Override
    public void close() throws IOException {
        final long hdl;
        synchronized (this) {
            hdl = peer;
            peer = 0L;
        }
        if (hdl == 0L) { return; }
        if (close(hdl) < 0) {
            throw new IOException(“Failed closing proximity sensor”);
        }
    }

    @Override
    protected void finalize() throws Throwable {
        try { close(); }
        finally { super.finalize(); }
    }

    private static native long open();

    private static native int poll(long handle, int precision);

    private static native int close(long handle);
}

There are several things to note in this code.

The first is the use of the previously discussed System.loadLibrary method. It is called, as is frequently the case, from a static initializer that will be invoked when the class is loaded. This is a common strategy because the library must be loaded before any of the methods in the class can be used.

There are other strategies, though. A system that requires several native libraries may load them all at once, perhaps using some kind of registration system, as part of startup. Another possibility, especially in systems that require additional initialization, is loading necessary native libraries in an initialization method that client code must call explicitly before making any other use of the library.

Note, also, that System.loadLibrary loads the library named in its actual parameter in a system-dependent way. It modifies the name to conform to the platform library naming conventions and then attempts to load it from the library path. When run on a Linux OS, for example, the code in Listing 9.9 will load the libacmeproximityjni.so library. On a Windows system, though, it would load the acmeproximityjni.dll library.

The library path can be specified at JVM startup using the system parameter java.library.path or, on Android Linux, by setting the environment variable LD_LIBRARY_PATH. A second method, System.load, mentioned previously, will load a library from a specific file named in the fully qualified path passed as its argument.

Next, observe the use of the variable peer. In this code, the Java long variable peer contains the native reference to the HAL object as described in the discussion of Listing 9.8. Unlike C, in which a reference can be made opaque by declaring it void*, making a reference opaque in Java is fairly difficult. Nonetheless, it is crucial that the contents of a variable used in this way be treated as opaque. Any mutation of any kind by the Java code is almost certainly an error and probably a disastrous one. As usual, in situations like this, minimizing visibility and mutability is a useful strategy.

Together, Listings 9.8 and 9.9 illustrate the simplest form of a very common pattern. The responsibility for managing the Java use of specialized hardware is handled through the coordination of two objects, one Java and one native. The two objects are the two ends of a bridge—one end in the native code and the other in the Java code—through which all interactions take place. Client code instantiates the Java object and the Java object manages the native object, frequently called a native peer, or a companion object. The Java code holds a reference to the native object. The native code’s awareness of the Java code is kept minimal.

The Native Shim: Opaque Peer

Another example of this pattern attempts to enforce the opacity of the native reference in the Java variable by using reflection in the native code to set the value of the reference held in the Java variable. Listings 9.10 and 9.11 illustrate this technique.

Listing 9.10 Opaque Peer: Native

JNIEXPORT int
JNICALL Java_com_acme_device_proximity_AcmeProximitySensor_open
    (JNIEnv *env, jclass klass, jobject instance) {

    if (hw_get_module(ACME_PROXIMITY_SENSOR_MODULE, &module))
        return -1;

    hw_device_t *device;
    if (module->methods->open(module, nullptr, &device)))
        return -2;

    jfieldID peer = env->GetFieldID(klass, "peer", "J");
    if (!peer)
        return -3;

    env->SetLongField(instance, peer, reinterpret_cast<jlong>(mem));
    
    return 0;
}

Listing 9.11 Opaque Peer: Java

public class AcmeProximitySensor implements AutoCloseable {
    /...
    
    private long peer;

    /...

    public void init() throws IOException {
        synchronized (this) {
            int status = open(this);
            if (status != 0) {
                throw new IOException(
                    "Failed to open proximity sensor: " + status);
            }
        }
    }

    /...

    private static native int open(AcmeProximitySensor instance);
    
    /...
}

Two new JNI methods are used here without introduction: GetFieldID and SetLongField. Their names, though, are fairly self-explanatory (they return a reference to a Java field and assign the field value, respectively) so that the concept the listing introduces should be clear.

The open method in Listing 9.11 could, as easily, be an instance method instead of a static method. In JNI, the difference between the implementations of a static and an instance method is the second argument to the method. The second argument to a static native function is a reference to the class to which the function belongs. The second argument to an instance native function is a reference to the instance to which the function belongs. Holding a reference to the instance means that the native code can access the instance’s fields, exactly as the example code does.

In this case, however, we also need a reference to the object’s class (for the call to GetFieldID). Because both are needed, using a static method (which provides the reference to the class object) will work just as well. We pass the instance reference (this in the call to open, in
Listing 9.11) explicitly. The native implementation then has both of the references that it needs.

Although somewhat more complex, the strategy illustrated in Listings 9.10 and 9.11 has advantages. In this implementation, any Java use of the variable peer is now clearly an error. Distinguishing between appropriate and inappropriate use of the stored pointer is not necessary: Any use is an error. Another advantage is that the open method’s return value is now, unambiguously, a status code. No need exists to partition returns into legal values and illegal values.

The Native Shim: Finalization

Returning to Listing 9.9, one more issue is worthy of note: the management of the lifecycle of the companion object.

Although the garbage collector manages Java’s memory, native memory usually must be allocated and deallocated explicitly. An AcmeProximitySensor object allocates its companion in its “open” method. So how does it free it? There are several possible answers to that question.

The first and best is “explicitly.” For reasons that will be discussed momentarily, the most effective way to handle explicitly managed memory is explicitly. Ideally, an object with a native peer that must be freed explicitly would be marked somehow so that clients would know that they should explicitly free it.

That is exactly the purpose of Java’s Closeable interface. An object that implements Closeable is hinting to its user that it needs to be closed explicitly when the user is through with it. The AcmeProximitySensor implements Closeable’s sub-interface AutoCloseable and uses it to free its companion.

Note

The AutoCloseable interface, introduced in Java 7, extends Closeable: Prefer it, where possible. Most significantly, instances of a class that implements AutoCloseable can be used with the try-with-resources statement. They can also throw exceptions, other than an IOException, as appropriate to their specific failure modes.

The contract for the AutoCloseable’s close method is also different: idempotency is not a requirement. Rather, developers are strongly encouraged to mark closed instances and to prevent their use after closing.

A second strategy is a finalizer. A Java finalizer is a method with the specific signature, protected void finalize() throws Throwable. If an object has a method with that signature, it is called by the Java runtime just before the garbage collector frees the object’s memory. This seems perfect: with one small method, when the AcmeProximitySensor goes away, so does its companion.

Unfortunately, however, there’s no such thing as a free lunch. Finalizers are quite difficult to get right and, even when correct, have problems. Correctness first.

There is no guarantee about the order in which objects are finalized after they become eligible for garbage collection. The example code in Listing 9.12 has several problems, not the least of which is that it may get a NullPointerException in its finalizer. There is no guarantee that the list referenced by objects has itself not already been finalized when the finalizer for an instance of BrokenFinalizer is run. Its contents might well have been finalized first!

Listing 9.12 Broken Finalizer: Don’t Do This!

public class BrokenFinalizer implements Closeable {
    @NonNull
    private final List<NativeObject> objects

    // ...
    
    @Override
    protected void finalize() throws Throwable {
        for (NativeObject obj: objects) { obj.close(); }
    }
}

In addition to being brittle and very difficult to code, finalizers have two other problems. The first is that they impose a considerable inefficiency on the garbage collector. Because a finalizer can do all kinds of weird stuff (including “resurrect” the object being garbage collected by storing a reference to it somewhere!), the garbage collector has to do checks that are not necessary for an object that does not have a finalizer. These checks slow the collector down, cause it to place a heavier burden on the application, and mean that the lag between last use and deallocation gets longer.

The second problem, though, hinted at in the last sentence, is even worse. Although Java promises that it will run the garbage collector before it runs out of memory, there is no way to predict how long an object that is eligible for collection will sit around in memory before Java needs space and schedules it for collection. Furthermore, Java cannot guarantee that all finalizers will be run before an application runs out of memory. Finalizers are run in a platform-dependent way: almost universally on a single Java thread. If an application allocates and then frees a hundred large objects every second, each of those objects is scheduled for finalization, and each finalization takes a half a second to complete, the application is doomed. An application with native companion objects can build up a considerable backlog of dead objects before a garbage collection takes place. Instead of being freed incrementally, all of those objects are put on the finalization queue suddenly and all at once.

Despite these problems, finalizers can be a reasonable part of a “belt and suspenders” policy, as shown in Listing 9.9. An AcmeProximitySensor is Closeable: Client code is expected to explicitly close each instance when it is done with it. If the client code fails to do so, however, the finalizer will prevent a native memory leak.

The Native Shim: Reference Queues

The last and most complex way of managing native object lifecycles are reference queues. In return for somewhat more complicated code, reference queues remove many of the problems that finalizers have. They do not interfere with garbage collection, and they allow ordered freeing of objects.

As of Java 9, finalizers have officially been deprecated in favor of Cleaners. As part of Java 9, Cleaners are not available in Android. There is good news, though: PhantomReferences and ReferenceQueues, the technologies underlying Java 9’s Cleaner, are available in Android. The message to Android developers should be clear, even if Cleaners themselves are not available in the Android runtime environment.

The combination of a reference queue and a phantom reference works like this: The constructor for an instance of the PhantomReference class (or one of its subclasses) takes two arguments: an object and a reference queue. When the object whose reference is the first parameter to the constructor becomes eligible for garbage collection, the phantom reference (itself) is enqueued on the reference queue that was the second parameter to the constructor.

Calls to PhantomReference.get always return null: The referenced object itself is unreachable via the phantom reference and the existence of the reference cannot affect its reachability. What the reference can do, though, is provide a way to remember that there is unfinished business, when the object to which it is a reference no longer exists.

The implementation of a reference queue solution is somewhat difficult. Let’s take it in four parts. First, Listing 9.13 is the machinery that manages the lifecycle of an AcmeProximitySensor object and its peer.

Listing 9.13 Reference Queue: Native Companion and Its Lifecycle

public class AcmeProximitySensor implements AutoCloseable {

    // ...

    static final Map<AtomicLong, Reference<?>> CLEANERS = new HashMap<>();

    static { System.loadLibrary("acmeproximityjni"); }

    // ...

    @NonNull
    public static AcmeProximitySensor getSensor() {
        synchronized (CLEANERS) {
            final AtomicLong peerRef = new AtomicLong(open());
            final AcmeProximitySensor sensor = new AcmeProximitySensor(peerRef);
            CLEANERS.put(peerRef, new SensorCleaner(peerRef, sensor));
            return sensor;
        }
    }

    static void cleanup(AtomicLong peerRef) {
        final long peer = peerRef.getAndSet(0);
        if (peer == 0) { return; }

        synchronized (CLEANERS) {
            CLEANERS.remove(peerRef);
            close(peer);
        }
    }

    // ...

    @GuardedBy("CLEANERS")
    private static native void close(long handle);

    @GuardedBy("CLEANERS")
    private static native long open();

    @GuardedBy("CLEANERS")
    private static native int poll(long handle, int precision);
}

The static method getSensor is the factory method for instances of the class AcmeProximitySensor. Client code will call this method instead of the class constructor to create new instances. The class constructor is private, ensuring that this is the only way to create new instances: All new instances come from this method.

The method does four things:

  1. It creates the native companion object and stashes the reference to it in an AtomicLong.

  2. It creates the Java instance, passing the peer reference. The Java object now has access to its native peer.

  3. It creates a SensorCleaner. This is the object that will be responsible for cleaning up the native peer if the user fails to do so.

  4. It stores the SensorCleaner in a map.

The last step is important and yet easy to forget. Like any other Java object, the SensorCleaner is eligible for garbage collection as soon as there are no more references to it. Unless something, somewhere, remembers it, it will be garbage collected and will not be around to clean up after the AcmeProximitySensor instance with which it is associated.

The static method cleanup(AtomicLong) is the bottleneck at the end of the lifecycle of every AcmeProximitySensor instance. It simply undoes what the getSensor method did: It frees the native companion object and removes the SensorCleaner from the map so that it can be garbage collected.

It also ensures that the native close method will not be called more than once. The getAndSet in its first line ensures that the rest of the method will be executed no more than once for a given native peer, no matter how often it is called. The method is idempotent.

These two methods are the bookends for the AcmeProximitySensor lifecycle. The getSensor method is the only way to get one. All we have to do is make sure that the cleanup(AtomicLong) method is called at least once for every native companion.

As mentioned earlier, the best way to do this is explicitly. Listing 9.14 shows the implementation of the AcmeProximitySensor and, in particular, its implementation of the AutoCloseable interface.

Listing 9.14 Reference Queue: Explicit Close

public class AcmeProximitySensor implements AutoCloseable {

    // ...

    private final AtomicLong peerRef;

    private AcmeProximitySensor(AtomicLong peerRef) { this.peerRef = peerRef; }

    public int poll(int precision) throws IOException {
        synchronized (CLEANERS) {
            final long peer = peerRef.get();
            if (peer == 0) { throw new IOException("Device not open"); }
            return poll(peer, precision);
        }
    }

    @Override
    public void close() { cleanup(peerRef); }

    // ...
}

This code is similar to the equivalent code in Listing 9.9. The only significant difference, really, is that it delegates the call to close, required by the AutoCloseable interface, to cleanup(AtomicLong). Well-behaved client code will fulfill the outstanding condition: at least one call to cleanup(AtomicLong) via AcmeProximitySensor.close.

What happens, though, when client code is not well behaved? Listing 9.15 shows the backstop.

Listing 9.15 Reference Queue: Service Task

public class AcmeProximitySensor implements Closeable {
    private static final class SensorCleaner
        extends PhantomReference<AcmeProximitySensor> {
        private final AtomicLong peerRef;

        SensorCleaner(AtomicLong peerRef, AcmeProximitySensor sensor) {
            super(sensor, REF_QUEUE);
            this.peerRef = peerRef;
        }

        void cleanup() { AcmeProximitySensor.cleanup(peerRef); }
    }

    static final ReferenceQueue<AcmeProximitySensor> REF_QUEUE
        = new ReferenceQueue<>();

    @NonNull
    public static ScheduledTask getScheduledTask() {
        return new ScheduledTask(AcmeProximitySensor::cleanup, 100);
    }

    private static void cleanup() {
        Reference<? extends AcmeProximitySensor> ref;
        while ((ref = REF_QUEUE.poll()) instanceof SensorCleaner) {
              ((SensorCleaner) ref).cleanup();
        }
    }

    // ...
}

The two functions in Listing 9.15, getScheduledTask and cleanup(), manage the native companions objects left behind by ill-behaved clients. They also illustrate the biggest downside of using reference queues: scheduling cleanup.

Typically, finalizers are run on a special thread, maintained by the runtime, whose sole purpose is running finalizers. The finalizer thread receives notification for objects that need finalization and schedules their finalizer methods.

When using reference queues, however, scheduling object cleanup is not handled by the runtime. The application must poll the reference queue occasionally and schedule any work that it finds there.

There are good and bad aspects of this requirement. The bad parts are probably obvious: An application that uses reference queues must be able to schedule a job to service the reference queue, and it must be able to schedule any work that the job finds on a robust execution service. If the execution service fails for any reason, managed objects will no longer be freed correctly. That is likely to be disastrous.

Note, by the way, that reference queues do not change the indeterminate scheduling of object cleanup. Objects are only enqueued for cleanup when the garbage collector needs space and only processed when the application program gets around to scheduling a cleanup task.

The good aspects may be a little less apparent. Consider: Finalizers are problematic because it is possible to overwhelm the finalizer thread. As noted previously, freeing hundreds of finalizable objects quickly might very well add those objects to the finalizer’s queue faster than it can take them off for processing. Because the finalizer’s queue is difficult to access programmatically, little opportunity exists for an application to gauge whether or not its doom is impending.

Using a reference queue, however, the application controls the cleanup mechanism; it can scale the object recovery process to the need. A multi-threaded, high-priority execution service might be able to stay ahead of object allocation.

Better yet, though, suppose that the same thread processes both the application-specific tasks that cause memory allocation and the reference queue that cleans them up. If the thread is busy managing the reference queue, it cannot allocate new objects. Allocation is naturally limited to creating no more than it can free. Governing the object allocation rate in this way makes for extremely robust apps.

Listing 9.15 posits a scheduling mechanism elsewhere in the system that registers new periodic tasks by calling a class’s getScheduledTask method. The method returns the task to be run and the interval (in milliseconds) between runs. Surely, many other ways exist for accomplishing something similar: This particular implementation simply illustrates that the method cleanup() must be called periodically.

The cleanup() method polls the reference queue and calls the cleanup(AtomicLong) for each instance of the SensorCleaner object (whose associated AcmeProximitySensor has now been garbage collected) that it finds there. Recall from Listing 9.13 that each AcmeProximitySensor had a SensorCleaner associated with it in the factory method getSensor. This is the guarantee that, even if client code fails to close the sensor object, the cleanup(AtomicLong) will be called to free the native companion object.

There are a couple subtleties to be aware of. First, note that it is important that there are several references to the AtomicLong that contain the handle to the native peer. If the AcmeProximitySensor held the only reference, then when it became unreachable, the AtomicLong might be freed even before the sensor object itself. The SensorCleaner cannot depend on being able to find the native object unless it itself keeps a reference.

Second, notice that when a well-behaved client explicitly closes a sensor object, its SensorCleaner is removed from the map! That means the cleaner object is now unreachable and that the AcmeProximitySensor object holds the only reachable reference to the peer handle. It is entirely possible that the cleaner will be garbage collected first; thus, it will never be queued or scheduled. That’s perfectly okay because there’s nothing left for it to do.

Because the AtomicLong that is the reference to the native peer is in the map, a SensorCleaner is guaranteed that it will have access to it when the cleaner runs and that either it is the first attempt to free the native companion or that it will not try to do so.

JNI: Some Hints

As discussed earlier, entire books exist on the subject of writing JNI code. A comprehensive discussion is well outside the scope of this one. However, in our experience, some practices can make your code more robust and easier to maintain. Here are a few of them.

Don’t Break the Rules

It is entirely possible to break Java’s rules using native code. For instance, using native code, it is relatively easy to change the content of String or, indeed, any immutable object. Although JNI methods make some attempt to enforce visibility (public versus private) and mutability (final) constraints, sidestepping nearly any of them is quite possible. Java developers are used to being able to make assumptions about the environment in which their code runs. Changing the rules is asking for trouble.

Clearly and Without Exception, Document Native References to Java Code

Always and without exception, document any native use of Java code. If a native method refers to a class member by name, document that reference in a comment on the field. If native code calls a Java method, be certain that the Java method has a comment that indicates the fact. The same goes for native code that creates new instances of some Java class or other. As a related suggestion, be sure that any code that looks up Java identifiers—fields, classes, or methods—fails immediately and clearly if it cannot find its target. Failure to do these things will lead to bizarre crashes when some well-meaning developer does some small refactor.

Pass Simple Types to Native Code

The rest of the hints in this chapter have a single theme: When possible, don’t put the burden of dealing with Java constructs on native code. Whenever possible, Java code should deal with Java data structures and should communicate with native code using only primitive types, Strings and arrays of those types. This may mean violating other general rules. For instance, passing the information in a complex data structure to its native companion as primitive types may mean that the native method has an uncomfortably large number of arguments. That is the lesser evil. Somewhere, code will have to extract the information. Keep that code on the Java side.

Make Native Methods Static, When Convenient

This tip is more a corollary to the previous hint than a new suggestion. Native instance methods are passed references to the calling Java instance. Cases surely exist in which that may be useful. In general, though, avoiding it is best. Pass the data that the native method requires and pass it to a static method instead of passing the whole object to an instance method.

Beware the Garbage Collector

This is probably the most important and most easily forgotten of the hints. Remember that as your native code runs, the garbage collector daemon is also running. It may move data to which you have a native pointer. Worse yet, it might deallocate it.

To keep the garbage collector from freeing an object, the object must appear to the collector as “reachable”: There must be some way that running Java code can obtain a reference to it. Consider, however, a Java object that is created by native code. There are no references anywhere in the Java environment to this new object. Because there are no references, it is eligible for immediate garbage collection! The JNI provides two solutions to this problem: the LocalRef and the GlobalRef.

A LocalRef behaves as if a reference to an object had been put into the call stack of the nearest (in the stack) calling Java method. It creates a reference to the object that is visible to the garbage collector, making it reachable and thus ineligible for deallocation.

Many JNI methods that return references to Java objects also create a LocalRef to the returned object (be sure to verify this for specific calls). In general, if there is the potential for an object to disappear while native code is using it, a JNI call that returns a reference will create a LocalRef for it. Parameters to JNI method calls have LocalRefs as well. There is no danger that an object passed to a native method through the JNI will suddenly vanish.

LocalRefs are managed. They are automatically deleted (popped off the stack) when a native method returns to its Java language caller. Unfortunately, however, implementations of the JNI support only a limited number of LocalRefs: typically a few hundred but perhaps only a handful. As a rule of thumb, immediately deleting a LocalRef when it is no longer needed is best practice.

In fact, several circumstances exist under which deleting them explicitly is absolutely essential. Consider the example in Listing 9.16: native code that creates new objects of some kind and then adds them to a Java array.

Listing 9.16 Filling an Array

jobjectArray ds = env->NewObjectArray(arraySize, klass_MyObject, nullptr);
for (int i = 0; i < arraySize; i++) {
    jobject d = createNewObject(env, array[i]);
    env->SetObjectArrayElement(ds, i, d);
    env->DeleteLocalRef(d);
 }

The JNI creates a LocalRef for the array created in the first line of the code. It also creates a LocalRef for each new object to be inserted into the array. After a new object is inserted into the array, it is reachable via the array. The individual LocalRefs for the new objects can (and should) be deleted. If the array is large, it is entirely possible that failing to delete the LocalRefs will exhaust the local ref pool.

Another, somewhat trickier condition is one in which the calling code never returns to a Java method. This can happen when a native thread, for instance, calls into Java code. Imagine, for example, a native thread—perhaps a thread servicing network connections—that uses a Java logger. The native code might create a Java String, log it, and then go back about its business. The LocalRef to the String will never be released because the calling code never returns to a Java caller.

The second kind of reference in the JNI toolbox is a GlobalRef. GlobalRefs are the way that native code holds a reference to a Java object across call boundaries. A GlobalRef behaves as if a reference to the object to which it refers had been added to a permanent static array. The ref prevents the garbage collector from recovering the referenced object’s memory until the reference is deleted explicitly. Native developers, who are used to explicit memory management, will find this completely normal.

A common example of the need of a GlobalRef is native creation of a Java object, as shown in Listing 9.17.

Listing 9.17 Native Object Creation

jclass klass_MyObject = env->FindClass(“my/project/MyObject”);
if (!localClass)
    return nullptr;

method_MyObject_ctor = env->GetMethodID(
    klass_MyObject,
    “<init>”,
    “(I)V”);
if (!method_MyObject_ctor)
    return nullptr;

return env->NewObject(klass_MyObject, method_MyObject_ctor, (jint) param1);

If this call is used frequently, optimizing it might be possible. Both klass_MyObject and method_MyObject_ctor are references to Java objects: the class named “MyObject” and a constructor in that class with a single integer parameter, respectively. Using JNI methods to look up those references for every call to this code will take a substantial proportion of its execution time. To optimize it, an initialization method might be to look up the two references once and then hold them. Such an optimization requires a GlobalRef for each.

Use GlobalRefs with care. Although the limit on the number available (typically 65535) is usually much larger than the limit on the number of LocalRefs, they are memory leaks. Treat them as you would any other unmanaged memory.

Note that neither a LocalRef nor GlobalRef affects the garbage collector’s ability to move an object in memory. Although such movement is completely invisible from Java code, a native reference can become a pointer to garbage, quite literally, in the middle of a line of code. This is not a tolerable situation and JNI calls have two strategies for handling it: copying and pinning. When using JNI methods that allow access the contents of a Java object—as with methods that created implicit LocalRefs—be sure to verify which of these two strategies a specific call uses.

Copying is just what it sounds like: An atomic JNI call copies the contents of a Java object into native-managed memory. The native code is free to do anything it likes with the copy, including, at some point, atomically copying it back into the Java object.

When an object is pinned, on the other hand, the garbage collector is not allowed to move it. Native code can access the contents of the pinned object directly, perhaps without the overhead of the copy. Pinning an object, however, means that memory in the Java heap is no longer under the control of the garbage collector. This can easily lead to fragmentation and premature out-of-memory errors. Best practice is to pin objects only if necessary, and then for as short a time as possible.

Use Weak Refs When Native Code Must Hold a Reference to a Java Object

A significant portion of this chapter is devoted to the discussion of how a Java object can hold a reference to a native companion object. What happens, though, when a native object needs to be able to find a particular Java object?

Consider, for instance, a native network management library. Suppose that client code creates a new Java Connection object for each of several network connections. The Connection object in turn creates a native companion object that actually handles socket connections. Finally, suppose that the native object calls back into Java code for each of the various events in the connection lifecycle.

Clearly, the callbacks from the native companion object must be calls to the specific Java Connection instance that created it and not to any other instance. The native code must, therefore, hold a reference to its Java companion.

This is, certainly, possible. As we’ve just seen, the native companion object might hold a GlobalRef to its Java companion: the Java code now has a reference to the native object and the native object has a reference to the Java object.

The problem with this, of course, is that a GlobalRef to the Java object will make it ineligible for garbage collection. Unless there is some explicit means of freeing it (and that explicit mechanism is carefully used for every instance), its referent will never be freed, will never be finalized (or added to a reference queue), and will never free its native companion. The belt still works but the suspenders are gone. Unless a clear architectural reason exists for doing the aforementioned, a good practice is to leave the management of native objects to their Java companions, not vice versa.

There are two ways around this issue. The first is a special global reference, a WeakGlobalRef. A WeakGlobalRef is similar to a GlobalRef, except that (like a Java WeakReference) it does not prevent the garbage collection of the Java object to which it refers. It is different from a raw native reference in that it will never point at garbage: It will always either point at the intended object or be null.

It is important to note that the referent in a WeakGlobalRef can disappear at any time, even between two native instructions, causing intermittent failures. Listing 9.18 illustrates such a scenario: a snippet in which a native instance stores a reference to its Java companion.

Listing 9.18 Incorrect Use of a WeakRef: Don’t Do This!

mCompanion = reinterpret_cast<jobject>(env->NewWeakGlobalRef(javaObj));
if (env->isSameObject(mCompanion, NULL) 
    return;
jclass klass = env->getObjectClass(mCompanion) // mCompanion may be NULL!
// ...

Fortunately, a WeakGlobalRef can be used as the argument to LocalRef (or a GlobalRef). Local and global refs protect their referents from garbage collection. Listing 9.19 illustrates a corrected version of Listing 9.18.

Listing 9.19 Obtain a LocalRef from a WeakGlobalRef

mCompanion = reinterpret_cast<jobject>(env->NewWeakGlobalRef(javaObj));
companion = reinterpret_cast<jobject>(env->NewLocalRef(mCompanion));
if (env->isSameObject(companion, NULL) 
    return;
jclass klass = env->getObjectClass(companion) // safe!
// ...

There is one other way of managing native handles in Java objects. In this architecture, references to Java objects are never passed to native code at all. Instead, they are kept as weak references in a map at the Java/native boundary. Think of it as a “hat check”: Java hands native code a token, which can be redeemed for a Java object. The token, however, is completely opaque to the native code. Listing 9.20 shows a sample implementation.

Listing 9.20 Native Reference “Hat Check”

public class NativeRef<T> {
    @NonNull
    @GuardedBy("this")
    private final Random rnd = new Random();

    @NonNull
    @GuardedBy("this")
    private final Map<Integer, WeakReference<T>> refs = new HashMap<>();

    public synchronized int bind(@NonNull T obj) {
        int ref;
        do { ref = rnd.nextInt(Integer.MAX_VALUE); }
        while (refs.containsKey(ref));
        refs.put(ref, new WeakReference<>(obj));
        return ref;
    }

    public synchronized void unbind(int key) { refs.remove(key); }

    @Nullable
    public synchronized T getObjFromContext(long lref) {
        if ((lref < 0) || (lref > Integer.MAX_VALUE)) {
            throw new IllegalArgumentException("Ref out of bounds: " + lref);
        }

        final Integer key = (int) lref;
        final WeakReference<T> ref = refs.get(key);
        if (ref == null) { return null; }

        final T obj = ref.get();
        if (obj == null) { refs.remove(key); }
        return obj;
    }
}

This architecture has many of the same features that made WeakGlobalReferences attractive. Because the reference map holds weak references, native objects cannot force their Java companions to stay in memory. Perhaps an advantage, this architecture does not use the size-limited LocalRef pool.

Note that the implementation uses only positive integers for tokens. For most applications, this provides plenty of space and avoids problems that arise from sign extension.

Summary

This chapter, at last, brings us to Android’s implementation language, Java. In it, we meet and use Java’s JNI, the API through which Java language code invokes native code.

The chapter uses the Proximity Sensor project introduced in previous chapters to show three different implementations of a long-running daemon that logs proximity:

  1. A naïve native implementation

  2. A native implementation using the HAL (introduced in Chapter 7)

  3. A Java app, also using the HAL

The third implementation, the Java application, provides the basis for a discussion of one of the key issues for code at the Java/native interface: lifecycle management and how to handle unmanaged native memory in Java’s garbage-collected environment.

We recommend a “belt and suspenders” approach: Java objects with native companions should implement Java’s Autoclosable interface. This makes it clear that client code should explicitly inform the object that it is ready for disposal.

In addition, we recommend the use of a finalizer or reference queue to guarantee the proper handling of objects that evade explicit release.

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

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