使用Panama项目将Python解释器嵌入Java应用程序中

几天前,我看到了Brian Goetz的一条推文,但直到今天,我的手才开始尝试使用示例。

图片

我想简单地谈一谈。

关于哈布雷的巴拿马工程 已经写过 。 要了解它是什么以及为什么,您应该在这里阅读采访。 我将仅展示几个有关如何使用本机活页夹的简单示例。

首先,您需要C编译器,如果您使用的是Linux或MacOS,那么您已经有了C编译器。 如果使用Windows,则必须首先安装Visual Studio 2017的构建工具 。 而且,当然,您需要具有巴拿马支持的OpenJDK。 您可以通过构建相应存储库的“外国”分支或通过下载Early-Access构建来获取它。

让我们从一个最小的示例开始-一个简单的函数,将两个数字相加:

加法器
#ifndef _ADDER_H #define _ADDER_H __declspec(dllexport) int add(int, int); #endif 

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

在DLL中编译

 cl /LD adder.c 

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

对于使用JNR的人员,源应该有很多熟悉之处:声明本机库的接口,加载该库,将其连接到该接口,然后调用本机函数。 主要区别在于在批注中使用布局定义语言来描述本机数据到Java类型的映射。

很容易猜到表达式“ (i32 i32)i32 ”表示一个函数,该函数需要两个32位整数并返回一个32位整数。 标签i表示三种基本类型之一-具有小尾数字节顺序的整数。 除此之外,还经常找到uf分别是无符号整数和浮点数。 相同的标签用于指示big-endian的顺序,但大写字母IUF 另一个常见的标签是v ,用于void 。 标签后面的数字表示使用的位数。

如果数字在标签前面,则标签表示一个数组: [42f32] -由float类型的42个元素组成的数组。 方括号组标签。 除数组外,它还可用于指示结构( [i32 i32] -具有两个类型为int两个字段的结构)和联合( [u64|u64:f32]float的指针)。

冒号用于指示指针。 例如, u64:i32是指向int的指针, u64:v是类型为void的指针,而u64:[i32 i32]是指向结构的指针。

有了这些信息,让我们使示例复杂一些。

累加器
 __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)); } } } 

几个新元素同时出现在Java代码中ScopeArrayPointer 。 使用本机代码时,您将必须处理堆外数据,这意味着您将必须独立分配内存,独立释放和监视指针的相关性。 幸运的是, Scope所有这些问题。 它的方法可以轻松方便地分配未指定的内存,用于数组,结构和C线的内存,获取指向该内存的指针,并在try-with-resources块完成后自动释放它,并更改创建的指针的状态,以便进行调用它导致异常,而不是虚拟机崩溃。

为了看一下结构和指针的工作,让我们使这个例子复杂一些。

移动器
 #ifndef _ADDER_H #define _ADDER_H typedef struct { int x; int y; } Point; __declspec(dllexport) void move(Point*); #endif 

移动器
 #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()); } } } 

这里有趣的是如何声明结构的接口以及如何为其分配内存。 请注意,ldl声明中出现了一个新元素-标签后面括号中的值。 这是缩写形式的标签注释。 完整格式如下所示: i32(name=x) 。 标签注释使您可以将其与接口方法相关联。

在继续标题中的Promise之前,仍然需要强调另一种与本机代码交互的方式。 前面所有示例都称为本机函数,但有时本机代码需要调用Java代码。 例如,如果要使用qsort对数组进行排序,则需要回调。

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

不难看出,并非特别易于阅读的LDL广告很快就会变成疯狂的设计。 但是qsort不是最困难的功能。 另外,在实际的库中可以有数十种结构和数十种功能,为它们编写接口是一项艰巨的任务。 幸运的是,使用jextract实用程序可以轻松解决这两个问题,该实用程序将基于头文件生成所有必需的接口。 让我们回到第一个示例,并使用此实用程序对其进行处理。

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

并使用生成的jar文件来构建和运行Java代码:

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

虽然不是特别令人印象深刻,但是可以让您了解原理。 现在让我们对python37.dll进行相同的操作(最终!)

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

我们生成接口:

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

编译并运行:

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

恭喜,您的Java应用程序刚刚下载了Python解释器并在其中执行了脚本!

图片

有关开拓者说明,请参见更多示例。

带有本文示例的Maven项目可在GitHub上找到

PS API目前正在快速变化。 在几个月前的演示中,很容易看到无法编译的代码。 本文中的示例并非无法避免。 如果您遇到此问题,请给我发消息,我会尽力解决。

Source: https://habr.com/ru/post/zh-CN440470/


All Articles