JNA Как заполнить поле указателя на структуру внутри структуры, которая будет передана в собственную библиотеку? - PullRequest
1 голос
/ 15 марта 2019

Мне нужно передать структуру JNA на нативный уровень, который содержит поле указателя на структуру (может содержать ноль или более структур).

Вот «родительская» структура:

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

(Другие поля, классы @FieldOrder и ByReference / Value для краткости опущены)

А вот структура 'child':

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

Согласно документации JNA ( здесь ) поле указателя на массив должно быть полем 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 - Вышеописанное прекрасно работает для структур с изменчивыми размерами, но некоторые из тех, с которыми я имею дело, имеют огромное количество полей, подструктур и т. Д. Я предположил, что мог бы просто создать массив структур 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, который мне затем нужно отправить как поле указателя на структуру в 'parent' VkRenderPassCreateInfo.

Причиной использования JNA VkAttachmentStructure на стороне Java является то, что некоторые из этих структур содержат большое количество полей. то есть, вызов 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)

В Structure есть альтернативный метод toArray, который принимает массив, но, похоже, он не делает ничего другого, кроме вызова целочисленной версии?

Попытка 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 ]

1 голос
/ 18 марта 2019

Вот статический помощник для выполнения подхода копирования структуры, описанного выше:

    /**
     * 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[], извлеченный из структуры, равен нулю. Что на самом деле делает * 1011? В документе написано, что копирование происходит в собственную память, но я не могу понять, что делает настоящий код.

Должен ли я впоследствии освободить эту память?

Есть ли альтернативный метод получения структуры памяти?

1 голос
/ 17 марта 2019

Проблема, которую нужно решить (и источник ошибок Illegal memory access), заключается в том, что код на стороне C, принимающий ваш массив, ожидает от Pointer до непрерывного блока памяти. В C вам нужен только адрес памяти первого элемента плюс смещение размера; для доступа к массиву [1] вы найдете память массива [0] и смещены на размер структуры.

В вашем случае вы выделили несмежную память для каждой структуры в этом блоке:

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

Каждый VkAttachmentDescription сопоставлен с собственной памятью, и попытка чтения памяти в конце первой структуры вызывает ошибку. Если у вас нет контроля над тем, какая память используется при создании экземпляров этих объектов VkAttachmentDescription, вы в конечном итоге продублируете свои требования к памяти и будете вынуждены копировать собственную память из несмежного блока в непрерывный блок.

Отредактировано, чтобы добавить: Как указано в вашем другом ответе, если вы работали только со структурой VkAttachmentDescription на стороне Java и не передавали ее в функцию 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 внутри структуры и создать массив Structure, используя toArray(). Вы перечислили в своем коде один из способов создания массива с использованием типа ByReference. Это работает, или вы также можете создать его с типом (по умолчанию ByValue), а затем извлечь указатель на первый элемент позже, чтобы создать тип 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(), где он нужен только для поля структуры (которое можно просто определить как Pointer и полностью исключить необходимость в ByReference... но если вы делаете это, почему бы просто не скопировать в буфер Memory? Я бью мертвую лошадь здесь!).

Наконец, идеальным решением, если вы знаете, сколько элементов у вас будет в итоге (или верхняя граница этого числа), было бы создание экземпляра массива с использованием непрерывной памяти в самом начале.Тогда вместо создания нового экземпляра VkAttachmentDescription вы можете просто извлечь уже существующий экземпляр из массива.Это нормально, если вы перераспределяете и не используете их все, если вы используете их непрерывно с самого начала.Все, что вы передаёте C, это # ​​структур и адрес первой, вам все равно, если у вас есть дополнительные байты.

...