Incorporer un interpréteur Python dans une application Java à l'aide du projet Panama

Il y a quelques jours, j'ai vu un tweet de Brian Goetz, mais ce n'est qu'aujourd'hui que mes mains se sont mises à jouer avec des exemples.

image

Je veux en parler brièvement.

A propos du projet Panama sur Habré a déjà écrit . Pour comprendre ce que c'est et pourquoi, vous devriez lire l'interview ici. Je vais simplement vous montrer quelques exemples simples d'utilisation du classeur natif .

Tout d'abord, vous avez besoin du compilateur C. Si vous utilisez Linux ou MacOS, vous en avez déjà un. Si Windows, vous devez d'abord installer les outils de génération pour Visual Studio 2017 . Et, bien sûr, vous avez besoin d'OpenJDK avec le support Panama. Vous pouvez l'obtenir soit en créant la branche «étrangère» du référentiel correspondant , soit en téléchargeant la version Early-Access .

Commençons par un exemple minimal - une fonction simple qui ajoute deux nombres:

adder.h
#ifndef _ADDER_H #define _ADDER_H __declspec(dllexport) int add(int, int); #endif 

adder.c
 #include "adder.h" __declspec(dllexport) int add(int a, int b) { return a + b; } 

Compiler dans une DLL

 cl /LD adder.c 

Et utiliser en code java

 import java.foreign.Library; import java.foreign.Libraries; import java.foreign.annotations.NativeHeader; import java.foreign.annotations.NativeFunction; import java.lang.invoke.MethodHandles; public class App { @NativeHeader interface Adder { @NativeFunction("(i32 i32)i32") int add(int a, int b); } public static void main(String[] args) { Library lib = Libraries.loadLibrary(MethodHandles.lookup(), "adder"); Adder adder = Libraries.bind(Adder.class, lib); System.out.println(adder.add(3, 5)); } } 

La source devrait avoir beaucoup de familiarités pour ceux qui ont utilisé JNR: l'interface de la bibliothèque native est déclarée, la bibliothèque est chargée, associée à l'interface et la fonction native est appelée. La principale différence est l'utilisation du langage de définition de mise en page dans les annotations pour décrire le mappage des données natives aux types Java.

Il est facile de deviner que l'expression " (i32 i32)i32 " dénote une fonction qui prend deux entiers 32 bits et renvoie un entier 32 bits. L'étiquette i désigne l'un des trois types de base - un entier avec un ordre d'octets en petit bout. En plus de cela, u et f sont souvent trouvés - un entier non signé et un nombre à virgule flottante, respectivement. Les mêmes étiquettes sont utilisées pour indiquer l'ordre du big-endian, mais en majuscules - I , U , F Une autre étiquette courante est v , utilisée pour void . Le nombre suivant l'étiquette indique le nombre de bits utilisés.

Si le nombre est avant l'étiquette, alors l'étiquette dénote un tableau: [42f32] - un tableau de 42 éléments de type float . Étiquettes de groupe de crochets. En plus des tableaux, cela peut être utilisé pour indiquer des structures ( [i32 i32] - une structure avec deux champs de type int ) et des unions ( [u64|u64:f32] - long ou un pointeur à float ).

Un deux-points est utilisé pour indiquer des pointeurs. Par exemple, u64:i32 est un pointeur sur int , u64:v est un pointeur de type void et u64:[i32 i32] est un pointeur sur une structure.

Armés de ces informations, compliquons un peu l'exemple.

totalizer.c
 __declspec(dllexport) long sum(int vals[], int size) { long r = 0; for (int i = 0; i < size; i++) { r += vals[i]; } return r; } 

App.java
 import java.foreign.Library; import java.foreign.Libraries; import java.foreign.NativeTypes; import java.foreign.Scope; import java.foreign.annotations.NativeHeader; import java.foreign.annotations.NativeFunction; import java.foreign.memory.Array; import java.foreign.memory.Pointer; import java.lang.invoke.MethodHandles; public class App { @NativeHeader interface Totalizer { @NativeFunction("(u64:i32 i32)u64") long sum(Pointer<Integer> vals, int size); } public static void main(String[] args) { Library lib = Libraries.loadLibrary(MethodHandles.lookup(), "totalizer"); Totalizer totalizer = Libraries.bind(Totalizer.class, lib); try (Scope scope = Scope.newNativeScope()) { Array<Integer> array = scope.allocateArray(NativeTypes.INT, new int[] { 23, 15, 4, 16, 42, 8 }); System.out.println(totalizer.sum(array.elementPointer(), 3)); } } } 

Plusieurs nouveaux éléments sont apparus dans le code java à la fois - Scope , Array et Pointer . Lorsque vous travaillez avec du code natif, vous devrez exploiter des données hors segment, ce qui signifie que vous devrez allouer de la mémoire indépendamment, libérer et surveiller indépendamment la pertinence des pointeurs. Heureusement, Scope s'occupe de toutes ces préoccupations. Ses méthodes permettent d'allouer de la mémoire non spécifiée, de la mémoire pour des tableaux, des structures et des lignes C, d'obtenir des pointeurs vers cette mémoire et de la libérer automatiquement une fois le bloc try-with-resources terminé et de modifier l'état des pointeurs créés afin que l'appel cela a conduit à l'exception, pas au crash de la machine virtuelle.

Pour regarder le travail des structures et des pointeurs, compliquons un peu plus l'exemple.

mover.h
 #ifndef _ADDER_H #define _ADDER_H typedef struct { int x; int y; } Point; __declspec(dllexport) void move(Point*); #endif 

mover.c
 #include "mover.h" __declspec(dllexport) void move(Point *point) { point->x = 4; point->y = 2; } 

App.java
 import java.foreign.Library; import java.foreign.Libraries; import java.foreign.Scope; import java.foreign.annotations.NativeHeader; import java.foreign.annotations.NativeFunction; import java.foreign.annotations.NativeStruct; import java.foreign.annotations.NativeGetter; import java.foreign.memory.LayoutType; import java.foreign.memory.Pointer; import java.foreign.memory.Struct; import java.lang.invoke.MethodHandles; public class App { @NativeStruct("[i32(x) i32(y)](Point)") interface Point extends Struct<Point> { @NativeGetter("x") int x(); @NativeGetter("y") int y(); } @NativeHeader interface Mover { @NativeFunction("(u64:[i32 i32])v") void move(Pointer<Point> point); } public static void main(String[] args) { Library lib = Libraries.loadLibrary(MethodHandles.lookup(), "mover"); Mover mover = Libraries.bind(Mover.class, lib); try (Scope scope = Scope.newNativeScope()) { Pointer<Point> point = scope.allocate( LayoutType.ofStruct(Point.class)); mover.move(point); System.out.printf("X: %d Y: %d%n", point.get().x(), point.get().y()); } } } 

Il est intéressant de savoir ici comment l'interface de la structure est déclarée et comment la mémoire lui est allouée. Notez qu'un nouvel élément est apparu dans la déclaration ldl - les valeurs entre parenthèses après les étiquettes. Il s'agit d'une annotation de balise sous forme abrégée. Le formulaire complet ressemblerait à ceci: i32(name=x) . L'annotation d'étiquette vous permet de la corréler avec la méthode d'interface.

Avant de passer à la promesse du titre, il reste à mettre en évidence une autre façon d'interagir avec le code natif. Tous les exemples précédents appelaient des fonctions natives, mais parfois le code natif doit appeler du code java. Par exemple, si nous voulons trier un tableau à l'aide de qsort , nous avons besoin d'un rappel.

 import java.foreign.Library; import java.foreign.Libraries; import java.foreign.NativeTypes; import java.foreign.Scope; import java.foreign.annotations.NativeHeader; import java.foreign.annotations.NativeFunction; import java.foreign.annotations.NativeCallback; import java.foreign.memory.Array; import java.foreign.memory.Callback; import java.foreign.memory.Pointer; import java.lang.invoke.MethodHandles; public class App { @NativeHeader interface StdLib { @NativeFunction("(u64:[0i32] i32 i32 u64:(u64:i32 u64:i32) i32)v") void qsort(Pointer<Integer> base, int nitems, int size, Callback<QComparator> comparator); @NativeCallback("(u64:i32 u64:i32)i32") interface QComparator { int compare(Pointer<Integer> p1, Pointer<Integer> p2); } } public static void main(String[] args) { Library lib = Libraries.loadLibrary(MethodHandles.lookup(), "msvcr120"); StdLib stdLib = Libraries.bind(StdLib.class, lib); try (Scope scope = Scope.newNativeScope()) { Array<Integer> array = scope.allocateArray(NativeTypes.INT, new int[] { 23, 15, 4, 16, 42, 8 }); Callback<StdLib.QComparator> cb = scope.allocateCallback( (p1, p2) -> p1.get() - p2.get()); stdLib.qsort(array.elementPointer(), (int) array.length(), Integer.BYTES, cb); for (int i = 0; i < array.length(); i++) { System.out.printf("%d ", array.get(i)); } System.out.println(); } } } 

Il est facile de voir que les publicités LDL, qui ne sont pas particulièrement faciles à lire, se transforment rapidement en designs furieux. Mais qsort n'est pas la fonction la plus difficile. De plus, il peut y avoir des dizaines de structures et des dizaines de fonctions dans de vraies bibliothèques, écrire des interfaces pour elles est une tâche ingrate. Heureusement, les deux problèmes sont facilement résolus à l'aide de l'utilitaire jextract , qui générera toutes les interfaces nécessaires en fonction des fichiers d'en-tête. Revenons au premier exemple et traitons-le avec cet utilitaire.

 jextract -L . -l adder -o adder.jar -t "com.example" adder.h 

 //  jextract' ""  import static com.example.adder_h.*; public class Example { public static void main(String[] args) { System.out.println(add(3, 5)); } } 

Et utilisez le fichier jar résultant pour créer et exécuter du code java:

 javac -cp adder.jar Example.java java -cp .;adder.jar Example 

Bien que pas particulièrement impressionnant, mais vous permet de comprendre le principe. Et maintenant, faisons de même avec python37.dll (enfin!)

 import java.foreign.Scope; import java.foreign.memory.Pointer; import static org.python.Python_h.*; import static org.python.pylifecycle_h.*; import static org.python.pythonrun_h.*; public class App { public static void main(String[] args) { Py_Initialize(); try (Scope s = Scope.newNativeScope()) { PyRun_SimpleStringFlags( s.allocateCString("print(sum([23, 15, 4, 16, 42, 8]))\n"), Pointer.nullPointer()); } Py_Finalize(); } } 

Nous générons des interfaces:

 jextract -L "C:\Python37" -l python37 -o python.jar -t "org.python" --record-library-path C:\Python37\include\Python.h 

Compiler et exécuter:

 javac -cp python.jar App.java java -cp .;python.jar App 

Félicitations, votre application java vient de télécharger l'interpréteur Python et d'y exécuter le script!

image

Plus d'exemples peuvent être trouvés dans les instructions pour les pionniers .

Les projets Maven avec des exemples de l'article peuvent être trouvés sur GitHub .

L'API PS subit actuellement des changements rapides. Dans les présentations d'il y a quelques mois, il est facile de voir du code qui ne sera pas compilé. Les exemples de cet article n'en sont pas à l'abri. Si vous rencontrez cela, envoyez-moi un message, je vais essayer de le réparer.

Source: https://habr.com/ru/post/fr440470/


All Articles