JNA如何在结构中填充指向结构的指针到本机库?

问题描述 投票:1回答:2

我需要将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. 填充数组

所以:

// 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方法意味着我必须将所有内容复制到生成的数组中?是否有更好/更简单的方法,这意味着我不必创建和复制我本来已经在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(数组)

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,这意味着代码中的每个其他结构都必须是引用的?再一次,感觉就像我做错了。

java jna
2个回答
1
投票

您需要解决的问题(以及Illegal memory access错误的来源)是接受您的数组的C端代码期望Pointer到连续的内存块。在C中,您只需要第一个元素的内存地址加上大小偏移量;要访问数组[1],您会找到数组[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)类型创建它,然后将指针提取到第一个元素,以便在将其分配给结构字段时创建ByReference类型:

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

然后你可以这样设置:

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

在这种情况下,将值从List(由单独分配的内存块支持的结构)复制到数组(连续内存)有点复杂,因为内存映射的类型更窄,但它遵循相同的模式对于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
投票

这是一个静态助手,用于执行上面概述的结构复制方法:

    /**
     * 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实际上在做什么?该文档称复制到本机内存但我无法对实际代码所做的事情做出头脑清醒。

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

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

© www.soinside.com 2019 - 2024. All rights reserved.