问题的提出
经常有一些C/C++库,会自定义一些在不同平台上 长度不同的native类型,例如我之前遇到的8字节的enum问题,MvPixelType枚举类型,在windows平台是4字节整型,在Linux平台意外变成了8字节长整型,这种时候JNA默认给枚举分配的4字节就不合适了,需要通过某种方式指导JNA根据平台类型读写相应长度的内存
选择哪种方式解决
目前有2种方式
替换JNA默认的TypeMapper
介绍TypeMapper
前,需要先介绍TypeConverter
。所谓TypeConverter
,就是这样一个converter,它既能将一种Java数据类型,转换成对应的native数据类型,又能将该上述的native数据类型,转换成相应的Java数据类型。
TypeConverter
接口继承了2个接口(Java类不允许多继承,但接口允许),分别是FromNativeConverter
(从native内存读取出java对象)和ToNativeConverter
(将Java对象写入native内存),其源码见下:
public interface TypeConverter extends FromNativeConverter, ToNativeConverter {}
public interface FromNativeConverter {
/** Convert the given native object into its Java representation using
* the given context.
*/
Object fromNative(Object nativeValue, FromNativeContext context);
/** Indicate the native type used by this converter. */
Class<?> nativeType();
}
public interface ToNativeConverter {
/**
* Convert a Java type to an appropriate native type. The new type
* must be one of the following classes:
* <ul>
* <li>{@link Pointer}
* <li>Boolean
* <li>Byte
* <li>Short
* <li>Character
* <li>Integer
* <li>{@link NativeLong}
* <li>Long
* <li>Float
* <li>Double
* <li>{@link Structure}
* <li>String
* <li>{@link WString}
* <li>{@link java.nio.Buffer} (unsupported in direct mode)
* <li>primitive array (unsupported in direct mode)
* </ul>
*/
Object toNative(Object value, ToNativeContext context);
/** Indicate the type expected from {@link #toNative}. */
Class<?> nativeType();
}
而TypeMapper
,就是覆盖所有native类型的TypeConverter
的集合,它内部维护着2张表,分别是FromNativeConverter
列表和ToNativeConverter
列表,维护两张表的好处是某些应用场景只有读取或只有写入,这样就能屏蔽掉无关内容,防止误操作。当然,对于既有读又有写的场景,就会将TypeConverter
对象在上述两张表里分别注册一次。
/** Provides converters for conversion to and from native types. */
public interface TypeMapper {
/** Return the {@link FromNativeConverter} appropriate for the given Java class.
* @param javaType Java class representation of the native type.
* @return Converter from the native-compatible type.
*/
FromNativeConverter getFromNativeConverter(Class<?> javaType);
/** Return the {@link ToNativeConverter} appropriate for the given Java class.
* @param javaType Java class representation of the native type.
* @return Converter to the native-compatible type.
*/
ToNativeConverter getToNativeConverter(Class<?> javaType);
}
注册、查询等具体操作见JNA默认实现的DefaultTypeMapper
public class DefaultTypeMapper implements TypeMapper {
private static class Entry {
public Class<?> type;
public Object converter;
public Entry(Class<?> type, Object converter) {
this.type = type;
this.converter = converter;
}
}
private List<Entry> toNativeConverters = new ArrayList<Entry>();
private List<Entry> fromNativeConverters = new ArrayList<Entry>();
/** Add a {@link ToNativeConverter} to define the conversion into a native
* type from arguments of the given Java type. Converters are
* checked for in the order added.
* @param cls Java class requiring conversion
* @param converter {@link ToNativeConverter} to transform an object of
* the given Java class into its native-compatible form.
*/
public void addToNativeConverter(Class<?> cls, ToNativeConverter converter) {
toNativeConverters.add(new Entry(cls, converter));
Class<?> alt = getAltClass(cls);
if (alt != null) {
toNativeConverters.add(new Entry(alt, converter));
}
}
/**
* Add a {@link FromNativeConverter} to convert a native result type into the
* given Java type. Converters are checked for in the order added.
*
* @param cls Java class for the Java representation of a native type.
* @param converter {@link FromNativeConverter} to transform a
* native-compatible type into its Java equivalent.
*/
public void addFromNativeConverter(Class<?> cls, FromNativeConverter converter) {
fromNativeConverters.add(new Entry(cls, converter));
Class<?> alt = getAltClass(cls);
if (alt != null) {
fromNativeConverters.add(new Entry(alt, converter));
}
}
/**
* Add a {@link TypeConverter} to provide bidirectional mapping between
* a native and Java type.
*
* @param cls Java class representation for a native type
* @param converter {@link TypeConverter} to translate between native and
* Java types.
*/
public void addTypeConverter(Class<?> cls, TypeConverter converter) {
addFromNativeConverter(cls, converter);
addToNativeConverter(cls, converter);
}
private Object lookupConverter(Class<?> javaClass, Collection<? extends Entry> converters) {
for (Entry entry : converters) {
if (entry.type.isAssignableFrom(javaClass)) {
return entry.converter;
}
}
return null;
}
@Override
public FromNativeConverter getFromNativeConverter(Class<?> javaType) {
return (FromNativeConverter)lookupConverter(javaType, fromNativeConverters);
}
@Override
public ToNativeConverter getToNativeConverter(Class<?> javaType) {
return (ToNativeConverter)lookupConverter(javaType, toNativeConverters);
}
}
具体的用法参见src/com/sun/jna/win32/W32APITypeMapper.java
因为每个JNA实例只能有一个TypeMapper
,所以想改变enum
的行为,只能是定义DefaultTypeMapper
的子类,然后在Library
实例化时设置子类为生效的TypeMapper
如果工程里还有其他C/C++库,而其他C/C++库在Linux平台下enum的长度又是正常的4字节,则无法使用替换TypeMapper
方式,因为没法两全其美。
更好的方式,是最小化特殊case的影响,即下面这种
让目标类型实现NativeMapped接口
一般结构体都会拆分成JNA识别的Native类型并被DefaultTypeMapper
转换,如果某个类型你不想让DefaultTypeMapper
转换,而是想自己处理,则需要让该数据类型实现NativeMapped
接口,这样JNA遇到你的自定义类型时,会将其看作Native类型,不再做进一步的拆分,而是调用你提供的converter,将其转换成native,反之亦然。
我的问题就是用这种方式解决的,不过用arm指代Linux(大家可以将os.arch
改成os.name
来判定是否为Linux),具体见代码:
import com.sun.jna.FromNativeContext;
import com.sun.jna.NativeMapped;
public class MvGvspPixelType implements NativeMapped {
private static String os_arch;
private Long value;
static {
os_arch = System.getProperty("os.arch");
}
public MvGvspPixelType() {} // must exist!
private MvGvspPixelType(Long value){
this.value = value;
}
@Override
public MvGvspPixelType fromNative(Object o, FromNativeContext fromNativeContext) {
if (os_arch.equals("arm")) {
return new MvGvspPixelType(Long.valueOf((long) o));
} else {
return new MvGvspPixelType(Integer.toUnsignedLong((int) o));
}
}
@Override
public Object toNative() {
if (os_arch.equals("arm")) {
return this.value;
} else {
return this.value.intValue();
}
}
@Override
public Class<?> nativeType() {
if (os_arch.equals("arm")) {
return Long.class;
} else {
return Integer.class;
}
}
}
注意,数据类型必须提供一个无参的、public的构造函数,否则编译会报错:
The type "com.happen23.agv.hik_camera.lib_hik.MvGvspPixelType" is not supported: Can't create an instance of class com.happen23.agv.hik_camera.lib_hik.MvGvspPixelType, requires a public no-arg constructor: java.lang.NoSuchMethodException: com.happen23.agv.hik_camera.lib_hik.MvGvspPixelType.<init>()
总结
其实TypeConverter
干的活儿非常像序列化、反序列化操作,只不过目标内存是native的而已。