将一组 JNA 结构传递给本机方法

Passing a collection of JNA structures to native method

问题

我正在尝试将一组 JNA 结构传递给本机方法,但事实证明它非常繁琐:

假设我们有一个结构:

class MyStructure extends Structure {
    // fields...
}

和 JNA 接口中的一个方法:

void pass(MyStructure[] data);

哪个映射到本机方法:

void pass(const MYStructure* data);

现在的复杂性来自于应用程序正在动态构建这些结构的集合,即我们不是在处理静态数组,而是像这样的东西:

class Builder {
    private final Collection<MyStructure> list = new ArrayList<>();

    // Add some data
    public void add(MyStructure entry) {
        list.add(entry);
    }

    // Pass the data to the native library
    public void pass() {
        // TODO
    }
}

pass() 方法的简单实现可以是:

MyStructure[] array = list.toArray(MyStucture[]::new);
api.pass(array);

(其中 lib 是 JNA 库接口)。

当然这行不通,因为数组不是连续的内存块 - 很公平。

垃圾解决方案 #1

一种解决方案是从结构实例分配一个 JNA 数组并逐个字段填充它:

MYStructure[] array = (MyStructure[]) new MyStructure().toArray(size);
for(int n = 0; n < array.length; ++n) {
    array[n].field = list.get(n).field;
    // other fields...
}

这保证数组由连续的内存组成。但是我们不得不逐个字段地复制数据(我们已经在列表中填充了它)——这对于一个简单的结构来说是可以的,但是我正在处理的一些数据有几十个字段,指向进一步嵌套数组等的结构。基本上这种方法是不可行的。

垃圾解决方案 #2

另一种方法是将数据集合转换为简单的 JNA 指针,大致如下:

MyStructure[] array = list.toArray(MyStructure[]::new);
int size = array[0].size();
Memory mem = new Memory(array.length * size);
for(int n = 0; n < array.length; ++n) {
    if(array[n] != null) {
        array[n].write();
        byte[] bytes = array[n].getPointer().getByteArray(0, size);
        mem.write(n * size, bytes, 0, bytes.length);
    }
}

此解决方案是通用的,因此我们也可以将其应用于其他结构。但是我们必须将方法签名更改为 Pointer 而不是 MyStructure[],这会使代码更加迟钝,自我记录更少并且更难测试。我们也可以使用第三方库,这甚至可能不是一个选项。

(注意我刚才问过类似的问题 但没有得到满意的答案,我想我会再试一次,我会删除旧的/回答两个)。

总结

基本上我 expecting/hoping 有这样的东西:

MyStructure[] array = MyStructure.magicContiguousMemoryBlock(list.toArray());

类似于 JNA 助手 class 为字符串数组提供 StringArray 的方式:

StringArray array = new StringArray(new String[]{...});

但据我所知,不存在这样的 'magic'。有没有另一种更简单、更 'JNA' 的方法呢?必须分配我们基本上已经拥有的数据的逐字节副本似乎真的很愚蠢(而且可能是不正确的)!

我还有其他选择吗?感谢接受任何指点(双关语)。

如果您能够创建一个连续的内存块,为什么不简单地 de-serialize 将您的列表放入其中。

即类似于:

MyStructure[] array = list.get(0).toArray(list.size());
list.toArray(array);
pass(array);

无论如何,您最好不要将 Structure 存储在您的列表或任何其他集合中。最好在内部保存一个 POJO,然后直接使用 或手动将其重新映射到结构数组。

使用 MapStruct bean 映射库可能如下所示:

@Mapper
public interface FooStructMapper {
    FooStructMapper INSTANCE = Mappers.getMapper( FooStructMapper.class );
    void update(FooBean src, @MappingTarget MyStruct dst);
}

MyStrucure[] block = new MyStructure().toArray(list.size());
for(int i=0; i < block.length; i++) {
   FooStructMapper.INSTANCE.update(list.get(i), block[i]);
}

重点 - 结构构造函数使用 Memory 分配内存块,这确实是一个缓慢的操作。以及在 java 堆 space 之外分配的内存。最好尽可能避免这种分配。

作为上一个答案的作者,我意识到在实现我们主要在对您的答案的评论中讨论的更好的解决方案之前,很多困惑都是以一种方式接近它。我将尝试通过实际演示我对该答案的建议来回答这个额外的说明,我认为这是最好的方法。简单地说,如果你有一个 non-contiguous 结构并且需要一个连续的结构,你必须将连续的内存带到结构中,或者将结构复制到连续的内存中。我将在下面概述这两种方法。

Is there another, simpler and more 'JNA' way of doing it? It seems really dumb (and probably incorrect) to have to allocate a byte-by-byte copy of the data that we essentially already have!

我在另一个问题的回答中确实提到在这种情况下您可以使用 useMemory()。它是一个 protected 方法,但是如果你已经扩展了一个 Structure 你可以从 subclass (你的结构)中访问那个方法,以几乎相同的方式(并且恰恰是相同的目的)就像扩展子 class.

Pointer 构造函数一样

因此,您可以采用集合中的现有结构并将其本机后备内存更改为连续内存。这是一个工作示例:

public class Test {

    @FieldOrder({ "a", "b" })
    public static class Foo extends Structure {
        public int a;
        public int b;

        // You can either override or create a separate helper method
        @Override
        public void useMemory(Pointer m) {
            super.useMemory(m);
        }
    }

    public static void main(String[] args) {
        List<Foo> list = new ArrayList<>();
        for (int i = 1; i < 6; i += 2) {
            Foo x = new Foo();
            x.a = i;
            x.b = i + 1;
            list.add(x);
        }

        Foo[] array = (Foo[]) list.get(0).toArray(list.size());
        // Index 0 copied on toArray()
        System.out.println(array[0].toString());
        // but we still need to change backing memory for it to the copy
        list.get(0).useMemory(array[0].getPointer());
        // iterate to change backing and write the rest
        for (int i = 1; i < array.length; i++) {
            list.get(i).useMemory(array[i].getPointer());
            list.get(i).write();
            // Since sending the structure array as an argument will auto-write,
            // it's necessary to sync it here.
            array[1].read(); 
        }
        // At this point you could send the contiguous structure array to native.
        // Both list.get(n) and array[n] point to the same memory, for example:
        System.out.println(list.get(1).toString());
        System.out.println(array[1].toString());
    }

输出(注意连续分配)。后两个输出相同,来自列表或数组。

Test$Foo(allocated@0x7fb687f0d550 (8 bytes) (shared from auto-allocated@0x7fb687f0d550 (24 bytes))) {
  int a@0x0=0x0001
  int b@0x4=0x0002
}
Test$Foo(allocated@0x7fb687f0d558 (8 bytes) (shared from allocated@0x7fb687f0d558 (8 bytes) (shared from allocated@0x7fb687f0d558 (8 bytes) (shared from allocated@0x7fb687f0d550 (8 bytes) (shared from auto-allocated@0x7fb687f0d550 (24 bytes)))))) {
  int a@0x0=0x0003
  int b@0x4=0x0004
}
Test$Foo(allocated@0x7fb687f0d558 (8 bytes) (shared from allocated@0x7fb687f0d558 (8 bytes) (shared from allocated@0x7fb687f0d550 (8 bytes) (shared from auto-allocated@0x7fb687f0d550 (24 bytes))))) {
  int a@0x0=0x0003
  int b@0x4=0x0004
}

如果你不想把 useMemory 放在你的每一个结构定义中,你仍然可以把它放在一个中间 class 中,它扩展 Structure 然后扩展那个中间class 而不是 Structure.


如果您不想在您的结构定义(或它们的超class)中覆盖 useMemory(),您仍然可以在代码中“简单地”使用一点点复制内存导致效率低下。

为了“获取”该内存以将其写入别处,您必须从 Java-side 内存中读取它(通过反射,这是 JNA 将结构转换为本机内存块的方式), 或者从 Native-side 内存中读取它(这需要将它写在那里,即使你只想读取它)。 Under-the-hood,JNA 正在写入本机字节 field-by-field,全部隐藏在 API.

中的简单 write() 调用下

您的“垃圾解决方案 #2”似乎接近本例中的要求。以下是我们必须处理的约束,无论采用何种解决方案:

  • Structure的现有列表或数组中,本机内存不是连续的(除非你自己pre-allocate连续内存,并以受控方式使用该内存,或者覆盖useMemory() 如上所示),大小是可变的。
  • 采用数组参数的本机函数需要一块连续的内存。

以下是处理结构和内存的“JNA 方法”:

  • 结构具有 native-allocated 内存,指针值可通过 Structure.getPointer() 访问,大小为(至少)Structure.size().
  • 可以使用 Structure.getByteArray().
  • 批量读取结构本机内存
  • 可以使用 new Structure(Pointer p) 构造函数从指向本机内存的指针构造结构。
  • Structure.toArray() 方法创建了一个结构数组,该数组由一个大的、连续的本机内存块支持。

我认为你的解决方案 #2 是一种相当有效的方法,但你的问题表明你想要更多的类型安全性,或者至少 self-documenting 代码,在这种情况下我会指出用两个步骤修改#2 的更“JNA 方式”:

  • 将新的 Memory(array.length * size) 本机分配替换为解决方案 #1 中的 Structure.toArray() 分配。
    • 您仍然有一个 length * size 连续本机内存块和一个指向它的指针 (array[0].getPointer())。
    • 您还有指向偏移量的指针,因此您可以将 mem.write(n * size, ... ) 替换为 array[n].getPointer().write(0, ... )
  • 无法绕过内存复制,但是有两个 well-commented 行调用 getByteArray() 并立即 write() 那个字节数组对我来说似乎很清楚。
    • 您甚至可以 one-line 它... write(0, getByteArray(0, size), 0, size),尽管有人可能会争论这是否更清楚。

因此,调整您的方法 #2,我建议:

// Make your collection an array as you do, but you could just keep it in the list 
// using `size()` and `list.get(n)` rather than `length` and `array[n]`.
MyStructure[] array = list.toArray(MyStructure[]::new);

// Allocate a contiguous block of memory of the needed size
// This actually writes the native memory for index 0, 
// so you can start the below iteration from 1
MyStructure[] structureArray = (MyStructure[]) array[0].toArray(array.length);

// Iterate the contiguous memory and copy over bytes from the array/list
int size = array[0].size();
for(int n = 1; n < array.length; ++n) {
    if(array[n] != null) {
        // sync local structure to native (using reflection on fields)
        array[n].write();
        // read bytes from the non-contiguous native memory
        byte[] bytes = array[n].getPointer().getByteArray(0, size);
        // write bytes into the contiguous native memory
        structureArray[n].getPointer().write(0, bytes, 0, bytes.length);
        // sync native to local (using reflection on fields)
        structureArray[n].read();
    }
}

从“干净代码”的角度来看,我认为这相当有效地实现了您的目标。上述方法的一个“丑陋”部分是 JNA 没有提供一种简单的方法来在结构之间复制字段,而无需在此过程中将它们写入本机内存。不幸的是,这是“序列化”和“反序列化”对象的“JNA 方式”,它没有为您的用例设计任何“魔法”。字符串包括 built-in 转换为字节的方法,使这种“魔术”方法更容易。

也可以避免将结构写入本机内存只是为了再次读回它,如果您按照方法 #1 中暗示的那样进行 field-by-field 复制。但是,您可以使用 JNA 的字段访问器来更轻松地访问引擎盖下的反射。现场方法是 protected 所以你必须扩展 Structure 来做到这一点 - whih 如果您这样做,useMemory() 方法可能更好!但是您可以将此迭代从 write():

中拉出
for (StructField sf : fields().values()) {
    // do stuff with sf 
}

我最初的想法是使用上述循环遍历 non-contiguous Structure 字段,将 Field.copy() 存储在 HashMapsf.name 中作为关键。然后,对另一个(连续的)Structure 对象的字段执行相同的迭代,从 HashMap 读取并设置它们的值。

如果 确实 需要执行 JNA 结构的 byte-by-byte 副本,Daniel Widdis 提供的解决方案将解决这个 'problem'。

不过,我已经接受了其他一些发帖人表达的思维方式 - JNA 结构纯粹用于编组 to/from 本机层,不应真正用作 'data' .我们应该定义域 POJO 并根据需要将它们转换为 JNA 结构 - 需要做更多的工作,但我猜要处理。

编辑:这是我最终使用自定义流收集器实现的解决方案:

public class StructureCollector <T, R extends Structure> implements Collector<T, List<T>, R[]> {
    /**
     * Helper - Converts the given collection to a contiguous array referenced by the <b>first</b> element.
     * @param <T> Data type
     * @param <R> Resultant JNA structure type
     * @param data          Data
     * @param identity      Identity constructor
     * @param populate      Population function
     * @return <b>First</b> element of the array
     */
    public static <T, R extends Structure> R toArray(Collection<T> data, Supplier<R> identity, BiConsumer<T, R> populate) {
        final R[] array = data.stream().collect(new StructureCollector<>(identity, populate));

        if(array == null) {
            return null;
        }
        else {
            return array[0];
        }
    }

    private final Supplier<R> identity;
    private final BiConsumer<T, R> populate;
    private final Set<Characteristics> chars;

    /**
     * Constructor.
     * @param identity      Identity structure
     * @param populate      Population function
     * @param chars         Stream characteristics
     */
    public StructureCollector(Supplier<R> identity, BiConsumer<T, R> populate, Characteristics... chars) {
        this.identity = notNull(identity);
        this.populate = notNull(populate);
        this.chars = Set.copyOf(Arrays.asList(chars));
    }

    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (left, right) -> {
            left.addAll(right);
            return left;
        };
    }

    @Override
    public Function<List<T>, R[]> finisher() {
        return this::finish;
    }

    @SuppressWarnings("unchecked")
    private R[] finish(List<T> list) {
        // Check for empty data
        if(list.isEmpty()) {
            return null;
        }

        // Allocate contiguous array
        final R[] array = (R[]) identity.get().toArray(list.size());

        // Populate array
        final Iterator<T> itr = list.iterator();
        for(final R element : array) {
            populate.accept(itr.next(), element);
        }
        assert !itr.hasNext();

        return array;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return chars;
    }
}

这很好地结束了分配和填充连续数组的代码,示例用法:

class SomeDomainObject {
    private void populate(SomeStructure struct) {
        ...
    }
}

class SomeStructure extends Structure {
    ...
}

Collection<SomeDomainObject> collection = ...

SomeStructure[] array = collection
    .stream()
    .collect(new StructureCollector<>(SomeStructure::new, SomeStructure::populate));

希望这可以帮助任何正在做类似事情的人。