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

时间:2019-03-15 14:18:22

标签: java jna

我需要将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),指向数组的指针字段应为Structure.ByReference字段。

在其他帖子中,填充此字段的标准方法是:

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

  2. 使用Structure::toArray

  3. 从字段中分配结构数组
  4. 填充数组

所以:

// 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-上面的方法对于摆弄大小的结构很好,但是我要处理的一些结构具有大量的字段,子结构等。我假设我可以在该结构上构建一系列JNA结构。 Java端并将它们直接设置到父结构中,但是toArray方法意味着我必须然后将所有内容复制到生成的数组吗?是否有更好/更简单的方法,这意味着我不必创建和复制本来就已经在Java端已经拥有的数据?

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

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

请注意,以上内容是传递给本机层,我不是要检索任何内容。

编辑1:只是为了说明这个问题的意思-尽管上面的方法起作用,但除最琐碎的情况外,其他所有示例的代码都有些重复。我正在努力找出最简单/最好的方法来构建要传递给本机方法的复杂结构图。似乎缺少示例或教程,或者也许我不是在问正确的问题(?)指向示例,教程或传递包含其他结构的指针的结构的示例代码的任何指针都是非常感谢。

编辑2:因此,当我调用本机库时,我尝试了多种方法,所有这些方法均导致Illegal memory access错误。

我要发送的数据是由应用程序构建的-它可以是构建器模式,用户的选择等等。无论如何,结果是我随后需要发送的VkAttachmentDescription列表作为“父项” VkRenderPassCreateInfo中指向结构的指针字段。

在Java端使用JNA VkAttachmentStructure的原因是,其中一些结构包含大量字段。即调用Structure::toArray然后逐字段填充结果数组是站不住脚的:代码量巨大,容易出错且更改起来很脆弱(例如忘记复制新字段)。我可以创建另一个类来抽象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创建了一个结构数组,这些结构必须逐场填充,但是我已经已经有了结构数组填充了所有内容-如何将指向结构的字段设置为该数组(即等效于StringArray帮助器)?在我脑海中似乎做起来很简单,但是我根本找不到任何如何做自己想做的事的例子(除了琐碎的逐场复制的例子)。

令我困扰的另一件事是,父结构字段必须为ByReference,这意味着代码中的所有其他结构都必须被引用?再次感觉就像我做错了。

2 个答案:

答案 0 :(得分:1)

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

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

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

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

编辑后添加:如您在其他答案中所指出的那样,如果您仅使用Java方面的VkAttachmentDescription结构,而不将其传递给C函数,则本机内存可能尚未写入。以下基于Pointer.get*()方法的解决方案直接从C内存读取,因此它们需要在某个时候进行write()调用。

假设除了从List<VkAttachmentDescription>开始别无选择,您要做的第一件事就是分配C所需的连续内存。让我们获取所需的字节大小:

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

我们需要分配size * bytes的内存。

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

Memory mem = new Memory(size * bytes);

如果我们这样定义引用,我们可以直接将mem用作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);
}

(性能折衷:数组实例化开销与Java <-> C调用的关系。)

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

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

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

就个人而言,由于我们只是在复制已存在的内容,因此我更喜欢这种基于Memory的映射。但是...有些编码员是受虐狂。

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

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

然后您可以这样设置:

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

在这种情况下,将值(从列表(由单独分配的内存块支持的结构)复制)复制到(连续内存的)数组会比较复杂,因为内存映射的类型更窄,但是遵循与Memory映射相同的模式。您发现的一种方法是手动复制结构的每个元素! (糟糕)另一种可能使您免受某些复制/粘贴错误的影响的方法是使用Reflection(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()所示的那样,您只需要在Structure字段中使用它即可(您可以将其定义为Pointer并完全消除需要ByReference ...,但是如果要这样做,为什么不直接复制到Memory缓冲区呢?我在这里打败一匹老马!)。

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

答案 1 :(得分:1)

这是执行上述结构复制方法的静态助手:

    /**
     * Allocates a contiguous memory block for the given JNA structure array.
     * @param structures Structures array
     * @return Contiguous memory block or <tt>null</tt> for an empty list
     * @param <T> Structure type
     */
    public static <T extends Structure> Memory allocate(T[] structures) {
        // Check for empty case
        if(structures.length == 0) {
            return null;
        }

        // Allocate contiguous memory block
        final int size = structures[0].size();
        final Memory mem = new Memory(structures.length * size);

        // Copy structures
        for(int n = 0; n < structures.length; ++n) {
            structures[n].write(); // TODO - what is this actually doing? following line returns zeros unless write() is invoked
            final byte[] bytes = structures[n].getPointer().getByteArray(0, size);
            mem.write(n * size, bytes, 0, bytes.length);
        }

        return mem;
    }

该帮助程序可用于填充指向结构的指针字段,例如:

info.pAttachments = StructureHelper.allocate(attachments.toArray(VkAttachmentDescription[]::new));
info.attachmentCount = attachments.size();

似乎可以工作,但是我担心复制循环中似乎需要使用write。没有这个,从结构中提取的byte[]为零。 write实际在做什么?该文档说复制到本机内存,但是我无法完全了解实际代码的作用。

以后我应该释放这个记忆吗?

有没有其他方法可以获取结构内存?