JNA:仅更改一个外部本机库的字符串编码

JNA: Change String encoding for only one external native library

我们这里有一个 JAVA 应用程序,它加载和使用大量外部库。操作系统(Windows)的默认编码是“windows-1252”(或“cp-1252”)。但是有一个外部库需要“utf-8”中的所有字符串(传入和传出)。我怎样才能做到这一点?如何仅更改一个 JNA 库的字符串编码类型?

是针对此特定用例的更好解决方案。如果只需要字符编码请先阅读

我的原始答案在下面,并且对更广泛的应用程序更通用。

一个简单的处理方法是根本不直接映射 String fields/args。只需从库中发送和接收字节数组,并创建一个辅助函数来完成字符串和字节数组之间的转换。正如您所指出的,您可以将这些字节写入分配的 Memory 块并传递指针。

如果你想要一个更永久的解决方案来在幕后做同样的事情,你可以为那个特定的库使用 TypeMapper

W32APITypeMapper 是一个很好的参考,其中 stringConverter 变量向您展示了它如何在 unicode 中将 String 映射到宽字符串 WString (UTF16)。

创建您自己的 UTF8TypeMapper(或类似的)并使用 Java 的字符 set/encoding 函数将您的字符串转换为 UTF-8 字节序列。

这是未经测试的,但应该接近您的需要。您可以做更多的抽象来创建一个新的 UTF8String 类型来处理细节。

public class UTF8TypeMapper extends DefaultTypeMapper {

    public UTF8TypeMapper() {
        TypeConverter stringConverter = new TypeConverter() {
            @Override
            public Object toNative(Object value, ToNativeContext context) {
                if (value == null)
                    return null;

                String str = (String) value;
                byte[] bytes = str.getBytes(StandardCharsets.UTF_8);

                // Allocate an extra byte for null terminator
                Memory m = new Memory(bytes.length + 1);
                // write the string's bytes
                m.write(0, bytes, 0, bytes.length);
                // write the terminating null
                m.setByte((long) bytes.length, (byte) 0); 
                return m;
            }

            @Override
            public Object fromNative(Object value, FromNativeContext context) {
                if (value == null)
                    return null;
                Pointer p = (Pointer) value;
                // handles the null terminator
                return p.getString(0, StandardCharsets.UTF_8.name());
            }

            @Override
            public Class<?> nativeType() {
                return Pointer.class;
            }
        };
        addTypeConverter(String.class, stringConverter);
    }
}

然后,在加载库时将类型映射器添加到选项中:

private static final Map<String, ?> UTF8_OPTIONS =
        Collections.singletonMap(Library.OPTION_TYPE_MAPPER, new UTF8TypeMapper());

TheUTF8Lib INSTANCE = Native.load("TheUTF8Lib", TheUTF8Lib.class, UTF8_OPTIONS);

我发现的唯一方法是像这样使用 JNA 的函数 class(参见 https://java-native-access.github.io/jna/5.2.0/javadoc/com/sun/jna/Function.html):

public void setIp(String ip) {
    Function fSetIp = Function.getFunction("myLib", "setIp", Function.C_CONVENTION, "utf-8");

    Object[] args = {ip};

    fSetIp.invoke(args);
}

但是我必须为我想调用的每个函数实现它。不确定是否有 better/easier 方式。如果是:请回答我的问题。

正常的 JNA 模式是这样的:

public interface DemoLibrary extends Library {

    DemoLibrary INSTANCE = Native.load("demoLibrary", DemoLibrary.class);

    // abstract method declarations as interface to native library
}

但是,Native#load 被多次重载以支持自定义绑定。相关的重载是:Native#load(String, Class, Map<String,?>). The third argument can be used to pass options to the native library loader. The options can be found in the com.sun.jna.Library Interface.

这里的相关选项是Library.OPTION_STRING_ENCODING。该选项将传递给加载的 NativeLibrary 实例,并将用作此 class.

的默认编码

上面的例子就变成了

public interface DemoLibrary extends Library {

    DemoLibrary INSTANCE = Native.load("demoLibrary", DemoLibrary.class,
        Collections.singletonMap(Library.OPTION_STRING_ENCODING, "UTF-8"));

}

如果您需要自定义更多(类型映射器、调用约定),则需要在静态初始化程序块中创建选项映射。