In this fourth part of the JNI tutorial series, we use the Java native interface to maintain an external resource with a state. Specifically, our example opens and plays an Opus audio file through the opusfile API, but the pattern is general enough to be used for any kind of resource.
Getting a handle
As you’re probably aware, the normal pattern for using an external resource (a file, a network port, a device, or whatever) is through a code module that provides the following to client code that uses the resource:
- A constructor, which allocates the resource and sets up a record to keep track of it and its internal state. The constructor returns a handle (a pointer or some other kind of reference to the record) that the client uses to reference the resource.
- Some usage operations (read, write, flash, quack, or whatever), methods/functions which the client call, each time passing the handle it got from the constructor. In object-oriented contexts, the handle is often an implicit this or self reference.
- A close operation, to let the client say that it’s done with the resource and it should be released, rendering the handle invalid.
When we do this in JNI C, for a client written in Java, we face the problem that in C, the natural kind of handle is a pointer to a memory position (the location of a struct or something), a concept that doesn’t exist in Java. We somehow have to translate a handle that can exist on the Java side into a pointer on the C side. I know of two reasonable ways.
Let a lookup key be the handle
The most conservative way of translating handles to pointers is to maintain a lookup data structure on the C side. A symbol table or dictionary or whatever you prefer to call it, implemented using an array, list, hash table, or something, which maps Java-side handle values to C-side pointers to resource records. The handle is a surrogate key in database terms, which can be just a serial number. The constructor comes up with the number (from a counter that it increments), inserts a key/value pair into its lookup data structure with the number as the key and a resource record pointer as the value, and returns the number as the handle to the Java side. With every operation, the client passes the number, which is translated to the corresponding resource pointer through the lookup data structure.
The downside is the extra code to maintain the data structure, and the overhead of a lookup with every operation, which may have to be made thread safe as well, depending on the application. There is a more efficient way…
Pass pointer as long
In C, it’s normal that a pointer value can be cast to integer type and back again without being destroyed (much to the annoyance of anyone trying to implement a garbage collector for use in C or C++). The modern way is to use intptr_t (or uintptr_t) defined in stdint.h as the integer type. Casting a void pointer to intptr_t and back again is guaranteed to produce the original pointer value. Consequently, we can cast a pointer to intptr_t and then to jlong to get a value that we can pass as a handle to Java, and cast in the reverse direction when the client passes a jlong handle. It works with the following caveats:
- Memory addresses must be at most 64 bits. (Otherwise,
intptr_tis too large to be transported as Javalong.) - Your platform must have
intptr_t, which is optional on some systems. Processor architectures exist where it’s difficult to consistently represent pointers as integers, but these are now rare exceptions.
Personally, I am more obsessed with efficiency than worried that my code would ever need to be used on some obscure platform (or on some future platform that may never exist, and if it does there’s probably time for me to rewrite the code) so I use the intptr_t approach in my JNI code, and that’s what’s shown in all examples below.
External API installation
Perhaps the most common reason (it has certainly been my most common reason) to resort to using JNI is that you want your Java program to support something that is available through an external API that you have installed or can install on your system. As an example for this post, we will use the opusfile API to decode Opus audio files. (It’s one of the most recent formats I implemented support for in Klipspringer.)
So if you want to compile and run my examples, you have to get the opusfile library installed. (Of course you can just look at the code examples instead, and adapt them to your own needs.) On many systems, the installation is simple to do with the package manager. For instance, on Debian-based Linuxes (such as Ubuntu and Raspberry Pi OS), you can just enter
sudo apt-get install libopusfile-dev
in the terminal. On macOS it’s just as easy if you use Homebrew: use the command
brew install opusfile
If none of these installation methods works on your system, you might find useful suggestions on the official Opus page.
If you compile the example code using my generic JNI project Makefile, the following is what the project specific lines should look like. You can download the zip file with example code for this tutorial series and find the complete Makefile as playopus/Makefile.
# Name for executable script, JAR, and dynamic library PROG_NAME = playopus # The class that contains a main method to be run by the executable script MAIN_CLASS = net.avadeaux.playopus.Play # Classes that contain native methods, separated by space JNI_CLASSES = net.avadeaux.playopus.Play net.avadeaux.playopus.Library # Extra include paths for libraries in this project LIB_INCLUDE = -I$(LIB_BASE)/include -I$(LIB_BASE)/include/opus # Libraries to link with and where to find them LDFLAGS = -L$(LIB_BASE)/lib -lc -lopusfile
Note that the values of LIB_INCLUDE and LDFLAGS are specifically set to compile and link with opusfile. The value of LIB_BASE is set further down in the Makefile, to the root of where Homebrew places libraries on macOS, and to /usr on Linux and other systems.
The code
Let’s take a look near the end of playopus/src/java/net/avadeaux/playopus/Play.java (available in the zip file) where it has the following native method declarations:
private static native long create(byte[] fileName) throws IOException; private native int channels(long handle); private native boolean decode(long handle, ByteBuffer buf) throws IOException; private native void destroy(long handle);
The create method is the constructor for the opusfile part of the object implemented in JNI C code. It takes a file name argument, similarly to read in the previous part, and returns a long handle. That handle is then to be passed to the other native methods to reference the opened audio file: channels to get the number of channels of the opened file, decode to get the next chunk of data into a ByteBuffer, and destroy to close the Opus file connection.
All theses methods could have been declared static, because the only part of “this” object they need is the handle passed as a parameter. But since they belong to a specific object, I find it more natural to declare them as instance methods.
Note that the compiler wouldn’t have protested if I had left the throws declarations off create and decode. When we write native methods, it’s up to us to know which methods might throw which exception and make sure they are properly declared.
The C file that implements these methods, playopus/src/jni/Play.c, starts like this:
#include <errno.h> #include <string.h> #include "opusfile.h" #include "Library.h" #include "net_avadeaux_playopus_Play.h" #define METHOD(name) JNICALL Java_net_avadeaux_playopus_Play_ ## name JNIEXPORT jlong METHOD(create) (JNIEnv *env, jclass thisClass, jbyteArray jfnam) { jbyte *fnam = (*env)->GetByteArrayElements(env, jfnam, NULL); if (fnam == NULL) { raiseError(env, "failed to access byte[] array"); return 0; } int err; OggOpusFile *of = op_open_file((char *) fnam, &err); if (of == NULL) { if (err == OP_EFAULT) { raiseFileNotFound(env, (char *) fnam); } else { raiseIOException(env, "error opening Opus file"); } } (*env)->ReleaseByteArrayElements(env, jfnam, fnam, JNI_ABORT); return (intptr_t) of; }
METHOD(create) uses GetByteArrayElements to get a char * to the file name, just like we did in METHOD(read) in the last post. It then uses op_open_file from the opusfile API opening and closing group to open the file and get an OggOpusFile pointer to the open opus stream, and returns this pointer as a jlong, after casting it to intptr_t.
METHOD(channels) gets a handle as a jlong, casts it first to intptr_t and then to OggOpusFile * to get the pointer to the opusfile stream. Then it calls op_channel_count from the opusfile API stream information group to get the number of channels:
jint METHOD(channels)(JNIEnv *env, jobject this, jlong jof) { OggOpusFile *of = (OggOpusFile *) (intptr_t) jof; return op_channel_count(of, -1); }
METHOD(decode) works with a direct byte buffer in the same way as METHOD(read) in the last post, using op_read from the opusfile API decoding group to read as many samples as there is room for in the buffer. (The division by two is because each sample takes two bytes.) It sets the position of the buffer to the end of the read data, and returns nonzero (true) unless no more bytes were left to read from the stream
Finally, METHOD(destroy) simply passes the stream pointer on to op_free from the opusfile API opening and closing group.
jboolean METHOD(decode)(JNIEnv *env, jobject this, jlong jof, jobject jbuf) { OggOpusFile *of = (OggOpusFile *) (intptr_t) jof; int pos, lim; opus_int16 *p = byteBufferArray(env, jbuf, &pos, &lim); if (p == NULL) { return JNI_FALSE; } long n = op_read(of, p+pos, (lim-pos)/2, NULL); if (n < 1) { if (n < 0) { raiseIOException(env, "error decoding Opus data"); } return JNI_FALSE; } int channels = op_channel_count(of, -1); return byteBufferPosition(env, jbuf, pos + n*2*channels) > 0; } void METHOD(destroy)(JNIEnv *env, jobject this, jlong jof) { OggOpusFile *of = (OggOpusFile *) (intptr_t) jof; op_free(of); }
Now let’s go back to playopus/src/java/net/avadeaux/playopus/Play.java and look at it from the beginning:
package net.avadeaux.playopus; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import javax.sound.sampled.*; public class Play implements Closeable { static { Library.init(); } private final static int BUF_BYTES = 5760 * 4; private final long handle; private final SourceDataLine line; private boolean closed = false;
The long handle member is the handle returned from the C side constructor function. The SourceDataLine line member is the standard Java sound API object that we will send audio output to. The boolean closed member keeps track of whether the object is still active.
The Play constructor looks like this:
public Play(String fileName) throws IOException, LineUnavailableException { byte[] fn8 = fileName.getBytes(StandardCharsets.UTF_8); handle = create(Arrays.copyOf(fn8, fn8.length+1)); boolean bigEndian = ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN); AudioFormat format = new AudioFormat(48000, 16, channels(handle), true, bigEndian); line = AudioSystem.getSourceDataLine(format); line.open(); line.start(); }
It calls create with the file name argument converted to UTF-8 just like read in the previous part. The next couple of lines set up the audio format for the output line with some standard values of sample frequency etc. (Opus files are always 48 kHz.) Note the channels(handle) call to get the number of channels for the third parameter of the AudioFormat constructor with the native method channels. Finally, line is initialized to play sound on the default audio output.
Then follows playAll, which decodes a chunk at a time from the file and sends the decoded PCM data to line. It starts by allocating a direct byte buffer of suitable size for fetching data from the Opus stream. Unfortunately, it can’t then just pass this buffer on to line, because the Java sound API doesn’t use ByteBuffer to pass data, but byte[]. Therefore, we need to allocate an internal byte[] as well to use as an intermediary. Then follows a loop where one buffer-full of PCM data at a time is read from the Opus stream, copied, and passed to the audio line, until decode returns false which means that there is no more input to be read.
private void playAll() throws IOException { ByteBuffer bb = ByteBuffer.allocateDirect(BUF_BYTES); byte[] a = new byte[BUF_BYTES]; boolean done = false; while (!done) { bb.clear(); done = !decode(handle, bb); bb.flip(); int n = bb.remaining(); bb.get(a, 0, n); line.write(a, 0, n); } }
The final parts of the class, with the close and main methods, probably don’t need any further explanation:
public void close() throws IOException { if (!closed) { closed = true; line.close(); destroy(handle); } } public static void main(String[] args) throws Exception { Play play = new Play(args[0]); play.playAll(); play.close(); } private static native long create(byte[] fileName) throws IOException; private native int channels(long handle); private native boolean decode(long handle, ByteBuffer buf) throws IOException; private native void destroy(long handle); }
The Library class is exactly the same as in the previous part except for the package and loaded library file name, so I won’t repeat that. When you have everything in the zip file in place, and playopus is your current working directory, you can compile and run with:
make ./target/playopus some_audio.opus
You’ll have to supply the .opus file yourself. If you don’t have any, you can create some with opusenc in Opus Tools.
That’s all, folks?
That takes us to the end of the planned part of the JNI tutorial series. But who knows, maybe I’ll add a post or two with more examples.