Passer une collection de structures JNA à une méthode native

Oct 13 2020

Le problème

J'essaie de passer une collection de structures JNA à une méthode native mais cela s'avère très délicat:

Disons que nous avons une structure:

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

et une méthode dans une interface JNA:

void pass(MyStructure[] data);

qui correspond à la méthode native:

void pass(const MYStructure* data);

Maintenant, la complication vient du fait que l'application construit dynamiquement une collection de ces structures, c'est-à-dire que nous n'avons PAS affaire à un tableau statique mais à quelque chose comme ceci:

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
    }
}

Une implémentation naïve de la pass()méthode pourrait être:

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

(où libest l'interface de la bibliothèque JNA).

Bien sûr, cela ne fonctionne pas car le tableau n'est pas un bloc de mémoire contigu - assez juste.

Solution poubelle n ° 1

Une solution consiste à allouer un tableau JNA à partir d'une instance de structure et à le remplir champ par champ:

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

Cela garantit que le tableau est constitué d'une mémoire contiguë. Mais nous avons dû implémenter une copie champ par champ des données (que nous avons déjà renseignées dans la liste) - c'est OK pour une structure simple, mais certaines des données que je traite ont des dizaines de champs, structures qui pointent vers d'autres tableaux imbriqués, etc. Fondamentalement, cette approche n'est tout simplement pas viable.

Solution poubelle n ° 2

Une autre alternative consiste à convertir la collection de données en un simple pointeur JNA, quelque chose du genre:

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

Cette solution est générique, nous pouvons donc l'appliquer à d'autres structures également. Mais nous devons changer les signatures de méthode pour qu'elles soient Pointerau lieu de MyStructure[]ce qui rend le code plus obtus, moins auto-documenté et plus difficile à tester. Nous pourrions également utiliser une bibliothèque tierce où cela pourrait même ne pas être une option.

(Notez que j'ai posé une question similaire il y a quelque temps ici mais je n'ai pas obtenu de réponse satisfaisante, j'ai pensé réessayer et je supprimerai l'ancienne / répondrai à la fois).

Résumé

Fondamentalement, je m'attendais / j'espérais avoir quelque chose comme ceci:

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

similaire à la façon dont la classe d'assistance JNA fournit StringArrayun tableau de chaînes:

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

Mais pour autant que je sache, une telle «magie» n'existe pas. Existe-t-il une autre façon, plus simple et plus «JNA» de le faire? Il semble vraiment stupide (et probablement incorrect) de devoir allouer une copie octet par octet des données que nous avons essentiellement déjà!

Ai-je d'autres options? Tous les pointeurs (jeu de mots) acceptés avec gratitude.

Réponses

1 DanielWiddis Oct 13 2020 at 07:40

En tant qu'auteur de la réponse précédente, je me rends compte qu'une grande partie de la confusion était de l'aborder dans un sens avant de réaliser une meilleure solution dont nous avons discuté principalement dans les commentaires à votre réponse. J'essaierai de répondre à cette clarification supplémentaire par une démonstration concrète de ma suggestion sur cette réponse qui, à mon avis, est la meilleure approche. Simplement, si vous avez une structure non contiguë et avez besoin d'une structure contiguë, vous devez soit amener la mémoire contiguë à la structure, soit copier la structure dans la mémoire contiguë. Je vais décrire les deux approches ci-dessous.

Existe-t-il une autre façon, plus simple et plus «JNA» de le faire? Il semble vraiment stupide (et probablement incorrect) de devoir allouer une copie octet par octet des données que nous avons essentiellement déjà!

J'ai mentionné dans ma réponse sur l'autre question que vous pourriez utiliser useMemory()dans cette situation. C'est une protectedméthode, mais si vous étendez déjà une, Structurevous avez accès à cette méthode à partir de la sous-classe (votre structure), à ​​peu près de la même manière (et précisément dans le même but) que vous étendriez le Pointerconstructeur d'une sous-classe.

Vous pouvez donc prendre une structure existante dans votre collection et changer sa mémoire de sauvegarde native en mémoire contiguë. Voici un exemple de travail:

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

Sortie (notez l'allocation contiguë). Les deux secondes sorties sont identiques, à partir de la liste ou du tableau.

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
}

Si vous ne souhaitez pas insérer useMemorychacune de vos définitions de structure, vous pouvez toujours la placer dans une classe intermédiaire qui étend Structurepuis étend cette classe intermédiaire à la place de Structure.


Si vous ne voulez pas remplacer useMemory()vos définitions de structure (ou une superclasse d'entre elles), vous pouvez toujours le faire "simplement" dans le code avec un peu d'inefficacité en copiant la mémoire.

Pour «obtenir» cette mémoire pour l'écrire ailleurs, vous devez soit la lire à partir de la mémoire côté Java (via la réflexion, ce que fait JNA pour convertir la structure en bloc de mémoire natif), soit la lire à partir de Native -side mémoire (ce qui nécessite de l'écrire là-bas, même si tout ce que vous voulez faire est de le lire). Sous le capot, JNA écrit les octets natifs champ par champ, tous cachés sous un simple write()appel dans l'API.

Votre "Solution des déchets n ° 2" semble proche de ce qui est souhaité dans ce cas. Voici les contraintes auxquelles nous devons faire face, quelle que soit la solution:

  • Dans la liste ou le tableau existant de Structure, la mémoire native n'est pas contiguë (sauf si vous pré-allouez vous-même la mémoire contiguë et utilisez cette mémoire de manière contrôlée, ou remplacez useMemory()comme illustré ci-dessus), et la taille est variable.
  • La fonction native prenant un argument de tableau attend un bloc de mémoire contiguë.

Voici les «façons JNA» de gérer les structures et la mémoire:

  • Les structures ont une mémoire allouée en natif à une valeur de pointeur accessible via Structure.getPointer()avec une taille de (au moins) Structure.size().
  • La mémoire native de structure peut être lue en masse à l'aide de Structure.getByteArray().
  • Les structures peuvent être construites à partir d'un pointeur vers la mémoire native à l'aide du new Structure(Pointer p)constructeur.
  • La Structure.toArray()méthode crée un tableau de structures soutenu par un grand bloc contigu de mémoire native.

Je pense que votre solution n ° 2 est un moyen plutôt efficace de le faire, mais votre question indique que vous souhaitez plus de sécurité de type, ou au moins un code auto-documenté, auquel cas je vous indiquerais une méthode plus "JNA" de modifier # 2 en deux étapes:

  • Remplacez la nouvelle Memory(array.length * size)allocation native par l' Structure.toArray()allocation de votre solution n ° 1.
    • Vous avez toujours un length * sizebloc de mémoire native contiguë et un pointeur vers celui-ci ( array[0].getPointer()).
    • Vous avez également des pointeurs vers les décalages, vous pouvez donc les remplacer mem.write(n * size, ... )par array[n].getPointer().write(0, ... ).
  • Il est impossible de contourner la copie de la mémoire, mais avoir deux lignes bien commentées qui appellent getByteArray()et immédiatement write()ce tableau d'octets me semblent assez claires.
    • Vous pouvez même le mettre sur une seule ligne ... write(0, getByteArray(0, size), 0, size), bien que l'on puisse se demander si c'est plus ou moins clair.

Donc, en adaptant votre méthode n ° 2, je suggérerais:

// 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();
    }
}

Du point de vue du "code propre", je pense que cela accomplit assez efficacement votre objectif. La seule partie "laide" de la méthode ci-dessus est que JNA ne fournit pas un moyen facile de copier des champs entre des structures sans les écrire dans la mémoire native dans le processus. Malheureusement, c'est la "manière JNA" de "sérialiser" et de "désérialiser" des objets, et elle n'est pas conçue avec une "magie" pour votre cas d'utilisation. Les chaînes incluent des méthodes intégrées pour convertir en octets, ce qui facilite ces méthodes "magiques".

Il est également possible d'éviter d'écrire la structure dans la mémoire native juste pour la relire si vous faites la copie champ par champ comme vous l'avez laissé entendre dans votre méthode n ° 1. Cependant, vous pouvez utiliser les accesseurs de terrain de JNA pour faciliter l'accès à la réflexion sous le capot. Les méthodes de terrain sont protecteddonc nécessaires pour que vous deviez s'étendre Structurepour ce faire - et si vous le faites, l' useMemory()approche est probablement meilleure! Mais vous pouvez ensuite extraire cette itération de write():

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

Ma première pensée serait d'itérer sur les Structurechamps non contigus en utilisant la boucle ci-dessus, en stockant un Field.copy()dans un HashMapavec sf.namecomme clé. Ensuite, effectuez la même itération sur les Structurechamps des autres objets (contigus) , en lisant à partir de HashMapet en définissant leurs valeurs.

1 VictorGubin Oct 13 2020 at 01:45

Si vous êtes en mesure de créer un bloc de mémoire continue, pourquoi ne désérialisez-vous pas simplement votre liste.

Ie quelque chose comme:

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

Dans tous les cas, vous feriez mieux de ne pas stocker Structuredans votre liste ou dans une autre collection. Il est préférable de conserver un POJO à l'intérieur, puis de le remapper sur un tableau de structures directement à l'aide d'une bibliothèque de mappage de bean ou manuellement.

Avec la bibliothèque de mappage de bean MapStruct , cela peut ressembler à:

@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]);
}

Quel est le point - Le constructeur de structure alloue le bloc de mémoire en utilisant la mémoire, c'est un fonctionnement vraiment lent. Ainsi que la mémoire allouée en dehors de l'espace de tas Java. Il est toujours préférable d'éviter cette allocation chaque fois que vous le pouvez.

stridecolossus Oct 25 2020 at 18:02

Les solutions proposées par Daniel Widdis résoudront ce «problème» si l'on a vraiment besoin d'effectuer une copie octet par octet d'une structure JNA.

Cependant, je suis arrivé à la façon de penser exprimée par certaines des autres affiches - les structures JNA sont uniquement destinées à être rassemblées vers / depuis la couche native et ne devraient pas vraiment être utilisées comme des «données». Nous devrions définir les POJO de domaine et les transformer en structures JNA selon les besoins - un peu plus de travail, mais je suppose.

EDIT: Voici la solution que j'ai finalement implémentée à l'aide d'un collecteur de flux personnalisé:

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

Cela encapsule bien le code qui alloue et remplit un tableau contigu, exemple d'utilisation:

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

Espérons que cela pourrait aider quiconque fait quelque chose de similaire.