Incorporar um intérprete Python em um aplicativo java usando o projeto Panama

Alguns dias atrás, vi um tweet de Brian Goetz, mas só hoje minhas mãos chegaram para brincar com exemplos.

imagem

Eu quero falar brevemente sobre isso.

Sobre o projeto do Panamá em Habré já escreveu . Para entender o que é e por que, você deve ler a entrevista aqui. Vou apenas mostrar alguns exemplos simples de como usar o fichário nativo .

Primeiro de tudo, você precisa do compilador C. Se você estiver usando Linux ou MacOS, já possui um. Se o Windows, você primeiro precisará instalar o Build Tools para Visual Studio 2017 . E, claro, você precisa do OpenJDK com suporte ao Panamá. Você pode obtê-lo criando a ramificação “estrangeira” do repositório correspondente ou fazendo o download da compilação Early-Access .

Vamos começar com um exemplo mínimo - uma função simples que adiciona dois 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 em uma DLL

 cl /LD adder.c 

E use no 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)); } } 

A fonte deve ter muitas familiaridades para aqueles que usaram o JNR: a interface da biblioteca nativa é declarada, a biblioteca é carregada, está conectada à interface e a função nativa é chamada. A principal diferença é o uso da Linguagem de Definição de Layout nas anotações para descrever o mapeamento de dados nativos para tipos Java.

É fácil adivinhar que a expressão " (i32 i32)i32 " denota uma função que usa dois números inteiros de 32 bits e retorna um número inteiro de 32 bits. O rótulo i indica um dos três tipos básicos - um número inteiro com ordem de bytes little-endian. Além disso, u são frequentemente encontrados - um número inteiro não assinado e um número de ponto flutuante, respectivamente. Os mesmos rótulos são usados ​​para indicar a ordem do big endian, mas em maiúsculas - I , U , F Outro rótulo comum é v , usado para void . O número após o rótulo indica o número de bits usados.

Se o número estiver antes do rótulo, o rótulo indica uma matriz: [42f32] - uma matriz de 42 elementos do tipo float . Rótulos de grupo de colchetes. Além das matrizes, isso pode ser usado para indicar estruturas ( [i32 i32] - uma estrutura com dois campos do tipo int ) e uniões ( [u64|u64:f32] - long ou um ponteiro para float ).

Dois pontos são usados ​​para indicar ponteiros. Por exemplo, u64:i32 é um ponteiro para int , u64:v é um ponteiro do tipo void e u64:[i32 i32] é um ponteiro para uma estrutura.

Armado com esta informação, vamos complicar um pouco o exemplo.

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

Vários novos elementos apareceram no código java de uma vez - Scope , Array e Pointer . Ao trabalhar com código nativo, você terá que operar dados fora da pilha, o que significa que precisará alocar memória independentemente, liberar e monitorar independentemente a relevância dos ponteiros. Felizmente, há a Scope cuidando de todas essas preocupações. Seus métodos tornam fácil e conveniente alocar memória não especificada, memória para matrizes, estruturas e linhas C, obter ponteiros para essa memória e também liberá-la automaticamente após a conclusão do bloco try-with-resources e alterar o estado dos ponteiros criados para que a chamada isso levou à exceção, não à falha da máquina virtual.

Para analisar o trabalho de estruturas e ponteiros, vamos complicar um pouco mais o exemplo.

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

De interesse aqui é como a interface da estrutura é declarada e como a memória é alocada para ela. Observe que um novo elemento apareceu na declaração ldl - os valores entre parênteses após os rótulos. Esta é uma anotação de tag em formato abreviado. O formulário completo ficaria assim: i32(name=x) . A anotação de etiqueta permite correlacioná-la com o método da interface.

Antes de avançar para a promessa no título, resta destacar outra maneira de interagir com o código nativo. Todos os exemplos anteriores chamavam funções nativas, mas às vezes o código nativo precisa chamar o código java. Por exemplo, se queremos classificar uma matriz usando qsort , precisamos de um retorno de chamada.

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

É fácil ver que os anúncios LDL, que não são particularmente fáceis de ler, rapidamente se transformam em designs furiosos. Mas qsort não é a função mais difícil. Além disso, pode haver dezenas de estruturas e dezenas de funções em bibliotecas reais, escrever interfaces para elas é uma tarefa ingrata. Felizmente, ambos os problemas são facilmente resolvidos usando o utilitário jextract , que gera todas as interfaces necessárias com base nos arquivos de cabeçalho. Vamos voltar ao primeiro exemplo e processá-lo com este utilitário.

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

E use o arquivo jar resultante para criar e executar o código java:

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

Embora não seja particularmente impressionante, mas permite que você entenda o princípio. E agora vamos fazer o mesmo com 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(); } } 

Geramos interfaces:

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

Compile e execute:

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

Parabéns, seu aplicativo java acabou de baixar o interpretador Python e executou o script nele!

imagem

Mais exemplos podem ser encontrados nas instruções para pioneiros .

Projetos Maven com exemplos do artigo podem ser encontrados no GitHub .

A API do PS está passando por mudanças rápidas. Nas apresentações de alguns meses atrás, é fácil ver o código que não será compilado. Os exemplos deste artigo não estão imunes a isso. Se você se deparar com isso, envie-me uma mensagem, tentarei corrigi-lo.

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


All Articles