Pyjnius: PyJnius 与 JPype

创建于 2020-07-16  ·  27评论  ·  资料来源: kivy/pyjnius

我是 JPype 的主要作者。 作为更新 JPype 文档的一部分,我将 PyJnius 添加到 JPype 的替代代码列表中。 不幸的是,在使用 PyJnius 玩了两个小时后,我无法想出任何看起来 PyJnius 优于 JPype 的东西。 我从代理、定制器、多维数组处理、javadoc 集成、GC 处理、缓冲区、科学代码集成(numpy、matplotlib 等)、调用者敏感方法、文档,甚至大多数情况下的执行速度所看到的每一个方面目前都已涵盖在 JPype 中比 PyJnius 更完整。 然而,作为 JPype 的作者,也许我正在关注我看重的方面,而不是 PyJnius 团队的方面。 你能更好地阐述这个项目的优势吗? 这个项目的价值主张是什么,它的目标受众是什么,它的目标是做什么而替代方案尚未涵盖?

最有用的评论

大约 2 年前,我接触了所有 Java 桥接代码。 不幸的是,PyJnius 代码显然被遗漏了,因为它从未出现在我的搜索中。 我也会在这一轮中错过它,除非我正在搜索上次发布不错的技术新闻稿的页面并偶然发现了一个讨论这两个项目的博客。 不知道我是如何在两年内错过了同一地区的另一个活跃项目,但这显然是我的错。

听起来您将 JPype 与 Py4J 混淆了,后者是另一个主要的桥接代码。 他们使用套接字做所有事情,既有好处也有缺点。 我同样发现该项目不符合我的要求。

我还没有对支持 android 需要什么做任何研究。 不过,如果我有一些技术规格,那应该是可能的。 除了原生 JNI 和对 Python 的 C API 的普通调用之外,我们没有做任何事情。

就方法而言,JPype 使用“startJVM()”命令专门使用 JNI 将 JVM 与 Python 结合。 尽管下一个版本 (2.0) 也将提供相反的能力,其中 Python 可以从 Java 中启动。 它通过分层方法来做到这一点。 有一个 Python 层,它包含所有作为前端的高级类,一个 CPython 私有模块,带有包含入口点的基类,一个 C++ 支持层,处理所有类型转换和匹配以及充当本机模块用于 Java 库,以及一个 Java 库,其中包含所有实用程序任务(保存对象的生命周期、为切片和异常创建支持类以及 javadoc 提取器/渲染)。

8 年前 JPype 有点乱。 它试图同时支持 Ruby 和 Python,因此 C++ 层是一堆要使用的包装器,而前端全部使用 Python,因此速度非常慢。 它也在返回类型上分叉,因为可以编译 numpy 支持导致返回不同的对象。 它需要很多像 JException 这样的适配器类来充当原生 Python 和 Java 对象不同的代理。 但所有这些问题在我加入项目后的 3 年里都得到了解决。 JPype 的两个主要目标(对我而言)是提供足够简单的语法,让熟悉编程的物理学家能够使用 Java 并与科学的 Python 代码高度集成。 我们通过使那些由 Java 支持的对象具有“所有的装饰”CPython 对象包装器来实现这一点。 我们不是转换 Java 原始数组,而是将 Java 原始数组设为新的本机 Python 类型,它实现了与 numpy 相同的所有入口点,并且所有这些都由内存缓冲区传输支持。 因此,我们通过调用list(jarray)np.array(jarray)进行快速转换。

为了使其适应 Android,启动序列可能需要重新设计,并且用于加载其内部库的 thunk 代码需要替换为更传统的 JNI 模型。 我已经在下一个版本中删除了 thunk 代码,因此已经满足了后者。 只需要前者。

我可以看到的方法的主要区别是 PyJnius 转换数组。 这对于科学编码来说似乎是非常令人望而却步的,其中来回传递大数组(通常永远不会被转换)是首选的 JPype 风格。 要求转换的决定然后会强制选择传递值和传递引用等选项,但这可能会导致更大的问题,就像您有一个多参数调用一样,您正在为所有参数选择一个策略。 它还会使处理多维数组变得困难。 (我认为,如果可以实现,像obj.method(1, jnius.byref(list1), list2)这样使用的适配器类会提供更好的控制)。 另外,还有很多JPype解决的问题,比如GC联动、调用者敏感方法等等。 如果不出意外,请查看 JPype 代码,看看是否有任何可以使用的好主意。

所有27条评论

感谢您伸出援手!

我可能记错了,因为多年来我没有看过 jpype,但我虽然它使用了不同的方法,通过服务器(因此是 IPC)而不是共享内存与 JVM 对话(但你的自述文件暗示相反,并且我没有看到通过快速浏览代码来反驳这一点的提示,所以我可能完全错了),并且这种方法使其在 android 上无法使用,例如(这是开发 pyjnius 的主要原因,尽管有些人确实在桌面平台上使用它)。 另一方面,我在 JPype 代码库中没有看到太多关于 android 支持的提示,除非这就是native目录的内容,因为它的 jni.h 似乎取自 AOSP?

但老实说,当项目开始时,我们根本不知道 JPype,这只是从必须手动编写 jni 代码到接口到特定 android 类以获得 kivy 支持的一步。 我相信@tito做了一些挖掘以在我们了解之后进行比较,但我不记得他是否看到了不尝试切换的具体原因。

你好。 我记得 JPype 的名字,但是在搜索我们可以使用什么的时候,我不记得为什么它没有被使用,老实说,8 年前 :) 一开始的唯一目标是能够与 Android 通信API,但没有像当时 P4A 项目那样使用中间 RPC 服务器。

大约 2 年前,我接触了所有 Java 桥接代码。 不幸的是,PyJnius 代码显然被遗漏了,因为它从未出现在我的搜索中。 我也会在这一轮中错过它,除非我正在搜索上次发布不错的技术新闻稿的页面并偶然发现了一个讨论这两个项目的博客。 不知道我是如何在两年内错过了同一地区的另一个活跃项目,但这显然是我的错。

听起来您将 JPype 与 Py4J 混淆了,后者是另一个主要的桥接代码。 他们使用套接字做所有事情,既有好处也有缺点。 我同样发现该项目不符合我的要求。

我还没有对支持 android 需要什么做任何研究。 不过,如果我有一些技术规格,那应该是可能的。 除了原生 JNI 和对 Python 的 C API 的普通调用之外,我们没有做任何事情。

就方法而言,JPype 使用“startJVM()”命令专门使用 JNI 将 JVM 与 Python 结合。 尽管下一个版本 (2.0) 也将提供相反的能力,其中 Python 可以从 Java 中启动。 它通过分层方法来做到这一点。 有一个 Python 层,它包含所有作为前端的高级类,一个 CPython 私有模块,带有包含入口点的基类,一个 C++ 支持层,处理所有类型转换和匹配以及充当本机模块用于 Java 库,以及一个 Java 库,其中包含所有实用程序任务(保存对象的生命周期、为切片和异常创建支持类以及 javadoc 提取器/渲染)。

8 年前 JPype 有点乱。 它试图同时支持 Ruby 和 Python,因此 C++ 层是一堆要使用的包装器,而前端全部使用 Python,因此速度非常慢。 它也在返回类型上分叉,因为可以编译 numpy 支持导致返回不同的对象。 它需要很多像 JException 这样的适配器类来充当原生 Python 和 Java 对象不同的代理。 但所有这些问题在我加入项目后的 3 年里都得到了解决。 JPype 的两个主要目标(对我而言)是提供足够简单的语法,让熟悉编程的物理学家能够使用 Java 并与科学的 Python 代码高度集成。 我们通过使那些由 Java 支持的对象具有“所有的装饰”CPython 对象包装器来实现这一点。 我们不是转换 Java 原始数组,而是将 Java 原始数组设为新的本机 Python 类型,它实现了与 numpy 相同的所有入口点,并且所有这些都由内存缓冲区传输支持。 因此,我们通过调用list(jarray)np.array(jarray)进行快速转换。

为了使其适应 Android,启动序列可能需要重新设计,并且用于加载其内部库的 thunk 代码需要替换为更传统的 JNI 模型。 我已经在下一个版本中删除了 thunk 代码,因此已经满足了后者。 只需要前者。

我可以看到的方法的主要区别是 PyJnius 转换数组。 这对于科学编码来说似乎是非常令人望而却步的,其中来回传递大数组(通常永远不会被转换)是首选的 JPype 风格。 要求转换的决定然后会强制选择传递值和传递引用等选项,但这可能会导致更大的问题,就像您有一个多参数调用一样,您正在为所有参数选择一个策略。 它还会使处理多维数组变得困难。 (我认为,如果可以实现,像obj.method(1, jnius.byref(list1), list2)这样使用的适配器类会提供更好的控制)。 另外,还有很多JPype解决的问题,比如GC联动、调用者敏感方法等等。 如果不出意外,请查看 JPype 代码,看看是否有任何可以使用的好主意。

@Thrameos我们最近希望合并的一件事是将 Python lambda 用于 Java 函数式接口。 见https://github.com/kivy/pyjnius/pull/515 ; 我在 JPype 中没有看到?

JPype 从 1.0.0 开始支持来自 Functional 接口的 lambda。 这是 3 月 30 天内推至 1.0 的 30 次拉动的一部分。

JPype 的潜伏期很长。 最初于 2004 年开始运行,一直持续到 2007 年。随着用户群在 2015 年左右将其复活以将其移植到 Python 3,它得到了很大的推动。然后在 2017 年,它被一个国家实验室接收使用,该实验室从 0.6 开始携带它。 3 到 0.7.2。 在此期间,所有努力都集中在改进和强化提供接口的核心技术上。 但在第二次内核重写后,这终于在 3 月完成,所以我们终于可以推动 1.0.0。 从那时起,我们一直在添加我的“30 晚 30 次拉取”愿望清单活动中“缺失”的所有内容。 由于工作量太大而我无法实施的所有积压终于被清除了(问题从 50 下降到 20,征求用户询问他们需要什么等)。 因此,可能有许多您在以前版本中没有的功能现在可用。 在不到一周的时间里,我一直在处理大多数功能请求,因此我只剩下 3 个大的请求(反向桥接、在 Python 中扩展类、启动第二个 JVM 的能力)。

该项目将重新陷入沉睡,因为我在 6 个月的时间里努力完成反向桥代码,这将允许 Java 调用 Python 并为 Python 库生成存根,以便它们可以用作 Java 本地库。 它使用 ASM 动态构建 Java 类,以便实现对 Python 的本机支持。 仍然没有像 Jython 那样完全集成,但可能足够接近以至于不会有太大区别。

非常感谢您的详细解释,事实上,如果有的话,肯定有我们可以使用的想法,值得研究代码,更早地查看代码,我看到的代码质量和结构都非常干净该项目,所以祝贺所有的工作。 您对我与 Py4J 的混淆是正确的,我想当我查看 JPype 时,它​​一定处于您描述的混乱状态,并且此时使用它一定比 PyJNIus 复杂得多。

您关于按值传递/转换的观点非常正确,并在此处引发了一些最近的讨论,因为@hx2A研究了如何提高性能,并将转换回 python 类型设为可选(如将一次性列表传递给 java,获取它)转换为java列表,无论是否被java修改,然后转换回python,只是为了垃圾收集它,当然是次优的,我们现在至少可以避免第二部分,代价是使用关键字参数,这是安全的,因为java 不支持它们,所以没有签名冲突,但它在语法方面肯定有点嘈杂)。

至于 JPype 和 PyJNIus 之间可能的区别,我们可以使用 python 类实现 java 接口并将它们传递给 java 用作回调,另一方面,如果我们想扩展 java 类,我们确实需要生成 java 字节码来自 python,因为需要使用一些 android api,我们现在无法涵盖,我不确定我是否从你的评论中正确推断,但也许你没有能力让 java 类调用你的像这样的python代码(使用接口)。

JPype 可以在 Python 中实现接口。 只需向普通 Python 类添加装饰器即可。

from java.util.function import Consumer

@jpype.JImplements(Consumer)
class MyConsumer:
   @jpype.JOverride
   def apply(self, obj):
       pass

如果类是在 JVM 启动之前定义的,则在@JImplements 中使用字符串,可以一次实现多个接口,但检查所有方法以查看它们是否已实现。 主要区别在于 JPype 使用分派方法(所有重载都转到同一方法)而不是重载方法。 这是因为我们希望能够从 Java 和 Python 调用方法。 如果这是一个需要的特性,我可以添加单独的重载,但没有人要求它。

(编辑:我们没有使用 Python 继承的原因是在添加它时我们仍然支持 Python 2,这导致了很多元类问题,一旦类扩展到位,我们将进一步清理。所以注释在声明时评估一次更干净。)

我们使用相同的系统为类实现定制器(dunder?)

@jpype.JImplementationFor("java.util.ArrayList")
class ArrayListImpl:
    def __getitem__(self, i):
        return self.get(i)
    @jpype.JOverride
    def addAll(self, list):
        # Decide if we need to convert or can call directly.
        ...

我们还使用注释来定义隐式转换器,例如“所有 Python 路径对象都转换为 java.io.File”

 @jpype.JConversion("java.io.File", instanceof=pathlib.PurePath)
 def _JFileConvert(jcls, obj):
       Paths = jpype.JClass("java.nio.file.Paths")
       return Paths.get(str(obj))

显然,使用这种逻辑比特殊的 C 方法要慢一些,但它保持了较高的可读性和灵活性。 我一直在根据需要将已被证明是瓶颈的关键路径推回 C。

我们还提供了一些语法糖来使事情变得干净MyJavaClass@obj => 转换为 MyJavaClass (Java 等效(MyJavaClass)obj )或cls=JInt[:] => 创建一个数组类型( cls=int[].class ) 或a=JDouble[10][5] => 创建一个多维数组 ( double[][] a = new double[10][5] )。

我一直在研究从 JPype 扩展类的原型。 我们遇到了与某些 Swing 类和其他 API 需要扩展类的问题相同的问题。 到目前为止,我已经制定了一个解决方案,它为每个重写的方法创建一个带有 HashMap 的扩展类。 如果该入口点的哈希图中没有任何内容,则将其传递给 super,否则它调用代理方法处理程序。 但是我决定在反向桥接完成后这将是最容易实现的,这样 Java 就可以实际处理 Python 方法,而不是通过 Java 代理方法。 所以在我让原型工作之前还有大约 6 个月的时间。 您可以查看 epypj 分支(我的反向桥接器的名字)以了解从 Java 调用回 Python 的工作原理以及使用 ASM 生成调用程序以动态生成 Java 类的模式。

至于管理垃圾收集,很少有 JPype 可以完成这项工作。 首先是 JPypeReferenceQueue (native/java/org/jpype/ref/JPypeReferenceQueue),它将 Python 对象的生命周期绑定到 Java 对象。 这用于创建缓冲区和其他 Java 需要在一段时间内访问 Python 概念的东西。 第二个是使用全局引用,以便 Python 可以在范围内保存 Java 对象。 这需要垃圾收集器链接 (native/common/jp_gc.cpp),它侦听任一系统触发 GC 并在特定条件(池大小、相对增长)时将其 ping 到另一个系统。 最后代理需要使用弱引用,否则它们会形成循环(因为代理持有对 Java 部分的引用,而 Java 部分又指向 Python 实现)。 最终我打算使用一个代理来允许 Python 遍历 Java,但这是在路上。

我是在桌面上而不是在 android 上使用 pyjnius 的人之一。 当我开始构建我的项目时,我不知道 JPype,但我做了一些调查,看看有什么区别。

pyjnius 的一项独特功能是调用者可以决定是否包含受保护和私有的方法和字段。 我的偏好是仅公开,但我理解使非公开字段和方法可用是有用的论点。

性能对我的项目至关重要。 我对以下课程进行了一些测试:

package org.pkg;

public class MyClass {

  public MyClass() {
  }

  public int number = 42;

  public float add1(float x, float y) {
    return x + y;
  }

  public float add2(float x, float y) {
    return x + y;
  }

  public float add2(int x, int y) {
    return x + y;
  }
}

在JPype中:

In [1]: import jpype
   ...: import jpype.imports
   ...: jpype.startJVM()
   ...: from org.pkg import MyClass
   ...: myInstance = MyClass()
   ...:

In [2]: %timeit myInstance.number
640 ns ± 2.65 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [3]: %timeit myInstance.add1(10.3, 20.5)
2.13 µs ± 24.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [4]: %timeit myInstance.add2(10.3, 20.5)
2.19 µs ± 9.41 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

在pyjnius:

In [1]: import jnius

In [2]: MyClass = jnius.autoclass('org.pkg.MyClass')

In [3]: myInstance = MyClass()

In [4]: %timeit myInstance.number
161 ns ± 0.104 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [5]: %timeit myInstance.add1(10.3, 20.5)
1.04 µs ± 8.16 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [6]: %timeit myInstance.add2(10.3, 20.5)
2.71 µs ± 11.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

除了重载方法之外,Pyjnius 的速度要快一些。 我们应该比较注意当方法重载时如何决定调用哪个方法。 Pyjnius 有这种评分机制,这似乎增加了很多开销。 JPype 可以更快地做出决定。

最后,出于基准测试的目的:

In [9]: def add(x, y):
   ...:     return x + y
   ...:

In [10]: %timeit add(10.3, 20.5)
82.9 ns ± 0.187 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

当然,几微秒的差异是微不足道的,但如果一个人非常快速地拨打数千个小电话,这确实会加起来,而我需要这样做。

JPype 与 numpy 的集成非常好且易于使用。 我可以看到研究人员如何使用它编写脚本,无需复杂的语法即可将大型数组传递给 Java 库。 我还需要传递大数组,我使用tobytes()和可以接收字节的特殊 Java 代码来传递,但显然这不是那么整洁或方便。

不幸的是,JPype 的速度是一种“现状”的前景。 JPype 非常具有防御性,试图防范坏事,这意味着有很多重要的开销。 这意味着,例如,每当进行 Java 调用时,它都会确保线程已连接,因此如果从外部线程(例如 IDE)调用,我们不会出现段错误。 我的本地用户组都是科学家,所以入口点都非常小心,以防出现一些非常可怕的交叉布线。 如果它们出现了段错误(无论它有多疯狂),那么我就失败了。 (这可以解释 1500 次测试,包括故意构建坏对象。)

其次,个人行动的速度可能因正在执行的工作中的微小差异而有很大差异。 您给出的微不足道的例子是最糟糕的情况之一。 在某些情况下,根据正在访问的字段类型实际上可能会改变访问速度。

对于您的速度示例,您正在请求一个 int 字段。

  • 在 PyJnius 中,它为对象查找创建一个描述符,访问该字段,创建一个新的 Python long 然后返回它。
  • 在 JPype 中,它为对象查找创建一个描述符,访问该字段,创建一个新的 Python long,然后为 Java int 创建一个包装类型,将 Python long 内存复制到 JInt(因为 Python 缺乏创建派生的方法)整数类),然后将插槽与 Java 值绑定,最后返回结果 JInt。

因此,即使是在访问字段时进行速度基准测试这样微不足道的事情,也不是那么微不足道。
不同之处在于一个返回一个 Python 长整数,另一个返回一个实际的 Java 整数(遵循 Java 转换规则),它可以传递给另一个重载方法并正确绑定。 返回包装器类型的工作不仅仅是简单地返回 Python 类型,因此在速度上存在巨大差异。

我试图通过测试几种不同的字段类型来证明这一点。 不幸的是,当我测试对象字段时,jnius 在代码“harness.objectField = Harness”上出现了段错误。 我不确定为什么那条特定的交叉布线会导致问题,但对我来说却失败了。 我对 JPype 的速度没有太大兴趣,除了消除严重的违规行为,这使调用速度提高了 3-5 倍,某些数组访问速度提高了 300 倍。 但也许我应该回顾一下,看看哪些方面可以改进。 我怀疑我是否可以将其简化为像 PyJnius 那样的简单框架,而无需取消安全性或取消退货合同(我做不到)。 最多还有 10-30% 的加速是可能的,

至于访问私有和受保护字段的功能,当然可以做到。 我更喜欢用户使用反射或其他内部访问方法,而不是直接公开对象。 如果我必须提供类似的东西,我可能会创建一个名为_private字段,其中包含未公开的字段。 JPype 只为每种类型提供一个类包装器,所以我没有太多细粒度控制的方式。 因此,您不能选择创建一个具有私有访问权限的类,然后创建相同类型的第二个对象,而最终不会公开私有。 我在字符串转换方面走上了这条道路,这是一场灾难,一些图书馆选择了一种策略,而另一些图书馆选择了不同的策略,导致不兼容。

我使用数组列表运行了一些测试。

import jpype
import timeit
jpype.startJVM()
ArrayList = jpype.JClass("java.util.ArrayList")

def pack():
    ja = ArrayList()
    for i in range(1000):
        ja.add(i)

def iter(ja):
    u = 0
    for i in ja:
        u+=i

def access(ja):
    u = 0
    for i in range(len(ja)):
        u+=ja.get(i)

def access2(ja):
    u = 0
    for i in range(len(ja)):
        u+=ja[i]


ja = ArrayList()
for i in range(1000):
   ja.add(i)

print("Pack arraylist %e"%( timeit.timeit("pack()", globals=globals(), number=1000)/1e6))
print("Iterate arraylist %e"%(timeit.timeit("iter(ja)", globals=globals(), number=1000)/1e6))
# Get is a direct call
print("Access(get) arraylist %e"%(timeit.timeit("access(ja)", globals=globals(), number=1000)/1e6))
# [] is emulated
print("Access([]) arraylist %e"%(timeit.timeit("access2(ja)", globals=globals(), number=1000)/1e6))

JPype

打包arraylist 2.768904e-06
迭代数组列表 5.208071e-06
访问(获取)数组列表 4.037985e-06
访问([])数组列表 4.690264e-06

尤尼斯

打包数组列表 3.322248e-06
迭代数组列表 4.099314e-06
访问(获取)数组列表 5.653444e-06
访问([])数组列表 7.762727e-06

这不是一个非常一致的故事,只是说它们可能略有不同,除非它们提供非常不同的功能。 如果您所做的只是访问方法,那么它们可能非常相似。 传递预先转换为 JPype 的数组快 100 倍,转换列表和元组 JPype 慢 2 倍(我们目前不使用向量访问或对元组有特殊的旁路)。 因此,底线取决于您的编码风格,其中一种可能会快得多。 但后来我的用户通常选择 JPype 以方便使用和防弹结构而不是速度。 (啊,我在开玩笑!他们很可能是从互联网上偶然发现的,因为 JPype 恰好是他们在 Google 上找到的第一个链接。)

至于 JPype 如何进行方法绑定,该细节应该在开发人员/用户指南中。 方法绑定首先根据 Java 规范中给出的规则对调度的方法列表进行预排序,这样如果某些东西隐藏了其他东西,它就会首先出现在列表中(代码可以在 native/java/org/jpype 中找到,因为我们使用 Java 实用程序类在第一次创建分派时进行排序)。 此外,每个方法都给出了哪个方法隐藏另一个方法的偏好列表。 解析首先检查每个参数以查看该参数是否有“Java 插槽”。 Java 插槽指向不需要转换的现有对象,因此在匹配之前将它们排除在外意味着我们可以使用直接规则而不是隐式规则。 然后它根据类型将参数匹配到 4 个级别(精确、隐式、显式、无)。 它是显式的捷径,没有跳转到下一个方法。 如果它得到一个精确的然后它会缩短整个过程以跳转到调用。 如果匹配,它将隐藏具有较少特定绑定的方法。 如果发现两个未隐藏的隐式匹配项,则将进入 TypeError。 一旦所有匹配都用完,就执行转换例程。 然后它释放 Python 全局锁并进行调用并重新获取全局锁。 查找返回类型并根据返回的类型创建一个新的 Python 包装器(它使用协变返回,因此返回的类型是派生最多的类型,而不是方法的类型)。 尽管可变参数存在一些复杂性,但这主要与重载数量成线性关系,但是预先构造的表意味着它会在尝试 foo(double, double) 之前尝试 foo(long, long) 并命中 (long , long) 会阻止 double, double 由于 Java 方法解析规则而被匹配。 我仍然可以实现一些加速,但这需要额外的缓存表。

我在 2017 年开始该项目时继承了带有捷径的订购系统。我添加了隐藏缓存和 java 插槽的方法,以消除我们的大部分开销。

我优化了方法的执行路径。 JPype 的修订数字是

打包arraylist 2.226081e-06
迭代数组列表 4.082152e-06
访问(获取)数组列表 2.962606e-06
访问([])数组列表 3.644642e-06

我的本地用户组都是科学家,所以入口点都非常小心,以防出现一些非常可怕的交叉布线。 如果它们出现了段错误(无论它有多疯狂),那么我就失败了。

是的,段错误是可怕的,当我开始使用 pyjnius 时,我得到了数百个。 我已经很长时间没有收到任何信息了,因为也许我解决了安全问题并将其内置到我的代码中。 现在一切正常。 我理解你的用例。 如果您的用户是直接使用 Java 对象使用各种 Java 库进行数据分析的科学家,那么段错误将导致他们丢失所有工作。 JPype 似乎更适合做最终用户通过 Python 直接使用 Java 对象的科学工作。 pyjnius 的主要用例是不同的,它与 Android 通信。 在这种情况下,安全问题是开发人员的问题,因此在安全与速度方面做出不同的选择也许是合适的。

我承认我不是“只要你按这个顺序踩这些方块就是安全的”的忠实粉丝。 当我开始在 JPype 上工作时,我花了将近一年的时间来证明所有入口点足以让我可以将代码传递给我的本地团队。 从那以后,我又增加了两年的 API 装甲。 除了少数少数人的 JVM 无法加载(这很难解决)之外,由于 JPype 已达到生产代码标准,因此几乎没有遗留问题。

至于速度和安全的权衡,速度很好,但如果你通过跳过安全操作来获得速度,对大多数用户来说通常是一个糟糕的交易。 无论您是在修改原型代码还是编写生产系统,不得不停止工作并尝试解决段错误都是用户不应该面临的分心。

如果有人愿意给我一些关于如何在 Android 模拟器上测试 JPype 的示例,我可以看到如何进行所需的修改。

为了在 android 上使用,我们将 pyjnius 打包为由 python-for-android 构建的 python 发行版的艺术,(通常使用 buildozer 作为更简单的接口,但它是相同的),然后构建一个提供此发行版的 python 应用程序,那么您的python代码可以在用户运行应用程序时导入pyjnius或任何其他python发行版中内置的python库。

因此,第一步是在发行版中编译 jpype,当您告诉它是您要求的一部分时,这通常由 p4a (https://github.com/kivy/python-for-android/) 完成(通常(但并非总是)需要一个“配方”来向 p4a 解释如何构建非纯 python 的库,pyjnius 的一个可以在这里找到https://github.com/kivy/python-for-android/blob/以develop/pythonforandroid/recipes/pyjnius/__init__.py为例。 如果你使用 buildozer,你可以使用buildozer.specp4a.local_recipes设置来声明一个可以找到需求配方的目录,这样你就不需要 fork python-for-android使用你的食谱。

我建议使用 buildozer,因为它可以自动化更多事情https://buildozer.readthedocs.io/en/latest/installation.html#targeting -android 并且您仍然可以设置本地食谱来尝试。 第一次构建需要时间,因为它需要为 arm 构建 python 和许多依赖项,并且需要为它下载 android ndk 和 sdk。 您可能可以为应用程序使用默认的 kivy 引导程序,并创建一个“hello world”之类的应用程序,它会导入 jpype 并仅在标签中显示某些代码的结果,甚至在使用打印的 logcat 中,我不记住 kivy 在 android 模拟器中的运行情况,我从未使用过,但我认为有些用户使用过,并且通过加速设置它应该可以工作,afaik,否则您可以使用 sdl2 包装器或 webview 包装器,并使用烧瓶或瓶子服务器来显示东西,我首先尝试使用 kivy 服务器,因为它是迄今为止测试最多的。

您需要一台 linux 或 osx 机器(VM 很好,Windows 10 上的 WSL 也很好),以便为 android 构建。

如果你开始为 python-for-android 编写 jpype 配方,欢迎打开一个正在进行的 PR,以供可能出现的任何讨论。 如果它可以工作,那就太好了,尤其是如果它确实可以解决一些长期存在的 pyjnius 限制。 正如线程前面所讨论的,pyjnius 基本上涵盖了我们对 kivy 使用的核心要求,但它没有足够的开发能力来显着超越这一点。

@inclement我在 jpype-project/jpype#799 为 android 端口设置了 PR。 不幸的是,我真的不确定从这里去哪里。 它似乎试图运行 gradle,这确实不是正确的构建路径。

需要做的动作如下:

  • [x] 在构建(或它们的预编译版本)中包含所有 jpype/*.py 文件。
  • [x] 在 native/build.xml 上运行 Apache ant,并将生成的 jar 文件放在可以访问的地方。
  • [x] 在构建中包含 jar 文件(或等效文件)。
  • [x] 将 native/common 和 native/python 中的 C++ 代码编译到名为 _jpype 的模块中以包含在构建中。
  • [x] 包含一个 main.py 文件,它只是启动一个交互式 shell,以便我们现在可以手动测试它。
  • [ ] 将来我需要包含“ASM”或类似它的东西适用于android,以便我可以加载动态创建的类。
  • [x] 修补 C++ 代码,使其使用自定义引导程序加载 JVM 和配套 jar 文件并连接所有本机方法。
  • [ ] 用在 android 上运行的东西修补 jvmfinder,并且“startJVM”是自动调用的,而不是在 main 的开始处调用。
  • [ ] 修补 org.jpype 以便 jar 导航系统(这是导入的工作方式)可以在 android 上运行。

我查看了一些文档,但对于如何实现这一点,没有什么特别突出的。 我的项目布局与正常情况有些不同,因为我们没有将所有内容都放在主模块下(因为我们实际上正在构建组成系统的 3 个模块。jpype、_jpype 和 org.jpype。)我可能需要一个自定义配方来执行所有这些操作并禁用不需要的模式,例如运行 gradle(除非它正在做一些我无法分辨的有用的事情。)

它似乎试图运行 gradle,这确实不是正确的构建路径。

Gradle 是用作打包 APK 的最后一步的构建工具,它可能与您包含 jpype 无关。

在构建(或它们的预编译版本)中包含所有 jpype/*.py 文件。

一般来说,如果 jpype 本质上作为一个普通的 Python 模块执行,那么你的第一次配方尝试可能会完成大部分繁重的工作 - CppCompiledComponentsPythonRecipe会像运行python setup.py build_extpython setup.py install使用 NDK 环境。 这应该在为包含在应用程序中而构建的 python 环境中安装 jpype python 包。

在构建中包含 jar 文件(或等效文件)。

这可能是配方需要执行的额外步骤,归结为将您的 jar 文件(或任何您需要的文件)复制到 python-for-android 正在构建的 Android 项目中的某个适当位置。

将 native/common 和 native/python 中的 C++ 代码编译到名为 _jpype 的模块中以包含在构建中。

如果这是由 setup.py 处理的,这应该已经可以工作了,但可能需要一些调整。 如果不是,您可以在配方中包含您的编译命令(并通过设置适当的 env 变量让它们为 android 环境构建,正如您将在其他配方中使用self.get_env看到的那样)。

修补 C++ 代码,使其使用自定义引导程序加载 JVM 和配套 jar 文件并连接所有本机方法。

我希望这部分应该相当简单,只取决于使用正确的 android JNI 接口函数。 我们通过修补 pyjnius 而不是做一些适当的条件编译,以一种稍微有点hacky的方式来做到这一点,因为不同的帮助库提供不同的包装,如这个补丁所示。 这种复杂性不需要影响您,您只需调用正确的 android api 函数即可。

用在 android 上运行的东西修补 jvmfinder,并自动调用“startJVM”而不是在 main 的开始。

我对 JVM 不够熟悉,无法真正了解,但我认为 Android 希望您始终访问现有的 jvm,而您无法启动新实例。 这有关系吗(或者只是错了?)?

在 native/build.xml 上运行 Apache ant,并将生成的 jar 文件放在可以访问的地方。

由于不熟悉,我也不确定这个,这只是内部 jpype 构建步骤吗? 我不确定 java 版本如何交互,但我想在这里使用系统原生 ant 应该没问题。

它警告了 buildozer 不尊重 setup.py,因此它直接从顶部跳过到 gradle 步骤。 因此需要手动添加这些中间步骤。 我们的 setup.py 做了一堆自定义编译步骤,例如创建 hooks 文件以将 jar 文件与 dll 合并,这可能在此处不适用,因此跳过它很好,但它也无法找到 cpp 和 java 文件在 setup.py 中定义。 部分问题是我的 setup.py 太大了,我不得不拆分到模块 setupext 中。

我想首先我需要弄清楚如何运行一个干净的命令,以便我可以让进程运行一次并显示日志。 (我在玩的时候第一次跑了它。所以我没有干净的日志。)此外,我们应该将此对话移至 PR,以便我们可以更多地关注线程的主题。

FWIW 我能够将我的非 android 项目的核心移植到 JPype。 有两个小困难或差异:

  1. 无论我做什么, jpype.startJVM()classpath kwarg 似乎都被忽略了。 不过addClassPath()函数确实有效。 与他们使用jnius_config.add_classpath()相比,关心类路径顺序的开发人员可能需要进行一些调整。

  2. 实现 Java 接口效果很好,但有点不同。 在我的代码中,我有一个返回 Python 字符串列表的函数。 回想起来,我可能应该将每个字符串转换为 Java 字符串,但我没有想到这一点,并让接口函数定义返回一个 Java 对象列表以使其工作。 这在 pyjnius 中运行良好,它有效地使 python 字符串成为 Java 对象的子类。 这在 JPype 中不起作用,但我通过使用 JPype 的JString类转换字符串轻松解决了这个问题。

为实现的方法编写@JOverride比像@java_method('(Ljava/lang/String;[Ljava/lang/Object;)V')这样的代码简单得多。

一旦我处理了这两个问题,一切就基本正常了。 我确实有一些传递字节数组的代码,这些代码必须更改,但 JPype 对此有很好的支持。 此外,JPype 的隐式类型转换非常方便,可以替代我不得不到处撒的许多装饰器。

@Thrameos我尊重你的决心。 祝 JPype 在 Android 上工作好运。

感谢您的评论。

不确定类路径中可能有什么问题,但如果我不得不猜测它是您从哪里启动 Python 的话。 有时人们从一个模块启动 JVM,因为 Java 路径和 Python 路径相对于起始目录的规则不同,所以可能会导致 jars 被遗漏。 (用户指南中有关于此问题的部分)。

我每天都使用类路径功能,它是我们测试模式的一部分。 所以如果类路径没有得到尊重,它会导致一些相当大的失败。 也就是说,您不会是第一个在找到使复杂模式起作用所需的神奇位置和绝对路径集方面遇到困难的人。

这是一个示例,我从相对于开发区域的 Py 目录启动我的测试系统。

import jpype
import os

devel = os.path.dirname(__file__)
devel = os.path.join(devel, '..', '..')
devel = os.path.abspath(devel)  # Notice that I converted the path to absolute so that it doesn't matter where the
# PWD of Java will be when this script is called.   Otherwise, if I import this from a different location it will use the
# original PWD and Java will find nothing in the classpath.

classpath = [
    '%s/gov.llnl.math/dist/*' % devel,
    '%s/gov.llnl.rdak/dist/*' % devel,
    '%s/gov.llnl.rnak/dist/*' % devel,
    '%s/gov.llnl.rtk/dist/*' % devel,
    '%s/gov.llnl.rtk.gadras/dist/*' % devel,
    '%s/gov.llnl.rtk.response/dist/*' % devel,
    '%s/gov.llnl.utility/dist/*' % devel,
    ]

jpype.startJVM(classpath=classpath, convertStrings=False)

相对路径和绝对路径确实有效,但这在很大程度上取决于您从哪里开始。 addClassPath 具有特殊的魔法,可以确保所有路径都与调用者位置相关。 我想在 classpath 关键字参数中需要相同的逻辑。

我可以看到返回字符串列表的问题。 这将取决于触发行为的返回类型。 如果该方法被声明为返回String[]我希望在返回时将每个 Python 字符串强制转换为 Java。 如果该方法被声明为返回List<String> ,则会出现问题,因为 Java 泛型将其剥离,因此它将是List<Object> 。 根据 JPype 如何查看列表,它可能会也可能不会尝试转换,但这应该是定义的行为,所以我会检查它。 安全的解决方案是列表推导式,它应该能够转换所有返回项。

不确定类路径中可能有什么问题,但如果我不得不猜测它是您从哪里启动 Python 的话。 有时人们从一个模块启动 JVM

我就是这样做的!

我每天都使用类路径功能,它是我们测试模式的一部分。 所以如果类路径没有得到尊重,它会导致一些相当大的失败。 也就是说,您不会是第一个在找到使复杂模式起作用所需的神奇位置和绝对路径集方面遇到困难的人。

是的,我认为您会立即注意到这一点,而且我不是第一个遇到此问题的人。 感谢您提供代码示例,这很有帮助。 因此,我对我的代码进行了一些改进。

安全的解决方案是列表推导式,它应该能够转换所有返回项。

这正是我所做的,现在它可以正常工作。 谢谢!

几年前,我对 py4j 和 jnius 进行了比较,发现 jnius 的速度要快得多。

当我在某些地方看到 jpype 时,我总是认为它指的是 - http://jpype.sourceforge.net/并远离它,因为它似乎没有维护(没有奇怪地看到新项目)

在阅读此线程时,我再次尝试了我的基准测试(我的主要速度测试用例是读取 PMML 文件并为其评分) - 发现 jpype 的性能非常好。
一些结果:
https://gist.github.com/AbdealiJK/1dd5b7677435ba22f9ab3e26016bb3e7

# jpype
# createjvm: 0.550s
# loadmodel: tot=1.466451 max=1.064521s avg=0.014665s
# fields   : tot=0.019881 max=0.009795s avg=0.000199s
# score    : tot=0.033356 max=0.023338s avg=0.000334s

# jnius
# createjvm: 0.249s
# loadmodel: tot=1.773011 max=1.385274s avg=0.017730s
# fields   : tot=0.039058 max=0.012234s avg=0.000391s
# score    : tot=0.067590 max=0.031904s avg=0.000676s

# py4j
# createjvm: 0.222s
# loadmodel: tot=0.616913 max=0.027464s avg=0.006169s
# fields   : tot=0.699152 max=0.026426s avg=0.006992s
# score    : tot=0.389583 max=0.017620s avg=0.003896s

公平地说,直到 2020 年 3 月,JPype 的前端 API 都是用 Python(而不是 CPython)编写的。所以有很多旧的基准测试表明它提供的东西相当慢。 有很多后端问题必须通过内存管理来解决,移除整个多层包装器以支持 Ruby、多线程等,速度是最后需要解决的问题。

在这一点上,它应该非常快。 但是,如果您确实发现某些相对于其他软件包需要额外工作的东西,只需在问题中添加注释即可。 由于返回契约有一些限制,但是调用、数组传输和内存缓冲的主要路径在这一点上已经很好地解决了。

我也用 JPype 获得了不错的结果。 特别是,我从与 numpy 数组的集成中获得了很好的价值。 我已经能够添加以前无法实现的新功能,这使我能够以有意义的方式提高性能。

如果有人愿意尝试更新 kivy-remote-shell 工具以使其能够使用当前的 Python3 和 buildozer 进行构建,并逐步改进说明,这将有助于 JPype 移植工作大大。 我已经完成了启动和测试阶段的所有必要步骤。 但是如果没有一个环境来完成引导过程,就很难取得进展。 我可以在这个周末再次尝试更新远程shell工具(也许会成功)或者对kivy有更好了解的感兴趣的人可以完成这个前提任务,然后我可以用周末的时间完成我最有资格完成的技术工作. 尽管我会免费提供我的时间来帮助他人,但这是一种有限的资源,我在 android 移植工作上所做的任何工作都是对 Python 从 Java 桥接的延迟,许多其他人也对此感兴趣。

我希望 android 移植工作可以避免走 PyPy 移植工作的方式,我花了几个星期重新编写核心代码以能够处理差异,但随后遇到了一个技术问题,即对象系统中的一个微不足道的差异产生了错误,我找不到任何人可以帮助我追踪如何调试生成的代码中生成的错误报告。 虽然我不会为打翻的牛奶而哭泣,并且所有这些努力都被用于将 JPype 代码改进为其他有意义的方式,但最终那些想要使用 JPype 的用户被抛在了脑后。 如果这项努力失败了,并不是所有的努力都会丢失,因为我会循环回到它,但是一旦有东西排在队列的末尾,我很难在 6 个月内回到它,除非有人能够及时帮助我.

利益相关方的进度更新。

我成功地让 JPype 从 pythonforandroid 中启动,并且能够测试基本功能。 尽管由于 JVM 的差异,某些高级功能可能无法实现,但我相信绝大多数 JPype 都可以在 Android 平台上使用。 移植需要一段时间,因为它确实需要对 buildozer 和 pythonforandroid 项目进行一些升级(因此需要大量阅读源代码并寻求帮助)。 非常感谢这里的开发人员的响应,让我可以在一个周末完成这个过程中最困难的部分。 没有您的投入,这是不可能的。 我已将相关更改作为 PR 放入,但查看 PR 的积压,可能需要一段时间才能考虑。 现在我有了我需要的关键技术规范,我应该能够集成它并在 JPype 1.2 附近的某个地方有一个工作发布代码,名义上是在秋季晚些时候的日历上。 如果有很大的用户兴趣,我可以推动它,但它正在与来自 Java 的 Python 竞争,这是其他项目的一个重要特性。

如果有人想帮助加快这项工作,下一个艰难的步骤将是弄清楚如何使用部分构建系统构建一个一切就绪的 docker 镜像,以便我们可以执行构建、加载模拟器并运行测试台azure 管道(或其他一些 CI 系统)上的 android。 一旦我们有了一个可以检测哪些有效哪些无效的工作 CI,我们将更接近能够部署为稳定的软件。 不确定这是否应该放在 JPype 项目中,或者我们是否应该有一个单独的 android 测试项目。

此页面是否有帮助?
0 / 5 - 0 等级