Steffen
Steffen

Reputation: 2662

JNA: Change String encoding for only one external native library

We have here an JAVA application which loads and use a lots of external librarys. The default encoding of the operating system (Windows) is "windows-1252" (or "cp-1252"). But there is one external library which want all String (incoming and outgoing) in "utf-8". How can I do that? How can I change the String encoding type for only one JNA library?

Upvotes: 3

Views: 1100

Answers (4)

caprica
caprica

Reputation: 4146

The answer by Matthias is correct, but in case you are using the "direct mapping" approach with JNA, the solution is slightly different:

In the library class that binds the native methods:

public final class LibSomething {

    static {
        Native.register(
            NativeLibrary.getInstance(
                "something",
                Collections.singletonMap(Library.OPTION_STRING_ENCODING, "UTF-8")
            )
        );
    }

    public static native void doSomething();
}

Upvotes: 0

Daniel Widdis
Daniel Widdis

Reputation: 9130

Matthias Bläsing's answer is a much better solution to this specific use case. Please read it first if you only need character encoding.

My original answer is below and is more general to a wider range of applications.

An easy way to handle it is to not directly map String fields/args at all. Just send and receive byte arrays from the library, and create a helper function to do the translation between Strings and the byte arrays. As you've pointed out, you can write those bytes to an allocated Memory block and pass the pointer.

If you want a more permanent solution to do the same thing behind the scenes, you can use a TypeMapper for that particular library.

The W32APITypeMapper is a good reference, with the stringConverter variable showing you how in unicode it maps String to the wide string WString (UTF16).

Create your own UTF8TypeMapper (or similar) and use Java's character set/encoding functions to translate your strings to a sequence of UTF-8 bytes.

This is untested, but should be close to what you need. You could do a bit more abstraction to create a new UTF8String type that handles the details.

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);
    }
}

Then, add the type mapper to the options when loading the library:

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

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

Upvotes: 1

Matthias Bl&#228;sing
Matthias Bl&#228;sing

Reputation: 351

The normal JNA pattern is this:

public interface DemoLibrary extends Library {

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

    // abstract method declarations as interface to native library
}

However, Native#load is overloaded multiple times to support customizing the bindings. The relevant overload is: 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.

The relevant option here is Library.OPTION_STRING_ENCODING. That option is passed to the NativeLibrary instance loaded and will be used as the default encoding for this class.

The sample above becomes then

public interface DemoLibrary extends Library {

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

}

If you need to customize more (typemapper, calling convention), you'll need to create the option map for example in a static initializer block.

Upvotes: 4

Steffen
Steffen

Reputation: 2662

The only way I have found is to use the Function class of JNA (see at https://java-native-access.github.io/jna/5.2.0/javadoc/com/sun/jna/Function.html ) like this:

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

    Object[] args = {ip};

    fSetIp.invoke(args);
}

But I must implementing this for each function I want to call. Not sure if there is a better/easier way. If so: Please answer my question.

Upvotes: 0

Related Questions