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.

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
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!
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.