JNA: Как указать массив переменной длины (0+) в структуре?

Пример структуры, где count — количество байтов в массиве, которое может быть равно 0. Я хочу выделить новые экземпляры в Java, а также прочитать экземпляры, выделенные в собственном коде.

    public class VarArray extends Structure {
        public byte dummy0;
        public short dummy1;
        public int count;
        public byte[] array;
    }

Использование array = new byte[0] запрещено в Structure.
Объявление по умолчанию array = new byte[1] будет читать с нераспределенного адреса, если счетчик равен 0.

Удаление поля array подходит для чтения, так как я могу получить доступ к байтам по смещению указателя Structure.size() [Редактировать: неверно, зависит от заполнения]. Однако для выделения нового экземпляра мне нужно вручную определить размеры полей и заполнение выравнивания, чтобы выделить правильный размер памяти.

У меня есть решение, использующее два типа: один без array для собственных экземпляров и экземпляров, выделенных для Java, с нулевым счетом, и подтип с array для экземпляров, выделенных Java для 1+ экземпляров. Это кажется довольно раздутым, особенно с обязательным стандартным кодом.

Есть ли способ лучше?
Или, может быть, простой способ рассчитать размер поля и выравнивание, чтобы одного типа было достаточно?

import java.util.Arrays;
import java.util.List;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;

public class JnaStructTester {

    /**
     * For native-allocated, and 0-count JNA-allocated instances.
     */
    public static class VarArray extends Structure {
        public byte dummy0;
        public short dummy1;
        public int count;

        public VarArray() {}

        public VarArray(Pointer p) {
            super(p);
        }

        public byte[] getArray() {
            byte[] array = new byte[count];
            if (count > 0) {
                int offset = size();
                getPointer().read(offset, array, 0, count);
            }
            return array;
        }
        
        @Override
        protected List<String> getFieldOrder() {
            return List.of("dummy0", "dummy1", "count");
        }
    }
    
    /**
     * For 1+ count JNA-allocated instances.
     */
    public static class VarArrayX extends VarArray {
        public byte[] array;

        public VarArrayX() {}

        @Override
        public byte[] getArray() {
            return array;
        }
        
        @Override
        protected List<String> getFieldOrder() {
            return List.of("dummy0", "dummy1", "count", "array");
        }
    }
    
    public static void main(String[] args) {
        var va0 = new VarArrayX();
        va0.dummy0 = (byte) 0xef;
        va0.dummy1 = (short) 0xabcd;
        va0.count = 7;
        va0.array = new byte[] { 1, 2, 3, 4, 5, 6, 7 };
        va0.write();
        
        var va1 = new VarArray();
        va1.dummy0 = (byte) 0xab;
        va1.dummy1 = (short) 0xcdef;
        va1.write();
        
        print(new Pointer(Pointer.nativeValue(va0.getPointer())));
        print(new Pointer(Pointer.nativeValue(va1.getPointer())));
    }
    
    private static void print(Pointer p) {
        var va = new VarArray(p);
        va.read();
        System.out.println(va);
        System.out.println("byte[] array = " + Arrays.toString(va.getArray()));
        System.out.println();
    }
}

Выход:

JnaStructTester$VarArray(native@0x7fb6835524b0) (8 bytes) {
  byte dummy0@0=ffffffef
  short dummy1@2=ffffabcd
  int count@4=7
}
byte[] array=[1, 2, 3, 4, 5, 6, 7]

JnaStructTester$VarArray(native@0x7fb683551210) (8 bytes) {
  byte dummy0@0=ffffffab
  short dummy1@2=ffffcdef
  int count@4=0
}
byte[] array=[]

(Я использую довольно старую версию JNA 4.2.2)

Обновление (2020-01-07)

Благодаря предложениям Дэниела Виддиса, вот хорошее решение.

Динамическое изменение списка полей в зависимости от того, пуст ли массив, невозможно. Макет статически кэшируется, когда поле массива переменных не включено, поэтому в будущем экземпляр с непустым массивом потерпит неудачу. Вместо:

  1. Массив корректируется в ensureAllocated(), чтобы избежать ошибок пустого массива.
  2. writeField() записывает поле только в том случае, если массив не пуст.
  3. readField() устанавливает размер массива из числа, которое уже было прочитано, и пропускает чтение массива, если число равно 0.

Установив правильный размер массива в readField(), весь массив заполняется автоматически, нет необходимости в ручном создании.

import java.util.Arrays;
import java.util.List;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;

public class JnaStructTester {

    public static class VarArray extends Structure {
        public short dummy0;
        public int dummy1;
        public byte count;
        public byte[] array = new byte[0];

        public VarArray() {}

        public VarArray(byte[] array) {
            this.count = (byte) array.length;
            this.array = array;
        }

        public VarArray(Pointer p) {
            super(p);
        }

        @Override
        protected void ensureAllocated() {
            if (count == 0) array = new byte[1];
            super.ensureAllocated();
            if (count == 0) array = new byte[0];
        }

        @Override
        protected void writeField(StructField structField) {
            if (structField.name.equals("array") && count == 0) return;
            super.writeField(structField);
        }

        @Override
        protected Object readField(StructField structField) {
            if (structField.name.equals("array")) {
                array = new byte[count];
                if (count == 0) return null;
            }
            return super.readField(structField);
        }

        @Override
        protected List<String> getFieldOrder() {
            return List.of("dummy0", "dummy1", "count", "array");
        }
    }

    public static void main(String[] args) {
        var va0 = new VarArray(new byte[] { 1, 2, 3, 4, 5, 6, 7 });
        va0.dummy0 = 0x4321;
        va0.dummy1 = 0xabcdef;
        va0.write();

        var va1 = new VarArray();
        va1.dummy0 = 0x4321;
        va1.dummy1 = 0xabcdef;
        va1.write();
        
        print(new Pointer(Pointer.nativeValue(va0.getPointer())));
        print(new Pointer(Pointer.nativeValue(va1.getPointer())));
    }

    private static void print(Pointer p) {
        var va = new VarArray(p);
        va.read();
        System.out.println(va);
        System.out.println("byte[] array = " + Arrays.toString(va.array));
        System.out.println();
    }
}

Выход:

JnaStructTester$VarArray(native@0x7fd85cf1ffb0) (12 bytes) {
  short dummy0@0=4321
  int dummy1@4=abcdef
  byte count@8=7
  byte array[7]@9=[B@4f2410ac
}
byte[] array=[1, 2, 3, 4, 5, 6, 7]

JnaStructTester$VarArray(native@0x7fd85cf20690) (12 bytes) {
  short dummy0@0=4321
  int dummy1@4=abcdef
  byte count@8=0
  byte array[0]@9=[B@722c41f4
}
byte[] array=[]

Я тестировал это только в моем узком случае использования, это может не сработать, если вызываются другие методы Structure.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
3
0
409
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я действительно думаю, что решение с двумя структурами, с версией массива, расширяющей другую, но просто добавляющей новое поле, является разумным решением. Ваша озабоченность по поводу «раздувания шаблона» значительно снижается с JNA 5.X, который имеет аннотацию @FieldOrder, которая значительно уменьшает этот шаблон. Ваши структуры будут довольно простыми:

@FieldOrder({"dummy0", "dummy1", "count"})
public class VarArray extends Structure {
    public byte dummy0;
    public short dummy1;
    public int count;
}

@FieldOrder({"dummy0", "dummy1", "count", "array"})
public class VarArrayX extends VarArray  {
    public byte[] array = new byte[1];

    public VarArrayX(int arraySize) {
        array = new byte[arraySize];
        super.count = arraySize;
        allocateMemory();
    }
}

Помимо добавления конструкторов, если вы хотите инициализировать с помощью указателя, этого должно быть достаточно.

Однако вы можете сделать версию с одной структурой, сделав то же изменение FieldOrder. Выделение памяти JNA зависит от порядка полей, определенного с помощью методов getFieldList() и getFieldOrder() (для которых новая аннотация удаляет стандартные требования).

Вы можете сохранить выделение массива в структуре, но изменить переопределение getFieldOrder(), чтобы пропустить поле, определяющее массив в случае, если он имеет нулевой размер, сделав то же самое для getFieldList(). Мне пришлось столкнуться именно с такой ситуацией, когда я писал структуру Sysinfo в сопоставлении JNA Linux LibC. Основная структура включает в себя это поле:

public byte[] _f = new byte[PADDING_SIZE];

Но переопределение getFieldList() включает в себя:

if (PADDING_SIZE == 0) {
    Iterator<Field> fieldIterator = fields.iterator();
    while (fieldIterator.hasNext()) {
        Field field = fieldIterator.next();
        if ("_f".equals(field.getName())) {
            fieldIterator.remove();
        }
    }
}

и getFieldOrder() включает:

if (PADDING_SIZE == 0) {
    fieldOrder.remove("_f");
}

Аналогичные условия удаляют поле _f_unused в структуре Statvfs в том же файле.

В случае вашей структуры, если вы знаете count перед созданием экземпляра структуры, что-то вроде этого должно работать (непроверено):

public static class VarArray extends Structure {
    public byte dummy0;
    public short dummy1;
    public int count;
    public byte[] array = new byte[1];

    public VarArray(int arraySize) {
        array = new byte[arraySize];
        count = arraySize;
        allocateMemory();
    }

    @Override
    protected List<String> getFieldOrder() {
        if (count == 0) {
            return List.of("dummy0", "dummy1", "count");
        } else {
            return List.of("dummy0", "dummy1", "count", "array");
        }
    }

    // do the same for getFieldList()
}

Спасибо подробный ответ. Я использовал ваши предложения для хорошего решения, которое работает для моего случая - обновлено в вопросе.

boot-and-bonnet 15.12.2020 03:03

Хорошее решение! Я должен добавить это в закладки, когда это снова появится.

Daniel Widdis 15.12.2020 05:57

Оказывается, я не могу динамически изменять список полей, так как он статически кэшируется. Я обновил решение с переопределениями для ensureAllocated/readField/writeField.

boot-and-bonnet 08.01.2021 08:18

Другие вопросы по теме