Incruste un intérprete de Python en una aplicación Java utilizando el proyecto Panamá

Hace un par de días vi un tweet de Brian Goetz, pero solo hoy mis manos llegaron a jugar con ejemplos.

imagen

Quiero hablar brevemente sobre esto.

Sobre el proyecto de Panamá sobre Habré ya escribió . Para entender qué es y por qué, debe leer la entrevista aquí. Solo mostraré un par de ejemplos simples de cómo usar el aglutinante nativo .

En primer lugar, necesita el compilador de C. Si está utilizando Linux o MacOS, entonces ya tiene uno. Si es Windows, primero deberá instalar Build Tools para Visual Studio 2017 . Y, por supuesto, necesita OpenJDK con soporte de Panamá. Puede obtenerlo compilando la rama "extranjera" del repositorio correspondiente o descargando la compilación Early-Access .

Comencemos con un ejemplo mínimo: una función simple que agrega dos números:

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

Compilar en una DLL

 cl /LD adder.c 

Y usar en código 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 fuente debe tener muchas familiaridades para aquellos que usaron JNR: se declara la interfaz de la biblioteca nativa, se carga la biblioteca, se asocia con la interfaz y se llama a la función nativa. La principal diferencia es el uso del lenguaje de definición de diseño en las anotaciones para describir la asignación de datos nativos a los tipos de Java.

Es fácil adivinar que la expresión " (i32 i32)i32 " denota una función que toma dos enteros de 32 bits y devuelve un entero de 32 bits. La etiqueta i denota uno de los tres tipos básicos: un entero con orden de bytes little endian. Además, a menudo se encuentran u y f : un número entero sin signo y un número de coma flotante, respectivamente. Se usan las mismas etiquetas para indicar el orden de big-endian, pero en mayúsculas: I , U , F Otra etiqueta común es v , utilizada para void . El número que sigue a la etiqueta indica el número de bits utilizados.

Si el número está antes de la etiqueta, entonces la etiqueta denota una matriz: [42f32] - una matriz de 42 elementos de tipo float . Etiquetas de grupo de corchetes. Además de las matrices, esto puede usarse para indicar estructuras ( [i32 i32] - una estructura con dos campos de tipo int ) y uniones ( [u64|u64:f32] - long o un puntero para float ).

Se usan dos puntos para indicar punteros. Por ejemplo, u64:i32 es un puntero a int , u64:v es un puntero de tipo void y u64:[i32 i32] es un puntero a una estructura.

Armado con esta información, complicaremos un poco el ejemplo.

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

Varios elementos nuevos aparecieron en el código de Java a la vez: Scope , Array y Pointer . Cuando trabaje con código nativo, tendrá que operar datos fuera del montón, lo que significa que tendrá que asignar memoria de forma independiente, liberar de forma independiente y controlar la relevancia de los punteros. Afortunadamente, Scope se ocupa de todas estas preocupaciones. Sus métodos hacen que sea fácil y conveniente asignar memoria no especificada, memoria para matrices, estructuras y líneas C, obtener punteros a esta memoria y también liberarla automáticamente después de completar el bloque de prueba con recursos y cambiar el estado de los punteros creados para que la llamada condujo a la excepción, no a la caída de la máquina virtual.

Para ver el trabajo de las estructuras y los punteros, vamos a complicar un poco más el ejemplo.

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

Aquí es interesante cómo se declara la interfaz de la estructura y cómo se le asigna memoria. Tenga en cuenta que ha aparecido un nuevo elemento en la declaración ldl: los valores entre paréntesis después de las etiquetas. Esta es una anotación de etiqueta en forma abreviada. El formulario completo se vería así: i32(name=x) . La anotación de etiqueta le permite correlacionarla con el método de interfaz.

Antes de pasar a la promesa en el título, queda por resaltar otra forma de interactuar con el código nativo. Todos los ejemplos anteriores se denominan funciones nativas, pero a veces el código nativo necesita llamar al código java. Por ejemplo, si queremos ordenar una matriz usando qsort , necesitamos una devolución de llamada.

 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 fácil ver que los anuncios ldl, que no son particularmente fáciles de leer, se convierten rápidamente en diseños furiosos. Pero qsort no es la función más difícil. Además, puede haber docenas de estructuras y docenas de funciones en bibliotecas reales, escribir interfaces para ellos es una tarea ingrata. Afortunadamente, ambos problemas se resuelven fácilmente utilizando la utilidad jextract , que generará todas las interfaces necesarias basadas en los archivos de encabezado. Volvamos al primer ejemplo y procesémoslo con esta utilidad.

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

Y use el archivo jar resultante para compilar y ejecutar código java:

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

Si bien no es particularmente impresionante, pero le permite comprender el principio. Y ahora hagamos lo mismo con python37.dll (¡finalmente!)

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

Generamos interfaces:

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

Compilar y ejecutar:

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

¡Felicitaciones, su aplicación Java acaba de descargar el intérprete de Python y ejecutó el script en él!

imagen

Se pueden encontrar más ejemplos en las instrucciones para pioneros .

Los proyectos de Maven con ejemplos del artículo se pueden encontrar en GitHub .

PS API actualmente está experimentando cambios rápidos. En las presentaciones de hace un par de meses, es fácil ver el código que no se compilará. Los ejemplos de este artículo no son inmunes a esto. Si te encuentras con esto, envíame un mensaje, intentaré solucionarlo.

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


All Articles