Actually as it turns out there are a lot of booleans in the various native library structures, several hundred of them in fact! It would be nice to preserve the intention of the boolean fields, rather than replacing them all with int
just because the implementation enforces that restriction. So I spent some time looking into JNA type conversion...
JNA supports mapping of custom types using a TypeMapper
passed as an additional argument to Native::load
when the native library is created. Custom type mappings are defined using the Java-to/from-native converter interface TypeConverter
.
Defining a custom boolean wrapper that maps Java boolean
to/from a C int
with 1=true and 0=false is fairly straight-forward:
public final class VulkanBoolean {
static final TypeConverter MAPPER = new TypeConverter() {
@Override
public Class<?> nativeType() {
return Integer.class;
}
@Override
public Object toNative(Object value, ToNativeContext context) {
if(value == null) {
return VulkanBoolean.FALSE.toInteger();
}
else {
final VulkanBoolean bool = (VulkanBoolean) value;
return bool.toInteger();
}
}
@Override
public Object fromNative(Object nativeValue, FromNativeContext context) {
if(nativeValue == null) {
return VulkanBoolean.FALSE;
}
else {
final int value = (int) nativeValue;
return value == 1 ? VulkanBoolean.TRUE : VulkanBoolean.FALSE;
}
}
};
public static final VulkanBoolean TRUE = VulkanBoolean(true);
public static final VulkanBoolean FALSE = VulkanBoolean(false);
private final boolean value;
private VulkanBoolean(boolean value) {
this.value = value;
}
public boolean value() {
return value;
}
public int toInteger() {
return value ? 1 : 0;
}
}
The type mapper(s) are registered thus:
final DefaultTypeMapper mapper = new DefaultTypeMapper();
mapper.addTypeConverter(VulkanBoolean.class, VulkanBoolean.MAPPER);
...
final Map<String, Object> options = new HashMap<>();
options.put(Library.OPTION_TYPE_MAPPER, mapper);
Native.load("vulkan-1", VulkanLibrary.class, options);
However this only works if the structure(s) in question is defined inside the JNA library interface - trivial if one is writing a small library with a handful of structures (which is usually the case) but a bit of a headache when you have several hundred methods and ~500 structures (that are code-generated).
Alternatively the type mapper can be specified in the structure constructor but this requires:
instrumenting every structure that needs the custom mapping(s).
every custom type has to additionally implement NativeMapped
so that JNA can determine the native size of the custom type (no idea why essentially the same information has to be specified twice).
each custom type must support a default constructor.
Neither of these are particularly pleasant options, it would nice if JNA supported global type mappings that covered both cases. Guess I need to re code-generate all the structures with the type-mapper. Sigh.
However this only works if the structure(s) in question are defined inside the JNA library interface. A simple workaround is to define a base-class structure within the library and extend all the others from that:
public interface Library {
abstract class VulkanStructure extends Structure {
protected VulkanStructure() {
super(VulkanLibrary.TYPE_MAPPER);
}
}
...
}
public class VkSwapchainCreateInfoKHR extends VulkanStructure { ... }
I have used the same mechanism to auto-magically map the ~300 code-generated enumerations to native int
that currently look like this:
public enum VkSubgroupFeatureFlag implements IntegerEnumeration {
VK_SUBGROUP_FEATURE_BASIC_BIT(1),
VK_SUBGROUP_FEATURE_VOTE_BIT(2),
...
private final int value;
private VkSubgroupFeatureFlag(int value) {
this.value = value;
}
@Override
public int value() {
return value;
}
}
Currently all structures that refer to an 'enumeration' are actually implemented as an int
. With a custom type converter for IntegerEnumeration
in place the field type can be the actual Java enumeration and JNA will handle the conversion to/from the integer value (which I currently have to by-hand). This obviously makes the structures slightly more type-safe, definitely clearer, and explicitly refers to the actual enumeration rather than an int
- nice.
i.e.
public class VkSwapchainCreateInfoKHR extends VulkanStructure {
...
public int flags;
public Pointer surface;
public int minImageCount;
// The following fields were int but are now the Java enumerations
public VkFormat imageFormat = VkFormat.VK_FORMAT_UNDEFINED;
public VkColorSpaceKHR imageColorSpace;
...
}
(recently found an example doing exactly that here).
Hopefully all this waffling helps someone trying to get their head around the vagaries of JNA.