我们正在构建一个在多种上下文中使用的 C++ 库,Java/Kotlin 就是其中之一。我们非常广泛地使用 JNI,但最近我们认为它周围有很多样板文件,并且认为 JNA 可能是一个更好的主意。由本机代码调用 Java 回调真的很好——对象的生命周期不再是问题。在尝试实现我假设的“名义”案例时,我们遇到了瓶颈。假设我们有以下 C++ 结构将由本机函数“按值”返回并在 Java 中使用:
#include <string>
#include <vector>
struct MyCustomStruct {
int x;
std::vector<std::string> lines;
};
JNA需要C联动,不懂C++,所以我相应地调整了结构。此外,我们不能在返回结构时天真地复制指针,而是深复制字符串,因为一旦堆栈展开,我们就会松开字符串:
#include <cstdlib>
#include <string>
#include <vector>
extern "C" {
struct MyCustomStruct {
int x;
char** lines;
int linesCount;
};
MyCustomStruct getMyCustomStructInstance() {
std::vector<std::string> lines{"abc", "xyz"};
MyCustomStruct result;
result.lines = (char**)malloc(lines.size());
for(size_t i = 0; i < lines.size(); i++) {
result.lines[i] = strdup(s.c_str());
}
result.x = 123;
result.linesCount = lines.size();
return result;
}
}
以及对应的Java代码:
package my.native.package;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Structure;
import java.util.List;
import java.util.Arrays;
import com.sun.jna.ptr.PointerByReference;
public interface MyNativeCLibrary extends Library {
public MyCustomStruct getMyCustomStructInstance();
public static class MyCustomStruct extends Structure {
public static class ByValue extends MyCustomStruct implements Structure.ByValue {}
public int x;
public PointerByReference lines;
public int linesCount;
public String[] linesAsArray() {
return lines.getPointer().getStringArray(0);
}
@Override
protected List getFieldOrder() {
return Arrays.asList(new String[] { "x", "lines", "linesCount" });
}
}
}
现在,在调用方,我需要存储该字符串集合(如:它是在整个 Java 应用程序的生命周期内保存在内存中的静态变量),因此深度复制它也是明智的。复制这个
PointerByReference
听起来很糟糕,但对于习惯 C++ 和处理原始字节的人来说是可行的。我将很难向我的客户解释用法,但是,好吧,我远程想象它。
现在,直截了当:我们已经通过
strdup
分配了一些内存,现在清理它是有意义的(考虑到我们已经在 Java 端获得了深度复制)。按照 eshayne 的示例,我添加了清理方法。最后,用法如下所示:
MyCustomStruct foo = myLibraryHandle.getMyCustomStructInstance();
Application.foo = deepCopyMyCustomStruct(foo);
myLibraryHandle.cleanUpMyCustomStruct(foo);
不过,这看起来很可怕。尽管我们在 JNA 类之上有一个 Java 包装器,它抽象了所有这些技术细节,但我无法想出一种公开此类功能的“原子”方式。
有没有更好的机制?我觉得必须有一种更自然的方式来做到这一点。
我想用某种 lambda 来包装这种“不安全”的调用:
myLibraryHandle.unsafeContext(() -> { myLibraryHandle.getMyCustomStructInstance(); }
但是如果
unsafeContext
接受任何 void 函数,则没有元方法可以知道内部调用了哪些函数。
我还尝试在本机函数之后立即调用“cleanup”(有点希望 JNA 能够深度复制...),但惨败。
如果你可以使用 strdup,这意味着你的字符串中没有零字节。
[ 1].这使得实现slightly
在两端都更加笨拙,但(1)更容易在概念上理解,(2)更高效的缓存,(3)更容易清理,因为所有的释放都可以用一次调用替换
char**
在你的
char*
[1] 你可能天真地使用两个连续的零字节来指示字符串的结尾,但这会阻止你有空字符串
size_t
NB:很明显,如果你从这里抛出,或者返回一个
free
,你就不再是一个好的C函数。我可能会为逻辑创建一个纯 C++ 函数,然后是一个纯 C 函数,它调用 C++ 函数并处理它可能返回的任何错误,用 C 语言来说,例如像这样:
char*