将Python脚本添加到非Python应用的“正确”方法

22 投票
3 回答
5732 浏览
提问于 2025-04-16 02:05

我现在正在为我的桌面应用程序(C++)添加一个功能,让用户可以通过用 Python 编写的插件来扩展应用的功能。

最简单的方法就是把 Python 的静态库嵌入进来,然后跟着网上的各种教程,学习如何初始化和调用 Python 文件,这样基本上就完成了。

但是……

我想要的更像是 Blender 的做法。Blender 允许用户通过 Python 脚本进行完全自定义,而且它需要一个外部的 Python 可执行文件。(也就是说,Python 并没有真正地嵌入到 Blender 的可执行文件里。)这样,你在编写 Blender 脚本时,可以使用你在 site-packages 目录下已经有的任何模块。不过,这样做并不推荐,因为会限制你脚本的可移植性。

所以,我想知道是否已经有一种方法可以兼顾两者。我想要一个插件系统,使用:

  • 一个嵌入式的 Python 解释器。

    Blender 的方法的缺点是,它要求你在系统上全局安装一个特定的、可能过时的 Python 版本。而嵌入式解释器让我可以控制使用的 Python 版本。

  • 防火墙插件。

    类似于每个插件的 virtualenv;允许它们安装所需的所有模块,但又能避免与其他插件可能产生的冲突。也许 zc.buildout 是一个更好的选择,但我对此非常开放,欢迎建议。我对实现这个目标的最佳方法有些迷茫。

  • 尽可能无痛……

    对用户来说。我愿意多花点时间,只要以上大部分对插件开发者来说尽可能透明。


如果你们中有谁有这方面的经验,帮忙的话我会非常感激。:)


编辑: 简单来说,我想要的是 virtualenv 的简单性,但没有捆绑的 Python 解释器,并且有一种方法可以像 zc.buildout 那样以编程方式激活特定的“虚拟环境”,比如通过操作 sys.path(使用 sys.path[0:0] = [...] 的技巧)。

虽然 virtualenvzc.buildout 各自包含了我想要的一部分功能,但都不能生成可移动的构建,这样我或插件开发者就不能简单地打包并发送到另一台电脑上。

仅仅操作 .pth 文件,或者在我的应用程序中直接操作 sys.path 的脚本只能让我走到一半。但当需要编译模块时,比如 PIL,这样做就不够了。

3 个回答

4

我觉得把Python嵌入到其他程序里,比如用Boost.Python,没什么问题。这样做你能得到你想要的一切:

  • 它会被嵌入进来,并且会成为一个解释器(这样你就可以实现自动补全等功能)
  • 你可以为每个脚本创建一个新的解释器,这样就能有完全独立的Python环境
  • ...而且尽可能透明

我的意思是,你还是需要提供和实现一个API,但1)这其实是件好事,2)Blender也这么做,3)我实在想不出还有什么其他方法能让你避免这项工作...

附言:我对Python和Boost.Python的经验不多,但我在Lua和LuaBind方面有很多经验,它们其实有点相似。

4

如果你想让自己和用户的体验尽可能简单,考虑扩展 Python 而不是嵌入它。

  • 嵌入 Python 的话,和其他软件的结合会很麻烦——一次只能有一个程序在用 Python 脚本。而扩展的话,用户可以在任何运行 Python 的地方使用你的软件;
  • 为了让脚本编写者能用到你的东西,你不需要自己去初始化一个解释器。解释器已经为你准备好了,这样可以省去不少麻烦。
  • 你不需要创建特殊的内置变量或假模块来注入到你的嵌入式解释器里。只需提供一个真正的扩展模块,当你的模块第一次被导入时就可以初始化所有内容。
  • 你可以使用 distutils 来分发你的软件。
  • 像 virtualenv 这样的工具可以直接使用——你或用户不需要再想出新的工具。用户也可以使用她喜欢的 IDE、调试工具或测试框架。

嵌入其实对你和用户都没有什么好处。

9

一种有效的方法是使用消息传递或通信进程的架构,这样你就可以用Python来实现你的目标,但也不局限于Python。

------------------------------------
| App  <--> Ext. API <--> Protocol | <--> (Socket) <--> API.py <--> Script
------------------------------------

这个图示试图展示的是:你的应用程序通过消息传递与外部进程(比如Python)进行沟通。这在本地机器上是高效的,并且可以移植,因为你可以定义自己的协议。你只需要给用户提供一个Python库,这个库实现了你的自定义API,并通过发送-接收的通信循环在用户的脚本和你的应用之间进行交流。

定义你的应用程序的外部API

你的应用程序的外部API描述了外部进程必须能够交互的所有功能。例如,如果你希望你的Python脚本能够在你的应用中画一个红色圆圈,你的外部API可能包括Draw(Object, Color, Position)这个函数。

定义一个通信协议

这是外部进程通过你的外部API与应用程序沟通所使用的协议。常见的选择有XML-RPC、SunRPC、JSON,或者你自己定义的协议和数据格式。这里的选择需要适合你的API。例如,如果你要传输二进制数据,JSON可能需要进行base64编码,而SunRPC则假设使用二进制通信。

构建你的应用程序的消息系统

这就简单了,只需一个无限循环来接收消息,处理应用中的请求,并通过同一个通道回复。例如,如果你选择了JSON,那么你会收到一个包含执行Draw(Object, Color, Position)指令的消息。在执行请求后,你会回复这个请求。

为Python(或其他语言)构建一个消息库

这甚至更简单。再次强调,这也是一个循环,代表库的用户(也就是编写Python脚本的用户)发送和接收消息。这个库唯一需要做的就是提供一个程序接口,连接到你的应用程序的外部API,并将请求转换为你的通信协议,这一切对用户都是隐藏的。

例如,使用Unix套接字会非常快速。

插件/应用程序的会合

发现应用插件的一个常见做法是指定一个“众所周知”的目录,插件应该放在这里。比如说:

~/.myapp/plugins

下一步是让你的应用程序查看这个目录中是否存在插件。你的应用程序应该具备一些智能,能够区分哪些Python脚本是真正的应用脚本,哪些不是。

假设你的通信协议规定每个脚本将通过标准输入/输出使用JSON进行通信。一种简单有效的方法是在你的协议中指定,脚本第一次运行时会向标准输出发送一个MAGIC_ID。也就是说,你的应用程序读取前8个字节,寻找一个特定的64位值,以识别它是一个脚本。

此外,你应该在外部API中包含一些方法,让你的脚本能够自我识别。例如,一个脚本应该能够通过外部API告知应用程序一些信息,比如名称描述功能期望,基本上就是告诉应用程序它是什么,以及它将要做什么。

撰写回答