嵌入 PyPy(已弃用)¶
PyPy 具有非常简陋且奇怪的嵌入接口,基于使用 cffi 以及 Python 比 C 更好的语言理念。它是在与 uwsgi 项目的 Roberto De Ioris 合作开发的。 PyPy uwsgi 插件 是使用嵌入 API 的一个很好的例子。
注意:您需要使用 --shared
选项编译的 PyPy,即具有 libpypy*-c.so
或 pypy*-c.dll
文件。这是默认设置。
注意
此页面中描述的接口保留用于向后兼容性。从 PyPy 4.1 开始,建议改为使用 CFFI 的 原生嵌入支持,它提供了一种更简单的方法,适用于 CPython 和 PyPy。
生成的共享库导出很少的函数。这些函数在 PyPy.h
中定义,该文件在 v7.3.8 中已删除,但 仍然可用。只要遵循一些原则,它们就足以完成您需要的一切。API 是
-
void rpython_startup_code(void);
这是一个您必须在调用任何其他内容之前(一次)调用的函数。它初始化 RPython/PyPy GC 并执行大量必要的启动代码。此函数不会失败。
-
int pypy_setup_home(char* home, int verbose);
此函数从给定的“PyPy 主目录”开始搜索 PyPy 标准库。参数是
home
:pypy 目录中可执行文件的路径(可以是 .so 名称,可以是虚构的)。用于查找标准库,并也设置为sys.executable
。从 PyPy 5.5 开始,只要libpypy-c.so/dylib/dll
本身位于此目录中,您就可以在这里直接说 NULL。verbose
:如果非零,它将错误消息打印到 stderr
函数在成功时返回 0,在失败时返回 -1,可以多次调用,直到找到库。
-
void pypy_init_threads(void);
初始化线程。仅当涉及任何线程时才需要调用。必须在 pypy_setup_home() 之后调用
-
int pypy_execute_source(char* source);
执行
source
参数中给出的 Python 源代码。如果出现异常,它将 Python 追溯信息打印到 stderr 并返回 1,否则返回 0。您应该在源代码中真正进行自己的错误处理。它将获取 GIL。注意:这仅用于一次或最多几次调用。请参见下面的 更完整的示例。在 PyPy <= 2.6.0 中,全局字典在多次调用中重复使用,这会导致潜在的奇怪结果(例如,对象过早死亡)。在 PyPy >= 2.6.1 中,每次调用都会获得一个新的全局字典(但随后,所有全局字典都将永远保存在
sys._pypy_execute_source
中)。
-
int pypy_execute_source_ptr(char* source, void* ptr);
注意
于 2014 年 6 月的 PyPy 2.3.1 版本中添加
与上面类似,但它在源代码作用域中注册了一个魔法参数,名为
c_argument
,其中void*
被编码为 Python 整数。
-
void pypy_thread_attach(void);
如果您的应用程序使用在 PyPy 之外初始化的线程,则需要调用此函数以告知 PyPy GC 跟踪此线程。请注意,此函数本身不是线程安全的,因此您需要用互斥锁保护它。
最小示例¶
请注意,此 API 比 CPython C API 简洁得多,因此一开始您可能会认为无法做太多事情。但是,诀窍是在 Python 中完成所有逻辑,并通过 cffi 回调公开它。我们编写了一个简单的 C 程序
#include "PyPy.h"
#include <stdio.h>
#include <stdlib.h>
static char source[] = "print 'hello from pypy'";
int main(void)
{
int res;
rpython_startup_code();
/* Before PyPy 5.5, you may need to say e.g. "/opt/pypy/bin" instead
* of NULL. */
res = pypy_setup_home(NULL, 1);
if (res) {
printf("Error setting pypy home!\n");
return 1;
}
res = pypy_execute_source((char*)source);
if (res) {
printf("Error calling pypy_execute_source!\n");
}
return res;
}
如果我们将其保存为 x.c
,现在编译并运行它(在 Linux 上)使用
$ gcc -g -o x x.c -lpypy-c -L/opt/pypy/bin -I/opt/pypy/include
$ LD_LIBRARY_PATH=/opt/pypy/bin ./x
hello from pypy
在 OSX 上,如果要链接到它,则需要设置二进制文件的 rpath,使用类似以下的命令
gcc -o x x.c -lpypy-c -L. -Wl,-rpath -Wl,@executable_path
./x
hello from pypy
更完整的示例¶
注意
请注意,我们没有使用 extern "Python"
,这是 CFFI 1.4 中进行回调的新方法:这是因为这些示例使用 ABI 模式,而不是 API 模式,在 ABI 模式下,您仍然需要使用 ffi.callback()
。将 extern "Python"
与嵌入的想法集成正在进行中(预计最终将导致比这里描述的更好的嵌入方式,并且在 CPython 和 PyPy 上都能很好地工作)。
通常,我们需要做的事情不仅仅是执行源代码。以下是一个完整的示例,有关详细信息,请参阅 cffi 文档。它有点长,但它概括了使用 PyPy 嵌入接口可以做些什么
# file "interface.py"
import cffi
ffi = cffi.FFI()
ffi.cdef('''
struct API {
double (*add_numbers)(double x, double y);
};
''')
# Better define callbacks at module scope, it's important to
# keep this object alive.
@ffi.callback("double (double, double)")
def add_numbers(x, y):
return x + y
def fill_api(ptr):
global api
api = ffi.cast("struct API*", ptr)
api.add_numbers = add_numbers
/* C example */
#include "PyPy.h"
#include <stdio.h>
#include <stdlib.h>
struct API {
double (*add_numbers)(double x, double y);
};
struct API api; /* global var */
int initialize_api(void)
{
static char source[] =
"import sys; sys.path.insert(0, '.'); "
"import interface; interface.fill_api(c_argument)";
int res;
rpython_startup_code();
res = pypy_setup_home(NULL, 1);
if (res) {
fprintf(stderr, "Error setting pypy home!\n");
return -1;
}
res = pypy_execute_source_ptr(source, &api);
if (res) {
fprintf(stderr, "Error calling pypy_execute_source_ptr!\n");
return -1;
}
return 0;
}
int main(void)
{
if (initialize_api() < 0)
return 1;
printf("sum: %f\n", api.add_numbers(12.3, 45.6));
return 0;
}
您可以使用以下命令编译并运行它
$ gcc -g -o x x.c -lpypy-c -L/opt/pypy/bin -I/opt/pypy/include
$ LD_LIBRARY_PATH=/opt/pypy/bin ./x
sum: 57.900000
如您所见,我们所做的是创建一个 struct API
,其中包含我们在此特定情况下所需的自定义 API。此结构由 Python 填充,以包含一个函数指针,然后从 C 端调用该函数指针。也可以使用其他函数指针,这些指针由 C 端填充并由 Python 端调用,甚至是非函数指针字段:基本上,两端通过定义 API 的单个 C 结构进行通信。
查找 pypy_home¶
如果您运行的是 PyPy >= 5.5(2016 年 10 月发布),通常可以跳过此部分
函数 pypy_setup_home() 以文件路径作为第一个参数,它可以从该路径推断出标准库的位置。更准确地说,它会尝试删除最终组件,直到找到 lib-python
和 lib_pypy
。目前没有“干净”的方法(pkg-config 想到)来查找此路径。您可以尝试以下(GNU 特定)技巧(不要忘记链接到 *dl*),它假设 libpypy-c.so
位于标准库目录中。(无论如何,这或多或少必须是这种情况,否则 pypy
程序本身将无法运行。)
#if !(_GNU_SOURCE)
#define _GNU_SOURCE
#endif
#include <dlfcn.h>
#include <limits.h>
#include <stdlib.h>
// caller should free returned pointer to avoid memleaks
// returns NULL on error
char* guess_pypyhome(void) {
// glibc-only (dladdr is why we #define _GNU_SOURCE)
Dl_info info;
void *_rpython_startup_code = dlsym(0,"rpython_startup_code");
if (_rpython_startup_code == 0) {
return 0;
}
if (dladdr(_rpython_startup_code, &info) != 0) {
const char* lib_path = info.dli_fname;
char* lib_realpath = realpath(lib_path, 0);
return lib_realpath;
}
return 0;
}
线程¶
如果您想使用 pthreads,您需要做的是从您创建的每个线程(但不是从主线程)调用 pypy_thread_attach
,并从主线程调用 pypy_init_threads
。