問題的提出
經常有一些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的而已。