Search code examples
javac++java-native-interfaceshared-librariesjna

Returning `const char*` from native code and getting `String` in java


I'm using JNA to interface my C++ code with java. I have a native function which takes a string as input and returns a string as output. Following is the C++ implementation of the function.

const char* decrypt(char* value){
    std::string res = TripleDes::getInstance().decrypt(value);
    std::cout<<res.c_str()<<"\n";
    return res.c_str();
}

I'm loading this function in a simple java program using JNA and trying to get a string from java. The problem is, I'm getting an empty string from java. Following is the java code:

interface NativeExample extends Library {
    NativeExample ne = (NativeExample) Native.loadLibrary("foo", NativeExample.class);
    String decrypt(String value);
}

public class Main{

        public static void main(String[] args){

                String decrypted =  NativeExample.ne.decrypt("foo");
                System.out.println(decrypted);

        }
}

The printed values from the C++ code are perfect but from Java, an empty string is printed. I've seen this question but it gives a solution for JNI. I want to use JNA and return a string. How should I go about this?

I also tried to return JNA Pointer type and called getString() method on it. But prints gibberish which is not same across all invocations.

I do understand that I'm returning a dangling pointer in function scope which would get destroyed by the time it reaches java invocation. I want a simple solution to which I can return a String from C++ code to Java using JNA.

It is mentioned in the JNA documentation here that you should use String in java for the corresponding const char* in C/C++.


Solution

  • Caninonos' answer explains the problem sufficiently. Here's two different solutions.

    A) Dynamically allocate a string and provide a function for freeing it

    You're going to have to free the string somehow, so do it properly. Provide a function which takes the pointer returned and frees it. Consider using AutoCloseable with try-with-resources statements.

    C++

    char* decrypt(char* value) {
        std::string res = TripleDes::getInstance().decrypt(value);
        std::cout << res.c_str() << "\n";
        return strndup(res.c_str(), res.size());
    }
    
    void free_decrypted_string(char* str) {
        free(str);
    }
    

    Java

    interface NativeExample extends Library {
        NativeExample ne = (NativeExample) Native.loadLibrary("foo", NativeExample.class);
    
        Pointer decrypt(String value);
        void free_decrypted_string(Pointer str);
    }
    
    public class Main {
        public static void main(String[] args) {
            Pointer decrypted = NativeExample.ne.decrypt("foo");
            System.out.println(decrypted.getString(0));
            NativeExample.ne.free_decrypted_string(decrypted);
        }
    }
    

    In case you choose to utilize AutoClosable, you could benefit from a custom PointerType which JNA allows you to use as an almost drop-in replacement for Pointer. However, since you're only really just getting the result, it might be better to encapsulate the JNA interface in a Java "decryptor" class which deals with the freeing. An AutoClosable would be better suited for things like file or process handles.

    interface NativeExample extends Library {
        NativeExample ne = (NativeExample) Native.loadLibrary("foo", NativeExample.class);
    
        FreeableString decrypt(String value);
        void free_decrypted_string(FreeableString str);
    
        class FreeableString extends PointerType implements AutoCloseable {
            @Override
            public void close() {
                ne.free_decrypted_string(this);
            }
            public String getString() {
                return this.getPointer().getString(0);
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            try (NativeExample.FreeableString decrypted = NativeExample.ne.decrypt("foo")) {
                System.out.println(decrypted.getString());
            }
        }
    }
    

    B) Change the decrypt function to accept an output buffer

    Instead of having to remember to release the dynamically allocated string, you could use output parameters. Ideally you'd want to use size_t instead of int, but using it is a bit awkward from JNA. If you need to work with strings longer than int max, figure out size_t.

    Since you're using Triple DES, it may apply padding so the size of your output may differ from the input's length. To get around this, the function outputs the required size if the buffer was too small.

    Notice that the function writes no null terminator, so make sure you use the returned value.

    C++

    int decrypt(const char *value, char *output, int *output_size) {
        std::string res = TripleDes::getInstance().decrypt(value);
        std::cout << res.c_str() << "\n";
    
        if (*output_size < res.size()) {
            *output_size = res.size();
            return 0;
        }
    
        size_t bytes_written = res.copy(output, *output_size);
        return (int)bytes_written;
    }
    

    Java

    interface NativeExample extends Library {
        NativeExample ne = (NativeExample) Native.loadLibrary("foo", NativeExample.class);
        int decrypt(String value, byte[] output, IntByReference outputBufferSize);
    }
    
    public class Main {
        public static void main(String[] args) {
            byte[] buffer = new byte[4096];
            IntByReference bufferSize = new IntByReference(buffer.length);
    
            int bytesWritten = NativeExample.ne.decrypt("foo", buffer, bufferSize);
            if (bytesWritten == 0 && bufferSize.getValue() > buffer.length) {
                // buffer was too small for decrypted output
                buffer = new byte[bufferSize.getValue()];
                bytesWritten = NativeExample.ne.decrypt("foo", buffer, bufferSize);
            }
    
            String decrypted = new String(buffer, 0, bytesWritten);
            System.out.println(decrypted);
        }
    }
    

    If you always know the size of your output, you can simplify the call to ignore the updated required buffer size, or drop it from the C++ function completely.