Glk for Java: Technical notes

Design decisions

There are a number of ways that Glk can be mapped to Java. I hope that the choices I've made will allow a native Java implementation with the same classes, methods and interfaces.

Types

The most common type in Glk is an unsigned 32-bit integer. Unfortunately Java (the language) doesn't provide such a type, though the Java VM does. It has been replaced somewhat arbitrarily with int (32-bit signed integer) and long (64-bit signed integer) depending on whether I thought the full range was likely to be needed. The place where this is most likely to bite is in keycodes, which are signed integers (I would have used longs, but longs can't be case labels in switch statements). Since the "val1" and "val2" members of Event are longs, you have to cast them to int before checking for equality with Glk.keycode_*.

Java Strings are used for most string parameters to functions; any characters that don't fit into ISO-Latin1 are changed to ? marks.

Arrays and opaque objects get their own sections below.

Class structure

There are several ways to convert a flat C API into a class structure. At one extreme, you could have a single class containing every single function; at the other, an ornate hierarchy with a different class for each kind of Glk event and window.

The approach I've used is that each kind of opaque object has its own wrapper class (Window, Stream, FileRef and SoundChannel) and all these are derived from OpaqueObject.

Other structures in the system (event_t, streamresult_t) also become little classes. Functions which don't map nicely to any of the wrapper classes have all been made static members of the Glk class.

Since this library calls into a C implementation, all the C methods in Glk have to be wrapped as native methods in Java. They have all been put in the Glk class.

Opaque objects

An OpaqueObject is connected to the corresponding Glk object by its "m_dispid" member; this is a nonzero integer. The C layer stores a mapping between these IDs and Glk pointers using the dispatch registry. The dispatch registry is also used to ensure that all Glk objects, whether created by the Java layer or the underlying library, have a wrapper. If there are opaque objects of unknown type, then the wrapper objects are of class OpaqueObject itself.

If the "m_dispid" is zero, then this corresponds to a null pointer. If the Glk library destroys an object, the "m_dispid" of the matching Java object is set to zero.

If an OpaqueObject is garbage-collected, the matching Glk object is destroyed. However, it's better to destroy the Glk object explicitly (using eg Window.close() or SoundChannel.destroy() ) rather than trust to the garbage collector.

Arrays

In most functions, arrays aren't a problem. In the C interface, arrays are accompanied by a "length" parameter; in Java, this parameter becomes unnecessary as the array's built-in length is used. The array is then locked in memory before the C function, and unlocked afterwards.

However, when Glk wants to retain the array (as it does in requestLineEvent(), for example) then the retained array registry has to be used. The sequence is as follows:

  1. The array is added to the retained array registry in Java code. A nonzero integer is allocated to it, and it is locked in memory so C functions can access it. Once the array has been registered, it is illegal to access it from Java.
  2. The integer is passed to the C layer, which converts it to a C-style pointer and passes it to the Glk function.
  3. If Glk wants to retain the array, it now asks to register the array as retained. Since the array was added in step 1, all that has to be done is to set a flag on it.
  4. When the Glk function returns, if the array has not been marked as retained, it is unlocked and removed from the registry.
    If it was marked as retained, then this happens when Glk makes the correct call to say that it's finished with the array.

In the dispatch layer, this approach is used all the time whether or not Glk has the slightest intention of retaining passed arrays. If Glk doesn't retain the array, it simply gets unlocked at the end of the function call.

Rocks

In a pure Java system, the natural type for an object's rock is java.lang.Object. In this particular Java/C hybrid, we also have to try and keep compatibility with functions that expect a rock to be a 32-bit integer.

The solution used has been to store a reference to java.lang.Object in the Java wrapper. If the reference is null (as it will be for any objects created by the library rather than by the Java calling program) then the Glk rock is returned, wrapped in a Java.lang.Integer.

When an object is created by Java, and the rock can be cast to a Java.lang.Number, then its integer value is what gets passed to the C layer as a rock.

Dispatch layer

The dispatch layer has two entry points - one which tries to mimic the C dispatch layer as far as possible (using the UniversalUnion class instead of gluniversal_t) and one which takes a more Java approach to polymorphism (using java.lang.Object).

In theory, only glk_dispatch_call() and a handful of other methods need to be implemented in native code; all the other functions can simply be wrappers around Glk.dispatchCall(). The main reason I decided not to do this was simply that by the time I got round to implementing dispatchCall, I'd already written native methods to call most of the other Glk functions. There's also the question of efficiency; native methods allow me to pass integers to and fro as integers, not UniversalUnions.

Another reason to have custom wrappers rather than pushing everything through dispatchCall() is that it allows extra conversions to be done in some places. For example, if you use the dispatch layer to call glk_request_line_event(), your results will end up in a byte array and you have to keep track of the number of valid bytes separately. If you use Window.requestLineEvent(), the result will be returned to you as a java.lang.String of the correct length.

Changes to xglk

The changes made to xglk are pretty small - see xglk.diffs. The only files changed were:

Makefile
so that it produces shared libraries rather than static.
main.c
so that it calls glkunix_jni_hook() immediately on startup.
glkstart.h
to prototype glkunix_jni_hook().

The reason for adding the new glkunix_jni_hook() function is that when a program incorporates a Java VM (as jniglk does) no system calls can be made until the Java VM has been started. Therefore the Java VM must be started right away. By the time glkunix_startup_code() is called, it's too late - xglk has attempted to make system calls, and crashed.

Porting to other Glk implementations

The only things that make jniglk depend specifically on xglk are:

To port to another Glk implementation, you would need to find your own replacements for these functions. The code that would need changing is clearly marked in jniglk.c.