Moving native data in byte buffers

This third part in the JNI tutorial series focuses on processing chunks of data, either moving them between native code and Java, or just keeping tabs on them in Java while keeping the actual data outside the JVM.

We’ve seen how values can be passed to and from native methods as parameters and return values, but passing, say, megabytes of data back or forth one value at a time would be slow and impractical. An efficient approach would be if we could share memory between JNI C code and Java, for instance if a char * on the C side could point to the underlying memory of a Java byte[]. But the JVM is very protective of its memory, and won’t let native methods have direct access to it. Exposing the contents of a Java array to JNI C code can only be done by copying it to another memory area – outside of the JVM’s sandbox – and for any changes made to it in C to take effect, the memory has to be copied back again.

But there is another approach, which is usually more efficient: using direct byte buffers. In case you’re not familiar with ByteBuffer, we’ll start with a crash course.

ByteBuffer basics

A byte buffer object works as an interface to an underlying byte array. Its capacity is the size of the byte array, and it maintains a position and a limit, which are the start and end position of the remaining part of the array (to read from or write to). The position is always some value between zero and the capacity, and the limit is a value between the position and the capacity.

bytebuffer.svg

When you pass a byte buffer to an input method for the method to put data into the buffer, the position is where it should start writing and the limit where it should stop, if not before. When the method returns, the position is at the end of what it wrote.

Similarly for an output method that sends data from the buffer somewhere, the position is where the data block starts and the limit is where it ends. When the method returns, the position is at the end of what it read, which is equal to the limit if the full block was output but smaller otherwise.

In a new byte buffer, or one that has just been cleared, the position is zero and the limit is the capacity. When an input method has filled up part of a buffer, you get the buffer ready to pass the data on to an output method by flipping the buffer, which means setting the limit to the position and then the position to zero.

Byte buffers come in two flavors. A non-direct buffer has a Java byte[] as its underlying byte array, which can be directly accessed from Java code through the array method. By contrast, in a direct buffer, the byte array can’t be accessed from Java code, but can instead by accessed from the C side in a native method. Other than that they work the same: you can read from them and write to them with get and put, and a whole bunch of other methods.

ByteBuffer in JNI

When you use a direct byte buffer for passing data to or from a native method, you can either allocate it in Java using allocateDirect, and get the pointer to the underlying storage on the C side with GetDirectBufferAddress, or you can create a byte buffer object on the C side with NewDirectByteBuffer, which wraps a piece of memory (for instance a static global array defined in the C code, or memory allocated with malloc) as its byte array. In either case, accessing or modifying the position or limit value of the buffer is done by calling the same methods that you would use in Java. To call a Java method in JNI, you use GetMethodID to get a method handle and then a function in the Call<type>Method family to make the call.

It’s convenient to get your method IDs once and for all during static initialization. My convention is to do this in the Library class, just like we initialized exception class references in the previous part. In the zip file with the examples for this tutorial series, you find a Library class with the following Java code in readfile/src/java/net/avadeaux/readfile/Library.java:

package net.avadeaux.readfile;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;

public class Library {
    static {
        System.loadLibrary("readfile");
        init(Error.class, IOException.class, FileNotFoundException.class, ByteBuffer.class);
    }

    /** Makes sure static resources are properly initialized. */
    public static void init() {
        // Nothing here, since all is done in static initialization block.
    }

    private static native void init(Class errClass, Class ioexClass, Class fnofClass, Class bbClass);
}

The private native init method is called in the static block, passing the exception class objects to keep ready in case of errors, along with the ByteBuffer class object to save us from having to FindClass it on the C side. The implementation in readfile/src/jni/Library.c looks like this:

// Classes and methods set in init.
static jobject errClass, ioexClass, fnotfClass;
static jmethodID clearMid, getPositionMid, getLimitMid, setPositionMid, setLimitMid;

/* ... other sections ... */

// ------------------------------------------------------------------------------
// Native method implementations

#define METHOD(name) JNICALL Java_net_avadeaux_readfile_Library_ ## name

void METHOD(init)(JNIEnv *env,
                  jclass thisClass,
                  jclass jerrClass,
                  jclass jioexClass,
                  jclass jfnotfClass,
                  jclass jbbClass)
{
    errClass = init_global(env, jerrClass);
    ioexClass = init_global(env, jioexClass);
    fnotfClass = init_global(env, jfnotfClass);

    if ((clearMid = (*env)->GetMethodID(env, jbbClass, "clear", "()Ljava/nio/ByteBuffer;")) == NULL) { return; }
    if ((getPositionMid = (*env)->GetMethodID(env, jbbClass, "position", "()I")) == NULL) { return; }
    if ((getLimitMid = (*env)->GetMethodID(env, jbbClass, "limit", "()I")) == NULL) { return; }
    if ((setPositionMid = (*env)->GetMethodID(env, jbbClass, "position", "(I)Ljava/nio/ByteBuffer;")) == NULL) { return; }
    if ((setLimitMid = (*env)->GetMethodID(env, jbbClass, "limit", "(I)Ljava/nio/ByteBuffer;")) == NULL) { return; }
}

Since Java allows different methods to have the same name and differ only in parameter lists, GetMethodID has, in addition to the method name, a signature parameter, which specifies the parameter and return types as a string. You can figure out what a signature string should be from the specification, but I find it easier to use javap. The following commands are examples that fish out signatures we need from the ByteBuffer and Buffer classes.

javap -s java.nio.ByteBuffer | grep -A 1 'ByteBuffer clear'
javap -s java.nio.Buffer | grep -A 1 'int position'

If you get the name or signature wrong, GetMethodID throws NoSuchMethodError and returns NULL, which we check for in the code.

Library.c has a section with byte buffer convenience functions which make use of the method IDs looked up in init: one for creating a byte buffer by wrapping a pointer to a piece of memory, one to extract the underlying memory pointer from a direct byte buffer along with its current position and limit values, one to clear a byte buffer, one to set its position, and one to set its limit:

// Wraps an array in a byte buffer.
jobject byteBufferWrap(JNIEnv *env, void *p, jlong capacity) {
    jobject bb = (*env)->NewDirectByteBuffer(env, p, capacity);
    if (bb == NULL) {
        raiseError(env, "Failed to wrap array as byte buffer");
        return NULL;
    }
    return bb;
}

// Gets byte array of byte buffer, and optionally position and limit.
void *byteBufferArray(JNIEnv *env, jobject bb, int *pos, int *lim) {
    void *p = (*env)->GetDirectBufferAddress(env, bb);
    if (p == NULL) { raiseIOException(env, "failed to access byte buffer array"); return NULL; }
    if (pos != NULL) {
        *pos = (*env)->CallIntMethod(env, bb, getPositionMid);
        if ((*env)->ExceptionCheck(env)) { return NULL; }
    }
    if (lim != NULL) {
        *lim = (*env)->CallIntMethod(env, bb, getLimitMid);
        if ((*env)->ExceptionCheck(env)) { return NULL; }
    }
    return p;
}

// Calls clear on byte buffer, returns true if successful.
bool byteBufferClear(JNIEnv *env, jobject bb) {
    (*env)->CallObjectMethod(env, bb, clearMid);
    return !(*env)->ExceptionCheck(env);
}

// Sets position of byte buffer, returns true if successful.
bool byteBufferPosition(JNIEnv *env, jobject bb, jint pos) {
    (*env)->CallObjectMethod(env, bb, setPositionMid, pos);
    return !(*env)->ExceptionCheck(env);
}

// Sets limit of byte buffer, returns true if successful.
bool byteBufferLimit(JNIEnv *env, jobject bb, jint lim) {
    (*env)->CallObjectMethod(env, bb, setLimitMid, lim);
    return !(*env)->ExceptionCheck(env);
}

We put declarations of these functions, along with the raise* methods from the previous part in readfile/src/jni/Library.h:

#ifndef LIBRARY_H
#define LIBRARY_H

#include <jni.h>
#include <stdbool.h>

// ------------------------------------------------------------------------------
// Functions that throw unless already in thrown state, returning message.

void raiseThrowable(JNIEnv *env, const jclass throwClass, const char *message);
void raiseError(JNIEnv *env, const char *message);
void raiseIOException(JNIEnv *env, const char *message);
void raiseFileNotFound(JNIEnv *env, const char *message);

// ------------------------------------------------------------------------------
// Global byte buffer convenience functions.

jobject byteBufferWrap(JNIEnv *env, void *p, jlong capacity);
void *byteBufferArray(JNIEnv *env, jobject bb, int *pos, int *lim);
bool byteBufferClear(JNIEnv *env, jobject bb);
bool byteBufferPosition(JNIEnv *env, jobject bb, jint pos);
bool byteBufferLimit(JNIEnv *env, jobject bb, jint lim);

#endif

Test program

Now let’s try an actual example: a Read class with a main method that takes a file name as a command line parameter, uses a native method to read up to a thousand bytes from the file, and prints the bytes one at a time under the assumption that they represent text characters.

Obviously, there is no actual reason to use a native method for reading a file, the Java standard library already has several choices for that! What you would typically need JNI code for is to access some non-standard library you installed, or hardware that Java doesn’t support by default. But this example just does standard file reading in C, so that you can see an example of how to get data and compile it without anything extra installed. (The next tutorial part shows an example that does something that you can’t do with the standard Java library.)

We can choose either to allocate the direct byte buffer in Java and pass it to the native method, or to allocate it in the native method and return it to Java. Which is more convenient depends on the context, and in this case it doesn’t really matter. Let’s try the former option first.

Allocating the buffer in Java code

Here is readfile/src/java/net/avadeaux/readfile/Read.java:

package net.avadeaux.readfile;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class Read {
    static { Library.init(); }

    private static native void read(byte[] fileName, ByteBuffer buffer);

    public static void main(String[] args) {
        String fileName = args[0];
        ByteBuffer buf = ByteBuffer.allocateDirect(1000);
        byte[] fn8 = fileName.getBytes(StandardCharsets.UTF_8);
        read(Arrays.copyOf(fn8, fn8.length+1), buf);
        buf.flip();
        System.out.print("Contents of "+fileName+": ");
        while (buf.hasRemaining()) {
            System.out.print((char) buf.get());
        }
        System.out.println();
    }
}

Java uses a peculiar UTF-16 string representation, which needs to be converted to UTF-8 to get the kind of strings normally used in C. It’s easiest to do this on the Java side with getBytes, and pass the file name as a byte[] parameter to the native read function. To make sure that the file name string is null-terminated like they should be in C, we use Arrays.copyOf to make an array one byte larger than the UTF-8 array.

The ByteBuffer parameter for the read method to fill with data from the file must be a direct byte buffer, so we allocate it with allocateDirect. When read returns, the position of the byte buffer is at the end of what was read, and we flip the buffer to make the part containing the data the remaining part. Then we use ByteBuffer.get to read one byte at a time and print it, until there is nothing more remaining.

The implementation of Read.read goes in readfile/src/jni/Read.c:

#include <errno.h>
#include <string.h>
#include "Library.h"
#include "net_avadeaux_readfile_Read.h"

#define METHOD(name) JNICALL Java_net_avadeaux_readfile_Read_ ## name

void METHOD(read)(JNIEnv *env, jclass thisClass, jbyteArray jfnam, jobject jbuf) {
    int pos, lim;
    char *p = byteBufferArray(env, jbuf, &pos, &lim);
    if (p == NULL) { return; }

    jbyte *fnam = (*env)->GetByteArrayElements(env, jfnam, NULL);
    if (fnam == NULL) { raiseError(env, "failed to access byte[] array"); return; }
    FILE *f = fopen((char *) fnam, "r");
    if (f == NULL) { raiseFileNotFound(env, (char *) fnam); }
    (*env)->ReleaseByteArrayElements(env, jfnam, fnam, JNI_ABORT);
    if (f == NULL) { return; }

    size_t n = fread(p+pos, 1, lim-pos, f);
    if (ferror(f)) { raiseIOException(env, strerror(errno)); return; }
    if (!byteBufferPosition(env, jbuf, pos+n)) { return; }

    if (fclose(f)) { raiseIOException(env, strerror(errno)); }
}

Note that, as explained above, when we get a pointer to the string passed as a byte array using GetByteArrayElements, what we get is a copy, not the actual JVM representation of the array. (Although the JNI specification allows exposing the internal representation and setting *isCopy false, this never actually happens as far as I know.) After use, the copy is deallocated with ReleaseByteArrayElements. The overhead for copying the small file name string is negligible.

We open, read, and close the file using stdio.h functions, carefully checking for errors and using our raise* functions to throw exceptions if something bad happens.

To compile and run this program, readfile/Makefile has three lines changed from the version in the Hello project:

# Name for executable script, JAR, and dynamic library
PROG_NAME = readfile

# The class that contains a main method to be run by the executable script
MAIN_CLASS = net.avadeaux.readfile.Read

# Classes that contain native methods, separated by space
JNI_CLASSES = net.avadeaux.readfile.Read net.avadeaux.readfile.Read2 net.avadeaux.readfile.Library

(Read2 in JNI_CLASSES isn’t actually used, it just contains some example code for below, but we include it to compile everything.)

The rest of the Makefile stays exactly the same. With readfile as your current working directory, the following is all it takes to compile and run the file reading example:

make
./target/read some_text_file.txt

External or internal data processing

Our example moved data from the external memory underlying the direct byte buffer into the realm of the JVM one byte at a time with ByteBuffer.get(). We could also have copied the buffer to an internal array using a single call to ByteBuffer.get(byte[]). But another alternative is to never actually get the data into the JVM, but just keep it in external memory wrapped in a direct byte buffer.

For instance, if we had used NIO to write the data to another file with FileChannel.write(ByteBuffer), our direct byte buffer would have been passed to a (standard) native method that does the write analogously to how we did the read, using standard platform I/O with external memory buffers. This is why NIO with direct byte buffers can be more efficient than, say, passing data around in Java byte arrays. Of course, this only works if all you do with the data is external. To process or analyze the data in Java code, they have to be copied into JVM memory.

Creating the buffer in C code

To demonstrate the other option for creating a direct byte buffer, wrapping a piece of memory in C, let’s change the native read method to return a ByteBuffer instead of accepting it as a parameter:

private static native ByteBuffer read(byte[] fileName);

This version of read creates a byte buffer, sets its limit to the end of the data it reads from the file (so we don’t have to flip it), and returns it. The main method is changed like this (you can find this code in the zip file in readfile/src/java/net/avadeaux/readfile/Read2.java):

public static void main(String[] args) {
    String fileName = args[0];
    byte[] fn8 = fileName.getBytes(StandardCharsets.UTF_8);
    ByteBuffer buf = read(Arrays.copyOf(fn8, fn8.length+1));
    System.out.print("Contents of "+fileName+": ");
    while (buf.hasRemaining()) {
        System.out.print((char) buf.get());
    }
    System.out.println();
}

The C implementation part in readfile/src/jni/Read2.c looks like this:

#include <errno.h>
#include <string.h>
#include "Library.h"
#include "net_avadeaux_readfile_Read2.h"

#define METHOD(name) JNICALL Java_net_avadeaux_readfile_Read2_ ## name

static char buf[1000];

jobject METHOD(read)(JNIEnv *env, jclass thisClass, jbyteArray jfnam) {
    jobject bb = byteBufferWrap(env, buf, sizeof buf);
    if (bb == NULL) { return NULL; }

    jbyte *fnam = (*env)->GetByteArrayElements(env, jfnam, NULL);
    if (fnam == NULL) { raiseError(env, "failed to access byte[] array"); return NULL; }
    FILE *f = fopen((char *) fnam, "r");
    if (f == NULL) { raiseFileNotFound(env, (char *) fnam); }
    (*env)->ReleaseByteArrayElements(env, jfnam, fnam, JNI_ABORT);
    if (f == NULL) { return NULL; }

    size_t n = fread(buf, 1, sizeof buf, f);
    if (ferror(f)) { raiseIOException(env, strerror(errno)); return NULL; }
    if (!byteBufferLimit(env, bb, n)) { return NULL; }

    if (fclose(f)) { raiseIOException(env, strerror(errno)); }
    return bb;
}

As you can see, a static global array buf is used for the actual memory buffer here. An alternative would be to malloc some memory, but if METHOD(read) had done that, without any free to release it anywhere, it would have been a memory leak. The JVM doesn’t know where the byte array comes from, and won’t do anything with it when it garbage collects the ByteArray object.

There isn’t any natural place for a free call in our example, but there can be in other contexts, for instance if you retain a pointer to the byte array on the C side. Perhaps you even keep a static global jobject pointer to the byte buffer you create, to be able to reuse the same ByteBuffer object. But if you do that, don’t forget to use NewGlobalRef to make your reference global! Otherwise, the JVM doesn’t know that you keep referencing the object, and can deallocate or move it during garbage collection.

In the next part

Our native methods so far have operated on values or pointers to values that were either passed as parameters or retained as static global variables on the C side. Methods that invoke JNI have all been declared static in Java, not invoked for any specific object. In the next part, we will look at implementing a JNI module that manages an external resource and its state, associated with this object.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.