I Wanted Orange (The machine would not make a mistake.)

30Nov/10Off

Friendly Enums in Java with JNA

Well, it's a slow week so I think I might as well share some old code to apologize for not making new code. In jHllib, which allows Java programs to inter-operate with a certain C/C++ DLL, I found myself needing to refer to certain integer values that were defined in C. All the documentation and examples for JNA (the Java<->native bridging platform) tell you to use non-type-safe integer constants in your program, but this just didn't sit well with me.

After some experimentation, I found a way around to use Java's type-safe enums with JNA. First I'll show you what the final product looks like, and then look at the guts that make it possible.

The Function

Here's an example function call where I wanted to use type-safe enums. The original method in C code looks like this:

HLStreamType hlStreamGetType(const HLStream *pStream);

And now in the Java code:

// Slightly modified from released code to remove spurious changes
public StreamType hlStreamGetType(HlStream streamObj);

The Values

As defined in C:

typedef enum
{
        HL_STREAM_NONE = 0,
        HL_STREAM_FILE,
        HL_STREAM_GCF,
        HL_STREAM_MAPPING,
        HL_STREAM_MEMORY,
        HL_STREAM_PROC,
        HL_STREAM_NULL
} HLStreamType;

And again in Java. You might consider it bad-practice to make the enum values order-dependent, but it just follows the C version that way.

import com.technofovea.hllib.JnaEnum;
public enum StreamType implements JnaEnum<StreamType> {
        
        NONE,
        FILE,
        GCF,
        MAPPING,
        MEMORY,
        PROC,
        NULL;
        
    private static int start = 0;

    public int getIntValue() {
        return this.ordinal() + start;
    }

    public StreamType getForValue(int i) {
        for (StreamType o : this.values()) {
            if (o.getIntValue() == i) {
                return o;
            }
        }
        return null;
    }
}

As you can see, the only strange bit is this "JnaEnum" interface and some extra methods it requires. It's actually very simple:

public interface JnaEnum<T> {
    public int getIntValue();
    public T getForValue(int i);
}

We'll see what uses the interface in just a bit.

The Wiring

The secret is something JNA calls a TypeConverter. It's a mechanism you can use to inject custom behavior to how JNA converts types behind the scenes. Before we dive into the actual conversion step, I want to go over how it is wired together for context.

First, we change the initialization of JNA to include out TypeMapper. Yes, Mapper, not Converter. A TypeMapper provides access to multiple TypeConverters, one of which being our EnumConverter.

Map<String, Object> options = new HashMap<String, Object>();
options.put(Library.OPTION_TYPE_MAPPER, new HlTypeMapper());
FullLibrary instance = (FullLibrary) Native.loadLibrary("hllib", FullLibrary.class, options);

Next, we need to define the values we want to get converted and what classes are responsible for doing so.

class HlTypeMapper extends DefaultTypeMapper {

    HlTypeMapper() {
        
        // The EnumConverter is set to fire when instances of 
        // our interface, JnaEnum, are seen.
        addTypeConverter(JnaEnum.class, new EnumConverter());
        
        // Remember HlStream from the example several steps back?
        // I left this line in to show it uses a similar technique.
        addTypeConverter(HlStream.class, new StreamConverter());
    }
}

The secret sauce

This last class, the EnumConverter, is where most of the hard work happens. It takes the kind of enum that the Java code is giving/expecting, and converts values between enums and integers as appropriate.

In order to do this, it relies on individual enums themselves to do some of the logic, through the JnaEnum interface we defined earlier.

class EnumConverter implements TypeConverter {

    private static final Logger logger = LoggerFactory.getLogger(EnumConverter.class);

    public Object fromNative(Object input, FromNativeContext context) {
        Integer i = (Integer) input;
        Class targetClass = context.getTargetType();
        if (!JnaEnum.class.isAssignableFrom(targetClass)) {
            return null;
        }
        Object[] enums = targetClass.getEnumConstants();
        if (enums.length == 0) {
            logger.error("Could not convert desired enum type (), no valid values are defined.",targetClass.getName());
            return null;
        }
        // In order to avoid nasty reflective junk and to avoid needing
        // to know about every subclass of JnaEnum, we retrieve the first
        // element of the enum and make IT do the conversion for us.
        
        JnaEnum instance = (JnaEnum) enums[0];
        return instance.getForValue(i);

    }

    public Object toNative(Object input, ToNativeContext context) {
        JnaEnum j = (JnaEnum) input;
        return new Integer(j.getIntValue());
    }

    public Class nativeType() {
        return Integer.class;
    }
}

Conclusion

So that's how you get type-safe enums in Java with JNA. Note that there is a weakness in this system: If the other end ever sends back a number your enum does not expect, things will break and you'll get a null value back instead of an enum object. A constant-integer system would likely also break, but at least you'd have the actual number on-hand for error messages and things.

On the positive side, any native code withs lots of enumerated values is much easier to manage: You don't need to deal with an "integer soup" and and IDEs with auto-complete can intelligently suggest what values a function may take or return.

Comments (2) Trackbacks (0)
  1. routine stuff.

  2. I think it would be “routine” if JNA was able to naturally map integers to the correct ordinal in an enum, but it doesn’t.

    What makes this not-so-routine is that it’s a way to use a single TypeConverter. You can add a hundred more enums and as long as they implement JnaEnum you don’t need to write and wire up a hundred corresponding TypeConverters.


Trackbacks are disabled.