Passer une collection de structures JNA à une méthode native
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
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 remplacezuseMemory()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, ... )pararray[n].getPointer().write(0, ... ).
- Vous avez toujours un
- Il est impossible de contourner la copie de la mémoire, mais avoir deux lignes bien commentées qui appellent
getByteArray()et immédiatementwrite()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.
- Vous pouvez même le mettre sur une seule ligne ...
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.
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.
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.