从本机代码返回 `const char*` 并在 java 中获取 `String`

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

我正在使用 JNA 将我的 C++ 代码与 java 连接起来。我有一个本机函数,它接受一个字符串作为输入,returns 一个字符串作为输出。 以下是该函数的 C++ 实现。

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

我正在使用 JNA 在一个简单的 java 程序中加载此函数并尝试从 java 获取字符串。问题是,我从 java 得到一个空字符串。以下是 java 代码:

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

        }
}

C++ 代码的打印值是完美的,但是 Java 打印了一个空字符串。我看过这个 但它为 JNI 提供了一个解决方案。我想使用 JNA 和 return 一个字符串。我该怎么办?

我还尝试了 return JNA Pointer 类型并在其上调用了 getString() 方法。但是打印的乱码在所有调用中都不相同。

我明白我在函数作用域中 return 一个悬挂指针,它会在到达 java 调用时被销毁。我想要一个简单的解决方案,我可以使用 JNA return 从 C++ 代码到 Java 的字符串。

在JNA文档here中提到,你应该在java中使用String来对应C/C++中的const char*

如评论中所述,问题源于悬挂指针。换句话说,一旦 C++ 函数 decrypt returns 指向您的字符串的指针,该字符串在 Java 有机会访问它之前被释放,导致从 Java边。

解决问题最直接的方法就是延长该字符串的生命周期。例如,通过使用 strdup.

创建一个动态分配的副本

也就是说,虽然仅用 return strdup(res.c_str()); 替换 return res.c_str(); 似乎可行,但它会遇到另一个问题:它会泄漏内存。事实上,动态分配的内存必须在 C++ 中显式和手动释放,但如果 Java 端什么都不做,该字符串将保留在内存中,直到您的程序终止。因此,一旦 Java 端处理完这个字符串,它需要通知 C++ 端它不再需要并且可以安全地释放(在 strdup 的情况下 free 作为the documentation 中提到的相同逻辑将适用于 newdelete)。

为此,您需要将 C++ 的调用结果存储在 Java Pointer 对象中,该对象将正确存储后续调用所需的 C++ char*free(从 Java 端到 C++ 端)。

Caninonos的回答充分说明了问题。这里有两种不同的解决方案。

A) 动态分配字符串并提供释放它的函数

您将不得不以某种方式释放字符串,因此请正确执行。提供一个函数,它接受返回的指针并释放它。考虑使用 AutoCloseable with try-with-resources 语句。

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

如果您选择使用 AutoClosable,您可以从自定义 PointerType 中受益,JNA 允许您将其用作 Pointer 的几乎直接替代品。但是,由于您实际上只是得到结果,因此最好将 JNA 接口封装在处理释放的 Java "decryptor" class 中。 AutoClosable 更适合文件或进程句柄之类的东西。

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) 更改 decrypt 函数以接受输出缓冲区

不必记住释放动态分配的字符串,您可以使用输出参数。理想情况下,您希望使用 size_t 而不是 int,但使用它在 JNA 中有点笨拙。如果您需要使用比 int max 更长的字符串,请找出 size_t.

由于您使用的是三重 DES,它可能会应用填充,因此输出的大小可能与输入的长度不同。为了解决这个问题,如果缓冲区太小,函数会输出所需的大小。

请注意,该函数写入 无空终止符,因此请确保使用返回值。

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

如果您始终知道输出的大小,则可以简化调用以忽略更新后的所需缓冲区大小,或者将其从 C++ 函数中完全删除。

此问题的另一种解决方案是使用回调函数:

C++:

typedef void (*callback_t) (const char*);

extern "C" {

void decrypt(char* value, callback_t output_func){
    std::string res = TripleDes::getInstance().decrypt(value);
    std::cout<<res.c_str()<<"\n";
    output_func(res.c_str());
}

}

Java:

private interface MyCallback extends Callback {
    public void callback(String val);
}

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){
                StringBuilder decryptedBuilder = new StringBuilder();
                String decrypted =  NativeExample.ne.decrypt("foo", decryptedBuilder::append);
                System.out.println(decryptedBuilder.toString());

        }
}

这可能不是最优雅的解决方案,但一个关键优势是您在处理完数据后不必处理内存重新分配。

为了更有趣,还可以将回调函数包装在 std::ostream 中。这在 C++ 代码已经使用 << 运算符时特别有用:

C++:

#include <iostream>
#include <sstream>

typedef void (*callback_t) (const char*);

class callback_buffer : public std::stringbuf
{
    private:
        callback_t* func;
    public:
        callback_buffer(callback_t* func): func(func){}

        ~callback_buffer()
        {
            sync();
        }

        int sync()
        {
            (*func)(str().c_str());
            str("");
            return 0;
        }
};

extern "C" {

void decrypt(char* value, callback_t output_func){
    std::string res = TripleDes::getInstance().decrypt(value);
    std::cout<<res.c_str()<<"\n";
    callback_buffer buf(&func);
    std::ostream stream(&buf);
    stream << res;
}

}