Betten Sie einen Python-Interpreter mithilfe des Panama-Projekts in eine Java-Anwendung ein

Vor ein paar Tagen habe ich einen Tweet von Brian Goetz gesehen, aber erst heute haben meine Hände nach Beispielen gegriffen.

Bild

Ich möchte kurz darüber sprechen.

Über das Panama-Projekt auf Habré wurde bereits geschrieben . Um zu verstehen, was es ist und warum, sollten Sie das Interview hier lesen. Ich zeige nur einige einfache Beispiele für die Verwendung von nativem Binder .

Zunächst benötigen Sie den C-Compiler. Wenn Sie Linux oder MacOS verwenden, haben Sie bereits einen. Unter Windows müssen Sie zuerst Build Tools für Visual Studio 2017 installieren. Und natürlich benötigen Sie OpenJDK mit Panama-Unterstützung. Sie können es entweder durch Erstellen des "fremden" Zweigs des entsprechenden Repositorys oder durch Herunterladen des Early-Access-Builds erhalten .

Beginnen wir mit einem minimalen Beispiel - einer einfachen Funktion, die zwei Zahlen hinzufügt:

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

In einer DLL kompilieren

 cl /LD adder.c 

Und in Java-Code verwenden

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

Die Quelle sollte für diejenigen, die JNR verwendet haben, viele Vertrautheiten aufweisen: Die Schnittstelle der nativen Bibliothek wird deklariert, die Bibliothek wird geladen, der Schnittstelle zugeordnet und die native Funktion wird aufgerufen. Der Hauptunterschied besteht in der Verwendung der Layoutdefinitionssprache in Anmerkungen zur Beschreibung der Zuordnung nativer Daten zu Java-Typen.

Es ist leicht zu erraten, dass der Ausdruck " (i32 i32)i32 " eine Funktion bezeichnet, die zwei 32-Bit-Ganzzahlen (i32 i32)i32 und eine 32-Bit-Ganzzahl zurückgibt. Die Bezeichnung i bezeichnet einen von drei Grundtypen - eine Ganzzahl mit Little-Endian-Bytereihenfolge. Darüber hinaus werden häufig u und f gefunden - eine vorzeichenlose Ganzzahl bzw. eine Gleitkommazahl. Die gleichen Bezeichnungen werden verwendet, um die Reihenfolge des Big-Endian anzugeben, jedoch in Großbuchstaben - I , U , F Ein weiteres gängiges Label ist v , das für void . Die Zahl hinter dem Etikett gibt die Anzahl der verwendeten Bits an.

Wenn die Zahl vor dem Etikett steht, bezeichnet das Etikett ein Array: [42f32] - ein Array von 42 Elementen vom Typ float . Gruppenbeschriftungen in eckigen Klammern. Zusätzlich zu Arrays können hierdurch Strukturen ( [i32 i32] - eine Struktur mit zwei Feldern vom Typ int ) und Gewerkschaften ( [u64|u64:f32] - long oder ein Zeiger auf float ) angegeben werden.

Ein Doppelpunkt wird verwendet, um Zeiger anzuzeigen. Beispielsweise ist u64:i32 ein Zeiger auf int , u64:v ist ein Zeiger vom Typ void und u64:[i32 i32] ist ein Zeiger auf eine Struktur.

Mit diesen Informationen können wir das Beispiel etwas komplizieren.

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

Im Java-Code wurden mehrere neue Elemente gleichzeitig angezeigt - Scope , Array und Pointer . Wenn Sie mit nativem Code arbeiten, müssen Sie Off-Heap-Daten verarbeiten. Dies bedeutet, dass Sie unabhängig voneinander Speicher zuweisen, die Relevanz von Zeigern unabhängig freigeben und überwachen müssen. Glücklicherweise kümmert sich Scope um all diese Probleme. Seine Methoden machen es einfach und bequem, nicht spezifizierten Speicher, Speicher für Arrays, Strukturen und C-Zeilen zuzuweisen, Zeiger auf diesen Speicher abzurufen und ihn auch automatisch freizugeben, nachdem der Block "Try-with-Resources" abgeschlossen ist, und den Status der erstellten Zeiger so zu ändern, dass der Aufruf erfolgt Dies führte zur Ausnahme und nicht zum Absturz der virtuellen Maschine.

Um die Arbeit von Strukturen und Zeigern zu betrachten, wollen wir das Beispiel etwas komplizierter machen.

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

Interessant ist hier, wie die Schnittstelle der Struktur deklariert wird und wie Speicher dafür zugewiesen wird. Beachten Sie, dass in der ldl-Deklaration ein neues Element angezeigt wurde - die Werte in Klammern nach den Beschriftungen. Dies ist eine Tag-Annotation in Kurzform. Das vollständige Formular würde folgendermaßen aussehen: i32(name=x) . Mit der Beschriftungsanmerkung können Sie sie mit der Schnittstellenmethode korrelieren.

Bevor wir zum Versprechen im Titel übergehen, müssen wir noch eine andere Möglichkeit zur Interaktion mit nativem Code hervorheben. Alle vorherigen Beispiele wurden als native Funktionen bezeichnet, aber manchmal muss nativer Code Java-Code aufrufen. Wenn wir beispielsweise ein Array mit qsort sortieren möchten, benötigen wir einen Rückruf.

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

Es ist leicht zu erkennen, dass ldl-Anzeigen, die nicht besonders leicht zu lesen sind, schnell zu wütenden Designs werden. qsort ist jedoch nicht die schwierigste Funktion. Darüber hinaus kann es in realen Bibliotheken Dutzende von Strukturen und Dutzenden von Funktionen geben. Das Schreiben von Schnittstellen für sie ist eine undankbare Aufgabe. Glücklicherweise lassen sich beide Probleme leicht mit dem Dienstprogramm jextract lösen, das alle erforderlichen Schnittstellen basierend auf den Header-Dateien generiert. Kehren wir zum ersten Beispiel zurück und verarbeiten es mit diesem Dienstprogramm.

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

Verwenden Sie die resultierende JAR-Datei, um Java-Code zu erstellen und auszuführen:

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

Das ist zwar nicht besonders beeindruckend, lässt Sie aber das Prinzip verstehen. Und jetzt machen wir dasselbe mit python37.dll (endlich!)

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

Wir generieren Schnittstellen:

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

Kompilieren und ausführen:

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

Herzlichen Glückwunsch, Ihre Java-Anwendung hat gerade den Python-Interpreter heruntergeladen und das Skript darin ausgeführt!

Bild

Weitere Beispiele finden Sie in der Anleitung für Pioniere .

Maven-Projekte mit Beispielen aus dem Artikel finden Sie auf GitHub .

Die PS-API wird derzeit schnell geändert. In den Präsentationen vor einigen Monaten ist der Code, der nicht kompiliert werden kann, leicht zu erkennen. Die Beispiele aus diesem Artikel sind dagegen nicht immun. Wenn Sie darauf stoßen, senden Sie mir eine Nachricht, ich werde versuchen, es zu beheben.

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


All Articles