JNA 如何在要传递到本机库的结构中填充结构指针字段?

2024-02-20

我需要将 JNA 结构传递到包含结构指针字段(可能包含零个或多个结构)的本机层。

这是“父”结构:

public class VkRenderPassCreateInfo extends Structure {
    public int attachmentCount;
    public VkAttachmentDescription.ByReference pAttachments;
}

(为简洁起见,省略了其他字段、@FieldOrder 和 ByReference/Value 类)

这是“子”结构:

public class VkAttachmentDescription extends Structure {
    public int flags;
    // ... lots and lots of other simple fields
}

根据 JNA 文档(here http://java-native-access.github.io/jna/3.5.1/javadoc/overview-summary.html#structures)指向数组的指针字段应该是Structure.ByReference field.

从其他帖子来看,填充该字段的标准方法是:

  1. 将字段初始化为引用结构

  2. 使用以下命令从字段中分配结构体数组Structure::toArray

  3. 填充数组

So:

// Init structure fields
renderPass.pAttachments = new VkRenderPassCreateInfo.ByReference();
renderPass.attachmentCount = size;

// Allocate memory
VkAttachmentDescription[] attachments = (VkAttachmentDescription[]) renderPass.pAttachments.toArray(size);

// Populate array
for(int n = 0; n < size; ++n) {
    attachments[n].flags = ...
    // and so on for other fields
}

1 - 这是在结构中初始化和分配结构指针字段的正确方法吗?看起来有很多乱七八糟的事情吗?

2 - 上面的方法对于摆弄大小的结构来说效果很好,但是我正在处理的一些结构有大量的字段、子结构等。我以为我可以在 Java 端构建一个 JNA 结构数组,将它们直接设置到父结构中,但是toArray方法意味着我have然后将所有内容复制到生成的数组中?是否有更好/更简单的方法,这意味着我不必创建和复制我基本上已经在 J​​ava 端拥有的数据?

3 - JNA 提供StringArray处理结构中字符串数组的类似情况的帮助器类:

// Array of strings maintained on the Java side
List<String> strings = ...

// Nice and easy means of populating the JNA structure
structure.pStrings = new StringArray(strings.toArray(String[]::new));
...

// Send the data
library.fireandForget(structure);

这就是我试图用上面的结构代码实现的目标,但显然仅适用于字符串情况 - 是否还有我错过的其他类似的帮助器?

请注意,以上是passing结构to本机层,我不想检索任何内容。

EDIT 1:只是为了限定这个问题的要点 - 尽管上面的方法有效,但除了最微不足道的情况之外,它会导致大量的样板代码。我正在努力找出构建要传递给本机方法的复杂结构图的最简单/最好的方法。似乎缺少示例或教程,或者也许我只是没有提出正确的问题(?)任何指向示例、教程或传递包含指向其他结构的指针的结构的示例代码的指针都将是very赞赏。

EDIT 2:所以我尝试了多种方法,所有这些都导致Illegal memory access当我调用本机库时出错。

我想要发送的数据是由应用程序构建的 - 它可以是构建器模式、用户的选择等。在任何情况下,结果都是一个列表VkAttachmentDescription然后我需要将其作为“父级”中的结构指针字段发送VkRenderPassCreateInfo.

使用 JNA 的原因VkAttachmentStructure在Java方面,这些结构中的一些包含大量字段。即打电话Structure::toArray然后逐个字段填充结果数组是站不住脚的:代码量会很大,容易出错且难以更改(例如忘记复制新字段)。我可以创造another类来抽象 JNA 类,但这只会转移问题。

以下是代码的作用:

// Application builds the attachments
final List<VkAttachmentDescription> attachments = ...

...

// At some point we then send the render pass including the attachments

// Populate the render pass descriptor
final VkRenderPassCreateInfo info = new VkRenderPassCreateInfo();
info.pAttachments = ??? <--- what?
// ... other fields

// Send the descriptor
library.sendRenderPass(info);

尝试 1:天真地将结构体指针设置为数组:

final VkRenderPassCreateInfo info = new VkRenderPassCreateInfo();
final var array = attachments.toArray(VkAttachmentDescription.ByReference[]::new);
info.pAttachments = array[0];
library.sendRenderPass(info);

结果是内存访问错误,没想到这还管用!

尝试 2:使用 Structure::toArray(int) 并将字段设置为第一个元素

final VkAttachmentDescription.ByReference[] array = (VkAttachmentDescription.ByReference[]) new VkAttachmentDescription.ByReference().toArray(attachments.size());

for(int n = 0; n < size; ++n) {
    array[n] = attachments.get(n);
}

info.pAttachments = array[0];

library.sendRenderPass(info);

相同的结果。

尝试 3:使用 Structure::toArray(array)

还有一个替代方案toArray中的方法Structure它需要一个数组,但它似乎与调用整数版本没有任何不同?

尝试 4:逐字段复制

final VkAttachmentDescription.ByReference[] array = (VkAttachmentDescription.ByReference[]) new VkAttachmentDescription.ByReference().toArray(attachments.size());

for(int n = 0; n < size; ++n) {
    array[n].field = attachments.get(n).field;
    // ...lots of other fields
}

info.pAttachments = array[0];

library.sendRenderPass(info);

这可行但很讨厌。

我显然完全错过了关于 JNA 的一些事情。我的主要症结是Structure::toArray创建一个数组empty必须逐个字段填充的结构,但我already拥有填充了所有内容的结构数组 - 如何将指向结构的指针字段设置为该数组(即相当于StringArray帮手)?在我的脑海中,这似乎是一件简单的事情,但我根本找不到任何如何做我想做的事情的例子(除了逐个字段复制的琐碎例子)。

另一件困扰我的事情是父结构字段必须是ByReference这意味着代码中的所有其他结构都必须通过引用?再次感觉我做的这一切都是错的。


您需要解决的问题(以及问题的来源)Illegal memory access错误)是接受数组的 C 端代码期望Pointer to a 连续块的记忆。在C中,只需要第一个元素的内存地址,加上大小偏移量;要访问 array[1],您可以找到 array[0] 的内存以及结构体大小的偏移量。

在您的情况下,您已经为此块中的每个结构分配了非连续内存:

// Application builds the attachments
final List<VkAttachmentDescription> attachments = ...

Each VkAttachmentDescription映射到它自己的内存,并且尝试读取第一个结构末尾的内存会导致错误。如果您无法控制在这些情况下使用哪些内存VkAttachmentDescription实例化对象后,您最终将重复内存需求,并且必须将本机内存从非连续块复制到连续块。

编辑添加:正如您在其他答案中所指出的,如果您只使用过VkAttachmentDescriptionJava端的结构并没有将其传递给C函数,本机内存可能还没有被写入。以下解决方案基于Pointer.get*()方法直接从 C 内存读取,因此它们需要write()在某个时刻进行调用。

假设你别无选择,只能从List<VkAttachmentDescription>,你需要做的第一件事是分配C需要的连续内存。让我们得到我们需要的字节大小:

int size = attachments.size();
int bytes = attachments.get(0).size();

我们需要分配size * bytes的记忆。

这里有两个选择:使用 a 直接分配内存Memory对象(的子类Pointer)或使用Structure.toArray。对于直接分配:

Memory mem = new Memory(size * bytes);

我们可以直接使用mem as a Pointer如果我们这样定义引用:

public class VkRenderPassCreateInfo extends Structure {
    public int attachmentCount;
    public Pointer pAttachments;
}

然后就很简单了:

info.pAttachments = mem;

现在剩下的就是将字节从非连续内存复制到分配的内存中。我们可以逐字节进行(更容易查看 C 端字节级别发生的情况):

for (int n = 0; n < size; ++n) {
    Pointer p = attachments.get(n).getPointer();
    for (int b = 0; b < bytes; ++b) {
        mem.setByte(n * bytes + b, p.getByte(b));
    }
}

或者我们可以逐个结构地进行:

for (int n = 0; n < size; ++n) {
    byte[] attachment = attachments.get(n).getPointer().getByteArray(0, bytes);
    mem.write(n * bytes, attachment, 0, bytes);
}

(性能权衡:数组实例化开销与 JavaC 调用。)

现在缓冲区已写入,您可以将其发送到 C,它需要结构数组,但它不会知道其中的区别...字节就是字节!

编辑添加:我认为可以使用更改本机内存支持useMemory()然后直接写入新的(连续的)位置。这段代码未经测试,但我怀疑可能实际上有效:

for (int n = 0; n < size; ++n) {
    attachments.get(n).useMemory(mem, n * bytes);
    attachments.get(n).write();
}

就我个人而言,由于我们只是复制已经存在的东西,所以我更喜欢这个Memory基于映射。然而......有些程序员是受虐狂。

如果你想更加“类型安全”,你可以使用ByReference结构内部的类声明并使用创建结构数组toArray()。 您已在代码中列出了使用 ByReference 类型创建数组的一种方法。这可行,或者您也可以使用(默认 ByValue)类型创建它,然后在将其分配给结构字段时提取指向第一个元素的指针以创建 ByReference 类型:

VkAttachmentDescription[] array = 
    (VkAttachmentDescription[]) new VkAttachmentDescription().toArray(attachments.size());

然后你可以这样设置:

info.pAttachments = new VkAttachmentDescription.ByReference(array[0].getPointer());

在这种情况下,将值从列表(由单独分配的内存块支持的结构)复制到数组(连续内存)会更复杂一些,因为内存映射的类型更窄,但它遵循与为了Memory映射。您发现的一种方法是手动复制结构的每个元素! (呃。)另一种可以避免复制/粘贴错误的方法是使用反射(JNA 在幕后所做的事情)。这也需要大量工作,并且重复 JNA 所做的事情,因此很丑陋且容易出错。但是,仍然可以将原始本机字节从非连续内存块复制到连续内存块。 (在这种情况下......为什么不直接去Memory但我的偏见已经显现出来。)你可以像下面这样迭代字节Memory例如,像这样:

for (int n = 0; n < size; ++n) {
    Pointer p = attachments.get(n).getPointer();
    Pointer q = array[n].getPointer();
    for (int b = 0; b < bytes; ++b) {
        q.setByte(b, p.getByte(b));
    }
}

或者您可以像这样读取块中的字节:

for (int n = 0; n < size; ++n) {
    byte[] attachment = attachments.get(n).getPointer().getByteArray(0, bytes);
    array[n].getPointer().write(0, attachment, 0, bytes);
}

请注意,我还没有测试过这段代码;它写入本机端而不是Java结构,所以我认为它会按原样工作,但你可能需要一个array[n].read()在上述循环末尾调用以从 C 读取到 Java,以防存在我不知道的内置 Java 到 C 的副本。

响应您的“父结构字段必须是 ByReference”:如上所示,Pointer映射有效并允许更多的灵活性,但代价是“类型安全”,也许(或不是)“可读性”。你不需要使用ByReference正如我在其他地方所展示的toArray()您只需要它用于“结构”字段(您可以将其定义为Pointer并完全消除对ByReference...但是如果你这样做为什么不直接复制到Memory缓冲?我在这里打败了一匹死马!)。

最后,如果您知道最终将拥有多少个元素(或该数字的上限),理想的解决方案是从一开始就使用连续内存实例化数组。然后而不是创建一个新实例VkAttachmentDescription您可以从数组中获取预先存在的一个。如果你过度分配并且不使用它们也没关系,只要你从一开始就连续使用它们即可。您传递给 C 的只是结构的# 和第一个结构的地址,它并不关心您是否有额外的字节。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JNA 如何在要传递到本机库的结构中填充结构指针字段? 的相关文章

随机推荐